From b5b5b038acd6e1f93fbd3f616357ab7d9f474eca Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Wed, 1 Jul 2020 17:27:50 -0700 Subject: [PATCH 1/7] OpenX: Analytics Adapter update --- modules/openxAnalyticsAdapter.js | 859 ++++++++++++++---- modules/openxAnalyticsAdapter.md | 10 +- .../modules/openxAnalyticsAdapter_spec.js | 737 +++++++++------ 3 files changed, 1132 insertions(+), 474 deletions(-) diff --git a/modules/openxAnalyticsAdapter.js b/modules/openxAnalyticsAdapter.js index 7addfe68bc6..f2f5e02d52c 100644 --- a/modules/openxAnalyticsAdapter.js +++ b/modules/openxAnalyticsAdapter.js @@ -1,260 +1,729 @@ import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { config } from '../src/config.js'; import { ajax } from '../src/ajax.js'; -import * as utils from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; +import includes from 'core-js-pure/features/array/includes.js'; +const utils = require('../src/utils.js'); + +export const AUCTION_STATES = { + INIT: 'initialized', // auction has initialized + ENDED: 'ended', // all auction requests have been accounted for + COMPLETED: 'completed' // all slots have rendered +}; + +const ADAPTER_VERSION = '0.1'; +const SCHEMA_VERSION = '0.1'; + +const AUCTION_END_WAIT_TIME = 1000; +const URL_PARAM = ''; +const ANALYTICS_TYPE = 'endpoint'; +const ENDPOINT = 'https://prebid.openx.net/ox/analytics/'; +// Event Types const { - EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON } + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, AUCTION_END, BID_WON } } = CONSTANTS; - const SLOT_LOADED = 'slotOnload'; -const ENDPOINT = 'https://ads.openx.net/w/1.0/pban'; +const UTM_TAGS = [ + 'utm_campaign', + 'utm_source', + 'utm_medium', + 'utm_term', + 'utm_content' +]; +const UTM_TO_CAMPAIGN_PROPERTIES = { + 'utm_campaign': 'name', + 'utm_source': 'source', + 'utm_medium': 'medium', + 'utm_term': 'term', + 'utm_content': 'content' +}; -let initOptions; +/** + * @typedef {Object} OxAnalyticsConfig + * @property {string} orgId + * @property {string} publisherPlatformId + * @property {number} publisherAccountId + * @property {number} sampling + * @property {boolean} enableV2 + * @property {boolean} testPipeline + * @property {Object} campaign + * @property {number} payloadWaitTime + * @property {number} payloadWaitTimePadding + * @property {Array} adUnits + */ + +/** + * @type {OxAnalyticsConfig} + */ +const DEFAULT_ANALYTICS_CONFIG = { + orgId: void (0), + publisherPlatformId: void (0), + publisherAccountId: void (0), + sampling: 0.05, // default sampling rate of 5% + testCode: 'default', + campaign: {}, + adUnits: [], + payloadWaitTime: AUCTION_END_WAIT_TIME, + payloadWaitTimePadding: 2000 +}; +// Initialization +/** + * @type {OxAnalyticsConfig} + */ +let analyticsConfig; let auctionMap = {}; +let auctionOrder = 1; // tracks the number of auctions ran on the page -function onAuctionInit({ auctionId }) { - auctionMap[auctionId] = { - adUnitMap: {} - }; +let googletag = window.googletag || {}; +googletag.cmd = googletag.cmd || []; + +let openxAdapter = Object.assign(adapter({ urlParam: URL_PARAM, analyticsType: ANALYTICS_TYPE })); + +openxAdapter.originEnableAnalytics = openxAdapter.enableAnalytics; + +openxAdapter.enableAnalytics = function(adapterConfig = {options: {}}) { + if (isValidConfig(adapterConfig)) { + analyticsConfig = {...DEFAULT_ANALYTICS_CONFIG, ...adapterConfig.options}; + + // campaign properties defined by config will override utm query parameters + analyticsConfig.campaign = {...buildCampaignFromUtmCodes(), ...analyticsConfig.campaign}; + + utils.logInfo('OpenX Analytics enabled with config', analyticsConfig); + + // override track method with v2 handlers + openxAdapter.track = prebidAnalyticsEventHandler; + + googletag.cmd.push(function () { + googletag.pubads().addEventListener(SLOT_LOADED, args => { + openxAdapter.track({eventType: SLOT_LOADED, args}); + utils.logInfo('OX: SlotOnLoad event triggered'); + }); + }); + + openxAdapter.originEnableAnalytics(adapterConfig); + } +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: openxAdapter, + code: 'openx' +}); + +export default openxAdapter; + +/** + * Test Helper Functions + */ + +// reset the cache for unit tests +openxAdapter.reset = function() { + auctionMap = {}; + auctionOrder = 1; +}; + +/** + * Private Functions + */ + +function isValidConfig({options: analyticsOptions}) { + let hasOrgId = analyticsOptions && analyticsOptions.orgId !== void (0); + + const fieldValidations = [ + // tuple of property, type, required + ['orgId', 'string', hasOrgId], + ['publisherPlatformId', 'string', !hasOrgId], + ['publisherAccountId', 'number', !hasOrgId], + ['sampling', 'number', false], + ['enableV2', 'boolean', false], + ['testPipeline', 'boolean', false], + ['adIdKey', 'string', false], + ['payloadWaitTime', 'number', false], + ['payloadWaitTimePadding', 'number', false], + ]; + + let failedValidation = find(fieldValidations, ([property, type, required]) => { + // if required, the property has to exist + // if property exists, type check value + return (required && !analyticsOptions.hasOwnProperty(property)) || + /* eslint-disable valid-typeof */ + (analyticsOptions.hasOwnProperty(property) && typeof analyticsOptions[property] !== type); + }); + if (failedValidation) { + let [property, type, required] = failedValidation; + + if (required) { + utils.logError(`OpenXAnalyticsAdapter: Expected '${property}' to exist and of type '${type}'`); + } else { + utils.logError(`OpenXAnalyticsAdapter: Expected '${property}' to be type '${type}'`); + } + } + + return !failedValidation; } -function onBidRequested({ auctionId, auctionStart, bids, start }) { - const adUnitMap = auctionMap[auctionId]['adUnitMap']; +function buildCampaignFromUtmCodes() { + let campaign = {}; + let queryParams = utils.parseQS(utils.getWindowLocation() && utils.getWindowLocation().search); - bids.forEach(bid => { - const { adUnitCode, bidId, bidder, params, transactionId } = bid; + UTM_TAGS.forEach(function(utmKey) { + let utmValue = queryParams[utmKey]; + if (utmValue) { + let key = UTM_TO_CAMPAIGN_PROPERTIES[utmKey]; + campaign[key] = utmValue; + } + }); + return campaign; +} + +function detectMob() { + if ( + navigator.userAgent.match(/Android/i) || + navigator.userAgent.match(/webOS/i) || + navigator.userAgent.match(/iPhone/i) || + navigator.userAgent.match(/iPad/i) || + navigator.userAgent.match(/iPod/i) || + navigator.userAgent.match(/BlackBerry/i) || + navigator.userAgent.match(/Windows Phone/i) + ) { + return true; + } else { + return false; + } +} + +function detectOS() { + if (navigator.userAgent.indexOf('Android') != -1) return 'Android'; + if (navigator.userAgent.indexOf('like Mac') != -1) return 'iOS'; + if (navigator.userAgent.indexOf('Win') != -1) return 'Windows'; + if (navigator.userAgent.indexOf('Mac') != -1) return 'Macintosh'; + if (navigator.userAgent.indexOf('Linux') != -1) return 'Linux'; + if (navigator.appVersion.indexOf('X11') != -1) return 'Unix'; + return 'Others'; +} - adUnitMap[adUnitCode] = adUnitMap[adUnitCode] || { - auctionId, - auctionStart, - transactionId, - bidMap: {} +function detectBrowser() { + var isChrome = + /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); + var isCriOS = navigator.userAgent.match('CriOS'); + var isSafari = + /Safari/.test(navigator.userAgent) && + /Apple Computer/.test(navigator.vendor); + var isFirefox = /Firefox/.test(navigator.userAgent); + var isIE = + /Trident/.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent); + var isEdge = /Edge/.test(navigator.userAgent); + if (isIE) return 'Internet Explorer'; + if (isEdge) return 'Microsoft Edge'; + if (isCriOS) return 'Chrome'; + if (isSafari) return 'Safari'; + if (isFirefox) return 'Firefox'; + if (isChrome) return 'Chrome'; + return 'Others'; +} + +function prebidAnalyticsEventHandler({eventType, args}) { + utils.logMessage(eventType, Object.assign({}, args)); + switch (eventType) { + case AUCTION_INIT: + onAuctionInit(args); + break; + case BID_REQUESTED: + onBidRequested(args); + break; + case BID_RESPONSE: + onBidResponse(args); + break; + case BID_TIMEOUT: + onBidTimeout(args); + break; + case AUCTION_END: + onAuctionEnd(args); + break; + case BID_WON: + onBidWon(args); + break; + case SLOT_LOADED: + onSlotLoadedV2(args); + break; + } +} + +/** + * @typedef {Object} PbAuction + * @property {string} auctionId - Auction ID of the request this bid responded to + * @property {number} timestamp //: 1586675964364 + * @property {number} auctionEnd - timestamp of when auction ended //: 1586675964364 + * @property {string} auctionStatus //: "inProgress" + * @property {Array} adUnits //: [{…}] + * @property {string} adUnitCodes //: ["video1"] + * @property {string} labels //: undefined + * @property {Array} bidderRequests //: (2) [{…}, {…}] + * @property {Array} noBids //: [] + * @property {Array} bidsReceived //: [] + * @property {Array} winningBids //: [] + * @property {number} timeout //: 3000 + * @property {Object} config //: {publisherPlatformId: "a3aece0c-9e80-4316-8deb-faf804779bd1", publisherAccountId: 537143056, sampling: 1, enableV2: true}/* + */ + +function onAuctionInit({auctionId, timestamp: startTime, timeout, adUnitCodes}) { + auctionMap[auctionId] = { + id: auctionId, + startTime, + endTime: void (0), + timeout, + auctionOrder, + userIds: [], + adUnitCodesCount: adUnitCodes.length, + adunitCodesRenderedCount: 0, + state: AUCTION_STATES.INIT, + auctionSendDelayTimer: void (0), + }; + + // setup adunit properties in map + auctionMap[auctionId].adUnitCodeToAdUnitMap = adUnitCodes.reduce((obj, adunitCode) => { + obj[adunitCode] = { + code: adunitCode, + adPosition: void (0), + bidRequestsMap: {} }; + return obj; + }, {}); - adUnitMap[adUnitCode]['bidMap'][bidId] = { + auctionOrder++; +} + +/** + * @typedef {Object} PbBidRequest + * @property {string} auctionId - Auction ID of the request this bid responded to + * @property {number} auctionStart //: 1586675964364 + * @property {Object} refererInfo + * @property {PbBidderRequest} bids + * @property {number} start - Start timestamp of the bidder request + * + */ + +/** + * @typedef {Object} PbBidderRequest + * @property {string} adUnitCode - Name of div or google adunit path + * @property {string} bidder - Bame of bidder + * @property {string} bidId - Identifies the bid request + * @property {Object} mediaTypes + * @property {Object} params + * @property {string} src + * @property {Object} userId - Map of userId module to module object + */ + +/** + * Tracks the bid request + * @param {PbBidRequest} bidRequest + */ +function onBidRequested(bidRequest) { + const {auctionId, bids: bidderRequests, start} = bidRequest; + const auction = auctionMap[auctionId]; + const adUnitCodeToAdUnitMap = auction.adUnitCodeToAdUnitMap; + + bidderRequests.forEach(bidderRequest => { + const { adUnitCode, bidder, bidId: requestId, mediaTypes, params, src, userId } = bidderRequest; + + auction.userIds.push(userId); + adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId] = { bidder, params, - requestTimestamp: start + mediaTypes, + source: src, + startTime: start, + timedOut: false, + bids: {} }; }); } -function onBidResponse({ - auctionId, - adUnitCode, - requestId: bidId, - cpm, - creativeId, - responseTimestamp, - ts, - adId -}) { - const adUnit = auctionMap[auctionId]['adUnitMap'][adUnitCode]; - const bid = adUnit['bidMap'][bidId]; - bid.cpm = cpm; - bid.creativeId = creativeId; - bid.responseTimestamp = responseTimestamp; - bid.ts = ts; - bid.adId = adId; +/** + * + * @param {BidResponse} bidResponse + */ +function onBidResponse(bidResponse) { + let { + auctionId, + adUnitCode, + requestId, + cpm, + creativeId, + requestTimestamp, + responseTimestamp, + ts, + mediaType, + dealId, + ttl, + netRevenue, + currency, + originalCpm, + originalCurrency, + width, + height, + timeToRespond: latency, + adId, + meta + } = bidResponse; + + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bids[adId] = { + cpm, + creativeId, + requestTimestamp, + responseTimestamp, + ts, + adId, + meta, + mediaType, + dealId, + ttl, + netRevenue, + currency, + originalCpm, + originalCurrency, + width, + height, + latency, + winner: false, + rendered: false, + renderTime: 0, + }; } function onBidTimeout(args) { - utils - ._map(args, value => value) - .forEach(({ auctionId, adUnitCode, bidId }) => { - const bid = - auctionMap[auctionId]['adUnitMap'][adUnitCode]['bidMap'][bidId]; - bid.timedOut = true; - }); + utils._each(args, ({auctionId, adUnitCode, bidId: requestId}) => { + if (auctionMap[auctionId] && + auctionMap[auctionId].adUnitCodeToAdUnitMap && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode] && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId] + ) { + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].timedOut = true; + } + }); +} +/** + * + * @param {PbAuction} endedAuction + */ +function onAuctionEnd(endedAuction) { + let auction = auctionMap[endedAuction.auctionId]; + + if (!auction) { + return; + } + + clearAuctionTimer(auction); + auction.endTime = endedAuction.auctionEnd; + auction.state = AUCTION_STATES.ENDED; + delayedSend(auction); } -function onBidWon({ auctionId, adUnitCode, requestId: bidId }) { - const adUnit = auctionMap[auctionId]['adUnitMap'][adUnitCode]; - const bid = adUnit['bidMap'][bidId]; - bid.won = true; +/** + * + * @param {BidResponse} bidResponse + */ +function onBidWon(bidResponse) { + const { auctionId, adUnitCode, requestId, adId } = bidResponse; + if (auctionMap[auctionId] && + auctionMap[auctionId].adUnitCodeToAdUnitMap && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode] && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId] && + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bids[adId] + ) { + auctionMap[auctionId].adUnitCodeToAdUnitMap[adUnitCode].bidRequestsMap[requestId].bids[adId].winner = true; + } } -function onSlotLoaded({ slot }) { - const targeting = slot.getTargetingKeys().reduce((targeting, key) => { - targeting[key] = slot.getTargeting(key); - return targeting; - }, {}); - utils.logMessage( - 'GPT slot is loaded. Current targeting set on slot:', - targeting - ); +/** + * + * @param {GoogleTagSlot} slot + * @param {string} serviceName + */ +function onSlotLoadedV2({ slot }) { + const renderTime = Date.now(); + const elementId = slot.getSlotElementId(); + const bidId = slot.getTargeting('hb_adid')[0]; - const adId = slot.getTargeting('hb_adid')[0]; - if (!adId) { - return; + let [auction, adUnit, bid] = getPathToBidResponseByBidId(bidId); + + if (!auction) { + // attempt to get auction by adUnitCode + auction = getAuctionByGoogleTagSLot(slot); + + if (!auction) { + return; // slot is not participating in an active prebid auction + } } - const adUnit = getAdUnitByAdId(adId); - if (!adUnit) { - return; + clearAuctionTimer(auction); + + // track that an adunit code has completed within an auction + auction.adunitCodesRenderedCount++; + + // mark adunit as rendered + if (bid) { + let {x, y} = getPageOffset(); + bid.rendered = true; + bid.renderTime = renderTime; + adUnit.adPosition = isAtf(elementId, x, y) ? 'ATF' : 'BTF'; } - const adUnitData = getAdUnitData(adUnit); - const performanceData = getPerformanceData(adUnit.auctionStart); - const commonFields = { - 'hb.asiid': slot.getAdUnitPath(), - 'hb.cur': config.getConfig('currency.adServerCurrency'), - 'hb.pubid': initOptions.publisherId - }; + if (auction.adunitCodesRenderedCount === auction.adUnitCodesCount) { + auction.state = AUCTION_STATES.COMPLETED; + } - const data = Object.assign({}, adUnitData, performanceData, commonFields); - sendEvent(data); + // prepare to send regardless if auction is complete or not as a failsafe in case not all events are tracked + // add additional padding when not all slots are rendered + delayedSend(auction); } -function getAdUnitByAdId(adId) { - let result; +function isAtf(elementId, scrollLeft = 0, scrollTop = 0) { + let elem = document.querySelector('#' + elementId); + let isAtf = false; + if (elem) { + let bounding = elem.getBoundingClientRect(); + if (bounding) { + let windowWidth = (window.innerWidth || document.documentElement.clientWidth); + let windowHeight = (window.innerHeight || document.documentElement.clientHeight); + + // intersection coordinates + let left = Math.max(0, bounding.left + scrollLeft); + let right = Math.min(windowWidth, bounding.right + scrollLeft); + let top = Math.max(0, bounding.top + scrollTop); + let bottom = Math.min(windowHeight, bounding.bottom + scrollTop); + + let intersectionWidth = right - left; + let intersectionHeight = bottom - top; + + let intersectionArea = (intersectionHeight > 0 && intersectionWidth > 0) ? (intersectionHeight * intersectionWidth) : 0; + let adSlotArea = (bounding.right - bounding.left) * (bounding.bottom - bounding.top); + + if (adSlotArea > 0) { + // Atleast 50% of intersection in window + isAtf = intersectionArea * 2 >= adSlotArea; + } + } + } else { + utils.logWarn('OX: DOM element not for id ' + elementId); + } + return isAtf; +} - utils._map(auctionMap, value => value).forEach(auction => { - utils._map(auction.adUnitMap, value => value).forEach(adUnit => { - utils._map(adUnit.bidMap, value => value).forEach(bid => { - if (adId === bid.adId) { - result = adUnit; - } - }) - }); - }); +// backwards compatible pageOffset from https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX +function getPageOffset() { + var x = (window.pageXOffset !== undefined) + ? window.pageXOffset + : (document.documentElement || document.body.parentNode || document.body).scrollLeft; - return result; + var y = (window.pageYOffset !== undefined) + ? window.pageYOffset + : (document.documentElement || document.body.parentNode || document.body).scrollTop; + return {x, y}; } -function getAdUnitData(adUnit) { - const bids = utils._map(adUnit.bidMap, value => value); - const bidders = bids.map(bid => bid.bidder); - const requestTimes = bids.map( - bid => bid.requestTimestamp && bid.requestTimestamp - adUnit.auctionStart - ); - const responseTimes = bids.map( - bid => bid.responseTimestamp && bid.responseTimestamp - adUnit.auctionStart - ); - const bidValues = bids.map(bid => bid.cpm || 0); - const timeouts = bids.map(bid => !!bid.timedOut); - const creativeIds = bids.map(bid => bid.creativeId); - const winningBid = bids.filter(bid => bid.won)[0]; - const winningExchangeIndex = bids.indexOf(winningBid); - const openxBid = bids.filter(bid => bid.bidder === 'openx')[0]; +function delayedSend(auction) { + const delayTime = auction.adunitCodesRenderedCount === auction.adUnitCodesCount + ? analyticsConfig.payloadWaitTime + : analyticsConfig.payloadWaitTime + analyticsConfig.payloadWaitTimePadding; - return { - 'hb.ct': adUnit.auctionStart, - 'hb.rid': adUnit.auctionId, - 'hb.exn': bidders.join(','), - 'hb.sts': requestTimes.join(','), - 'hb.ets': responseTimes.join(','), - 'hb.bv': bidValues.join(','), - 'hb.to': timeouts.join(','), - 'hb.crid': creativeIds.join(','), - 'hb.we': winningExchangeIndex, - 'hb.g1': winningExchangeIndex === -1, - dddid: adUnit.transactionId, - ts: openxBid && openxBid.ts, - auid: openxBid && openxBid.params && openxBid.params.unit - }; + auction.auctionSendDelayTimer = setTimeout(() => { + let payload = JSON.stringify([buildAuctionPayload(auction)]); + ajax(ENDPOINT, deleteAuctionMap, payload, { contentType: 'application/json' }); + + function deleteAuctionMap() { + delete auctionMap[auction.id]; + } + }, delayTime); } -function getPerformanceData(auctionStart) { - let timing; - try { - timing = window.top.performance.timing; - } catch (e) {} +function clearAuctionTimer(auction) { + // reset the delay timer to send the auction data + if (auction.auctionSendDelayTimer) { + clearTimeout(auction.auctionSendDelayTimer); + auction.auctionSendDelayTimer = void (0); + } +} - if (!timing) { - return; +/** + * Returns the path to a bid (auction, adunit, bidRequest, and bid) based on a bidId + * @param {string} bidId + * @returns {Array<*>} + */ +function getPathToBidResponseByBidId(bidId) { + let auction; + let adUnit; + let bidResponse; + + if (!bidId) { + return []; } - const { fetchStart, domContentLoadedEventEnd, loadEventEnd } = timing; - const domContentLoadTime = domContentLoadedEventEnd - fetchStart; - const pageLoadTime = loadEventEnd - fetchStart; - const timeToAuction = auctionStart - fetchStart; - const timeToRender = Date.now() - fetchStart; + utils._each(auctionMap, currentAuction => { + // skip completed auctions + if (currentAuction.state === AUCTION_STATES.COMPLETED) { + return; + } - return { - 'hb.dcl': domContentLoadTime, - 'hb.dl': pageLoadTime, - 'hb.tta': timeToAuction, - 'hb.ttr': timeToRender - }; + utils._each(currentAuction.adUnitCodeToAdUnitMap, (currentAdunit) => { + utils._each(currentAdunit.bidRequestsMap, currentBiddRequest => { + utils._each(currentBiddRequest.bids, (currentBidResponse, bidResponseId) => { + if (bidId === bidResponseId) { + auction = currentAuction; + adUnit = currentAdunit; + bidResponse = currentBidResponse; + } + }); + }); + }); + }); + return [auction, adUnit, bidResponse]; } -function sendEvent(data) { - utils._map(data, (value, key) => [key, value]).forEach(([key, value]) => { - if ( - value === undefined || - value === null || - (typeof value === 'number' && isNaN(value)) - ) { - delete data[key]; +function getAuctionByGoogleTagSLot(slot) { + let slotAdunitCodes = [slot.getSlotElementId(), slot.getAdUnitPath()]; + let slotAuction; + + utils._each(auctionMap, auction => { + if (auction.state === AUCTION_STATES.COMPLETED) { + return; } + + utils._each(auction.adUnitCodeToAdUnitMap, (bidderRequestIdMap, adUnitCode) => { + if (includes(slotAdunitCodes, adUnitCode)) { + slotAuction = auction; + } + }); }); - ajax(ENDPOINT, null, data, { method: 'GET' }); + + return slotAuction; } -let googletag = window.googletag || {}; -googletag.cmd = googletag.cmd || []; -googletag.cmd.push(function() { - googletag.pubads().addEventListener(SLOT_LOADED, args => { - openxAdapter.track({ eventType: SLOT_LOADED, args }); - }); -}); +function buildAuctionPayload(auction) { + let {startTime, endTime, state, timeout, auctionOrder, userIds, adUnitCodeToAdUnitMap} = auction; + let {orgId, publisherPlatformId, publisherAccountId, campaign} = analyticsConfig; -const openxAdapter = Object.assign( - adapter({ url: ENDPOINT, analyticsType: 'endpoint' }), - { - track({ eventType, args }) { - utils.logMessage(eventType, Object.assign({}, args)); - switch (eventType) { - case AUCTION_INIT: - onAuctionInit(args); - break; - case BID_REQUESTED: - onBidRequested(args); - break; - case BID_RESPONSE: - onBidResponse(args); - break; - case BID_TIMEOUT: - onBidTimeout(args); - break; - case BID_WON: - onBidWon(args); - break; - case SLOT_LOADED: - onSlotLoaded(args); - break; + return { + adapterVersion: ADAPTER_VERSION, + schemaVersion: SCHEMA_VERSION, + orgId, + publisherPlatformId, + publisherAccountId, + campaign, + state, + startTime, + endTime, + timeLimit: timeout, + auctionOrder, + deviceType: detectMob() ? 'Mobile' : 'Desktop', + deviceOSType: detectOS(), + browser: detectBrowser(), + testCode: analyticsConfig.testCode, + // return an array of module name that have user data + userIdProviders: buildUserIdProviders(userIds), + adUnits: buildAdUnitsPayload(adUnitCodeToAdUnitMap), + }; + + function buildAdUnitsPayload(adUnitCodeToAdUnitMap) { + return utils._map(adUnitCodeToAdUnitMap, (adUnit) => { + let {code, adPosition} = adUnit; + + return { + code, + adPosition, + bidRequests: buildBidRequestPayload(adUnit.bidRequestsMap) + }; + + function buildBidRequestPayload(bidRequestsMap) { + return utils._map(bidRequestsMap, (bidRequest) => { + let {bidder, source, bids, mediaTypes, timedOut} = bidRequest; + return { + bidder, + source, + hasBidderResponded: Object.keys(bids).length > 0, + availableAdSizes: getMediaTypeSizes(mediaTypes), + availableMediaTypes: getMediaTypes(mediaTypes), + timedOut, + bidResponses: utils._map(bidRequest.bids, (bidderBidResponse) => { + let { + cpm, + creativeId, + ts, + meta, + mediaType, + dealId, + ttl, + netRevenue, + currency, + width, + height, + latency, + winner, + rendered, + renderTime + } = bidderBidResponse; + + return { + microCpm: cpm * 1000000, + netRevenue, + currency, + mediaType, + height, + width, + size: `${width}x${height}`, + dealId, + latency, + ttl, + winner, + creativeId, + ts, + rendered, + renderTime, + meta + } + }) + } + }); } - } + }); } -); - -// save the base class function -openxAdapter.originEnableAnalytics = openxAdapter.enableAnalytics; -// override enableAnalytics so we can get access to the config passed in from the page -openxAdapter.enableAnalytics = function(config) { - if (!config || !config.options || !config.options.publisherId) { - utils.logError('OpenX analytics adapter: publisherId is required.'); - return; + function buildUserIdProviders(userIds) { + return utils._map(userIds, (userId) => { + return utils._map(userId, (id, module) => { + return hasUserData(module, id) ? module : false + }).filter(module => module); + }).reduce(utils.flatten, []).filter(utils.uniques).sort(); } - initOptions = config.options; - openxAdapter.originEnableAnalytics(config); // call the base class function -}; -// reset the cache for unit tests -openxAdapter.reset = function() { - auctionMap = {}; -}; + function hasUserData(module, idOrIdObject) { + let normalizedId; + + switch (module) { + case 'digitrustid': + normalizedId = utils.deepAccess(idOrIdObject, 'data.id'); + break; + case 'lipb': + normalizedId = idOrIdObject.lipbid; + break; + default: + normalizedId = idOrIdObject; + } -adapterManager.registerAnalyticsAdapter({ - adapter: openxAdapter, - code: 'openx' -}); + return !utils.isEmpty(normalizedId); + } -export default openxAdapter; + function getMediaTypeSizes(mediaTypes) { + return utils._map(mediaTypes, (mediaTypeConfig, mediaType) => { + return utils.parseSizesInput(mediaTypeConfig.sizes) + .map(size => `${mediaType}_${size}`); + }).reduce(utils.flatten, []); + } + + function getMediaTypes(mediaTypes) { + return utils._map(mediaTypes, (mediaTypeConfig, mediaType) => mediaType); + } +} diff --git a/modules/openxAnalyticsAdapter.md b/modules/openxAnalyticsAdapter.md index ac739f36c76..af40486f2a4 100644 --- a/modules/openxAnalyticsAdapter.md +++ b/modules/openxAnalyticsAdapter.md @@ -102,11 +102,13 @@ Configuration options are a follows: | Property | Type | Required? | Description | Example | |:---|:---|:---|:---|:---| -| `publisherPlatformId` | `string` | Yes | Used to determine ownership of data. | `a3aece0c-9e80-4316-8deb-faf804779bd1` | -| `publisherAccountId` | `number` | Yes | Used to determine ownership of data. | `1537143056` | +| `orgId` | `string` | Yes | Used to determine ownership of data. | `aa1bb2cc-3dd4-4316-8deb-faf804779bd1` | +| `publisherPlatformId` | `string` | No
**__Deprecated. Please use orgId__** | Used to determine ownership of data. | `a3aece0c-9e80-4316-8deb-faf804779bd1` | +| `publisherAccountId` | `number` | No
**__Deprecated. Please use orgId__** | Used to determine ownership of data. | `1537143056` | | `sampling` | `number` | Yes | Sampling rate | Undefined or `1.00` - No sampling. Analytics is sent all the time.
0.5 - 50% of users will send analytics data. | | `testCode` | `string` | No | Used to label analytics data for the purposes of tests.
This label is treated as a dimension and can be compared against other labels. | `timeout_config_1`
`timeout_config_2`
`timeout_default` | - +| `campaign` | `Object` | No | Object with 5 parameters:
  • content
  • medium
  • name
  • source
  • term
Each parameter is a free-form string. Refer to metrics doc on when to use these fields. By setting a value to one of these properties, you override the associated url utm query parameter. | | +| `payloadWaitTime` | `number` | No | Delay after all slots of an auction renders before the payload is sent.
Defaults to 100ms | 1000 | --- # Viewing Data @@ -114,7 +116,7 @@ The Prebid Report available in the Reporting in the Cloud tool, allows you to vi **To view your data:** -1. Log in to Reporting in the Cloud. +1. Log in to [OpenX Reporting](https://openx.sigmoid.io/app). 2. In the top right, click on the **View** list and then select **Prebidreport**. diff --git a/test/spec/modules/openxAnalyticsAdapter_spec.js b/test/spec/modules/openxAnalyticsAdapter_spec.js index 805435abf80..593df53152d 100644 --- a/test/spec/modules/openxAnalyticsAdapter_spec.js +++ b/test/spec/modules/openxAnalyticsAdapter_spec.js @@ -1,114 +1,189 @@ import { expect } from 'chai'; -import openxAdapter from 'modules/openxAnalyticsAdapter.js'; -import { config } from 'src/config.js'; +import openxAdapter, {AUCTION_STATES} from 'modules/openxAnalyticsAdapter.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; const { - EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON } + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON, AUCTION_END } } = CONSTANTS; - const SLOT_LOADED = 'slotOnload'; +const CURRENT_TIME = 1586000000000; describe('openx analytics adapter', function() { - it('should require publisher id', function() { - sinon.spy(utils, 'logError'); + describe('when validating the configuration', function () { + let spy; + beforeEach(function () { + spy = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + utils.logError.restore(); + }); + + it('should require organization id when no configuration is passed', function() { + openxAdapter.enableAnalytics(); + expect(spy.firstCall.args[0]).to.match(/publisherPlatformId/); + expect(spy.firstCall.args[0]).to.match(/to exist/); + }); + + it('should require publisher id when no orgId is passed', function() { + openxAdapter.enableAnalytics({ + provider: 'openx', + options: { + publisherAccountId: 12345 + } + }); + expect(spy.firstCall.args[0]).to.match(/publisherPlatformId/); + expect(spy.firstCall.args[0]).to.match(/to exist/); + }); - openxAdapter.enableAnalytics(); - expect( - utils.logError.calledWith( - 'OpenX analytics adapter: publisherId is required.' - ) - ).to.be.true; + it('should validate types', function() { + openxAdapter.enableAnalytics({ + provider: 'openx', + options: { + orgId: 'test platformId', + sampling: 'invalid-float' + } + }); - utils.logError.restore(); + expect(spy.firstCall.args[0]).to.match(/sampling/); + expect(spy.firstCall.args[0]).to.match(/type 'number'/); + }); }); - describe('sending analytics event', function() { - const auctionInit = { auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' }; + describe('when tracking analytic events', function () { + const AD_UNIT_CODE = 'test-div-1'; + const SLOT_LOAD_WAIT_TIME = 10; + + const DEFAULT_V2_ANALYTICS_CONFIG = { + orgId: 'test-org-id', + publisherAccountId: 123, + publisherPlatformId: 'test-platform-id', + sample: 1.0, + enableV2: true, + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME + }; + + const auctionInit = { + auctionId: 'test-auction-id', + timestamp: CURRENT_TIME, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], + }; const bidRequestedOpenX = { - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - auctionStart: 1540944528017, + auctionId: 'test-auction-id', + auctionStart: CURRENT_TIME, bids: [ { - adUnitCode: 'div-1', - bidId: '2f0c647b904e25', + adUnitCode: AD_UNIT_CODE, + bidId: 'test-openx-request-id', bidder: 'openx', - params: { unit: '540249866' }, - transactionId: 'ac66c3e6-3118-4213-a3ae-8cdbe4f72873' + params: { unit: 'test-openx-ad-unit-id' }, + userId: { + tdid: 'test-tradedesk-id', + empty_id: '', + null_id: null, + bla_id: '', + digitrustid: { data: { id: '1' } }, + lipbid: { lipb: '2' } + } } ], - start: 1540944528021 + start: CURRENT_TIME + 10 }; const bidRequestedCloseX = { - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - auctionStart: 1540944528017, + auctionId: 'test-auction-id', + auctionStart: CURRENT_TIME, bids: [ { - adUnitCode: 'div-1', - bidId: '43d454020e9409', + adUnitCode: AD_UNIT_CODE, + bidId: 'test-closex-request-id', bidder: 'closex', - params: { unit: '513144370' }, - transactionId: 'ac66c3e6-3118-4213-a3ae-8cdbe4f72873' + params: { unit: 'test-closex-ad-unit-id' }, + userId: { + bla_id: '2', + tdid: 'test-tradedesk-id' + } } ], - start: 1540944528026 + start: CURRENT_TIME + 20 }; const bidResponseOpenX = { - requestId: '2f0c647b904e25', - adId: '33dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', + adUnitCode: AD_UNIT_CODE, cpm: 0.5, + netRevenue: true, + requestId: 'test-openx-request-id', + mediaType: 'banner', + width: 300, + height: 250, + adId: 'test-openx-ad-id', + auctionId: 'test-auction-id', creativeId: 'openx-crid', - responseTimestamp: 1540944528184, - ts: '2DAABBgABAAECAAIBAAsAAgAAAJccGApKSGt6NUZxRXYyHBbinsLj' + currency: 'USD', + timeToRespond: 100, + responseTimestamp: CURRENT_TIME + 30, + ts: 'test-openx-ts' }; const bidResponseCloseX = { - requestId: '43d454020e9409', - adId: '43dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', + adUnitCode: AD_UNIT_CODE, cpm: 0.3, + netRevenue: true, + requestId: 'test-closex-request-id', + mediaType: 'video', + width: 300, + height: 250, + adId: 'test-closex-ad-id', + auctionId: 'test-auction-id', creativeId: 'closex-crid', - responseTimestamp: 1540944528196, - ts: 'hu1QWo6iD3MHs6NG_AQAcFtyNqsj9y4S0YRbX7Kb06IrGns0BABb' + currency: 'USD', + timeToRespond: 200, + dealId: 'test-closex-deal-id', + responseTimestamp: CURRENT_TIME + 40, + ts: 'test-closex-ts' }; const bidTimeoutOpenX = { 0: { - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - bidId: '2f0c647b904e25' - } - }; + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id', + bidId: 'test-openx-request-id' + }}; const bidTimeoutCloseX = { 0: { - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - bidId: '43d454020e9409' + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id', + bidId: 'test-closex-request-id' } }; const bidWonOpenX = { - requestId: '2f0c647b904e25', - adId: '33dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' + requestId: 'test-openx-request-id', + adId: 'test-openx-ad-id', + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id' + }; + + const auctionEnd = { + auctionId: 'test-auction-id', + timestamp: CURRENT_TIME, + auctionEnd: CURRENT_TIME + 100, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], }; const bidWonCloseX = { - requestId: '43d454020e9409', - adId: '43dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' + requestId: 'test-closex-request-id', + adId: 'test-closex-ad-id', + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id' }; function simulateAuction(events) { @@ -116,328 +191,440 @@ describe('openx analytics adapter', function() { events.forEach(event => { const [eventType, args] = event; - openxAdapter.track({ eventType, args }); if (eventType === BID_RESPONSE) { highestBid = highestBid || args; if (highestBid.cpm < args.cpm) { highestBid = args; } } - }); - openxAdapter.track({ - eventType: SLOT_LOADED, - args: { - slot: { - getAdUnitPath: () => { - return '/90577858/test_ad_unit'; - }, - getTargetingKeys: () => { - return []; - }, - getTargeting: sinon - .stub() - .withArgs('hb_adid') - .returns(highestBid ? [highestBid.adId] : []) - } + if (eventType === SLOT_LOADED) { + const slotLoaded = { + slot: { + getAdUnitPath: () => { + return '/12345678/test_ad_unit'; + }, + getSlotElementId: () => { + return AD_UNIT_CODE; + }, + getTargeting: (key) => { + if (key === 'hb_adid') { + return highestBid ? [highestBid.adId] : []; + } else { + return []; + } + } + } + }; + openxAdapter.track({ eventType, args: slotLoaded }); + } else { + openxAdapter.track({ eventType, args }); } }); } - function getQueryData(url) { - const queryArgs = url.split('?')[1].split('&'); - return queryArgs.reduce((data, arg) => { - const [key, val] = arg.split('='); - data[key] = val; - return data; - }, {}); - } + let clock; - before(function() { + beforeEach(function() { sinon.stub(events, 'getEvents').returns([]); - openxAdapter.enableAnalytics({ - options: { - publisherId: 'test123' - } - }); + clock = sinon.useFakeTimers(CURRENT_TIME); }); - after(function() { + afterEach(function() { events.getEvents.restore(); - openxAdapter.disableAnalytics(); + clock.restore(); }); - beforeEach(function() { - openxAdapter.reset(); - }); + describe('when there is an auction', function () { + let auction; + let auction2; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + + simulateAuction([ + [AUCTION_INIT, auctionInit], + [SLOT_LOADED] + ]); - afterEach(function() {}); + simulateAuction([ + [AUCTION_INIT, {...auctionInit, auctionId: 'second-auction-id'}], + [SLOT_LOADED] + ]); - it('should not send request if no bid response', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX] - ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + auction2 = JSON.parse(server.requests[1].requestBody)[0]; + }); - expect(server.requests.length).to.equal(0); - }); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track auction start time', function () { + expect(auction.startTime).to.equal(auctionInit.timestamp); + }); + + it('should track auction time limit', function () { + expect(auction.timeLimit).to.equal(auctionInit.timeout); + }); + + it('should track the \'default\' test code', function () { + expect(auction.testCode).to.equal('default'); + }); - it('should send 1 request to the right endpoint', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] - ]); + it('should track auction count', function () { + expect(auction.auctionOrder).to.equal(1); + expect(auction2.auctionOrder).to.equal(2); + }); - expect(server.requests.length).to.equal(1); + it('should track the orgId', function () { + expect(auction.orgId).to.equal(DEFAULT_V2_ANALYTICS_CONFIG.orgId); + }); + + it('should track the orgId', function () { + expect(auction.publisherPlatformId).to.equal(DEFAULT_V2_ANALYTICS_CONFIG.publisherPlatformId); + }); - const endpoint = server.requests[0].url.split('?')[0]; - // note IE11 returns the default secure port, so we look for this alternate value as well in these tests - expect(endpoint).to.be.oneOf(['https://ads.openx.net/w/1.0/pban', 'https://ads.openx.net:443/w/1.0/pban']); + it('should track the orgId', function () { + expect(auction.publisherAccountId).to.equal(DEFAULT_V2_ANALYTICS_CONFIG.publisherAccountId); + }); }); - describe('hb.ct, hb.rid, dddid, hb.asiid, hb.pubid', function() { - it('should always be in the query string', function() { + describe('when there is a custom test code', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({ + options: { + ...DEFAULT_V2_ANALYTICS_CONFIG, + testCode: 'test-code' + } + }); + simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.ct': String(bidRequestedOpenX.auctionStart), - 'hb.rid': auctionInit.auctionId, - dddid: bidRequestedOpenX.bids[0].transactionId, - 'hb.asiid': '/90577858/test_ad_unit', - 'hb.pubid': 'test123' - }); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the custom test code', function () { + expect(auction.testCode).to.equal('test-code'); }); }); - describe('hb.cur', function() { - it('should be in the query string if currency is set', function() { - sinon - .stub(config, 'getConfig') - .withArgs('currency.adServerCurrency') - .returns('bitcoin'); + describe('when there is campaign (utm) data', function () { + let auction; + beforeEach(function () { + + }); + + afterEach(function () { + openxAdapter.reset(); + utils.getWindowLocation.restore(); + openxAdapter.disableAnalytics(); + }); + + it('should track values from query params when they exist', function () { + sinon.stub(utils, 'getWindowLocation').returns({search: '?' + + 'utm_campaign=test-campaign-name&' + + 'utm_source=test-source&' + + 'utm_medium=test-medium&' + }); + + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + expect(auction.campaign.name).to.equal('test-campaign-name'); + expect(auction.campaign.source).to.equal('test-source'); + expect(auction.campaign.medium).to.equal('test-medium'); + expect(auction.campaign.content).to.be.undefined; + expect(auction.campaign.term).to.be.undefined; + }); - config.getConfig.restore(); + it('should override query params if configuration parameters exist', function () { + sinon.stub(utils, 'getWindowLocation').returns({search: '?' + + 'utm_campaign=test-campaign-name&' + + 'utm_source=test-source&' + + 'utm_medium=test-medium&' + + 'utm_content=test-content&' + + 'utm_term=test-term' + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.cur': 'bitcoin' + openxAdapter.enableAnalytics({ + options: { + ...DEFAULT_V2_ANALYTICS_CONFIG, + campaign: { + name: 'test-config-name', + source: 'test-config-source', + medium: 'test-config-medium' + } + } }); - }); - it('should not be in the query string if currency is not set', function() { simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); - - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.not.have.key('hb.cur'); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + expect(auction.campaign.name).to.equal('test-config-name'); + expect(auction.campaign.source).to.equal('test-config-source'); + expect(auction.campaign.medium).to.equal('test-config-medium'); + expect(auction.campaign.content).to.equal('test-content'); + expect(auction.campaign.term).to.equal('test-term'); }); }); - describe('hb.dcl, hb.dl, hb.tta, hb.ttr', function() { - it('should be in the query string if browser supports performance API', function() { - const timing = { - fetchStart: 1540944528000, - domContentLoadedEventEnd: 1540944528010, - loadEventEnd: 1540944528110 - }; - const originalPerf = window.top.performance; - window.top.performance = { timing }; + describe('when there are bid requests', function () { + let auction; + let openxBidder; + let closexBidder; - const renderTime = 1540944528100; - sinon.stub(Date, 'now').returns(renderTime); + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); simulateAuction([ [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedCloseX], [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + openxBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + closexBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + }); - window.top.performance = originalPerf; - Date.now.restore(); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.dcl': String(timing.domContentLoadedEventEnd - timing.fetchStart), - 'hb.dl': String(timing.loadEventEnd - timing.fetchStart), - 'hb.tta': String(bidRequestedOpenX.auctionStart - timing.fetchStart), - 'hb.ttr': String(renderTime - timing.fetchStart) - }); + it('should track the bidder', function () { + expect(openxBidder.bidder).to.equal('openx'); + expect(closexBidder.bidder).to.equal('closex'); + }); + + it('should track the adunit code', function () { + expect(auction.adUnits[0].code).to.equal(AD_UNIT_CODE); + }); + + it('should track the user ids', function () { + expect(auction.userIdProviders).to.deep.equal(['bla_id', 'digitrustid', 'lipbid', 'tdid']); + }); + + it('should not have responded', function () { + expect(openxBidder.hasBidderResponded).to.equal(false); + expect(closexBidder.hasBidderResponded).to.equal(false); }); + }); + + describe('when there are request timeouts', function () { + let auction; + let openxBidRequest; + let closexBidRequest; - it('should not be in the query string if browser does not support performance API', function() { - const originalPerf = window.top.performance; - window.top.performance = undefined; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); simulateAuction([ [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedCloseX], [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [BID_TIMEOUT, bidTimeoutCloseX], + [BID_TIMEOUT, bidTimeoutOpenX], + [AUCTION_END, auctionEnd] ]); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; - window.top.performance = originalPerf; + openxBidRequest = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + closexBidRequest = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.not.have.keys( - 'hb.dcl', - 'hb.dl', - 'hb.tta', - 'hb.ttr' - ); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the timeout', function () { + expect(openxBidRequest.timedOut).to.equal(true); + expect(closexBidRequest.timedOut).to.equal(true); }); }); - describe('ts, auid', function() { - it('OpenX is in auction and has a bid response', function() { + describe('when there are bid responses', function () { + let auction; + let openxBidResponse; + let closexBidResponse; + + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], [BID_REQUESTED, bidRequestedCloseX], + [BID_REQUESTED, bidRequestedOpenX], [BID_RESPONSE, bidResponseOpenX], - [BID_RESPONSE, bidResponseCloseX] + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd] ]); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - ts: bidResponseOpenX.ts, - auid: bidRequestedOpenX.bids[0].params.unit - }); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + openxBidResponse = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx').bidResponses[0]; + closexBidResponse = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex').bidResponses[0]; }); - it('OpenX is in auction but no bid response', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_REQUESTED, bidRequestedCloseX], - [BID_RESPONSE, bidResponseCloseX] - ]); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - auid: bidRequestedOpenX.bids[0].params.unit - }); - expect(queryData).to.not.have.key('ts'); + it('should track the cpm in microCPM', function () { + expect(openxBidResponse.microCpm).to.equal(bidResponseOpenX.cpm * 1000000); + expect(closexBidResponse.microCpm).to.equal(bidResponseCloseX.cpm * 1000000); }); - it('OpenX is not in auction', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedCloseX], - [BID_RESPONSE, bidResponseCloseX] - ]); + it('should track if the bid is in net revenue', function () { + expect(openxBidResponse.netRevenue).to.equal(bidResponseOpenX.netRevenue); + expect(closexBidResponse.netRevenue).to.equal(bidResponseCloseX.netRevenue); + }); + + it('should track the mediaType', function () { + expect(openxBidResponse.mediaType).to.equal(bidResponseOpenX.mediaType); + expect(closexBidResponse.mediaType).to.equal(bidResponseCloseX.mediaType); + }); + + it('should track the currency', function () { + expect(openxBidResponse.currency).to.equal(bidResponseOpenX.currency); + expect(closexBidResponse.currency).to.equal(bidResponseCloseX.currency); + }); + + it('should track the ad width and height', function () { + expect(openxBidResponse.width).to.equal(bidResponseOpenX.width); + expect(openxBidResponse.height).to.equal(bidResponseOpenX.height); + + expect(closexBidResponse.width).to.equal(bidResponseCloseX.width); + expect(closexBidResponse.height).to.equal(bidResponseCloseX.height); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.not.have.keys('auid', 'ts'); + it('should track the bid dealId', function () { + expect(openxBidResponse.dealId).to.equal(bidResponseOpenX.dealId); // no deal id defined + expect(closexBidResponse.dealId).to.equal(bidResponseCloseX.dealId); // deal id defined + }); + + it('should track the bid\'s latency', function () { + expect(openxBidResponse.latency).to.equal(bidResponseOpenX.timeToRespond); + expect(closexBidResponse.latency).to.equal(bidResponseCloseX.timeToRespond); + }); + + it('should not have any bid winners', function () { + expect(openxBidResponse.winner).to.equal(false); + expect(closexBidResponse.winner).to.equal(false); + }); + + it('should track the bid currency', function () { + expect(openxBidResponse.currency).to.equal(bidResponseOpenX.currency); + expect(closexBidResponse.currency).to.equal(bidResponseCloseX.currency); + }); + + it('should track the auction end time', function () { + expect(auction.endTime).to.equal(auctionEnd.auctionEnd); + }); + + it('should track that the auction ended', function () { + expect(auction.state).to.equal(AUCTION_STATES.ENDED); }); }); - describe('hb.exn, hb.sts, hb.ets, hb.bv, hb.crid, hb.to', function() { - it('2 bidders in auction', function() { + describe('when there are bidder wins', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], [BID_REQUESTED, bidRequestedCloseX], [BID_RESPONSE, bidResponseOpenX], - [BID_RESPONSE, bidResponseCloseX] + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX] ]); - const queryData = getQueryData(server.requests[0].url); - const auctionStart = bidRequestedOpenX.auctionStart; - expect(queryData).to.include({ - 'hb.exn': [ - bidRequestedOpenX.bids[0].bidder, - bidRequestedCloseX.bids[0].bidder - ].join(','), - 'hb.sts': [ - bidRequestedOpenX.start - auctionStart, - bidRequestedCloseX.start - auctionStart - ].join(','), - 'hb.ets': [ - bidResponseOpenX.responseTimestamp - auctionStart, - bidResponseCloseX.responseTimestamp - auctionStart - ].join(','), - 'hb.bv': [bidResponseOpenX.cpm, bidResponseCloseX.cpm].join(','), - 'hb.crid': [ - bidResponseOpenX.creativeId, - bidResponseCloseX.creativeId - ].join(','), - 'hb.to': [false, false].join(',') - }); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; }); - it('OpenX timed out', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_REQUESTED, bidRequestedCloseX], - [BID_RESPONSE, bidResponseCloseX], - [BID_TIMEOUT, bidTimeoutOpenX] - ]); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - const auctionStart = bidRequestedOpenX.auctionStart; - expect(queryData).to.include({ - 'hb.exn': [ - bidRequestedOpenX.bids[0].bidder, - bidRequestedCloseX.bids[0].bidder - ].join(','), - 'hb.sts': [ - bidRequestedOpenX.start - auctionStart, - bidRequestedCloseX.start - auctionStart - ].join(','), - 'hb.ets': [ - undefined, - bidResponseCloseX.responseTimestamp - auctionStart - ].join(','), - 'hb.bv': [0, bidResponseCloseX.cpm].join(','), - 'hb.crid': [undefined, bidResponseCloseX.creativeId].join(','), - 'hb.to': [true, false].join(',') - }); + it('should track that bidder as the winner', function () { + let openxBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({winner: true}); + }); + + it('should track that bidder as the losers', function () { + let closexBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'closex'); + expect(closexBidder.bidResponses[0]).to.contain({winner: false}); }); }); - describe('hb.we, hb.g1', function() { - it('OpenX won', function() { + describe('when a winning bid renders', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], + [BID_REQUESTED, bidRequestedCloseX], [BID_RESPONSE, bidResponseOpenX], - [BID_WON, bidWonOpenX] + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX], + [SLOT_LOADED] ]); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.we': '0', - 'hb.g1': 'false' - }); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; }); - it('DFP won', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] - ]); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.we': '-1', - 'hb.g1': 'true' - }); + it('should track that winning bid rendered', function () { + let openxBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({rendered: true}); + }); + + it('should track that winning bid render time', function () { + let openxBidder = auction.adUnits[0].bidRequests.find(bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({renderTime: CURRENT_TIME}); + }); + + it('should track that the auction completed', function () { + expect(auction.state).to.equal(AUCTION_STATES.COMPLETED); }); }); }); From 108b25799b3aaa2e7750eef1a9d42b09731b8114 Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Fri, 12 Jun 2020 16:40:09 -0700 Subject: [PATCH 2/7] OX UserId Module - first commit --- modules/openAudienceIdSystem.js | 122 ++++++++++++++++++ .../spec/modules/openAudienceIdSystem_spec.js | 92 +++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 modules/openAudienceIdSystem.js create mode 100644 test/spec/modules/openAudienceIdSystem_spec.js diff --git a/modules/openAudienceIdSystem.js b/modules/openAudienceIdSystem.js new file mode 100644 index 00000000000..c3f18d83463 --- /dev/null +++ b/modules/openAudienceIdSystem.js @@ -0,0 +1,122 @@ +/** + * This module adds OpenAudience to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/openAudienceSubmodule + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js'; + +/** @type {Submodule} */ +export const openAudienceSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'openAudience', + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{oaid:string}} + */ + decode(value) { + return { 'oaid': value } + }, + + /** + * ConfigObject for ID System + * TODO: remove this later. for internal referrence only + * @property {(string|undefined)} partner - partner url param value + * @property {(string|undefined)} url - webservice request url used to load Id data + * @property {(string|undefined)} pixelUrl - publisher pixel to extend/modify cookies + * @property {(boolean|undefined)} create - create id if missing. default is true. + * @property {(boolean|undefined)} extend - extend expiration time on each access. default is false. + * @property {(string|undefined)} pid - placement id url param value + * @property {(string|undefined)} publisherId - the unique identifier of the publisher in question + * @property {(string|undefined)} ajaxTimeout - the number of milliseconds a resolution request can take before automatically being terminated + * @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query + * @property {(string|undefined)} providedIdentifierName - defines the name of an identifier that can be found in local storage or in the cookie jar that can be sent along with the getId request. This parameter should be used whenever a customer is able to provide the most stable identifier possible + * @property {(LiveIntentCollectConfig|undefined)} liCollectConfig - the config for LiveIntent's collect requests + */ + + /** + * Initialization object for oa.js + * TODO: remove this later. for internal referrence only + * @property {string} oaID - () planned to be open audience resource id, or that id that will be public to users + * @property {string} age - () user age supplied by publishers + * @property {string} gender - () user gender supplied by publishers + * @property {string} ifa - () ifa -- ID for Ads + * @property {string} segments - () list of segments the user can be categorized into + * @property {string} tags - () a way of categorizing users by maps + * @property {string} tdidPartnerID - partner ID provided by the trade desk, if the publisher registered + * @property {string} mockTdidEndpoint - () if the pub provided their own tdid solution, otherwise + * @property {string} delayBeaconSendMs - (500) delay in ms before sending the beacon + * @property {string} __customBeaconUrl - (https://oajs.openx.net/beacon) beacon endpoint OAJS will try to contact + * @property {string} customPbjsInstance - (window.pbjs) variable/field where pbjs library is loaded. + */ + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {ConsentData} [consentData] + * @param {SubmoduleParams} [configParams] + * @returns {IdResponse|undefined} + */ + getId(configParams, consentData) { + if (!configParams || typeof configParams.publisherId !== 'string') { + utils.logError('openAudience submodule requires publisher id to be defined'); + return; + } + const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; + const gdprConsentString = hasGdpr ? consentData.consentString : ''; + // use protocol relative urls for http or https + const url = `https://openaudience.openx.org?publisher_id=${configParams.publisherId}${hasGdpr ? '&gdpr=' + hasGdpr : ''}${gdprConsentString ? '&gdpr_consent=' + gdprConsentString : ''}`; + let resp; + resp = function(callback) { + // Check ats during callback so it has a chance to initialise. + // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint + if (window.oajs) { + window.oajs.cmd.push(function () { + window.oajs.start({ + oaID: 'karma-karma-karma-karma-chameleon', + placementID: 16, + storageType: 'cookie', + email: 'chunkylover53@aol.com', // homer's e-mail... replace as you like + logging: 'debug', + __customBeaconUrl: 'https://devint-oajs.openx.net/beacon' + }); + }); + } else { + getOaid(url, callback); + } + }; + return {callback: resp}; + } +}; + +function getOaid(url, callback) { + const responseHanlders = { + success: response => { + let responseObj = {}; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + } + } + callback(responseObj.oaid ? responseObj.oaid : ''); + }, + error: errorResponse => { + utils.logError(`openAudience: ID fetch encountered an error`, errorResponse); + callback(); + } + }; + + ajax(url, responseHanlders, undefined, {method: 'GET', withCredentials: true}) +} + +submodule('userId', openAudienceSubmodule); diff --git a/test/spec/modules/openAudienceIdSystem_spec.js b/test/spec/modules/openAudienceIdSystem_spec.js new file mode 100644 index 00000000000..1d4d27c3622 --- /dev/null +++ b/test/spec/modules/openAudienceIdSystem_spec.js @@ -0,0 +1,92 @@ +import {openAudienceSubmodule} from 'modules/openAudienceIdSystem.js'; +import * as utils from 'src/utils.js'; +import {server} from 'test/mocks/xhr.js'; + +const PUBLISHER_ID = 'test-pub-id'; +const defaultConfigParams = {publisherId: PUBLISHER_ID}; +const responseHeader = {'Content-Type': 'application/json'}; + +describe('OpenAudienceId tests', function () { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + logErrorStub.restore(); + }); + + it('should log an error if no configParams were passed when getId', function () { + openAudienceSubmodule.getId(); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should log an error if pid configParam was not passed when getId', function () { + openAudienceSubmodule.getId({}); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should call the OpenAudience endpoint', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}`); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call the OpenAudience endpoint with consent string', function () { + let callBackSpy = sinon.spy(); + let consentData = { + gdprApplies: true, + consentString: 'BOkIpDSOkIpDSADABAENCc-AAAApOAFAAMAAsAMIAcAA_g' + }; + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams, consentData).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}&gdpr=1&gdpr_consent=${consentData.consentString}`); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should not throw Uncaught TypeError when endpoint returns empty response', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}`); + request.respond( + 204, + responseHeader, + '' + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(request.response).to.equal(''); + expect(logErrorStub.calledOnce).to.not.be.true; + }); + + it('should log an error and continue to callback if ajax request errors', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}`); + request.respond( + 503, + responseHeader, + 'Unavailable' + ); + expect(logErrorStub.calledOnce).to.be.true; + expect(callBackSpy.calledOnce).to.be.true; + }); +}); From 579be7fc3cb73fa864ac0469603c6b7c6e104171 Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Tue, 23 Jun 2020 22:49:57 -0700 Subject: [PATCH 3/7] Updated API calls per https://openxtechinc.atlassian.net/browse/OAPLT-1470 Added uspapi support Updated bidder adapter to support userId module --- modules/openAudienceIdSystem.js | 122 +++++------ modules/openxBidAdapter.js | 4 + .../spec/modules/openAudienceIdSystem_spec.js | 194 +++++++++++------- test/spec/modules/openxBidAdapter_spec.js | 4 + 4 files changed, 195 insertions(+), 129 deletions(-) diff --git a/modules/openAudienceIdSystem.js b/modules/openAudienceIdSystem.js index c3f18d83463..ef88b712cb5 100644 --- a/modules/openAudienceIdSystem.js +++ b/modules/openAudienceIdSystem.js @@ -1,13 +1,28 @@ /** * This module adds OpenAudience to the User ID module * The {@link module:modules/userId} module is required - * @module modules/openAudienceSubmodule + * @module modules/openAudienceIdSystem * @requires module:modules/userId */ import * as utils from '../src/utils.js' import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; +import {uspDataHandler} from '../src/adapterManager.js'; + +export const OA_URL = 'https://oajs.openx.net/beacon'; + +// Module types +/** + * @typedef {Object} OpenAudienceConfig + * @property {string} resourceId The OpenAudience resource id + */ + +/** + * @typedef {Object} OpenAudienceIdObject + * @property {Array} oa_ids A list of OpenAudience Ids + * @property {string|undefined} oauid OpenAudience Universal Id + */ /** @type {Submodule} */ export const openAudienceSubmodule = { @@ -22,83 +37,70 @@ export const openAudienceSubmodule = { * @param {string} value * @returns {{oaid:string}} */ - decode(value) { - return { 'oaid': value } - }, - - /** - * ConfigObject for ID System - * TODO: remove this later. for internal referrence only - * @property {(string|undefined)} partner - partner url param value - * @property {(string|undefined)} url - webservice request url used to load Id data - * @property {(string|undefined)} pixelUrl - publisher pixel to extend/modify cookies - * @property {(boolean|undefined)} create - create id if missing. default is true. - * @property {(boolean|undefined)} extend - extend expiration time on each access. default is false. - * @property {(string|undefined)} pid - placement id url param value - * @property {(string|undefined)} publisherId - the unique identifier of the publisher in question - * @property {(string|undefined)} ajaxTimeout - the number of milliseconds a resolution request can take before automatically being terminated - * @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query - * @property {(string|undefined)} providedIdentifierName - defines the name of an identifier that can be found in local storage or in the cookie jar that can be sent along with the getId request. This parameter should be used whenever a customer is able to provide the most stable identifier possible - * @property {(LiveIntentCollectConfig|undefined)} liCollectConfig - the config for LiveIntent's collect requests - */ /** - * Initialization object for oa.js - * TODO: remove this later. for internal referrence only - * @property {string} oaID - () planned to be open audience resource id, or that id that will be public to users - * @property {string} age - () user age supplied by publishers - * @property {string} gender - () user gender supplied by publishers - * @property {string} ifa - () ifa -- ID for Ads - * @property {string} segments - () list of segments the user can be categorized into - * @property {string} tags - () a way of categorizing users by maps - * @property {string} tdidPartnerID - partner ID provided by the trade desk, if the publisher registered - * @property {string} mockTdidEndpoint - () if the pub provided their own tdid solution, otherwise - * @property {string} delayBeaconSendMs - (500) delay in ms before sending the beacon - * @property {string} __customBeaconUrl - (https://oajs.openx.net/beacon) beacon endpoint OAJS will try to contact - * @property {string} customPbjsInstance - (window.pbjs) variable/field where pbjs library is loaded. + * + * @param {OpenAudienceIdObject|undefined} storedIdObj The current cached object + * @param {OpenAudienceConfig|undefined} configParams + * @returns {{oa: OpenAudienceIdObject}} */ + decode(storedIdObj, configParams) { + return { 'oa': storedIdObj }; + }, /** * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {ConsentData} [consentData] - * @param {SubmoduleParams} [configParams] - * @returns {IdResponse|undefined} + * @param {OpenAudienceConfig} configParams + * @param {ConsentData|undefined} consentData + * @param {(OpenAudienceIdObject|undefined)} cacheIdObj + * @return {(IdResponse|undefined)} A response object that contains id and/or callback. */ - getId(configParams, consentData) { - if (!configParams || typeof configParams.publisherId !== 'string') { - utils.logError('openAudience submodule requires publisher id to be defined'); + getId(configParams, consentData, cacheIdObj) { + if (!configParams || typeof configParams.resourceId !== 'string') { + utils.logError('openAudience submodule requires resource id to be defined'); return; } + const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const gdprConsentString = hasGdpr ? consentData.consentString : ''; - // use protocol relative urls for http or https - const url = `https://openaudience.openx.org?publisher_id=${configParams.publisherId}${hasGdpr ? '&gdpr=' + hasGdpr : ''}${gdprConsentString ? '&gdpr_consent=' + gdprConsentString : ''}`; - let resp; - resp = function(callback) { + let gdprConsent = consentData && consentData.consentString; + let usPrivacy = uspDataHandler.getConsentData(); + + let params = { + rid: configParams.resourceId, + gdpr: hasGdpr, + }; + + if (hasGdpr) { + params.gdpr_consent = gdprConsent + } + + if (usPrivacy) { + params.us_privacy = usPrivacy; + } + + return {callback: getOaIds}; + + /** + * Requests for OpenAudienceIDs through oajs and fallback directly to API. + * @param callback + */ + function getOaIds(callback) { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint if (window.oajs) { window.oajs.cmd.push(function () { - window.oajs.start({ - oaID: 'karma-karma-karma-karma-chameleon', - placementID: 16, - storageType: 'cookie', - email: 'chunkylover53@aol.com', // homer's e-mail... replace as you like - logging: 'debug', - __customBeaconUrl: 'https://devint-oajs.openx.net/beacon' - }); + window.oajs.getOaIds(params, callback); }); } else { - getOaid(url, callback); + let url = `${OA_URL}?${utils.formatQS(params)}`; + getOaData(url, callback); } - }; - return {callback: resp}; + } } }; -function getOaid(url, callback) { - const responseHanlders = { +function getOaData(url, callback) { + const responseHandlers = { success: response => { let responseObj = {}; if (response) { @@ -108,7 +110,7 @@ function getOaid(url, callback) { utils.logError(error); } } - callback(responseObj.oaid ? responseObj.oaid : ''); + callback(responseObj.oa_ids && responseObj.oa_ids.length !== undefined ? responseObj : undefined); }, error: errorResponse => { utils.logError(`openAudience: ID fetch encountered an error`, errorResponse); @@ -116,7 +118,7 @@ function getOaid(url, callback) { } }; - ajax(url, responseHanlders, undefined, {method: 'GET', withCredentials: true}) + ajax(url, responseHandlers, undefined, {method: 'GET', withCredentials: true}); } submodule('userId', openAudienceSubmodule); diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index d5630c2fad4..144a5b668fc 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -16,6 +16,7 @@ export const USER_ID_CODE_TO_QUERY_ARG = { idl_env: 'lre', // LiveRamp IdentityLink lipb: 'lipbid', // LiveIntent ID netId: 'netid', // netID + oa: 'oa_ids', // OpenAudience Ids parrableid: 'parrableid', // Parrable ID pubcid: 'pubcid', // PubCommon ID tdid: 'ttduuid', // The Trade Desk Unified ID @@ -276,6 +277,9 @@ function appendUserIdsToQueryParams(queryParams, userIds) { case 'lipb': queryParams[key] = userIdObjectOrValue.lipbid; break; + case 'oa': + queryParams[key] = encodeURIComponent(userIdObjectOrValue.oa_ids.join(',')); + break; default: queryParams[key] = userIdObjectOrValue; } diff --git a/test/spec/modules/openAudienceIdSystem_spec.js b/test/spec/modules/openAudienceIdSystem_spec.js index 1d4d27c3622..4b42e58af2d 100644 --- a/test/spec/modules/openAudienceIdSystem_spec.js +++ b/test/spec/modules/openAudienceIdSystem_spec.js @@ -1,9 +1,10 @@ -import {openAudienceSubmodule} from 'modules/openAudienceIdSystem.js'; +import {openAudienceSubmodule, OA_URL} from 'modules/openAudienceIdSystem.js'; import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; +import {uspDataHandler} from 'src/adapterManager'; -const PUBLISHER_ID = 'test-pub-id'; -const defaultConfigParams = {publisherId: PUBLISHER_ID}; +const RESOURCE_ID = 'test-resource-id'; +const defaultConfigParams = {resourceId: RESOURCE_ID}; const responseHeader = {'Content-Type': 'application/json'}; describe('OpenAudienceId tests', function () { @@ -17,76 +18,131 @@ describe('OpenAudienceId tests', function () { logErrorStub.restore(); }); - it('should log an error if no configParams were passed when getId', function () { - openAudienceSubmodule.getId(); - expect(logErrorStub.calledOnce).to.be.true; - }); + describe('getId()', function () { + let callbackSpy; + let oajsSpy; + let mockedGetConsentData; - it('should log an error if pid configParam was not passed when getId', function () { - openAudienceSubmodule.getId({}); - expect(logErrorStub.calledOnce).to.be.true; - }); + beforeEach(function () { + mockedGetConsentData = sinon.stub(uspDataHandler, 'getConsentData'); + }); - it('should call the OpenAudience endpoint', function () { - let callBackSpy = sinon.spy(); - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}`); - request.respond( - 200, - responseHeader, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; - }); + afterEach(function () { + mockedGetConsentData.restore(); + }); - it('should call the OpenAudience endpoint with consent string', function () { - let callBackSpy = sinon.spy(); - let consentData = { - gdprApplies: true, - consentString: 'BOkIpDSOkIpDSADABAENCc-AAAApOAFAAMAAsAMIAcAA_g' - }; - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams, consentData).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}&gdpr=1&gdpr_consent=${consentData.consentString}`); - request.respond( - 200, - responseHeader, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; - }); + it('should log an error if no configParams were passed when getId', function () { + openAudienceSubmodule.getId(); + expect(logErrorStub.calledOnce).to.be.true; + }); - it('should not throw Uncaught TypeError when endpoint returns empty response', function () { - let callBackSpy = sinon.spy(); - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}`); - request.respond( - 204, - responseHeader, - '' - ); - expect(callBackSpy.calledOnce).to.be.true; - expect(request.response).to.equal(''); - expect(logErrorStub.calledOnce).to.not.be.true; - }); + context('when oa.js is available', function () { + beforeEach(function () { + callbackSpy = sinon.spy(); + oajsSpy = sinon.spy(); + + window.oajs = { + cmd: [], + getOaIds: oajsSpy + }; + }); + + afterEach(function () { + delete window.oajs; + }); + + it('should call the OpenAudience endpoint', function () { + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callbackSpy); + window.oajs.cmd[0](); + + expect(oajsSpy.calledOnce).to.be.true; + expect(oajsSpy.getCall(0).args[0].gdpr_consent).to.equal(undefined); + }); + + it('should include GDRP parameters, if exists', function () { + let gdprObject = { + gdprApplies: true, + consentString: 'test-consent-string' + }; + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams, gdprObject).callback; + submoduleCallback(callbackSpy); + window.oajs.cmd[0](); + + expect(oajsSpy.calledOnce).to.be.true; + expect(oajsSpy.getCall(0).args[0].gdpr).to.equal(1); + expect(oajsSpy.getCall(0).args[0].gdpr_consent).to.equal(gdprObject.consentString); + }); + + it('should include US privacy parameter, if exists', function () { + mockedGetConsentData.returns('1YNY'); + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callbackSpy); + window.oajs.cmd[0](); + + expect(oajsSpy.getCall(0).args[0].us_privacy).to.equal('1YNY'); + }); + }); + + context('when oa.js is not available', function () { + beforeEach(function () { + callbackSpy = sinon.spy(); + }); + + it('should call the OpenAudience endpoint', function () { + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.have.string(OA_URL); + }); + + it('should include GDRP parameters, if exists', function () { + let gdprObject = { + gdprApplies: true, + consentString: 'test-consent-string' + } + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams, gdprObject).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.have.string('gdpr=1'); + expect(request.url).to.have.string(`gdpr_consent=${gdprObject.consentString}`); + }); + + it('should include US privacy parameter, if exists', function () { + mockedGetConsentData.returns('1YNY'); + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.have.string('us_privacy=1YNY'); + }); + + it('should not throw Uncaught TypeError when endpoint returns empty response', function () { + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callbackSpy); + let request = server.requests[0]; + request.respond( + 204, + responseHeader, + '' + ); + expect(callbackSpy.getCall(0).args[0]).to.be.undefined; + }); - it('should log an error and continue to callback if ajax request errors', function () { - let callBackSpy = sinon.spy(); - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq(`https://openaudience.openx.org?publisher_id=${PUBLISHER_ID}`); - request.respond( - 503, - responseHeader, - 'Unavailable' - ); - expect(logErrorStub.calledOnce).to.be.true; - expect(callBackSpy.calledOnce).to.be.true; + it('should log an error and continue to callback if ajax request errors', function () { + let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callbackSpy); + let request = server.requests[0]; + request.respond( + 503, + responseHeader, + 'Unavailable' + ); + expect(logErrorStub.calledOnce).to.be.true; + expect(callbackSpy.getCall(0).args[0]).to.be.undefined; + }); + }); }); }); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index a6fbc9666b9..f18bee6a0fc 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1034,6 +1034,7 @@ describe('OpenxAdapter', function () { idl_env: '1111-idl_env', lipb: {lipbid: '1111-lipb'}, netId: 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + oa: {oa_ids: ['segment-456', 'segment-789']}, parrableid: 'eidVersion.encryptionKeyReference.encryptedValue', pubcid: '1111-pubcid', tdid: '1111-tdid', @@ -1080,6 +1081,9 @@ describe('OpenxAdapter', function () { case 'lipb': userIdValue = EXAMPLE_DATA_BY_ATTR.lipb.lipbid; break; + case 'oa': + userIdValue = encodeURIComponent(EXAMPLE_DATA_BY_ATTR.oa.oa_ids.join(',')); + break; default: userIdValue = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; } From 6382886c20b5da5f2ab6e49736ade351d48935b9 Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Thu, 25 Jun 2020 21:33:54 -0700 Subject: [PATCH 4/7] updated bid adapter query param per https://openxtechinc.atlassian.net/browse/OMKT-1188 changed oajs method call from `getOaIds` to `getIds` removed oajs queue --- modules/openAudienceIdSystem.js | 8 +- modules/openxBidAdapter.js | 75 ++++++------ .../spec/modules/openAudienceIdSystem_spec.js | 3 - test/spec/modules/openxBidAdapter_spec.js | 107 +++++++++++------- 4 files changed, 105 insertions(+), 88 deletions(-) diff --git a/modules/openAudienceIdSystem.js b/modules/openAudienceIdSystem.js index ef88b712cb5..49ab00ae6a8 100644 --- a/modules/openAudienceIdSystem.js +++ b/modules/openAudienceIdSystem.js @@ -21,7 +21,6 @@ export const OA_URL = 'https://oajs.openx.net/beacon'; /** * @typedef {Object} OpenAudienceIdObject * @property {Array} oa_ids A list of OpenAudience Ids - * @property {string|undefined} oauid OpenAudience Universal Id */ /** @type {Submodule} */ @@ -85,12 +84,9 @@ export const openAudienceSubmodule = { * @param callback */ function getOaIds(callback) { - // Check ats during callback so it has a chance to initialise. - // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint + // If oajs is available, use it to retrieve id object. If not, fall back to API. if (window.oajs) { - window.oajs.cmd.push(function () { - window.oajs.getOaIds(params, callback); - }); + window.oajs.getIds(params, callback); } else { let url = `${OA_URL}?${utils.formatQS(params)}`; getOaData(url, callback); diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 144a5b668fc..866329cf465 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -8,20 +8,6 @@ const BIDDER_CODE = 'openx'; const BIDDER_CONFIG = 'hb_pb'; const BIDDER_VERSION = '3.0.2'; -export const USER_ID_CODE_TO_QUERY_ARG = { - britepoolid: 'britepoolid', // BritePool ID - criteoId: 'criteoid', // CriteoID - digitrustid: 'digitrustid', // DigiTrust - id5id: 'id5id', // ID5 ID - idl_env: 'lre', // LiveRamp IdentityLink - lipb: 'lipbid', // LiveIntent ID - netId: 'netid', // netID - oa: 'oa_ids', // OpenAudience Ids - parrableid: 'parrableid', // Parrable ID - pubcid: 'pubcid', // PubCommon ID - tdid: 'ttduuid', // The Trade Desk Unified ID -}; - export const spec = { code: BIDDER_CODE, gvlid: 69, @@ -212,9 +198,9 @@ function getMediaTypeFromRequest(serverRequest) { function buildCommonQueryParamsFromBids(bids, bidderRequest) { const isInIframe = utils.inIframe(); - let defaultParams; + let commonParams; - defaultParams = { + commonParams = { ju: config.getConfig('pageUrl') || bidderRequest.refererInfo.referer, ch: document.charSet || document.characterSet, res: `${screen.width}x${screen.height}x${screen.colorDepth}`, @@ -228,64 +214,73 @@ function buildCommonQueryParamsFromBids(bids, bidderRequest) { }; if (bids[0].params.platform) { - defaultParams.ph = bids[0].params.platform; + commonParams.ph = bids[0].params.platform; } if (bidderRequest.gdprConsent) { let gdprConsentConfig = bidderRequest.gdprConsent; if (gdprConsentConfig.consentString !== undefined) { - defaultParams.gdpr_consent = gdprConsentConfig.consentString; + commonParams.gdpr_consent = gdprConsentConfig.consentString; } if (gdprConsentConfig.gdprApplies !== undefined) { - defaultParams.gdpr = gdprConsentConfig.gdprApplies ? 1 : 0; + commonParams.gdpr = gdprConsentConfig.gdprApplies ? 1 : 0; } if (config.getConfig('consentManagement.cmpApi') === 'iab') { - defaultParams.x_gdpr_f = 1; + commonParams.x_gdpr_f = 1; } } if (bidderRequest && bidderRequest.uspConsent) { - defaultParams.us_privacy = bidderRequest.uspConsent; + commonParams.us_privacy = bidderRequest.uspConsent; } // normalize publisher common id if (utils.deepAccess(bids[0], 'crumbs.pubcid')) { utils.deepSetValue(bids[0], 'userId.pubcid', utils.deepAccess(bids[0], 'crumbs.pubcid')); } - defaultParams = appendUserIdsToQueryParams(defaultParams, bids[0].userId); + commonParams = appendUserIdsToQueryParams(commonParams, bids[0].userId); // supply chain support if (bids[0].schain) { - defaultParams.schain = serializeSupplyChain(bids[0].schain); + commonParams.schain = serializeSupplyChain(bids[0].schain); } - return defaultParams; + return commonParams; } function appendUserIdsToQueryParams(queryParams, userIds) { + let userIdQueryString; + let userIdMap = {}; utils._each(userIds, (userIdObjectOrValue, userIdProviderKey) => { - const key = USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey]; - - if (USER_ID_CODE_TO_QUERY_ARG.hasOwnProperty(userIdProviderKey)) { - switch (userIdProviderKey) { - case 'digitrustid': - queryParams[key] = utils.deepAccess(userIdObjectOrValue, 'data.id'); - break; - case 'lipb': - queryParams[key] = userIdObjectOrValue.lipbid; - break; - case 'oa': - queryParams[key] = encodeURIComponent(userIdObjectOrValue.oa_ids.join(',')); - break; - default: - queryParams[key] = userIdObjectOrValue; - } + switch (userIdProviderKey) { + case 'digitrustid': + userIdMap[userIdProviderKey] = utils.deepAccess(userIdObjectOrValue, 'data.id'); + break; + case 'lipb': + userIdMap[userIdProviderKey] = userIdObjectOrValue.lipbid; + break; + case 'oa': + userIdMap[userIdProviderKey] = userIdObjectOrValue.oa_ids.join('|'); + break; + default: + userIdMap[userIdProviderKey] = userIdObjectOrValue; } }); + userIdQueryString = utils._map(userIdMap, function (userIdString, userIdKey) { + return userIdString ? `${userIdKey}:${userIdString}` : ''; + }) + .filter(userIdProviderInfo => userIdProviderInfo) // filter out empty values + .join(','); + + if (userIdQueryString) { + console.log(userIdQueryString); + queryParams.sm = encodeURIComponent(userIdQueryString); + } + return queryParams; } diff --git a/test/spec/modules/openAudienceIdSystem_spec.js b/test/spec/modules/openAudienceIdSystem_spec.js index 4b42e58af2d..53f3f03c7d1 100644 --- a/test/spec/modules/openAudienceIdSystem_spec.js +++ b/test/spec/modules/openAudienceIdSystem_spec.js @@ -54,7 +54,6 @@ describe('OpenAudienceId tests', function () { it('should call the OpenAudience endpoint', function () { let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callbackSpy); - window.oajs.cmd[0](); expect(oajsSpy.calledOnce).to.be.true; expect(oajsSpy.getCall(0).args[0].gdpr_consent).to.equal(undefined); @@ -67,7 +66,6 @@ describe('OpenAudienceId tests', function () { }; let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams, gdprObject).callback; submoduleCallback(callbackSpy); - window.oajs.cmd[0](); expect(oajsSpy.calledOnce).to.be.true; expect(oajsSpy.getCall(0).args[0].gdpr).to.equal(1); @@ -78,7 +76,6 @@ describe('OpenAudienceId tests', function () { mockedGetConsentData.returns('1YNY'); let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callbackSpy); - window.oajs.cmd[0](); expect(oajsSpy.getCall(0).args[0].us_privacy).to.equal('1YNY'); }); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index f18bee6a0fc..a8dffd62496 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1040,58 +1040,87 @@ describe('OpenxAdapter', function () { tdid: '1111-tdid', }; + function getUserId(userIdKey){ + let userIdValue; + // handle cases where userId key refers to an object + switch (userIdKey) { + case 'digitrustid': + userIdValue = EXAMPLE_DATA_BY_ATTR.digitrustid.data.id; + break; + case 'lipb': + userIdValue = EXAMPLE_DATA_BY_ATTR.lipb.lipbid; + break; + case 'oa': + userIdValue = EXAMPLE_DATA_BY_ATTR.oa.oa_ids.join('|'); + break; + default: + userIdValue = EXAMPLE_DATA_BY_ATTR[userIdKey]; + } + return userIdValue; + } + + let bidRequestsWithUserId; + beforeEach(function () { + bidRequestsWithUserId = [{ + bidder: 'openx', + params: { + unit: '11', + delDomain: 'test-del-domain' + }, + userId: {}, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1' + }]; + }); + // generates the same set of tests for each id provider - utils._each(USER_ID_CODE_TO_QUERY_ARG, (userIdQueryArg, userIdProviderKey) => { + utils._each(EXAMPLE_DATA_BY_ATTR, (userIdObjOrValue, userIdProviderKey) => { describe(`with userId attribute: ${userIdProviderKey}`, function () { - it(`should not send a ${userIdQueryArg} query param when there is no userId.${userIdProviderKey} defined in the bid requests`, function () { + it(`should not send a cm query param when there is no userId.${userIdProviderKey} defined in the bid requests`, function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys(userIdQueryArg); + expect(request[0].data.cm).to.be.undefined; }); - it(`should send a ${userIdQueryArg} query param when userId.${userIdProviderKey} is defined in the bid requests`, function () { - const bidRequestsWithUserId = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - userId: { - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }]; + it(`should send a cm query param when userId.${userIdProviderKey} is defined in the bid requests`, function () { + // enrich bid request with userId key/value bidRequestsWithUserId[0].userId[userIdProviderKey] = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); + const userIdValue = getUserId(userIdProviderKey); - let userIdValue; - // handle cases where userId key refers to an object - switch (userIdProviderKey) { - case 'digitrustid': - userIdValue = EXAMPLE_DATA_BY_ATTR.digitrustid.data.id; - break; - case 'lipb': - userIdValue = EXAMPLE_DATA_BY_ATTR.lipb.lipbid; - break; - case 'oa': - userIdValue = encodeURIComponent(EXAMPLE_DATA_BY_ATTR.oa.oa_ids.join(',')); - break; - default: - userIdValue = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; - } - - expect(request[0].data[USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey]]).to.equal(userIdValue); + expect(request[0].data.sm).to.have.string(encodeURIComponent(`${userIdProviderKey}:${userIdValue}`)); }); }); }); + + it('when there are multiple ids, they should be separated by a commas', function () { + let expectedStrings = []; + let userIdKeys = [ + 'oa', + 'idl_env', + 'digitrustid' + ]; + + userIdKeys.forEach(userIdKey => { + // enrich bid request with userId key/value + bidRequestsWithUserId[0].userId[userIdKey] = EXAMPLE_DATA_BY_ATTR[userIdKey]; + + // build expected query data + const userIdValue = getUserId(userIdKey); + expectedStrings.push(`${userIdKey}:${userIdValue}`); + }); + const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); + + expect(request[0].data.sm).to.have.string(encodeURIComponent(expectedStrings.join(','))); + }) }); }); From eab0eca475444d9888b12dbd933d79e680593492 Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Tue, 30 Jun 2020 21:33:14 -0700 Subject: [PATCH 5/7] Updated openRtbBidAdapter per changes from https://github.com/openx/prebid-adapter/commit/87a3cd13f232e9d254f8921898236d686ffc6453 Changed oajs call to only pass in a callback --- modules/openAudienceIdSystem.js | 42 ++++++++------- modules/openxBidAdapter.js | 1 - .../spec/modules/openAudienceIdSystem_spec.js | 54 +++++++------------ 3 files changed, 44 insertions(+), 53 deletions(-) diff --git a/modules/openAudienceIdSystem.js b/modules/openAudienceIdSystem.js index 49ab00ae6a8..821300c7352 100644 --- a/modules/openAudienceIdSystem.js +++ b/modules/openAudienceIdSystem.js @@ -60,22 +60,7 @@ export const openAudienceSubmodule = { return; } - const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - let gdprConsent = consentData && consentData.consentString; - let usPrivacy = uspDataHandler.getConsentData(); - let params = { - rid: configParams.resourceId, - gdpr: hasGdpr, - }; - - if (hasGdpr) { - params.gdpr_consent = gdprConsent - } - - if (usPrivacy) { - params.us_privacy = usPrivacy; - } return {callback: getOaIds}; @@ -86,10 +71,31 @@ export const openAudienceSubmodule = { function getOaIds(callback) { // If oajs is available, use it to retrieve id object. If not, fall back to API. if (window.oajs) { - window.oajs.getIds(params, callback); + window.oajs.getIds(callback); } else { - let url = `${OA_URL}?${utils.formatQS(params)}`; - getOaData(url, callback); + let params = { + rid: configParams.resourceId, + ...getConsentQueryParams(consentData, uspDataHandler.getConsentData()) + }; + getOaData(`${OA_URL}?${utils.formatQS(params)}`, callback); + } + + function getConsentQueryParams(gdprConsent, usPrivacy) { + const hasGdpr = (gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) ? 1 : 0; + + let consentParams = { + gdpr: hasGdpr + }; + + if (hasGdpr) { + consentParams.gdpr_consent = gdprConsent && gdprConsent.consentString + } + + if (usPrivacy) { + consentParams.us_privacy = usPrivacy; + } + + return consentParams; } } } diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 866329cf465..249916c2b69 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -277,7 +277,6 @@ function appendUserIdsToQueryParams(queryParams, userIds) { .join(','); if (userIdQueryString) { - console.log(userIdQueryString); queryParams.sm = encodeURIComponent(userIdQueryString); } diff --git a/test/spec/modules/openAudienceIdSystem_spec.js b/test/spec/modules/openAudienceIdSystem_spec.js index 53f3f03c7d1..494c9af6710 100644 --- a/test/spec/modules/openAudienceIdSystem_spec.js +++ b/test/spec/modules/openAudienceIdSystem_spec.js @@ -4,7 +4,7 @@ import {server} from 'test/mocks/xhr.js'; import {uspDataHandler} from 'src/adapterManager'; const RESOURCE_ID = 'test-resource-id'; -const defaultConfigParams = {resourceId: RESOURCE_ID}; +const DEFAULT_CONFIG = {resourceId: RESOURCE_ID}; const responseHeader = {'Content-Type': 'application/json'}; describe('OpenAudienceId tests', function () { @@ -20,7 +20,7 @@ describe('OpenAudienceId tests', function () { describe('getId()', function () { let callbackSpy; - let oajsSpy; + let getIdsSpy; let mockedGetConsentData; beforeEach(function () { @@ -39,11 +39,10 @@ describe('OpenAudienceId tests', function () { context('when oa.js is available', function () { beforeEach(function () { callbackSpy = sinon.spy(); - oajsSpy = sinon.spy(); + getIdsSpy = sinon.spy(); window.oajs = { - cmd: [], - getOaIds: oajsSpy + getIds: getIdsSpy }; }); @@ -52,32 +51,11 @@ describe('OpenAudienceId tests', function () { }); it('should call the OpenAudience endpoint', function () { - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = openAudienceSubmodule.getId(DEFAULT_CONFIG).callback; submoduleCallback(callbackSpy); - expect(oajsSpy.calledOnce).to.be.true; - expect(oajsSpy.getCall(0).args[0].gdpr_consent).to.equal(undefined); - }); - - it('should include GDRP parameters, if exists', function () { - let gdprObject = { - gdprApplies: true, - consentString: 'test-consent-string' - }; - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams, gdprObject).callback; - submoduleCallback(callbackSpy); - - expect(oajsSpy.calledOnce).to.be.true; - expect(oajsSpy.getCall(0).args[0].gdpr).to.equal(1); - expect(oajsSpy.getCall(0).args[0].gdpr_consent).to.equal(gdprObject.consentString); - }); - - it('should include US privacy parameter, if exists', function () { - mockedGetConsentData.returns('1YNY'); - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callbackSpy); - - expect(oajsSpy.getCall(0).args[0].us_privacy).to.equal('1YNY'); + expect(getIdsSpy.calledOnce).to.be.true; + expect(getIdsSpy.getCall(0).args[0]).to.be.an.instanceof(Function); }); }); @@ -87,19 +65,27 @@ describe('OpenAudienceId tests', function () { }); it('should call the OpenAudience endpoint', function () { - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = openAudienceSubmodule.getId(DEFAULT_CONFIG).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; expect(request.url).to.have.string(OA_URL); }); + it('should send the resource id', function () { + let submoduleCallback = openAudienceSubmodule.getId(DEFAULT_CONFIG).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.have.string(`rid=${RESOURCE_ID}`); + }); + it('should include GDRP parameters, if exists', function () { let gdprObject = { gdprApplies: true, consentString: 'test-consent-string' } - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams, gdprObject).callback; + let submoduleCallback = openAudienceSubmodule.getId(DEFAULT_CONFIG, gdprObject).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; @@ -109,7 +95,7 @@ describe('OpenAudienceId tests', function () { it('should include US privacy parameter, if exists', function () { mockedGetConsentData.returns('1YNY'); - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = openAudienceSubmodule.getId(DEFAULT_CONFIG).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; @@ -117,7 +103,7 @@ describe('OpenAudienceId tests', function () { }); it('should not throw Uncaught TypeError when endpoint returns empty response', function () { - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = openAudienceSubmodule.getId(DEFAULT_CONFIG).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; request.respond( @@ -129,7 +115,7 @@ describe('OpenAudienceId tests', function () { }); it('should log an error and continue to callback if ajax request errors', function () { - let submoduleCallback = openAudienceSubmodule.getId(defaultConfigParams).callback; + let submoduleCallback = openAudienceSubmodule.getId(DEFAULT_CONFIG).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; request.respond( From 823868e23b25d4749b416c125cfcc49741fb6297 Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Wed, 1 Jul 2020 08:56:30 -0700 Subject: [PATCH 6/7] bump adapter version --- modules/openxBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 249916c2b69..89b9a0335a0 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -6,7 +6,7 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'openx'; const BIDDER_CONFIG = 'hb_pb'; -const BIDDER_VERSION = '3.0.2'; +const BIDDER_VERSION = '3.1.0'; export const spec = { code: BIDDER_CODE, From 3963299bfc514f34a9e0a34e2c637fd4bf86e542 Mon Sep 17 00:00:00 2001 From: Jimmy Tu Date: Tue, 7 Jul 2020 15:42:37 -0700 Subject: [PATCH 7/7] added config for eids --- modules/userId/eids.js | 11 +++++++++++ test/spec/modules/eids_spec.js | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 842737183a8..a086020d4bd 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -81,6 +81,17 @@ const USER_IDS_CONFIG = { atype: 1 }, + // OpenX OpenAudience + 'oa': { + source: 'openx.com', + atype: 1, + getValue: function (data) { + return data && data.oa_ids && data.oa_ids.length !== undefined + ? encodeURIComponent(data.oa_ids.join(',')) + : undefined + } + }, + // NetId 'netId': { source: 'netid.de', diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 160277204df..23f04c4725e 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -135,6 +135,20 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('OpenAudience', function() { + const userId = { + oa: { + oa_ids: ['oaid-1', 'oaid-2', 'oaid-3'] + } + }; + const newEids = createEidsArray(userId); + + expect(newEids[0]).to.deep.equal({ + source: 'openx.com', + uids: [{id: encodeURIComponent(userId.oa.oa_ids.join(',')), atype: 1}] + }); + }); + it('NetId', function() { const userId = { netId: 'some-random-id-value'