From 361e70d0d052a03361d0d07c8a0bb2b47915765a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Pettit Date: Wed, 29 Nov 2023 22:35:14 +0100 Subject: [PATCH 1/2] track billing events and modify sampling pattern --- modules/greenbidsAnalyticsAdapter.js | 96 ++++++++++-- modules/greenbidsAnalyticsAdapter.md | 37 ++--- modules/greenbidsRtdProvider.js | 67 +++++--- modules/greenbidsRtdProvider.md | 2 +- .../modules/greenbidsAnalyticsAdapter_spec.js | 129 ++++++++------- .../spec/modules/greenbidsRtdProvider_spec.js | 148 ++++++++++++++++-- 6 files changed, 348 insertions(+), 131 deletions(-) diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index 2189172e16f..800d1a83add 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -2,18 +2,20 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import {deepClone, logError, logInfo} from '../src/utils.js'; +import {deepClone, generateUUID, logError, logInfo, logWarn} from '../src/utils.js'; const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '1.0.0'; +export const ANALYTICS_VERSION = '2.0.0'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; const { EVENTS: { + AUCTION_INIT, AUCTION_END, BID_TIMEOUT, + BILLABLE_EVENT, } } = CONSTANTS; @@ -25,28 +27,60 @@ export const BIDDER_STATUS = { const analyticsOptions = {}; -export const parseBidderCode = function (bid) { - let bidderCode = bid.bidderCode || bid.bidder; - return bidderCode.toLowerCase(); -}; +export const isSampled = function(greenbidsId, samplingRate) { + if (samplingRate < 0 || samplingRate > 1) { + logWarn('Sampling rate must be between 0 and 1'); + return true; + } + const hashInt = parseInt(greenbidsId.slice(-4), 16); + + return hashInt < samplingRate * (0xFFFF + 1); +} export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { cachedAuctions: {}, + isSampled: true, + greenbidsId: null, + billingId: null, initConfig(config) { + analyticsOptions.options = deepClone(config.options); /** * Required option: pbuid * @type {boolean} */ - analyticsOptions.options = deepClone(config.options); - if (typeof config.options.pbuid !== 'string' || config.options.pbuid.length < 1) { + if (typeof analyticsOptions.options.pbuid !== 'string' || analyticsOptions.options.pbuid.length < 1) { logError('"options.pbuid" is required.'); return false; } + /** + * Deprecate use of integerated 'sampling' config + * replace by greenbidsSampling + */ + if (typeof analyticsOptions.options.sampling === 'number') { + logWarn('"options.sampling" is deprecated, please use "greenbidsSampling" instead.'); + analyticsOptions.options.greenbidsSampling = analyticsOptions.options.sampling; + // Set sampling to null to prevent prebid analytics integrated sampling to happen + analyticsOptions.options.sampling = null; + } + + /** + * Discourage unsampled analytics + */ + if (typeof analyticsOptions.options.greenbidsSampling !== 'number' || analyticsOptions.options.greenbidsSampling >= 1) { + logWarn('"options.greenbidsSampling" is not set or >=1, using this analytics module unsampled is discouraged.'); + analyticsOptions.options.greenbidsSampling = 1; + } + analyticsOptions.pbuid = config.options.pbuid analyticsOptions.server = ANALYTICS_SERVER; + // resetting object default values on init + this.isSampled = true; + this.billingId = null; + this.greenbidsId = null; + return true; }, sendEventMessage(endPoint, data) { @@ -61,9 +95,11 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER version: ANALYTICS_VERSION, auctionId: auctionId, referrer: window.location.href, - sampling: analyticsOptions.options.sampling, + sampling: analyticsOptions.options.greenbidsSampling, prebid: '$prebid.version$', + greenbidsId: this.greenbidsId, pbuid: analyticsOptions.pbuid, + billingId: this.billingId, adUnits: [], }; }, @@ -97,7 +133,6 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER } }, createBidMessage(auctionEndArgs, timeoutBids) { - logInfo(auctionEndArgs) const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs; const message = this.createCommonMessage(auctionId); @@ -125,6 +160,8 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER timeoutBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.TIMEOUT)); + message.billingId = this.billingId; + return message; }, getCachedAuction(auctionId) { @@ -133,6 +170,15 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }; return this.cachedAuctions[auctionId]; }, + handleAuctionInit(auctionInitArgs) { + try { + this.greenbidsId = auctionInitArgs.adUnits[0].ortb2Imp.ext.greenbids.greenbidsId; + } catch (e) { + logInfo("Couldn't find Greenbids RTD info, assuming analytics only"); + this.greenbidsId = generateUUID(); + } + this.isSampled = isSampled(this.greenbidsId, analyticsOptions.options.greenbidsSampling); + }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); this.sendEventMessage('/', @@ -145,14 +191,30 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER cachedAuction.timeoutBids.push(bid); }); }, + handleBillable(billableArgs) { + const vendor = billableArgs.vendor || 'unknown_vendor'; + const billingId = billableArgs.billingId || 'unknown_billing_id'; + /* Filter Greenbids Billable Events only */ + if (vendor === 'greenbidsRtdProvider') { + this.billingId = billingId; + } + }, track({eventType, args}) { - switch (eventType) { - case BID_TIMEOUT: - this.handleBidTimeout(args); - break; - case AUCTION_END: - this.handleAuctionEnd(args); - break; + if (eventType === AUCTION_INIT) { + this.handleAuctionInit(args); + } + if (this.isSampled) { + switch (eventType) { + case BID_TIMEOUT: + this.handleBidTimeout(args); + break; + case AUCTION_END: + this.handleAuctionEnd(args); + break; + case BILLABLE_EVENT: + this.handleBillable(args); + break; + } } }, getAnalyticsOptions() { diff --git a/modules/greenbidsAnalyticsAdapter.md b/modules/greenbidsAnalyticsAdapter.md index 46e3af2c5e2..1be2c1741ed 100644 --- a/modules/greenbidsAnalyticsAdapter.md +++ b/modules/greenbidsAnalyticsAdapter.md @@ -1,23 +1,24 @@ -# Overview +#### Registration -``` -Module Name: Greenbids Analytics Adapter -Module Type: Analytics Adapter -Maintainer: jb@greenbids.ai -``` +The Greenbids Analytics adapter requires setup and approval from the +Greenbids team. Please reach out to our team for more information [greenbids.ai](https://greenbids.ai). -# Description +#### Analytics Options -Analytics adapter for Greenbids +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|-------------|---------|--------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|------------------| +| pbuid | required | The Greenbids Publisher ID | greenbids-publisher-1 | string | +| greenbidsSampling | optional | sampling factor [0-1] (a value of 0.1 will filter 90% of the traffic) | 1.0 | float | -# Test Parameters +### Example Configuration -``` -{ - provider: 'greenbids', - options: { - pbuid: "PBUID_FROM_GREENBIDS" - sampling: 1.0 - } -} -``` +```javascript + pbjs.enableAnalytics({ + provider: 'greenbids', + options: { + pbuid: "greenbids-publisher-1" // please contact Greenbids to get a pbuid for yourself + greenbidsSampling: 1.0 + } + }); +``` \ No newline at end of file diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index b3d79f05996..45217b2d489 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -1,12 +1,13 @@ -import { logError } from '../src/utils.js'; +import { logError, deepClone, generateUUID, deepSetValue } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; const MODULE_NAME = 'greenbidsRtdProvider'; -const MODULE_VERSION = '1.0.0'; +const MODULE_VERSION = '2.0.0'; const ENDPOINT = 'https://t.greenbids.ai'; -const auctionInfo = {}; const rtdOptions = {}; function init(moduleConfig) { @@ -16,22 +17,28 @@ function init(moduleConfig) { return false; } else { rtdOptions.pbuid = params?.pbuid; - rtdOptions.targetTPR = params?.targetTPR || 0.99; rtdOptions.timeout = params?.timeout || 200; return true; } } function onAuctionInitEvent(auctionDetails) { - auctionInfo.auctionId = auctionDetails.auctionId; + /* Emitting one billing event per auction */ + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + type: 'auction', + billingId: generateUUID(), + auctionId: auctionDetails.auctionId, + vendor: MODULE_NAME + }); } function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - let promise = createPromise(reqBidsConfigObj); + let greenbidsId = generateUUID(); + let promise = createPromise(reqBidsConfigObj, greenbidsId); promise.then(callback); } -function createPromise(reqBidsConfigObj) { +function createPromise(reqBidsConfigObj, greenbidsId) { return new Promise((resolve) => { const timeoutId = setTimeout(() => { resolve(reqBidsConfigObj); @@ -40,7 +47,7 @@ function createPromise(reqBidsConfigObj) { ENDPOINT, { success: (response) => { - processSuccessResponse(response, timeoutId, reqBidsConfigObj); + processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId); resolve(reqBidsConfigObj); }, error: () => { @@ -48,24 +55,35 @@ function createPromise(reqBidsConfigObj) { resolve(reqBidsConfigObj); }, }, - createPayload(reqBidsConfigObj), - { contentType: 'application/json' } + createPayload(reqBidsConfigObj, greenbidsId), + { + contentType: 'application/json', + customHeaders: { + 'Greenbids-Pbuid': rtdOptions.pbuid + } + } ); }); } -function processSuccessResponse(response, timeoutId, reqBidsConfigObj) { +function processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId) { clearTimeout(timeoutId); const responseAdUnits = JSON.parse(response); - - updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits); + updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); } -function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits) { +function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { adUnits.forEach((adUnit) => { const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code); if (matchingAdUnit) { - removeFalseBidders(adUnit, matchingAdUnit); + deepSetValue(adUnit, 'ortb2Imp.ext.greenbids', { + greenbidsId: greenbidsId, + keptInAuction: matchingAdUnit.bidders, + isExploration: matchingAdUnit.isExploration + }); + if (!matchingAdUnit.isExploration) { + removeFalseBidders(adUnit, matchingAdUnit); + } } }); } @@ -85,14 +103,24 @@ function getFalseBidders(bidders) { .map(([bidder]) => bidder); } -function createPayload(reqBidsConfigObj) { +function stripAdUnits(adUnits) { + const stripedAdUnits = deepClone(adUnits); + return stripedAdUnits.map(adUnit => { + adUnit.bids = adUnit.bids.map(bid => { + return { bidder: bid.bidder }; + }); + return adUnit; + }); +} + +function createPayload(reqBidsConfigObj, greenbidsId) { return JSON.stringify({ - auctionId: auctionInfo.auctionId, version: MODULE_VERSION, + ...rtdOptions, referrer: window.location.href, prebid: '$prebid.version$', - rtdOptions: rtdOptions, - adUnits: reqBidsConfigObj.adUnits, + greenbidsId: greenbidsId, + adUnits: stripAdUnits(reqBidsConfigObj.adUnits), }); } @@ -105,6 +133,7 @@ export const greenbidsSubmodule = { findMatchingAdUnit: findMatchingAdUnit, removeFalseBidders: removeFalseBidders, getFalseBidders: getFalseBidders, + stripAdUnits: stripAdUnits, }; submodule('realTimeData', greenbidsSubmodule); diff --git a/modules/greenbidsRtdProvider.md b/modules/greenbidsRtdProvider.md index 85b8f5a7859..ab8105a4537 100644 --- a/modules/greenbidsRtdProvider.md +++ b/modules/greenbidsRtdProvider.md @@ -2,6 +2,7 @@ ``` Module Name: Greenbids RTD Provider +Module Version: 2.0.0 Module Type: RTD Provider Maintainer: jb@greenbids.ai ``` @@ -21,7 +22,6 @@ This module is configured as part of the `realTimeData.dataProviders` object. | `waitForIt ` | required (mandatory true value) | Tells prebid auction to wait for the result of this module | `'true'` | `boolean` | | `params` | required | | | `Object` | | `params.pbuid` | required | The client site id provided by Greenbids. | `'TEST_FROM_GREENBIDS'` | `string` | -| `params.targetTPR` | optional (default 0.95) | Target True positive rate for the throttling model | `0.99` | `[0-1]` | | `params.timeout` | optional (default 200) | Maximum amount of milliseconds allowed for module to finish working (has to be <= to the realTimeData.auctionDelay property) | `200` | `number` | #### Example diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js index 3cfdc9b9749..d4debb6f9a4 100644 --- a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -1,8 +1,11 @@ import { - greenbidsAnalyticsAdapter, parseBidderCode, + greenbidsAnalyticsAdapter, + isSampled, ANALYTICS_VERSION, BIDDER_STATUS } from 'modules/greenbidsAnalyticsAdapter.js'; - +import { + generateUUID, +} from '../../../src/utils.js'; import {expect} from 'chai'; import sinon from 'sinon'; @@ -13,11 +16,42 @@ const pbuid = 'pbuid-AA778D8A796AEA7A0843E2BBEB677766'; const auctionId = 'b0b39610-b941-4659-a87c-de9f62d3e13e'; describe('Greenbids Prebid AnalyticsAdapter Testing', function () { + describe('enableAnalytics and config parser', function () { + const configOptions = { + pbuid: pbuid, + greenbidsSampling: 0, + }; + beforeEach(function () { + greenbidsAnalyticsAdapter.enableAnalytics({ + provider: 'greenbidsAnalytics', + options: configOptions + }); + }); + + afterEach(function () { + greenbidsAnalyticsAdapter.disableAnalytics(); + }); + + it('should parse config correctly with optional values', function () { + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().options).to.deep.equal(configOptions); + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().pbuid).to.equal(configOptions.pbuid); + }); + + it('should not enable Analytics when pbuid is missing', function () { + const configOptions = { + options: { + } + }; + const validConfig = greenbidsAnalyticsAdapter.initConfig(configOptions); + expect(validConfig).to.equal(false); + }); + }); + describe('event tracking and message cache manager', function () { beforeEach(function () { const configOptions = { pbuid: pbuid, - sampling: 0, + greenbidsSampling: 0, }; greenbidsAnalyticsAdapter.enableAnalytics({ @@ -30,43 +64,6 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { greenbidsAnalyticsAdapter.disableAnalytics(); }); - describe('#parseBidderCode()', function() { - it('should get lower case bidder code from bidderCode field value', function() { - const receivedBids = [ - { - auctionId: auctionId, - adUnitCode: 'adunit_1', - bidder: 'greenbids', - bidderCode: 'GREENBIDS', - requestId: 'a1b2c3d4', - timeToRespond: 72, - cpm: 0.1, - currency: 'USD', - ad: 'fake ad1' - }, - ]; - const result = parseBidderCode(receivedBids[0]); - expect(result).to.equal('greenbids'); - }); - it('should get lower case bidder code from bidder field value as bidderCode field is missing', function() { - const receivedBids = [ - { - auctionId: auctionId, - adUnitCode: 'adunit_1', - bidder: 'greenbids', - bidderCode: '', - requestId: 'a1b2c3d4', - timeToRespond: 72, - cpm: 0.1, - currency: 'USD', - ad: 'fake ad1' - }, - ]; - const result = parseBidderCode(receivedBids[0]); - expect(result).to.equal('greenbids'); - }); - }); - describe('#getCachedAuction()', function() { const existing = {timeoutBids: [{}]}; greenbidsAnalyticsAdapter.cachedAuctions['test_auction_id'] = existing; @@ -330,7 +327,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { timeout: 3000, auctionEnd: 1234567990, bidsReceived: receivedBids, - noBids: noBids + noBids: noBids, }]; greenbidsAnalyticsAdapter.handleBidTimeout(args); @@ -353,7 +350,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { describe('greenbids Analytics Adapter track handler ', function () { const configOptions = { pbuid: pbuid, - sampling: 1, + greenbidsSampling: 1, }; beforeEach(function () { @@ -369,6 +366,13 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { events.getEvents.restore(); }); + it('should call handleAuctionInit as AUCTION_INIT trigger event', function() { + sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionInit'); + events.emit(constants.EVENTS.AUCTION_INIT, {}); + sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionInit, 1); + greenbidsAnalyticsAdapter.handleAuctionInit.restore(); + }); + it('should call handleBidTimeout as BID_TIMEOUT trigger event', function() { sinon.spy(greenbidsAnalyticsAdapter, 'handleBidTimeout'); events.emit(constants.EVENTS.BID_TIMEOUT, {}); @@ -382,37 +386,32 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionEnd, 1); greenbidsAnalyticsAdapter.handleAuctionEnd.restore(); }); - }); - - describe('enableAnalytics and config parser', function () { - const configOptions = { - pbuid: pbuid, - sampling: 0, - }; - beforeEach(function () { - greenbidsAnalyticsAdapter.enableAnalytics({ - provider: 'greenbidsAnalytics', - options: configOptions + it('should call handleBillable as BILLABLE_EVENT trigger event', function() { + sinon.spy(greenbidsAnalyticsAdapter, 'handleBillable'); + events.emit(constants.EVENTS.BILLABLE_EVENT, { + type: 'auction', + billingId: generateUUID(), + auctionId: 'auctionID-1', + vendor: 'greenbidsRtdProvider' }); + sinon.assert.callCount(greenbidsAnalyticsAdapter.handleBillable, 1); + greenbidsAnalyticsAdapter.handleBillable.restore(); }); + }); - afterEach(function () { - greenbidsAnalyticsAdapter.disableAnalytics(); + describe('isSampled', function() { + it('should return true for invalid sampling rates', function() { + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', -1)).to.be.true; + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 1.2)).to.be.true; }); - it('should parse config correctly with optional values', function () { - expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().options).to.deep.equal(configOptions); - expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().pbuid).to.equal(configOptions.pbuid); + it('should return determinist falsevalue for valid sampling rate given the predifined id and rate', function() { + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.0001)).to.be.false; }); - it('should not enable Analytics when pbuid is missing', function () { - const configOptions = { - options: { - } - }; - const validConfig = greenbidsAnalyticsAdapter.initConfig(configOptions); - expect(validConfig).to.equal(false); + it('should return determinist true value for valid sampling rate given the predifined id and rate', function() { + expect(isSampled('ce1f3692-632c-4cfd-9e40-0c2ad625ec56', 0.9999)).to.be.true; }); }); }); diff --git a/test/spec/modules/greenbidsRtdProvider_spec.js b/test/spec/modules/greenbidsRtdProvider_spec.js index cd93e9013c0..9b6425b0e38 100644 --- a/test/spec/modules/greenbidsRtdProvider_spec.js +++ b/test/spec/modules/greenbidsRtdProvider_spec.js @@ -7,6 +7,8 @@ import { greenbidsSubmodule } from 'modules/greenbidsRtdProvider.js'; import {server} from '../../mocks/xhr.js'; +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; describe('greenbidsRtdProvider', () => { const endPoint = 't.greenbids.ai'; @@ -39,14 +41,36 @@ describe('greenbidsRtdProvider', () => { }] }; - const SAMPLE_RESPONSE_ADUNITS = [ + const SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED = [ { code: 'adUnit1', bidders: { 'appnexus': true, 'rubicon': false, 'ix': true - } + }, + isExploration: false + }, + { + code: 'adUnit2', + bidders: { + 'appnexus': false, + 'rubicon': true, + 'openx': true + }, + isExploration: false + + }]; + + const SAMPLE_RESPONSE_ADUNITS_EXPLORED = [ + { + code: 'adUnit1', + bidders: { + 'appnexus': true, + 'rubicon': false, + 'ix': true + }, + isExploration: true }, { code: 'adUnit2', @@ -54,7 +78,9 @@ describe('greenbidsRtdProvider', () => { 'appnexus': false, 'rubicon': true, 'openx': true - } + }, + isExploration: true + }]; describe('init', () => { @@ -70,22 +96,37 @@ describe('greenbidsRtdProvider', () => { }); describe('updateAdUnitsBasedOnResponse', () => { - it('should update ad units based on response', () => { + it('should update ad units based on response if not exploring', () => { const adUnits = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits)); - greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS); + greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED); expect(adUnits[0].bids).to.have.length(2); expect(adUnits[1].bids).to.have.length(2); }); + + it('should not update ad units based on response if exploring', () => { + const adUnits = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits)); + greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS_EXPLORED); + + expect(adUnits[0].bids).to.have.length(3); + expect(adUnits[1].bids).to.have.length(3); + expect(adUnits[0].ortb2Imp.ext.greenbids.greenbidsId).to.be.a.string; + expect(adUnits[1].ortb2Imp.ext.greenbids.greenbidsId).to.be.a.string; + expect(adUnits[0].ortb2Imp.ext.greenbids.greenbidsId).to.equal(adUnits[0].ortb2Imp.ext.greenbids.greenbidsId); + expect(adUnits[0].ortb2Imp.ext.greenbids.keptInAuction).to.deep.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[0].bidders); + expect(adUnits[1].ortb2Imp.ext.greenbids.keptInAuction).to.deep.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[1].bidders); + expect(adUnits[0].ortb2Imp.ext.greenbids.isExploration).to.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[0].isExploration); + expect(adUnits[1].ortb2Imp.ext.greenbids.isExploration).to.equal(SAMPLE_RESPONSE_ADUNITS_EXPLORED[1].isExploration); + }); }); describe('findMatchingAdUnit', () => { it('should find matching ad unit by code', () => { - const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'adUnit1'); - expect(matchingAdUnit).to.deep.equal(SAMPLE_RESPONSE_ADUNITS[0]); + const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED, 'adUnit1'); + expect(matchingAdUnit).to.deep.equal(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED[0]); }); it('should return undefined if no matching ad unit is found', () => { - const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'nonexistent'); + const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED, 'nonexistent'); expect(matchingAdUnit).to.be.undefined; }); }); @@ -93,7 +134,7 @@ describe('greenbidsRtdProvider', () => { describe('removeFalseBidders', () => { it('should remove bidders with false value', () => { const adUnit = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits[0])); - const matchingAdUnit = SAMPLE_RESPONSE_ADUNITS[0]; + const matchingAdUnit = SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED[0]; greenbidsSubmodule.removeFalseBidders(adUnit, matchingAdUnit); expect(adUnit.bids).to.have.length(2); expect(adUnit.bids.map((bid) => bid.bidder)).to.not.include('rubicon'); @@ -126,13 +167,14 @@ describe('greenbidsRtdProvider', () => { server.requests[0].respond( 200, {'Content-Type': 'application/json'}, - JSON.stringify(SAMPLE_RESPONSE_ADUNITS) + JSON.stringify(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED) ); }, 50); setTimeout(() => { const requestUrl = new URL(server.requests[0].url); expect(requestUrl.host).to.be.eq(endPoint); + expect(requestBids.greenbidsId).to.be.a.string; expect(requestBids.adUnits[0].bids).to.have.length(2); expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.not.include('rubicon'); expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.include('ix'); @@ -158,7 +200,7 @@ describe('greenbidsRtdProvider', () => { server.requests[0].respond( 200, {'Content-Type': 'application/json'}, - JSON.stringify(SAMPLE_RESPONSE_ADUNITS) + JSON.stringify(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED) ); done(); }, 300); @@ -166,6 +208,7 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { const requestUrl = new URL(server.requests[0].url); expect(requestUrl.host).to.be.eq(endPoint); + expect(requestBids.greenbidsId).to.be.a.string; expect(requestBids.adUnits[0].bids).to.have.length(3); expect(requestBids.adUnits[1].bids).to.have.length(3); expect(callback.calledOnce).to.be.true; @@ -191,6 +234,7 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { const requestUrl = new URL(server.requests[0].url); expect(requestUrl.host).to.be.eq(endPoint); + expect(requestBids.greenbidsId).to.be.a.string; expect(requestBids.adUnits[0].bids).to.have.length(3); expect(requestBids.adUnits[1].bids).to.have.length(3); expect(callback.calledOnce).to.be.true; @@ -198,4 +242,86 @@ describe('greenbidsRtdProvider', () => { }, 60); }); }); + + describe('stripAdUnits', function() { + it('should strip all properties except bidder from each bid in adUnits', function() { + const adUnits = + [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: {'banner': {prop: 'value3'}} + } + ]; + const expectedOutput = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ], + mediaTypes: {'banner': {prop: 'value3'}} + } + ]; + + // Perform the test + const output = greenbidsSubmodule.stripAdUnits(adUnits); + expect(output).to.deep.equal(expectedOutput); + }); + + it('should strip all properties except bidder from each bid in adUnits but keep ortb2Imp', function() { + const adUnits = + [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: {'banner': {prop: 'value3'}}, + ortb2Imp: { + ext: { + greenbids: { + greenbidsId: 'test' + } + } + } + } + ]; + const expectedOutput = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ], + mediaTypes: {'banner': {prop: 'value3'}}, + ortb2Imp: { + ext: { + greenbids: { + greenbidsId: 'test' + } + } + } + } + ]; + + // Perform the test + const output = greenbidsSubmodule.stripAdUnits(adUnits); + expect(output).to.deep.equal(expectedOutput); + }); + }); + + describe('onAuctionInitEvent', function() { + it('should emit billable events', function (done) { + let counter = 0; + events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, function (event) { + if (event.vendor === 'greenbidsRtdProvider' && event.type === 'auction') { + counter += 1; + } + expect(counter).to.equal(1); + done(); + }); + greenbidsSubmodule.onAuctionInitEvent({auctionId: 'test'}); + }); + }); }); From f5a4167caa44a331c979d80d76380eeb13b4470f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Pettit Date: Sat, 16 Dec 2023 03:02:55 +0100 Subject: [PATCH 2/2] review updates --- modules/greenbidsAnalyticsAdapter.js | 78 ++++++++++--------- modules/greenbidsRtdProvider.js | 19 +++-- .../modules/greenbidsAnalyticsAdapter_spec.js | 16 ++-- .../spec/modules/greenbidsRtdProvider_spec.js | 60 ++++++++++---- 4 files changed, 106 insertions(+), 67 deletions(-) diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index 800d1a83add..5d1f35f24ff 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -40,9 +40,6 @@ export const isSampled = function(greenbidsId, samplingRate) { export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { cachedAuctions: {}, - isSampled: true, - greenbidsId: null, - billingId: null, initConfig(config) { analyticsOptions.options = deepClone(config.options); @@ -76,10 +73,6 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER analyticsOptions.pbuid = config.options.pbuid analyticsOptions.server = ANALYTICS_SERVER; - // resetting object default values on init - this.isSampled = true; - this.billingId = null; - this.greenbidsId = null; return true; }, @@ -91,15 +84,16 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }); }, createCommonMessage(auctionId) { + const cachedAuction = this.getCachedAuction(auctionId); return { version: ANALYTICS_VERSION, auctionId: auctionId, referrer: window.location.href, sampling: analyticsOptions.options.greenbidsSampling, prebid: '$prebid.version$', - greenbidsId: this.greenbidsId, + greenbidsId: cachedAuction.greenbidsId, pbuid: analyticsOptions.pbuid, - billingId: this.billingId, + billingId: cachedAuction.billingId, adUnits: [], }; }, @@ -132,20 +126,22 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER } } }, - createBidMessage(auctionEndArgs, timeoutBids) { + createBidMessage(auctionEndArgs) { const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs; + const cachedAuction = this.getCachedAuction(auctionId); const message = this.createCommonMessage(auctionId); + const timeoutBids = cachedAuction.timeoutBids || []; message.auctionElapsed = (auctionEnd - timestamp); adUnits.forEach((adUnit) => { - const adUnitCode = adUnit.code.toLowerCase(); + const adUnitCode = adUnit.code?.toLowerCase() || 'unknown_adunit_code'; message.adUnits.push({ code: adUnitCode, mediaTypes: { - ...(adUnit.mediaTypes.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, - ...(adUnit.mediaTypes.video !== undefined) && {video: adUnit.mediaTypes.video}, - ...(adUnit.mediaTypes.native !== undefined) && {native: adUnit.mediaTypes.native} + ...(adUnit.mediaTypes?.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, + ...(adUnit.mediaTypes?.video !== undefined) && {video: adUnit.mediaTypes.video}, + ...(adUnit.mediaTypes?.native !== undefined) && {native: adUnit.mediaTypes.native} }, ortb2Imp: adUnit.ortb2Imp || {}, bidders: [], @@ -160,29 +156,31 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER timeoutBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.TIMEOUT)); - message.billingId = this.billingId; - return message; }, getCachedAuction(auctionId) { this.cachedAuctions[auctionId] = this.cachedAuctions[auctionId] || { timeoutBids: [], + greenbidsId: null, + billingId: null, + isSampled: true, }; return this.cachedAuctions[auctionId]; }, handleAuctionInit(auctionInitArgs) { + const cachedAuction = this.getCachedAuction(auctionInitArgs.auctionId); try { - this.greenbidsId = auctionInitArgs.adUnits[0].ortb2Imp.ext.greenbids.greenbidsId; + cachedAuction.greenbidsId = auctionInitArgs.adUnits[0].ortb2Imp.ext.greenbids.greenbidsId; } catch (e) { logInfo("Couldn't find Greenbids RTD info, assuming analytics only"); - this.greenbidsId = generateUUID(); + cachedAuction.greenbidsId = generateUUID(); } - this.isSampled = isSampled(this.greenbidsId, analyticsOptions.options.greenbidsSampling); + cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling); }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); this.sendEventMessage('/', - this.createBidMessage(auctionEndArgs, cachedAuction.timeoutBids) + this.createBidMessage(auctionEndArgs, cachedAuction) ); }, handleBidTimeout(timeoutBids) { @@ -192,29 +190,33 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }); }, handleBillable(billableArgs) { - const vendor = billableArgs.vendor || 'unknown_vendor'; - const billingId = billableArgs.billingId || 'unknown_billing_id'; + const cachedAuction = this.getCachedAuction(billableArgs.auctionId); /* Filter Greenbids Billable Events only */ - if (vendor === 'greenbidsRtdProvider') { - this.billingId = billingId; + if (billableArgs.vendor === 'greenbidsRtdProvider') { + cachedAuction.billingId = billableArgs.billingId || 'unknown_billing_id'; } }, track({eventType, args}) { - if (eventType === AUCTION_INIT) { - this.handleAuctionInit(args); - } - if (this.isSampled) { - switch (eventType) { - case BID_TIMEOUT: - this.handleBidTimeout(args); - break; - case AUCTION_END: - this.handleAuctionEnd(args); - break; - case BILLABLE_EVENT: - this.handleBillable(args); - break; + try { + if (eventType === AUCTION_INIT) { + this.handleAuctionInit(args); } + + if (this.getCachedAuction(args?.auctionId)?.isSampled ?? true) { + switch (eventType) { + case BID_TIMEOUT: + this.handleBidTimeout(args); + break; + case AUCTION_END: + this.handleAuctionEnd(args); + break; + case BILLABLE_EVENT: + this.handleBillable(args); + break; + } + } + } catch (e) { + logWarn('There was an error handling event ' + eventType); } }, getAnalyticsOptions() { diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index 45217b2d489..7fcd163a7c2 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -1,4 +1,4 @@ -import { logError, deepClone, generateUUID, deepSetValue } from '../src/utils.js'; +import { logError, deepClone, generateUUID, deepSetValue, deepAccess } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import * as events from '../src/events.js'; @@ -24,12 +24,17 @@ function init(moduleConfig) { function onAuctionInitEvent(auctionDetails) { /* Emitting one billing event per auction */ - events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { - type: 'auction', - billingId: generateUUID(), - auctionId: auctionDetails.auctionId, - vendor: MODULE_NAME - }); + let defaultId = 'default_id'; + let greenbidsId = deepAccess(auctionDetails.adUnits[0], 'ortb2Imp.ext.greenbids.greenbidsId', defaultId); + /* greenbids was successfully called so we emit the event */ + if (greenbidsId !== defaultId) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + type: 'auction', + billingId: generateUUID(), + auctionId: auctionDetails.auctionId, + vendor: MODULE_NAME + }); + } } function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js index d4debb6f9a4..30361ca5661 100644 --- a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -19,7 +19,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { describe('enableAnalytics and config parser', function () { const configOptions = { pbuid: pbuid, - greenbidsSampling: 0, + greenbidsSampling: 1, }; beforeEach(function () { greenbidsAnalyticsAdapter.enableAnalytics({ @@ -51,7 +51,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { beforeEach(function () { const configOptions = { pbuid: pbuid, - greenbidsSampling: 0, + greenbidsSampling: 1, }; greenbidsAnalyticsAdapter.enableAnalytics({ @@ -143,7 +143,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { auctionId: auctionId, pbuid: pbuid, referrer: window.location.href, - sampling: 0, + sampling: 1, prebid: '$prebid.version$', }); } @@ -257,7 +257,9 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { noBids: noBids }; + sinon.stub(greenbidsAnalyticsAdapter, 'getCachedAuction').returns({timeoutBids: timeoutBids}); const result = greenbidsAnalyticsAdapter.createBidMessage(args, timeoutBids); + greenbidsAnalyticsAdapter.getCachedAuction.restore(); assertHavingRequiredMessageFields(result); expect(result).to.deep.include({ @@ -368,21 +370,21 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { it('should call handleAuctionInit as AUCTION_INIT trigger event', function() { sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionInit'); - events.emit(constants.EVENTS.AUCTION_INIT, {}); + events.emit(constants.EVENTS.AUCTION_INIT, {auctionId: 'auctionId'}); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionInit, 1); greenbidsAnalyticsAdapter.handleAuctionInit.restore(); }); it('should call handleBidTimeout as BID_TIMEOUT trigger event', function() { sinon.spy(greenbidsAnalyticsAdapter, 'handleBidTimeout'); - events.emit(constants.EVENTS.BID_TIMEOUT, {}); + events.emit(constants.EVENTS.BID_TIMEOUT, {auctionId: 'auctionId'}); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleBidTimeout, 1); greenbidsAnalyticsAdapter.handleBidTimeout.restore(); }); it('should call handleAuctionEnd as AUCTION_END trigger event', function() { sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionEnd'); - events.emit(constants.EVENTS.AUCTION_END, {}); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: 'auctionId'}); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionEnd, 1); greenbidsAnalyticsAdapter.handleAuctionEnd.restore(); }); @@ -392,7 +394,7 @@ describe('Greenbids Prebid AnalyticsAdapter Testing', function () { events.emit(constants.EVENTS.BILLABLE_EVENT, { type: 'auction', billingId: generateUUID(), - auctionId: 'auctionID-1', + auctionId: 'auctionId', vendor: 'greenbidsRtdProvider' }); sinon.assert.callCount(greenbidsAnalyticsAdapter.handleBillable, 1); diff --git a/test/spec/modules/greenbidsRtdProvider_spec.js b/test/spec/modules/greenbidsRtdProvider_spec.js index 9b6425b0e38..d0083d4dc7a 100644 --- a/test/spec/modules/greenbidsRtdProvider_spec.js +++ b/test/spec/modules/greenbidsRtdProvider_spec.js @@ -6,7 +6,7 @@ import { import { greenbidsSubmodule } from 'modules/greenbidsRtdProvider.js'; -import {server} from '../../mocks/xhr.js'; +import { server } from '../../mocks/xhr.js'; import * as events from '../../../src/events.js'; import CONSTANTS from '../../../src/constants.json'; @@ -166,7 +166,7 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { server.requests[0].respond( 200, - {'Content-Type': 'application/json'}, + { 'Content-Type': 'application/json' }, JSON.stringify(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED) ); }, 50); @@ -199,7 +199,7 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { server.requests[0].respond( 200, - {'Content-Type': 'application/json'}, + { 'Content-Type': 'application/json' }, JSON.stringify(SAMPLE_RESPONSE_ADUNITS_NOT_EXPLORED) ); done(); @@ -226,8 +226,8 @@ describe('greenbidsRtdProvider', () => { setTimeout(() => { server.requests[0].respond( 500, - {'Content-Type': 'application/json'}, - JSON.stringify({'failure': 'fail'}) + { 'Content-Type': 'application/json' }, + JSON.stringify({ 'failure': 'fail' }) ); }, 50); @@ -243,8 +243,8 @@ describe('greenbidsRtdProvider', () => { }); }); - describe('stripAdUnits', function() { - it('should strip all properties except bidder from each bid in adUnits', function() { + describe('stripAdUnits', function () { + it('should strip all properties except bidder from each bid in adUnits', function () { const adUnits = [ { @@ -252,7 +252,7 @@ describe('greenbidsRtdProvider', () => { { bidder: 'bidder1', otherProp: 'value1' }, { bidder: 'bidder2', otherProp: 'value2' } ], - mediaTypes: {'banner': {prop: 'value3'}} + mediaTypes: { 'banner': { prop: 'value3' } } } ]; const expectedOutput = [ @@ -261,7 +261,7 @@ describe('greenbidsRtdProvider', () => { { bidder: 'bidder1' }, { bidder: 'bidder2' } ], - mediaTypes: {'banner': {prop: 'value3'}} + mediaTypes: { 'banner': { prop: 'value3' } } } ]; @@ -270,7 +270,7 @@ describe('greenbidsRtdProvider', () => { expect(output).to.deep.equal(expectedOutput); }); - it('should strip all properties except bidder from each bid in adUnits but keep ortb2Imp', function() { + it('should strip all properties except bidder from each bid in adUnits but keep ortb2Imp', function () { const adUnits = [ { @@ -278,7 +278,7 @@ describe('greenbidsRtdProvider', () => { { bidder: 'bidder1', otherProp: 'value1' }, { bidder: 'bidder2', otherProp: 'value2' } ], - mediaTypes: {'banner': {prop: 'value3'}}, + mediaTypes: { 'banner': { prop: 'value3' } }, ortb2Imp: { ext: { greenbids: { @@ -294,7 +294,7 @@ describe('greenbidsRtdProvider', () => { { bidder: 'bidder1' }, { bidder: 'bidder2' } ], - mediaTypes: {'banner': {prop: 'value3'}}, + mediaTypes: { 'banner': { prop: 'value3' } }, ortb2Imp: { ext: { greenbids: { @@ -311,8 +311,26 @@ describe('greenbidsRtdProvider', () => { }); }); - describe('onAuctionInitEvent', function() { - it('should emit billable events', function (done) { + describe('onAuctionInitEvent', function () { + it('should not emit billable event if greenbids hasn\'t set the adunit.ext value', function () { + sinon.spy(events, 'emit'); + greenbidsSubmodule.onAuctionInitEvent({ + auctionId: 'test', + adUnits: [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } }, + } + ] + }); + sinon.assert.callCount(events.emit, 0); + events.emit.restore(); + }); + + it('should emit billable event if greenbids has set the adunit.ext value', function (done) { let counter = 0; events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, function (event) { if (event.vendor === 'greenbidsRtdProvider' && event.type === 'auction') { @@ -321,7 +339,19 @@ describe('greenbidsRtdProvider', () => { expect(counter).to.equal(1); done(); }); - greenbidsSubmodule.onAuctionInitEvent({auctionId: 'test'}); + greenbidsSubmodule.onAuctionInitEvent({ + auctionId: 'test', + adUnits: [ + { + bids: [ + { bidder: 'bidder1', otherProp: 'value1' }, + { bidder: 'bidder2', otherProp: 'value2' } + ], + mediaTypes: { 'banner': { prop: 'value3' } }, + ortb2Imp: { ext: { greenbids: { greenbidsId: 'b0b39610-b941-4659-a87c-de9f62d3e13e' } } } + } + ] + }); }); }); });