diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index 99ce89ee4d1..a56fd2b741b 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -1,20 +1,40 @@ -import {ajax} from '../src/ajax.js'; +import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import { EVENTS } from '../src/constants.js'; import adapterManager from '../src/adapterManager.js'; -import {deepClone, generateUUID, logError, logInfo, logWarn, getParameterByName} from '../src/utils.js'; +import { deepClone, generateUUID, logError, logInfo, logWarn, getParameterByName } from '../src/utils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest */ /** * @typedef {object} Message Payload message sent to the Greenbids API */ +/** + * @typedef AuctionEndArgs + * @type {object} + * @property {string} auctionId + * @property {number} timestamp - Auction start epoch + * @property {number} auctionEnd - Auction end epoch + * @property {Bid[]} bidsReceived + * @property {BidRequest[]} noBids + */ + +/** + * @typedef GreenbidsCachedOption + * @type {object} + * @property {any[]} timeoutBids + * @property {?string} greenbidsId + * @property {?string} billingId + * @property {boolean} isSampled + */ + const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '2.3.2'; +export const ANALYTICS_VERSION = '2.3.3'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; @@ -33,7 +53,7 @@ export const BIDDER_STATUS = { const analyticsOptions = {}; -export const isSampled = function(greenbidsId, samplingRate, exploratorySamplingSplit) { +export const isSampled = function (greenbidsId, samplingRate, exploratorySamplingSplit) { const isSamplingForced = getParameterByName('greenbids_force_sampling'); if (isSamplingForced) { logInfo('Greenbids Analytics: sampling flag detected, forcing analytics'); @@ -52,7 +72,7 @@ export const isSampled = function(greenbidsId, samplingRate, exploratorySampling return isExtraSampled; } -export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { +export const greenbidsAnalyticsAdapter = Object.assign(adapter({ ANALYTICS_SERVER, analyticsType }), { cachedAuctions: {}, exploratorySamplingSplit: 0.9, @@ -125,57 +145,65 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }; }, /** - * @param {Bid} bid - * @param {BIDDER_STATUS} status - */ - serializeBidResponse(bid, status) { - return { - bidder: bid.bidder, - isTimeout: (status === BIDDER_STATUS.TIMEOUT), - hasBid: (status === BIDDER_STATUS.BID), - params: (bid.params && Object.keys(bid.params).length > 0) ? bid.params : {}, - ...(status === BIDDER_STATUS.BID ? { - cpm: bid.cpm, - currency: bid.currency - } : {}), - }; - }, - /** - * @param {*} message Greenbids API payload - * @param {Bid} bid Bid to add to the payload - * @param {BIDDER_STATUS} status Bidding status + * @param {AuctionEndArgs} auctionEndArgs + * @param {GreenbidsCachedOption} cachedAuction + * @returns {Message} */ - addBidResponseToMessage(message, bid, status) { - const adUnitCode = bid.adUnitCode.toLowerCase(); - const adUnitIndex = message.adUnits.findIndex((adUnit) => { - return adUnit.code === adUnitCode; - }); - if (adUnitIndex === -1) { - logError('Trying to add to non registered adunit'); - return; - } - const bidderIndex = message.adUnits[adUnitIndex].bidders.findIndex((bidder) => { - return bidder.bidder === bid.bidder; - }); - if (bidderIndex === -1) { - message.adUnits[adUnitIndex].bidders.push(this.serializeBidResponse(bid, status)); - } else { - message.adUnits[adUnitIndex].bidders[bidderIndex].params = (bid.params && Object.keys(bid.params).length > 0) ? bid.params : {}; - if (status === BIDDER_STATUS.BID) { - message.adUnits[adUnitIndex].bidders[bidderIndex].hasBid = true; - message.adUnits[adUnitIndex].bidders[bidderIndex].cpm = bid.cpm; - message.adUnits[adUnitIndex].bidders[bidderIndex].currency = bid.currency; - } else if (status === BIDDER_STATUS.TIMEOUT) { - message.adUnits[adUnitIndex].bidders[bidderIndex].isTimeout = true; - } - } - }, - createBidMessage(auctionEndArgs) { - const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs; - const cachedAuction = this.getCachedAuction(auctionId); - const message = this.createCommonMessage(auctionId); - const timeoutBids = cachedAuction.timeoutBids || []; + createBidMessage(auctionEndArgs, cachedAuction) { + const { + auctionId, + timestamp, + auctionEnd, + adUnits, + bidsReceived, + noBids + } = auctionEndArgs; + // Prepare bidders sub-messages + const unfilteredBidderMessages = new Map(adUnits.map((adUnit) => [ + adUnit.code, + new Map((adUnit.bids || []).map((bid) => [ + bid.bidder, + { + bidder: bid.bidder, + params: (bid.params && Object.keys(bid.params).length > 0) ? bid.params : {}, + hasBid: false, + isTimeout: false, + } + ])) + ])) + // Keep only bidders that received a bid request and enrich the message + const bidderMessages = new Map(adUnits.map((adUnit) => [adUnit.code, new Map()])) + // We enrich no bids, then bids, then timeouts, because in case of a timeout, one response from a bidder + // Can be in all the arrays, and we want that case reflected in the call + noBids.forEach((rqst) => { + bidderMessages.get(rqst.adUnitCode).set(rqst.bidder, unfilteredBidderMessages.get(rqst.adUnitCode).get(rqst.bidder)) + }) + bidsReceived.forEach((bid) => { + bidderMessages.get(bid.adUnitCode).set(bid.bidder, + Object.assign( + bidderMessages.get(bid.adUnitCode).get(bid.bidder) || unfilteredBidderMessages.get(bid.adUnitCode).get(bid.bidder), + { + hasBid: true, + cpm: bid.cpm, + currency: bid.currency + } + ) + ) + }) + cachedAuction.timeoutBids.forEach((noBid) => { + bidderMessages.get(noBid.adUnitCode).set(noBid.bidder, + Object.assign( + bidderMessages.get(noBid.adUnitCode).get(noBid.bidder) || unfilteredBidderMessages.get(noBid.adUnitCode).get(noBid.bidder), + { + isTimeout: true, + } + ) + ) + }) + + // Build complete message + const message = this.createCommonMessage(auctionId); message.auctionElapsed = (auctionEnd - timestamp); adUnits.forEach((adUnit) => { @@ -183,25 +211,21 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER message.adUnits.push({ code: adUnitCode, mediaTypes: { - ...(adUnit.mediaTypes?.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, - ...(adUnit.mediaTypes?.video !== undefined) && {video: adUnit.mediaTypes.video}, - ...(adUnit.mediaTypes?.native !== undefined) && {native: adUnit.mediaTypes.native} + ...(adUnit.mediaTypes?.banner !== undefined) && { banner: adUnit.mediaTypes.banner }, + ...(adUnit.mediaTypes?.video !== undefined) && { video: adUnit.mediaTypes.video }, + ...(adUnit.mediaTypes?.native !== undefined) && { native: adUnit.mediaTypes.native } }, ortb2Imp: adUnit.ortb2Imp || {}, - bidders: [], - }); - }); - - // We enrich noBid then bids, then timeouts, because in case of a timeout, one response from a bidder - // Can be in the 3 arrays, and we want that case reflected in the call - noBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.NO_BID)); - - bidsReceived.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.BID)); - - timeoutBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.TIMEOUT)); + bidders: Array.from(bidderMessages.get(adUnitCode).values()) + }) + }) return message; }, + /** + * @param {string} auctionId + * @returns {GreenbidsCachedOption} + */ getCachedAuction(auctionId) { this.cachedAuctions[auctionId] = this.cachedAuctions[auctionId] || { timeoutBids: [], @@ -221,6 +245,9 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER } cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling, this.exploratorySamplingSplit); }, + /** + * @param {AuctionEndArgs} auctionEndArgs + */ handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); const isFilteringForced = getParameterByName('greenbids_force_filtering'); @@ -243,7 +270,7 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER cachedAuction.billingId = billableArgs.billingId || 'unknown_billing_id'; } }, - track({eventType, args}) { + track({ eventType, args }) { try { if (eventType === AUCTION_INIT) { this.handleAuctionInit(args); @@ -273,7 +300,7 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER greenbidsAnalyticsAdapter.originEnableAnalytics = greenbidsAnalyticsAdapter.enableAnalytics; -greenbidsAnalyticsAdapter.enableAnalytics = function(config) { +greenbidsAnalyticsAdapter.enableAnalytics = function (config) { this.initConfig(config); if (typeof config.options.sampling === 'number') { // Set sampling to 1 to prevent prebid analytics integrated sampling to happen diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js index 7eeaa7a6c67..af9a3fbda7e 100644 --- a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -107,7 +107,6 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { cpm: 0.08, currency: 'USD', ad: 'fake ad2', - params: {'placement ID': 12784} }, { auctionId: auctionId, @@ -158,65 +157,28 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { }); }); - describe('#serializeBidResponse', function () { - it('should handle BID properly with timeout false and hasBid true', function () { - const result = greenbidsAnalyticsAdapter.serializeBidResponse(receivedBids[0], BIDDER_STATUS.BID); - - expect(result).to.include({ - bidder: 'greenbids', - isTimeout: false, - hasBid: true, - }); - }); - - it('should handle NO_BID properly and set hasBid to false', function () { - const result = greenbidsAnalyticsAdapter.serializeBidResponse(noBids[0], BIDDER_STATUS.NO_BID); - - expect(result).to.include({ - bidder: 'greenbids', - isTimeout: false, - hasBid: false, - }); - }); - - it('should handle TIMEOUT properly and set isTimeout to true', function () { - const result = greenbidsAnalyticsAdapter.serializeBidResponse(noBids[0], BIDDER_STATUS.TIMEOUT); - - expect(result).to.include({ - bidder: 'greenbids', - isTimeout: true, - hasBid: false, - }); - }); - }); - - describe('#addBidResponseToMessage()', function () { - it('should add a bid response in the output message, grouped by adunit_id and bidder', function () { - const message = { + describe('#createBidMessage()', function () { + it('should support multiple adunits with same code', function() { + const args = { + auctionId: auctionId + timestamp: 0, + auctionEnd: 100, + adUnitCodes: ['adunit-1', 'adunit-1'], adUnits: [ { - code: 'adunit-2', - bidders: [] - } - ] - }; - greenbidsAnalyticsAdapter.addBidResponseToMessage(message, noBids[0], BIDDER_STATUS.NO_BID); - - expect(message.adUnits[0]).to.deep.include({ - code: 'adunit-2', - bidders: [ + code: 'adunit-1', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + bids: [{ bidder: "bidder-1" }] + }, { - bidder: 'greenbids', - isTimeout: false, - hasBid: false, - params: {} - } - ] - }); + code: 'adunit-1', + mediaTypes: { video: {}}, + bids: [{ bidder: "bidder-2" }] + }, + ], + } + // TODO }); - }); - - describe('#createBidMessage()', function () { it('should format auction message sent to the backend', function () { const args = { auctionId: auctionId, @@ -231,7 +193,11 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { banner: { sizes: [[300, 250], [300, 600]] }, - } + }, + bids: [ + { bidder: "greenbids" }, + { bidder: "greenbidsx", params: { 'placement ID': 12784 } }, + ] }, { code: 'adunit-2', @@ -253,16 +219,17 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { adunitDFP: 'adunitcustomPathExtension' } } - } + }, + bids: [ + { bidder: "greenbids" } + ] }, ], bidsReceived: receivedBids, noBids: noBids }; - sinon.stub(greenbidsAnalyticsAdapter, 'getCachedAuction').returns({ timeoutBids: timeoutBids }); - const result = greenbidsAnalyticsAdapter.createBidMessage(args, timeoutBids); - greenbidsAnalyticsAdapter.getCachedAuction.restore(); + const result = greenbidsAnalyticsAdapter.createBidMessage(args, { timeoutBids: timeoutBids }); assertHavingRequiredMessageFields(result); expect(result).to.deep.include({ auctionElapsed: 100, @@ -329,6 +296,63 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { ], }); }); + it("should not add filtered bidders", function () { + const auctionEndArgs = { + auctionId: auctionId, + timestamp: 0, + auctionEnd: 100, + adUnitCodes: ['adunit-1'], + adUnits: [ + { + code: 'adunit-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + }, + bids: [ + { bidder: "bidder", params: { 'placement ID': 12784 } }, + { bidder: "filtered" }, + ] + }, + ], + bidsReceived: [ + { + adUnitCode: 'adunit-1', + bidder: 'bidder', + cpm: 1, + currency: 'USD', + }, + ], + noBids: [] + }; + + const actual = greenbidsAnalyticsAdapter.createBidMessage(auctionEndArgs, { timeoutBids: [] }); + assertHavingRequiredMessageFields(actual); + expect(actual).to.deep.include({ + adUnits: [ + { + code: 'adunit-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + ortb2Imp: {}, + bidders: [ + { + bidder: 'bidder', + params: { 'placement ID': 12784 }, + cpm: 1, + currency: 'USD', + isTimeout: false, + hasBid: true + } + ] + } + ] + }); + }); }); describe('#handleBidTimeout()', function () {