From e91484ff1cf3d174d102cfac8b29082003f36dfe Mon Sep 17 00:00:00 2001 From: CPG Date: Wed, 14 May 2025 13:12:57 +0200 Subject: [PATCH 1/6] Nexx360 Bid Adapter: scoremedia alias added (#13102) --- modules/nexx360BidAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index 96aa5429809..0214d33c121 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -37,7 +37,8 @@ const ALIASES = [ { code: 'easybid', gvlid: 1068 }, { code: 'prismassp', gvlid: 965 }, { code: 'spm', gvlid: 965 }, - { code: 'bidstailamedia', gvlid: 965 } + { code: 'bidstailamedia', gvlid: 965 }, + { code: 'scoremedia', gvlid: 965 } ]; export const storage = getStorageManager({ From 3a16749058182719012ebb01d8f95213ecc9876b Mon Sep 17 00:00:00 2001 From: madmazoku Date: Wed, 14 May 2025 16:13:19 +0400 Subject: [PATCH 2/6] 8538: add video media type support to mediaforce bid adapter (#13101) Co-authored-by: Dmitry Moskalyov --- modules/mediaforceBidAdapter.js | 70 ++++- modules/mediaforceBidAdapter.md | 29 ++ .../spec/modules/mediaforceBidAdapter_spec.js | 280 +++++++++++++----- 3 files changed, 295 insertions(+), 84 deletions(-) diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 9f899974721..b35a781e6a3 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -1,6 +1,6 @@ import { getDNT, deepAccess, isStr, replaceAuctionPrice, triggerPixel, parseGPTSingleSizeArrayToRtbSize, isEmpty } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; /** @@ -94,9 +94,11 @@ Object.keys(NATIVE_PARAMS).forEach((key) => { NATIVE_ID_MAP[NATIVE_PARAMS[key].id] = key; }); +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; + export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, NATIVE], + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, /** * Determines whether or not the given bid request is valid. @@ -148,8 +150,9 @@ export const spec = { } }; - for (let mediaTypes in bid.mediaTypes) { - switch (mediaTypes) { + + Object.keys(bid.mediaTypes).forEach(mediaType => { + switch (mediaType) { case BANNER: impObj.banner = createBannerRequest(bid); validImp = true; @@ -158,9 +161,12 @@ export const spec = { impObj.native = createNativeRequest(bid); validImp = true; break; - default: return; + case VIDEO: + impObj.video = createVideoRequest(bid); + validImp = true; + break; } - } + }) let request = requestsMap[bid.params.publisher_id]; if (!request) { @@ -197,7 +203,7 @@ export const spec = { data: request }); } - validImp && request.imp.push(impObj); + if (validImp && impObj) request.imp.push(impObj); }); requests.forEach((req) => { if (isTest) { @@ -251,17 +257,21 @@ export const spec = { ext.native = jsonAdm.native; adm = null; } - if (adm) { + if (ext?.native) { + bid.native = parseNative(ext.native); + bid.mediaType = NATIVE; + } else if (adm?.trim().startsWith(' use first size + : video.playerSize; // [640, 480] + + const videoRequest = { + mimes: video.mimes || ['video/mp4'], + minduration: video.minduration || 1, + maxduration: video.maxduration || 60, + protocols: video.protocols || [2, 3, 5, 6], + w: playerSize[0], + h: playerSize[1], + startdelay: video.startdelay || 0, + linearity: video.linearity || 1, + skip: video.skip != null ? video.skip : 0, + skipmin: video.skipmin || 5, + skipafter: video.skipafter || 10, + playbackmethod: video.playbackmethod || [1], + api: video.api || [1, 2], + }; + + if (video.placement) { + videoRequest.placement = video.placement; + } + + return videoRequest; +} + diff --git a/modules/mediaforceBidAdapter.md b/modules/mediaforceBidAdapter.md index f8e6903516f..413fa2ca3ed 100644 --- a/modules/mediaforceBidAdapter.md +++ b/modules/mediaforceBidAdapter.md @@ -66,3 +66,32 @@ Module that connects to mediaforce's demand sources } ]; ``` + +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + mimes: ['video/mp4', 'video/webm'], + protocols: [2, 3, 5, 6], + skip: 0, + playbackmethod: [2], + api: [2] + } + }, + bids: [ + { + bidder: 'mediaforce', + params: { + placement_id: 'pl67890', // required + publisher_id: 'pub67890', // required + bidfloor: 1.0 + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/mediaforceBidAdapter_spec.js b/test/spec/modules/mediaforceBidAdapter_spec.js index 61e5678b03b..ee88f0a21b8 100644 --- a/test/spec/modules/mediaforceBidAdapter_spec.js +++ b/test/spec/modules/mediaforceBidAdapter_spec.js @@ -1,7 +1,7 @@ import {assert} from 'chai'; import {spec} from 'modules/mediaforceBidAdapter.js'; import * as utils from '../../../src/utils.js'; -import {BANNER, NATIVE} from '../../../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; describe('mediaforce bid adapter', function () { let sandbox; @@ -76,7 +76,7 @@ describe('mediaforce bid adapter', function () { sizes: [300, 250], }, sponsoredBy: { - required: true + required: false } }, mediaTypes: { @@ -93,8 +93,20 @@ describe('mediaforce bid adapter', function () { sizes: [300, 250], }, sponsoredBy: { - required: true + required: false } + }, + video: { + playerSize: [[640, 480]], + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + protocols: [2, 3], + linearity: 1, + skip: 1, + skipmin: 5, + skipafter: 10, + api: [1, 2] } }, ortb2Imp: { @@ -104,6 +116,82 @@ describe('mediaforce bid adapter', function () { } }; + const refererInfo = { + ref: 'https://www.prebid.org', + reachedTop: true, + stack: [ + 'https://www.prebid.org/page.html', + 'https://www.prebid.org/iframe1.html', + ] + }; + + const dnt = utils.getDNT() ? 1 : 0; + const secure = window.location.protocol === 'https:' ? 1 : 0; + const pageUrl = window.location.href; + const timeout = 1500; + const auctionId = '210a474e-88f0-4646-837f-4253b7cf14fb'; + + const expectedData = { + // id property removed as it is specific for each request generated + tmax: timeout, + ext: { + mediaforce: { + hb_key: auctionId + } + }, + site: { + id: defaultBid.params.publisher_id, + publisher: {id: defaultBid.params.publisher_id}, + ref: encodeURIComponent(refererInfo.ref), + page: pageUrl, + }, + device: { + ua: navigator.userAgent, + dnt: dnt, + js: 1, + language: language, + }, + imp: [{ + tagid: defaultBid.params.placement_id, + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: defaultBid.ortb2Imp.ext.tid, + } + }, + banner: {w: 300, h: 250}, + native: { + ver: '1.2', + request: { + assets: [ + {id: 1, title: {len: 800}, required: 1}, + {id: 3, img: {w: 300, h: 250, type: 3}, required: 1}, + {id: 5, data: {type: 1}, required: 0} + ], + context: 1, + plcmttype: 1, + ver: '1.2' + } + }, + video: { + mimes: ['video/mp4'], + minduration: 5, + maxduration: 30, + protocols: [2, 3], + w: 640, + h: 480, + startdelay: 0, + linearity: 1, + skip: 1, + skipmin: 5, + skipafter: 10, + playbackmethod: [1], + api: [1, 2] + } + }], + }; + const multiBid = [ { publisher_id: 'pub123', @@ -139,30 +227,72 @@ describe('mediaforce bid adapter', function () { } }); - const refererInfo = { - ref: 'https://www.prebid.org', - reachedTop: true, - stack: [ - 'https://www.prebid.org/page.html', - 'https://www.prebid.org/iframe1.html', - ] - }; - const requestUrl = `${baseUrl}/header_bid`; - const dnt = utils.getDNT() ? 1 : 0; - const secure = window.location.protocol === 'https:' ? 1 : 0; - const pageUrl = window.location.href; - const timeout = 1500; it('should return undefined if no validBidRequests passed', function () { assert.equal(spec.buildRequests([]), undefined); }); + it('should not stop on unsupported mediaType', function () { + const bid = utils.deepClone(defaultBid); + bid.mediaTypes.audio = { size: [300, 250] }; + + let bidRequests = [bid]; + let bidderRequest = { + bids: bidRequests, + refererInfo: refererInfo, + timeout: timeout, + auctionId: auctionId, + }; + + let [request] = spec.buildRequests(bidRequests, bidderRequest); + let data = JSON.parse(request.data); + + let expectedDataCopy = utils.deepClone(expectedData); + assert.exists(data.id); + + expectedDataCopy.id = data.id + assert.deepEqual(data, expectedDataCopy); + }); + it('should return proper request url: no refererInfo', function () { let [request] = spec.buildRequests([defaultBid]); assert.equal(request.url, requestUrl); }); + it('should use test endpoint when is_test is true', function () { + const bid = utils.deepClone(defaultBid); + bid.params.is_test = true; + + const [request] = spec.buildRequests([bid]); + assert.equal(request.url, `${baseUrl}/header_bid?debug_key=abcdefghijklmnop`); + }); + + it('should include aspect_ratios in native asset', function () { + const bid = utils.deepClone(defaultBid); + const aspect_ratios = [{ + min_width: 100, + ratio_width: 4, + ratio_height: 3 + }] + bid.mediaTypes.native.image.aspect_ratios = aspect_ratios; + bid.nativeParams.image.aspect_ratios = aspect_ratios; + + const [request] = spec.buildRequests([bid]); + const nativeAsset = JSON.parse(request.data).imp[0].native.request.assets.find(a => a.id === 3); + assert.equal(nativeAsset.img.wmin, 100); + assert.equal(nativeAsset.img.hmin, 75); + }); + + it('should include placement in video object if provided', function () { + const bid = utils.deepClone(defaultBid); + bid.mediaTypes.video.placement = 2; + + const [request] = spec.buildRequests([bid]); + const video = JSON.parse(request.data).imp[0].video; + assert.equal(video.placement, 2, 'placement should be passed into video object'); + }); + it('should return proper banner imp', function () { let bid = utils.deepClone(defaultBid); bid.params.bidfloor = 0; @@ -172,63 +302,19 @@ describe('mediaforce bid adapter', function () { bids: bidRequests, refererInfo: refererInfo, timeout: timeout, - auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + auctionId: auctionId, }; let [request] = spec.buildRequests(bidRequests, bidderRequest); let data = JSON.parse(request.data); - assert.deepEqual(data, { - id: data.id, - tmax: timeout, - ext: { - mediaforce: { - hb_key: bidderRequest.auctionId - } - }, - site: { - id: bid.params.publisher_id, - publisher: {id: bid.params.publisher_id}, - ref: encodeURIComponent(refererInfo.ref), - page: pageUrl, - }, - device: { - ua: navigator.userAgent, - dnt: dnt, - js: 1, - language: language, - }, - imp: [{ - tagid: bid.params.placement_id, - secure: secure, - bidfloor: bid.params.bidfloor, - ext: { - mediaforce: { - transactionId: bid.ortb2Imp.ext.tid, - } - }, - banner: {w: 300, h: 250}, - native: { - ver: '1.2', - request: { - assets: [ - {id: 1, title: {len: 800}, required: 1}, - {id: 3, img: {w: 300, h: 250, type: 3}, required: 1}, - {id: 5, data: {type: 1}, required: 1} - ], - context: 1, - plcmttype: 1, - ver: '1.2' - } - }, - }], - }); - assert.deepEqual(request, { - method: 'POST', - url: requestUrl, - data: '{"id":"' + data.id + '","site":{"page":"' + pageUrl + '","ref":"https%3A%2F%2Fwww.prebid.org","id":"pub123","publisher":{"id":"pub123"}},"device":{"ua":"' + navigator.userAgent + '","js":1,"dnt":' + dnt + ',"language":"' + language + '"},"ext":{"mediaforce":{"hb_key":"210a474e-88f0-4646-837f-4253b7cf14fb"}},"tmax":1500,"imp":[{"tagid":"202","secure":' + secure + ',"bidfloor":0,"ext":{"mediaforce":{"transactionId":"d45dd707-a418-42ec-b8a7-b70a6c6fab0b"}},"banner":{"w":300,"h":250},"native":{"ver":"1.2","request":{"assets":[{"required":1,"id":1,"title":{"len":800}},{"required":1,"id":3,"img":{"type":3,"w":300,"h":250}},{"required":1,"id":5,"data":{"type":1}}],"context":1,"plcmttype":1,"ver":"1.2"}}}]}', - }); + let expectedDataCopy = utils.deepClone(expectedData); + assert.exists(data.id); + + expectedDataCopy.id = data.id + expectedDataCopy.imp[0].bidfloor = bid.params.bidfloor + assert.deepEqual(data, expectedDataCopy); }); it('multiple sizes', function () { @@ -244,12 +330,22 @@ describe('mediaforce bid adapter', function () { assert.deepEqual(data.imp[0].banner, {w: 300, h: 600, format: [{w: 300, h: 250}]}); }); + it('should skip banner with empty sizes', function () { + const bid = utils.deepClone(defaultBid); + bid.mediaTypes.banner = { sizes: [] }; + + const [request] = spec.buildRequests([bid]); + const data = JSON.parse(request.data); + assert.notExists(data.imp[0].banner, 'Banner object should be omitted'); + }); + + it('should return proper requests for multiple imps', function () { let bidderRequest = { bids: multiBid, refererInfo: refererInfo, timeout: timeout, - auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + auctionId: auctionId, }; let requests = spec.buildRequests(multiBid, bidderRequest); @@ -267,7 +363,7 @@ describe('mediaforce bid adapter', function () { tmax: timeout, ext: { mediaforce: { - hb_key: bidderRequest.auctionId + hb_key: auctionId } }, site: { @@ -323,7 +419,7 @@ describe('mediaforce bid adapter', function () { tmax: timeout, ext: { mediaforce: { - hb_key: bidderRequest.auctionId + hb_key: auctionId } }, site: { @@ -575,6 +671,50 @@ describe('mediaforce bid adapter', function () { }); }); + describe('interpretResponse() video', function () { + it('should interpret video response correctly', function () { + const vast = '...'; + + const bid = { + adid: '2_ssl', + adm: vast, + adomain: ["www3.thehealthyfat.com"], + burl: `${baseUrl}/burl/\${AUCTION_PRICE}`, + cat: ['IAB1-1'], + cid: '2_ssl', + crid: '2_ssl', + dealid: '3901521', + id: '65599d0a-42d2-446a-9d39-6086c1433ffe', + impid: '2b3c9d103723a7', + price: 5.5, + }; + + const response = { + body: { + seatbid: [{ bid: [bid] }], + cur: 'USD', + id: '620190c2-7eef-42fa-91e2-f5c7fbc2bdd3', + } + }; + + const [result] = spec.interpretResponse(response); + + assert.deepEqual(result, { + burl: bid.burl, + cpm: bid.price, + creativeId: bid.adid, + currency: response.body.cur, + dealId: bid.dealid, + mediaType: VIDEO, + meta: { advertiserDomains: bid.adomain }, + netRevenue: true, + requestId: bid.impid, + ttl: 300, + vastXml: vast, + }); + }); + }); + describe('onBidWon()', function () { beforeEach(function() { sinon.stub(utils, 'triggerPixel'); From 8a19e0b795502b61a38d50e92ed2300ca15bdf94 Mon Sep 17 00:00:00 2001 From: Alexander Pykhteyev Date: Wed, 14 May 2025 19:26:01 +0700 Subject: [PATCH 3/6] Limelight adapter: update user sync headers (#13090) * Update limelightDigitalBidAdapter.js * Update limelightDigitalBidAdapter.js * Update user sync headers --------- Co-authored-by: apykhteyev --- modules/limelightDigitalBidAdapter.js | 4 +-- .../limelightDigitalBidAdapter_spec.js | 32 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index ffca19f5df8..e89f7a2abaa 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -114,8 +114,8 @@ export const spec = { const imageSyncs = []; for (let i = 0; i < serverResponses.length; i++) { const serverResponseHeaders = serverResponses[i].headers; - const imgSync = (serverResponseHeaders != null && syncOptions.pixelEnabled) ? serverResponseHeaders.get('X-PLL-UserSync-Image') : null - const iframeSync = (serverResponseHeaders != null && syncOptions.iframeEnabled) ? serverResponseHeaders.get('X-PLL-UserSync-Iframe') : null + const imgSync = (serverResponseHeaders != null && syncOptions.pixelEnabled) ? serverResponseHeaders.get('x-pll-usersync-image') : null + const iframeSync = (serverResponseHeaders != null && syncOptions.iframeEnabled) ? serverResponseHeaders.get('x-pll-usersync-iframe') : null if (iframeSync != null) { iframeSyncs.push(iframeSync) } else if (imgSync != null) { diff --git a/test/spec/modules/limelightDigitalBidAdapter_spec.js b/test/spec/modules/limelightDigitalBidAdapter_spec.js index b13beb26d28..c84586e9064 100644 --- a/test/spec/modules/limelightDigitalBidAdapter_spec.js +++ b/test/spec/modules/limelightDigitalBidAdapter_spec.js @@ -516,10 +516,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-lm.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-lm.ortb.net/sync.html'; } } @@ -543,10 +543,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-1.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-1.ortb.net/sync.html'; } } @@ -578,10 +578,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-lm.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-lm.ortb.net/sync.html'; } } @@ -605,10 +605,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-1.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-1.ortb.net/sync.html'; } } @@ -618,10 +618,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-2.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-2.ortb.net/sync.html'; } } @@ -649,10 +649,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-lm.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-lm.ortb.net/sync.html'; } } @@ -662,10 +662,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-lm.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-lm.ortb.net/sync.html'; } } @@ -689,10 +689,10 @@ describe('limelightDigitalAdapter', function () { { headers: { get: function (header) { - if (header === 'X-PLL-UserSync-Image') { + if (header === 'x-pll-usersync-image') { return 'https://tracker-lm.ortb.net/sync'; } - if (header === 'X-PLL-UserSync-Iframe') { + if (header === 'x-pll-usersync-iframe') { return 'https://tracker-lm.ortb.net/sync.html'; } } From d4b2a7b8f677030bd83bc214dd28f224a2ff7acb Mon Sep 17 00:00:00 2001 From: Daniel Liebner Date: Wed, 14 May 2025 08:41:44 -0400 Subject: [PATCH 4/6] Bid Glass Bid Adapter : add consent data to requests (#13097) * Added bidglass adapter + test * PR Review Updates: - Added formal params to getUserSyncs function definition - getUserSyncs now always returns an array - Improved unit test coverage * PR Review Updates: - Removed unused methods: getUserSyncs, onTimeout, onBidWon, onSetTargeting - Removed getUserSyncs unit test - Removed "dead code" - Removed some unnecessary comments - Fixed usage of parseInt * Bid Glass Bid Adapter: pass options in bid request * Merge externally set targeting params * Updates to address gulp errors * Get `bidglass` reference from window * Add support for meta.advertiserDomains * Remove numeric requirement for params.adUnitId * Add GDPR/GPP consent data to bid requests * Replace `&replaceme` in server-returned `ad` HTML with encoded GDPR/GPP params from bid request * Bid Glass Adapter: Update tests * Fix interpretResponse: based on examining other adapters it seems the 2nd param is actually a `ServerRequest` object * Fix test (add serverRequest param to spec.interpretResponse()) * Fix linting --- modules/bidglassBidAdapter.js | 47 +++++++++++++++++++---- test/spec/modules/bidglassAdapter_spec.js | 23 +++++++---- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/modules/bidglassBidAdapter.js b/modules/bidglassBidAdapter.js index c0b5bddd5f7..43762162ace 100644 --- a/modules/bidglassBidAdapter.js +++ b/modules/bidglassBidAdapter.js @@ -5,7 +5,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest - * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse */ @@ -26,9 +26,9 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests} validBidRequests an array of bids + * @param {BidRequest[]} validBidRequests an array of bids * @param {BidderRequest} bidderRequest request by bidder - * @return ServerRequest Info describing the request to the server. + * @return {ServerRequest} Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { /* @@ -102,12 +102,31 @@ export const spec = { }); }); - // Stuff to send: page URL + // Consent data + const gdprConsentObj = bidderRequest && bidderRequest.gdprConsent; + const gppConsentObj = bidderRequest && bidderRequest.gppConsent; + const gppApplicableSections = gppConsentObj && gppConsentObj.applicableSections; + const ortb2Regs = bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.regs; + const ortb2Gpp = ortb2Regs && ortb2Regs.gpp; + + // Build bid request data to be sent to ad server const bidReq = { reqId: getUniqueIdentifierStr(), imps: imps, ref: getReferer(), - ori: getOrigins() + ori: getOrigins(), + + // GDPR applies? numeric boolean + gdprApplies: (gdprConsentObj && gdprConsentObj.gdprApplies) ? 1 : '', + // IAB TCF consent string + gdprConsent: (gdprConsentObj && gdprConsentObj.consentString) || '', + + // IAB GPP consent string + gppString: (gppConsentObj && gppConsentObj.gppString) || ortb2Gpp || '', + // GPP Applicable Section IDs + gppSid: (isArray(gppApplicableSections) && gppApplicableSections.length) + ? gppApplicableSections.join(',') + : ((ortb2Gpp && ortb2Regs.gpp_sid) || '') }; let url = 'https://bid.glass/ad/hb.php?' + @@ -128,10 +147,12 @@ export const spec = { * Unpack the response from the server into a list of bids. * * @param {ServerResponse} serverResponse A successful response from the server. + * @param {ServerRequest} serverRequest The original server request for this bid * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function(serverResponse) { + interpretResponse: function(serverResponse, serverRequest) { const bidResponses = []; + const bidReq = JSON.parse(serverRequest.data); _each(serverResponse.body.bidResponses, function(serverBid) { const bidResponse = { @@ -145,7 +166,19 @@ export const spec = { mediaType: serverBid.mediaType || 'banner', netRevenue: true, ttl: serverBid.ttl || 10, - ad: serverBid.ad, + // Replace the &replaceme placeholder in the returned ', 'cpm': '0.01', 'creativeId': '-1', 'width': '300', @@ -86,14 +95,14 @@ describe('Bid Glass Adapter', function () { 'mediaType': 'banner', 'netRevenue': true, 'ttl': 10, - 'ad': '', + 'ad': '', 'meta': { 'advertiserDomains': ['https://example.com'] } }]; - let result = spec.interpretResponse(response); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + let result = spec.interpretResponse(serverResponse, serverRequest); + expect(result[0]).to.deep.equal(expectedResponse[0]); }); it('handles empty bid response', function () { @@ -102,7 +111,7 @@ describe('Bid Glass Adapter', function () { 'bidResponses': [] } }; - let result = spec.interpretResponse(response); + let result = spec.interpretResponse(response, serverRequest); expect(result.length).to.equal(0); }); }); From 81e6e46eff34997826c52ab181bf9ea9a1169b3b Mon Sep 17 00:00:00 2001 From: f-cali Date: Wed, 14 May 2025 16:11:12 +0200 Subject: [PATCH 5/6] Onetag Bid Adapter : added native legacy support (#13084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MAINTAG-386 Citynews (76d8a3332520158) - Prebid Version Update Issue * MAINTAG-323 Native & Legacy Native | Gestione della retrocompatibilità * Add if statement on native tests for ortb format * Extended tests to native legacy version * Make tests more general * Fix native tests and priceFloor handling for native mediaType --------- Co-authored-by: Federico Liccione --- modules/onetagBidAdapter.js | 77 +++++++---- test/spec/modules/onetagBidAdapter_spec.js | 153 ++++++++++++++++----- 2 files changed, 165 insertions(+), 65 deletions(-) diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index eec7dca9c23..3de010f497f 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -8,6 +8,7 @@ import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { deepClone, logError, deepAccess, getWinDimensions } from '../src/utils.js'; import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +import { toOrtbNativeRequest } from '../src/native.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -39,6 +40,10 @@ export function hasTypeVideo(bid) { return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.video !== 'undefined'; } +export function hasTypeNative(bid) { + return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.native !== 'undefined'; +} + export function isValid(type, bid) { if (type === BANNER) { return parseSizes(bid).length > 0; @@ -49,13 +54,18 @@ export function isValid(type, bid) { } } else if (type === NATIVE) { if (typeof bid.mediaTypes.native !== 'object' || bid.mediaTypes.native === null) return false; - - const assets = bid.mediaTypes.native?.ortb?.assets; - const eventTrackers = bid.mediaTypes.native?.ortb?.eventtrackers; + if (!isNativeOrtbVersion(bid)) { + if (bid.nativeParams === undefined) return false; + const ortbConversion = toOrtbNativeRequest(bid.nativeParams); + return ortbConversion && ortbConversion.assets && Array.isArray(ortbConversion.assets) && ortbConversion.assets.length > 0 && ortbConversion.assets.every(asset => isValidAsset(asset)); + } let isValidAssets = false; let isValidEventTrackers = false; + const assets = bid.mediaTypes.native?.ortb?.assets; + const eventTrackers = bid.mediaTypes.native?.ortb?.eventtrackers; + if (assets && Array.isArray(assets) && assets.length > 0 && assets.every(asset => isValidAsset(asset))) { isValidAssets = true; } @@ -80,12 +90,11 @@ const isValidEventTracker = function(et) { } const isValidAsset = function(asset) { - if (!asset.id || !Number.isInteger(asset.id)) return false; + if (!asset.hasOwnProperty("id") || !Number.isInteger(asset.id)) return false; const hasValidContent = asset.title || asset.img || asset.data || asset.video; if (!hasValidContent) return false; if (asset.title && (!asset.title.len || !Number.isInteger(asset.title.len))) return false; - if (asset.img && ((!asset.img.wmin || !Number.isInteger(asset.img.wmin)) || (!asset.img.hmin || !Number.isInteger(asset.img.hmin)))) return false; - if (asset.data && !asset.data.type) return false; + if (asset.data && (!asset.data.type || !Number.isInteger(asset.data.type))) return false; if (asset.video && (!asset.video.mimes || !asset.video.minduration || !asset.video.maxduration || !asset.video.protocols)) return false; return true; } @@ -294,7 +303,7 @@ function getPageInfo(bidderRequest) { timing: getTiming(), version: { prebid: '$prebid.version$', - adapter: '1.1.2' + adapter: '1.1.3' } }; } @@ -324,17 +333,27 @@ function requestsToBids(bidRequests) { return bannerObj; }); const nativeBidRequests = bidRequests.filter(bidRequest => isValid(NATIVE, bidRequest)).map(bidRequest => { - const bannerObj = {}; - setGeneralInfo.call(bannerObj, bidRequest); - bannerObj['sizes'] = parseSizes(bidRequest); - bannerObj['type'] = NATIVE + NATIVE_SUFFIX; - bannerObj['mediaTypeInfo'] = deepClone(bidRequest.mediaTypes.native); - bannerObj['priceFloors'] = getBidFloor(bidRequest, NATIVE, bannerObj['sizes']); - return bannerObj; + const nativeObj = {}; + setGeneralInfo.call(nativeObj, bidRequest); + nativeObj['sizes'] = parseSizes(bidRequest); + nativeObj['type'] = NATIVE + NATIVE_SUFFIX; + nativeObj['mediaTypeInfo'] = deepClone(bidRequest.mediaTypes.native); + if (!isNativeOrtbVersion(bidRequest)) { + const ortbConversion = toOrtbNativeRequest(bidRequest.nativeParams); + nativeObj['mediaTypeInfo'] = {}; + nativeObj['mediaTypeInfo'].adTemplate = bidRequest.nativeParams.adTemplate; + nativeObj['mediaTypeInfo'].ortb = ortbConversion; + } + nativeObj['priceFloors'] = getBidFloor(bidRequest, NATIVE, nativeObj['sizes']); + return nativeObj; }); return videoBidRequests.concat(bannerBidRequests).concat(nativeBidRequests); } +function isNativeOrtbVersion(bidRequest) { + return bidRequest.mediaTypes.native.ortb && typeof bidRequest.mediaTypes.native.ortb === 'object'; +} + function setGeneralInfo(bidRequest) { const params = bidRequest.params; this['adUnitCode'] = bidRequest.adUnitCode; @@ -458,20 +477,24 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gpp } function getBidFloor(bidRequest, mediaType, sizes) { - const priceFloors = []; - if (typeof bidRequest.getFloor === 'function') { - sizes.forEach(size => { - const floor = bidRequest.getFloor({ - currency: 'EUR', - mediaType: mediaType || '*', - size: [size.width, size.height] - }) || {}; - floor.size = deepClone(size); - if (!floor.floor) { floor.floor = null; } - priceFloors.push(floor); - }); + if (typeof bidRequest.getFloor !== 'function') return []; + const getFloorObject = (size) => { + const floorData = bidRequest.getFloor({ + currency: 'EUR', + mediaType: mediaType || '*', + size: size || '*' + }) || {}; + + return { + ...floorData, + size: size ? deepClone(size) : undefined, + floor: floorData.floor != null ? floorData.floor : null + }; + }; + if (Array.isArray(sizes) && sizes.length > 0) { + return sizes.map(size => getFloorObject([size.width, size.height])); } - return priceFloors; + return [getFloorObject('*')]; } export function isSchainValid(schain) { diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index a274b141fd6..ac78b2b9ed1 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -3,6 +3,8 @@ import { expect } from 'chai'; import { find } from 'src/polyfill.js'; import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; import { INSTREAM, OUTSTREAM } from 'src/video.js'; +import { toOrtbNativeRequest } from 'src/native.js'; +import { hasTypeNative } from '../../../modules/onetagBidAdapter'; const NATIVE_SUFFIX = 'Ad'; @@ -44,6 +46,57 @@ describe('onetag', function () { }; } + function createNativeLegacyBid(bidRequest) { + let bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + bid.mediaTypes.native = { + adTemplate: "
\"##hb_native_brand##\"
##hb_native_brand##

##hb_native_title##

##hb_native_cta####hb_native_brand##
", + title: { + required: 1, + sendId: 1 + }, + body: { + required: 1, + sendId: 1 + }, + cta: { + required: 0, + sendId: 1 + }, + displayUrl: { + required: 0, + sendId: 1 + }, + icon: { + required: 0, + sendId: 1 + }, + image: { + required: 1, + sendId: 1 + }, + sponsoredBy: { + required: 1, + sendId: 1 + } + } + bid = addNativeParams(bid); + const ortbConversion = toOrtbNativeRequest(bid.nativeParams); + bid.mediaTypes.native = {}; + bid.mediaTypes.native.adTemplate = bid.nativeParams.adTemplate; + bid.mediaTypes.native.ortb = ortbConversion; + return bid; + } + + function addNativeParams(bidRequest) { + let bidParams = bidRequest.nativeParams || {}; + for (const property in bidRequest.mediaTypes.native) { + bidParams[property] = bidRequest.mediaTypes.native[property]; + } + bidRequest.nativeParams = bidParams; + return bidRequest; + } + function createNativeBid(bidRequest) { const bid = bidRequest || createBid(); bid.mediaTypes = bid.mediaTypes || {}; @@ -54,7 +107,7 @@ describe('onetag', function () { assets: [{ id: 1, required: 1, - title: { + title: { len: 140 } }, @@ -81,7 +134,7 @@ describe('onetag', function () { minduration: 5, maxduration: 30, protocols: [2, 3] - } + } }], eventtrackers: [{ event: 1, @@ -128,12 +181,13 @@ describe('onetag', function () { return createInstreamVideoBid(createBannerBid()); } - let bannerBid, instreamVideoBid, outstreamVideoBid, nativeBid; + let bannerBid, instreamVideoBid, outstreamVideoBid, nativeBid, nativeLegacyBid; beforeEach(() => { bannerBid = createBannerBid(); instreamVideoBid = createInstreamVideoBid(); outstreamVideoBid = createOutstreamVideoBid(); nativeBid = createNativeBid(); + nativeLegacyBid = createNativeLegacyBid(); }) describe('isBidRequestValid', function () { @@ -150,86 +204,94 @@ describe('onetag', function () { }); describe('banner bidRequest', function () { it('Should return false when the sizes array is empty', function () { - // TODO (dgirardi): this test used to pass because `bannerBid` was global state - // and other test code made it invalid for reasons other than sizes. - // cleaning up the setup code, it now (correctly) fails. - bannerBid.sizes = []; - // expect(spec.isBidRequestValid(bannerBid)).to.be.false; + bannerBid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); }); describe('native bidRequest', function () { it('Should return true when correct native bid is passed', function () { const nativeBid = createNativeBid(); - expect(spec.isBidRequestValid(nativeBid)).to.be.true; + const nativeLegacyBid = createNativeLegacyBid(); + expect(spec.isBidRequestValid(nativeBid)).to.be.true && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.true; }); it('Should return false when native is not an object', function () { const nativeBid = createNativeBid(); - nativeBid.mediaTypes.native = 30; - expect(spec.isBidRequestValid(nativeBid)).to.be.false; + const nativeLegacyBid = createNativeLegacyBid(); + nativeBid.mediaTypes.native = nativeLegacyBid.mediaTypes.native = 30; + expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false; }); - it('Should return false when native.ortb is not an object', function () { + it('Should return false when native.ortb if defined but it isn\'t an object', function () { const nativeBid = createNativeBid(); nativeBid.mediaTypes.native.ortb = 30 || 'string'; expect(spec.isBidRequestValid(nativeBid)).to.be.false; }); it('Should return false when native.ortb.assets is not an array', function () { const nativeBid = createNativeBid(); - nativeBid.mediaTypes.native.ortb.assets = 30; - expect(spec.isBidRequestValid(nativeBid)).to.be.false; + const nativeLegacyBid = createNativeLegacyBid(); + nativeBid.mediaTypes.native.ortb.assets = nativeLegacyBid.mediaTypes.native.ortb.assets = 30; + expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false; }); it('Should return false when native.ortb.assets is an empty array', function () { const nativeBid = createNativeBid(); - nativeBid.mediaTypes.native.ortb.assets = []; - expect(spec.isBidRequestValid(nativeBid)).to.be.false; + const nativeLegacyBid = createNativeLegacyBid(); + nativeBid.mediaTypes.native.ortb.assets = nativeLegacyBid.mediaTypes.native.ortb.assets = []; + expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] doesnt have \'id\'', function () { const nativeBid = createNativeBid(); + const nativeLegacyBid = createNativeLegacyBid(); Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0], 'id'); - expect(spec.isBidRequestValid(nativeBid)).to.be.false; + Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[0], 'id'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] doesnt have any of \'title\', \'img\', \'data\' and \'video\' properties', function () { const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0], 'title'); - expect(spec.isBidRequestValid(nativeBid)).to.be.false; + const nativeLegacyBid = createNativeLegacyBid(); + const titleIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title); + const legacyTitleIndex = nativeLegacyBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[titleIndex], 'title'); + Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[legacyTitleIndex], 'title'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] have title, but doesnt have \'len\' property', function () { const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0].title, 'len'); - expect(spec.isBidRequestValid(nativeBid)).to.be.false; - }); - it('Should return false when native.ortb.assets[i] is image but doesnt have \'wmin\' property', function () { - const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[1].img, 'wmin'); - expect(spec.isBidRequestValid(nativeBid)).to.be.false; - }); - it('Should return false when native.ortb.assets[i] is image but doesnt have \'hmin\' property', function () { - const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[1].img, 'hmin'); - expect(spec.isBidRequestValid(nativeBid)).to.be.false; + const nativeLegacyBid = createNativeLegacyBid(); + const titleIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title); + const legacyTitleIndex = nativeLegacyBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[titleIndex].title, 'len'); + Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[legacyTitleIndex].title, 'len'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] is data but doesnt have \'type\' property', function () { const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[2].data, 'type'); - expect(spec.isBidRequestValid(nativeBid)).to.be.false; + const nativeLegacyBid = createNativeLegacyBid(); + const dataIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.data); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[dataIndex].data, 'type'); + Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[dataIndex].data, 'type'); + expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] is video but doesnt have \'mimes\' property', function () { const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'mimes'); + const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'mimes'); expect(spec.isBidRequestValid(nativeBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] is video but doesnt have \'minduration\' property', function () { const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'minduration'); + const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'minduration'); expect(spec.isBidRequestValid(nativeBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] is video but doesnt have \'maxduration\' property', function () { const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'maxduration'); + const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'maxduration'); expect(spec.isBidRequestValid(nativeBid)).to.be.false; }); it('Should return false when native.ortb.assets[i] is video but doesnt have \'protocols\' property', function () { const nativeBid = createNativeBid(); - Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'protocols'); + const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video); + Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'protocols'); expect(spec.isBidRequestValid(nativeBid)).to.be.false; }); it('Should return false when native.ortb.eventtrackers is not an array', function () { @@ -324,7 +386,7 @@ describe('onetag', function () { describe('buildRequests', function () { let serverRequest, data; before(() => { - serverRequest = spec.buildRequests([bannerBid, instreamVideoBid, nativeBid]); + serverRequest = spec.buildRequests([bannerBid, instreamVideoBid, nativeBid, nativeLegacyBid]); data = JSON.parse(serverRequest.data); }); @@ -387,6 +449,21 @@ describe('onetag', function () { 'type', 'priceFloors' ); + } else if (hasTypeNative(bid)) { + expect(bid).to.have.all.keys( + 'adUnitCode', + 'auctionId', + 'bidId', + 'bidderRequestId', + 'pubId', + 'ortb2Imp', + 'transactionId', + 'mediaTypeInfo', + 'sizes', + 'type', + 'priceFloors' + ) && + expect(bid.mediaTypeInfo).to.have.key('ortb'); } else if (isValid(BANNER, bid)) { expect(bid).to.have.all.keys( 'adUnitCode', From f3945367a4240b4aadeb298721b03b3cbf247b42 Mon Sep 17 00:00:00 2001 From: "aarprice@publicisgroupe.net" Date: Wed, 14 May 2025 16:45:19 -0700 Subject: [PATCH 6/6] Epsilon - removing analytics adapter --- modules/conversantAnalyticsAdapter.js | 699 ----------- .../conversantAnalyticsAdapter_spec.js | 1113 ----------------- 2 files changed, 1812 deletions(-) delete mode 100644 modules/conversantAnalyticsAdapter.js delete mode 100644 test/spec/modules/conversantAnalyticsAdapter_spec.js diff --git a/modules/conversantAnalyticsAdapter.js b/modules/conversantAnalyticsAdapter.js deleted file mode 100644 index a08eed41c53..00000000000 --- a/modules/conversantAnalyticsAdapter.js +++ /dev/null @@ -1,699 +0,0 @@ -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 {logInfo, logWarn, logError, logMessage, deepAccess, isInteger} from '../src/utils.js'; -import {getRefererInfo} from '../src/refererDetection.js'; - -// Maintainer: mediapsr@epsilon.com - -const { AUCTION_END, AD_RENDER_FAILED, BID_TIMEOUT, BID_WON, BIDDER_ERROR } = EVENTS; -// STALE_RENDER, TCF2_ENFORCEMENT would need to add extra calls for these as they likely occur after AUCTION_END? -const GVLID = 24; -const ANALYTICS_TYPE = 'endpoint'; - -// for local testing set domain to 127.0.0.1:8290 -const DOMAIN = 'https://web.hb.ad.cpe.dotomi.com/'; -const ANALYTICS_URL = DOMAIN + 'cvx/event/prebidanalytics'; -const ERROR_URL = DOMAIN + 'cvx/event/prebidanalyticerrors'; -const ANALYTICS_CODE = 'conversant'; -const ANALYTICS_ALIASES = [ANALYTICS_CODE, 'epsilon', 'cnvr']; - -export const CNVR_CONSTANTS = { - LOG_PREFIX: 'Conversant analytics adapter: ', - ERROR_MISSING_DATA_PREFIX: 'Parsing method failed because of missing data: ', - // Maximum time to keep an item in the cache before it gets purged - MAX_MILLISECONDS_IN_CACHE: 30000, - // How often cache cleanup will run - CACHE_CLEANUP_TIME_IN_MILLIS: 30000, - // Should be float from 0-1, 0 is turned off, 1 is sample every instance - DEFAULT_SAMPLE_RATE: 1, - - // BID STATUS CODES - WIN: 10, - BID: 20, - NO_BID: 30, - TIMEOUT: 40, - RENDER_FAILED: 50 -}; - -// Saves passed in options from the bid adapter -const initOptions = {}; - -// Simple flag to help handle any tear down needed on disable -let conversantAnalyticsEnabled = false; - -export const cnvrHelper = { - // Turns on sampling for an instance of prebid analytics. - doSample: true, - doSendErrorData: false, - - /** - * Used to hold data for RENDER FAILED events so we can send a payload back that will match our original auction data. - * Contains the following key/value data: - * => { - * 'bidderCode': , - * 'adUnitCode': , - * 'auctionId': , - * 'timeReceived': Date.now() //For cache cleaning - * } - */ - adIdLookup: {}, - - /** - * Time out events happen before AUCTION END so we can save them in a cache and report them at the same time as the - * AUCTION END event. Has the following data and key is based off of auctionId, adUnitCode, bidderCode from - * keyStr = getLookupKey(auctionId, adUnitCode, bidderCode); - * => { - * timeReceived: Date.now() //so cache can be purged in case it doesn't get cleaned out at auctionEnd - * } - */ - timeoutCache: {}, - - /** - * Lookup of auction IDs to auction start timestamps - */ - auctionIdTimestampCache: {}, - - /** - * Capture any bidder errors and bundle them with AUCTION_END - */ - bidderErrorCache: {} -}; - -/** - * Cleanup timer for the adIdLookup and timeoutCache caches. If all works properly then the caches are self-cleaning - * but in case something goes sideways we poll periodically to cleanup old values to prevent a memory leak - */ -let cacheCleanupInterval; - -let conversantAnalytics = Object.assign( - adapter({URL: ANALYTICS_URL, ANALYTICS_TYPE}), - { - track({eventType, args}) { - try { - if (cnvrHelper.doSample) { - logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' track(): ' + eventType, args); - switch (eventType) { - case AUCTION_END: - onAuctionEnd(args); - break; - case AD_RENDER_FAILED: - onAdRenderFailed(args); - break; - case BID_WON: - onBidWon(args); - break; - case BID_TIMEOUT: - onBidTimeout(args); - break; - case BIDDER_ERROR: - onBidderError(args) - } // END switch - } else { - logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' - ' + eventType + ': skipped due to sampling'); - }// END IF(cnvrHelper.doSample) - } catch (e) { - // e = {stack:"...",message:"..."} - logError(CNVR_CONSTANTS.LOG_PREFIX + 'Caught error in handling ' + eventType + ' event: ' + e.message); - cnvrHelper.sendErrorData(eventType, e); - } - } // END track() - } -); - -// ================================================== EVENT HANDLERS =================================================== - -/** - * Handler for BIDDER_ERROR events, tries to capture as much data, save it in cache which is then picked up by - * AUCTION_END event and included in that payload. Was not able to see an easy way to get adUnitCode in this event - * so not including it for now. - * https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error - * Trigger when the HTTP response status code is not between 200-299 and not equal to 304. - { - error: XMLHttpRequest, https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest - bidderRequest: { https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error - { - auctionId: "b06c5141-fe8f-4cdf-9d7d-54415490a917", - auctionStart: 1579746300522, - bidderCode: "myBidderCode", - bidderRequestId: "15246a574e859f", - bids: [{...}], - gdprConsent: {consentString: "BOtmiBKOtmiBKABABAENAFAAAAACeAAA", vendorData: {...}, gdprApplies: true}, - refererInfo: { - canonicalUrl: null, - page: "http://mypage.org?pbjs_debug=true", - domain: "mypage.org", - ref: null, - numIframes: 0, - reachedTop: true, - isAmp: false, - stack: ["http://mypage.org?pbjs_debug=true"] - } - } - } -} - */ -function onBidderError(args) { - if (!cnvrHelper.doSendErrorData) { - logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'Skipping bidder error parsing due to config disabling error logging, bidder error status = ' + args.error.status + ', Message = ' + args.error.statusText); - return; - } - - let error = args.error; - let bidRequest = args.bidderRequest; - let auctionId = bidRequest.auctionId; - let bidderCode = bidRequest.bidderCode; - logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onBidderError(): error received from bidder ' + bidderCode + '. Status = ' + error.status + ', Message = ' + error.statusText); - let errorObj = { - status: error.status, - message: error.statusText, - bidderCode: bidderCode, - url: cnvrHelper.getPageUrl(), - }; - if (cnvrHelper.bidderErrorCache[auctionId]) { - cnvrHelper.bidderErrorCache[auctionId]['errors'].push(errorObj); - } else { - cnvrHelper.bidderErrorCache[auctionId] = { - errors: [errorObj], - timeReceived: Date.now() - }; - } -} - -/** - * We get the list of timeouts before the endAution, cache them temporarily in a global cache and the endAuction event - * will pick them up. Uses getLookupKey() to create the key to the entry from auctionId, adUnitCode and bidderCode. - * Saves a single value of timeReceived so we can do cache purging periodically. - * - * Current assumption is that the timeout will always be an array even if it is just one object in the array. - * @param args [{ - "bidId": "80882409358b8a8", - "bidder": "conversant", - "adUnitCode": "MedRect", - "auctionId": "afbd6e0b-e45b-46ab-87bf-c0bac0cb8881" - }, { - "bidId": "9da4c107a6f24c8", - "bidder": "conversant", - "adUnitCode": "Leaderboard", - "auctionId": "afbd6e0b-e45b-46ab-87bf-c0bac0cb8881" - } - ] - */ -function onBidTimeout(args) { - args.forEach(timedOutBid => { - const timeoutCacheKey = cnvrHelper.getLookupKey(timedOutBid.auctionId, timedOutBid.adUnitCode, timedOutBid.bidder); - cnvrHelper.timeoutCache[timeoutCacheKey] = { - timeReceived: Date.now() - } - }); -} - -/** - * Bid won occurs after auctionEnd so we need to send this separately. We also save an entry in the adIdLookup cache - * so that if the render fails we can match up important data so we can send a valid RENDER FAILED event back. - * @param args bidWon args - */ -function onBidWon(args) { - const bidderCode = args.bidderCode; - const adUnitCode = args.adUnitCode; - const auctionId = args.auctionId; - let timestamp = args.requestTimestamp ? args.requestTimestamp : Date.now(); - - // Make sure we have all the data we need - if (!bidderCode || !adUnitCode || !auctionId) { - let errorReason = 'auction id'; - if (!bidderCode) { - errorReason = 'bidder code'; - } else if (!adUnitCode) { - errorReason = 'ad unit code' - } - throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorReason); - } - - if (cnvrHelper.auctionIdTimestampCache[auctionId]) { - timestamp = cnvrHelper.auctionIdTimestampCache[auctionId].timeReceived; // Don't delete, could be multiple winners/auction, allow cleanup to handle - } - - const bidWonPayload = cnvrHelper.createPayload('bid_won', auctionId, timestamp); - - const adUnitPayload = cnvrHelper.createAdUnit(); - bidWonPayload.adUnits[adUnitCode] = adUnitPayload; - - const bidPayload = cnvrHelper.createBid(CNVR_CONSTANTS.WIN, args.timeToRespond); - bidPayload.adSize = cnvrHelper.createAdSize(args.width, args.height); - bidPayload.cpm = args.cpm; - bidPayload.originalCpm = args.originalCpm; - bidPayload.currency = args.currency; - bidPayload.mediaType = args.mediaType; - adUnitPayload.bids[bidderCode] = [bidPayload]; - - if (!cnvrHelper.adIdLookup[args.adId]) { - cnvrHelper.adIdLookup[args.adId] = { - 'bidderCode': bidderCode, - 'adUnitCode': adUnitCode, - 'auctionId': auctionId, - 'timeReceived': Date.now() // For cache cleaning - }; - } - - sendData(bidWonPayload); -} - -/** - * RENDER FAILED occurs after AUCTION END and BID WON, the payload does not have all the data we need so we use - * adIdLookup to pull data from a BID WON event to populate our payload - * @param args = { - * reason: - * message: - * adId: --optional - * bid: {object?} --optional: unsure what this looks like but guessing it is {bidder: , params: {object}} - * } - */ -function onAdRenderFailed(args) { - const adId = args.adId; - // Make sure we have all the data we need, adId is optional so it's not guaranteed, without that we can't match it up - // to our adIdLookup data. - if (!adId || !cnvrHelper.adIdLookup[adId]) { - let errorMsg = 'ad id'; - if (adId) { - errorMsg = 'no lookup data for ad id'; - } - // Either no adId to match against a bidWon event, or no data saved from a bidWon event that matches the adId - throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorMsg); - } - const adIdObj = cnvrHelper.adIdLookup[adId]; - const adUnitCode = adIdObj['adUnitCode']; - const bidderCode = adIdObj['bidderCode']; - const auctionId = adIdObj['auctionId']; - delete cnvrHelper.adIdLookup[adId]; // cleanup our cache - - if (!bidderCode || !adUnitCode || !auctionId) { - let errorReason = 'auction id'; - if (!bidderCode) { - errorReason = 'bidder code'; - } else if (!adUnitCode) { - errorReason = 'ad unit code' - } - throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorReason); - } - - let timestamp = Date.now(); - if (cnvrHelper.auctionIdTimestampCache[auctionId]) { - timestamp = cnvrHelper.auctionIdTimestampCache[auctionId].timeReceived; // Don't delete, could be multiple winners/auction, allow cleanup to handle - } - - const renderFailedPayload = cnvrHelper.createPayload('render_failed', auctionId, timestamp); - const adUnitPayload = cnvrHelper.createAdUnit(); - adUnitPayload.bids[bidderCode] = [cnvrHelper.createBid(CNVR_CONSTANTS.RENDER_FAILED, 0)]; - adUnitPayload.bids[bidderCode][0].message = 'REASON: ' + args.reason + '. MESSAGE: ' + args.message; - renderFailedPayload.adUnits[adUnitCode] = adUnitPayload; - sendData(renderFailedPayload); -} - -/** - * AUCTION END contains bid and no bid info and all of the auction info we need. This sends the bulk of the information - * about the auction back to the servers. It will also check the timeoutCache for any matching bids, if any are found - * then they will be removed from the cache and send back with this payload. - * @param args AUCTION END payload, fairly large data structure, main objects are 'adUnits[]', 'bidderRequests[]', - * 'noBids[]', 'bidsReceived[]'... 'winningBids[]' seems to be always blank. - */ -function onAuctionEnd(args) { - const auctionId = args.auctionId; - if (!auctionId) { - throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + 'auction id'); - } - - const auctionTimestamp = args.timestamp ? args.timestamp : Date.now(); - cnvrHelper.auctionIdTimestampCache[auctionId] = { timeReceived: auctionTimestamp }; - - const auctionEndPayload = cnvrHelper.createPayload('auction_end', auctionId, auctionTimestamp); - // Get bid request information from adUnits - if (!Array.isArray(args.adUnits)) { - throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + 'no adUnits in event args'); - } - - // Write out any bid errors - if (cnvrHelper.bidderErrorCache[auctionId]) { - auctionEndPayload.bidderErrors = cnvrHelper.bidderErrorCache[auctionId].errors; - delete cnvrHelper.bidderErrorCache[auctionId]; - } - - args.adUnits.forEach(adUnit => { - const cnvrAdUnit = cnvrHelper.createAdUnit(); - // Initialize bids with bidderCode - adUnit.bids.forEach(bid => { - cnvrAdUnit.bids[bid.bidder] = []; // support multiple bids from a bidder for different sizes/media types //cnvrHelper.initializeBidDefaults(); - - // Check for cached timeout responses - const timeoutKey = cnvrHelper.getLookupKey(auctionId, adUnit.code, bid.bidder); - if (cnvrHelper.timeoutCache[timeoutKey]) { - cnvrAdUnit.bids[bid.bidder].push(cnvrHelper.createBid(CNVR_CONSTANTS.TIMEOUT, args.timeout)); - delete cnvrHelper.timeoutCache[timeoutKey]; - } - }); - - // Ad media types for the ad slot - if (cnvrHelper.keyExistsAndIsObject(adUnit, 'mediaTypes')) { - Object.entries(adUnit.mediaTypes).forEach(([mediaTypeName]) => { - cnvrAdUnit.mediaTypes.push(mediaTypeName); - }); - } - - // Ad sizes listed under the size key - if (Array.isArray(adUnit.sizes) && adUnit.sizes.length >= 1) { - adUnit.sizes.forEach(size => { - if (!Array.isArray(size) || size.length !== 2) { - logMessage(CNVR_CONSTANTS.LOG_PREFIX + 'Unknown object while retrieving adUnit sizes.', adUnit); - return; // skips to next item - } - cnvrAdUnit.sizes.push(cnvrHelper.createAdSize(size[0], size[1])); - }); - } - - // If the Ad Slot is not unique then ad sizes and media types merge them together - if (auctionEndPayload.adUnits[adUnit.code]) { - // Merge ad sizes - Array.prototype.push.apply(auctionEndPayload.adUnits[adUnit.code].sizes, cnvrAdUnit.sizes); - // Merge mediaTypes - Array.prototype.push.apply(auctionEndPayload.adUnits[adUnit.code].mediaTypes, cnvrAdUnit.mediaTypes); - } else { - auctionEndPayload.adUnits[adUnit.code] = cnvrAdUnit; - } - }); - - if (Array.isArray(args.noBids)) { - args.noBids.forEach(noBid => { - const bidPayloadArray = deepAccess(auctionEndPayload, 'adUnits.' + noBid.adUnitCode + '.bids.' + noBid.bidder); - - if (bidPayloadArray) { - bidPayloadArray.push(cnvrHelper.createBid(CNVR_CONSTANTS.NO_BID, 0)); // no time to respond info for this, would have to capture event and save it there - } else { - logMessage(CNVR_CONSTANTS.LOG_PREFIX + 'Unable to locate bid object via adUnitCode/bidderCode in payload for noBid reply in END_AUCTION', Object.assign({}, noBid)); - } - }); - } else { - logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): noBids not defined in arguments.'); - } - - // Get bid data from bids sent - if (Array.isArray(args.bidsReceived)) { - args.bidsReceived.forEach(bid => { - const bidPayloadArray = deepAccess(auctionEndPayload, 'adUnits.' + bid.adUnitCode + '.bids.' + bid.bidderCode); - if (bidPayloadArray) { - const bidPayload = cnvrHelper.createBid(CNVR_CONSTANTS.BID, bid.timeToRespond); - bidPayload.originalCpm = bid.originalCpm; - bidPayload.cpm = bid.cpm; - bidPayload.currency = bid.currency; - bidPayload.mediaType = bid.mediaType; - bidPayload.adSize = { - 'w': bid.width, - 'h': bid.height - }; - bidPayloadArray.push(bidPayload); - } else { - logMessage(CNVR_CONSTANTS.LOG_PREFIX + 'Unable to locate bid object via adUnitCode/bidderCode in payload for bid reply in END_AUCTION', Object.assign({}, bid)); - } - }); - } else { - logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): bidsReceived not defined in arguments.'); - } - // We need to remove any duplicate ad sizes from merging ad-slots or overlap in different media types and also - // media-types from merged ad-slots in twin bids. - Object.keys(auctionEndPayload.adUnits).forEach(function(adCode) { - auctionEndPayload.adUnits[adCode].sizes = cnvrHelper.deduplicateArray(auctionEndPayload.adUnits[adCode].sizes); - auctionEndPayload.adUnits[adCode].mediaTypes = cnvrHelper.deduplicateArray(auctionEndPayload.adUnits[adCode].mediaTypes); - }); - - sendData(auctionEndPayload); -} - -// =============================================== START OF HELPERS =================================================== - -/** - * Helper to verify a key exists and is a data type of Object (not a function, or array) - * @param parent The parent that we want to check the key for - * @param key The key which we want to check - * @returns {boolean} True if it's an object and exists, false otherwise (null, array, primitive, function) - */ -cnvrHelper.keyExistsAndIsObject = function (parent, key) { - if (!parent.hasOwnProperty(key)) { - return false; - } - return typeof parent[key] === 'object' && - !Array.isArray(parent[key]) && - parent[key] !== null; -} - -/** - * De-duplicate an array that could contain primitives or objects/associative arrays. - * A temporary array is used to store a string representation of each object that we look at. If an object matches - * one found in the temp array then it is ignored. - * @param array An array - * @returns {*} A de-duplicated array. - */ -cnvrHelper.deduplicateArray = function(array) { - if (!array || !Array.isArray(array)) { - return array; - } - - const tmpArray = []; - return array.filter(function (tmpObj) { - if (tmpArray.indexOf(JSON.stringify(tmpObj)) < 0) { - tmpArray.push(JSON.stringify(tmpObj)); - return tmpObj; - } - }); -}; - -/** - * Generic method to look at each key/value pair of a cache object and looks at the 'timeReceived' key, if more than - * the max wait time has passed then just delete the key. - * @param cacheObj one of our cache objects [adIdLookup or timeoutCache] - * @param currTime the current timestamp at the start of the most recent timer execution. - */ -cnvrHelper.cleanCache = function(cacheObj, currTime) { - Object.keys(cacheObj).forEach(key => { - const timeInCache = currTime - cacheObj[key].timeReceived; - if (timeInCache >= CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE) { - delete cacheObj[key]; - } - }); -}; - -/** - * Helper to create an object lookup key for our timeoutCache - * @param auctionId id of the auction - * @param adUnitCode ad unit code - * @param bidderCode bidder code - * @returns string concatenation of all the params into a string key for timeoutCache - */ -cnvrHelper.getLookupKey = function(auctionId, adUnitCode, bidderCode) { - return auctionId + '-' + adUnitCode + '-' + bidderCode; -}; - -/** - * Creates our root payload object that gets sent back to the server - * @param payloadType string type of payload (AUCTION_END, BID_WON, RENDER_FAILED) - * @param auctionId id for the auction - * @param timestamp timestamp in milliseconds of auction start time. - * @returns - * {{ - * requestType: *, - * adUnits: {}, - * auction: { - * auctionId: *, - * preBidVersion: *, - * sid: *} - * }} Basic structure of our object that we return to the server. - */ -cnvrHelper.createPayload = function(payloadType, auctionId, timestamp) { - return { - requestType: payloadType, - globalSampleRate: initOptions.global_sample_rate, - cnvrSampleRate: initOptions.cnvr_sample_rate, - auction: { - auctionId: auctionId, - preBidVersion: '$prebid.version$', - sid: initOptions.site_id, - auctionTimestamp: timestamp - }, - adUnits: {}, - bidderErrors: [] - }; -}; - -/** - * Helper to create an adSize object, if the value passed in is not an int then set it to -1 - * @param width in pixels (must be an int) - * @param height in peixl (must be an int) - * @returns {{w: *, h: *}} a fully valid adSize object - */ -cnvrHelper.createAdSize = function(width, height) { - if (!isInteger(width)) { - width = -1; - } - if (!isInteger(height)) { - height = -1; - } - return { - 'w': width, - 'h': height - }; -}; - -/** - * Helper to create the basic structure of our adUnit payload - * @returns {{sizes: [], bids: {}}} Basic adUnit payload structure as follows - */ -cnvrHelper.createAdUnit = function() { - return { - sizes: [], - mediaTypes: [], - bids: {} - }; -}; - -/** - * Helper to create a basic bid payload object. - */ -cnvrHelper.createBid = function (eventCode, timeToRespond) { - return { - 'eventCodes': [eventCode], - 'timeToRespond': timeToRespond - }; -}; - -/** - * Helper to get the sampling rates from an object and validate the result. - * @param parentObj Parent object that has the sampling property - * @param propNm Name of the sampling property - * @param defaultSampleRate A default value to apply if there is a problem - * @returns {number} returns a float number from 0 (always off) to 1 (always on) - */ -cnvrHelper.getSampleRate = function(parentObj, propNm, defaultSampleRate) { - let sampleRate = defaultSampleRate; - if (parentObj && typeof parentObj[propNm] !== 'undefined') { - sampleRate = parseFloat(parentObj[propNm]); - if (Number.isNaN(sampleRate) || sampleRate > 1) { - sampleRate = defaultSampleRate; - } else if (sampleRate < 0) { - sampleRate = 0; - } - } - return sampleRate; -} - -/** - * Helper to encapsulate logic for getting best known page url. Small but helpful in debugging/testing and if we ever want - * to add more logic to this. - * - * From getRefererInfo(): page = the best candidate for the current page URL: `canonicalUrl`, falling back to `location` - * @returns {*} Best guess at top URL based on logic from RefererInfo. - */ -cnvrHelper.getPageUrl = function() { - return getRefererInfo().page; -} - -/** - * Packages up an error that occured in analytics handling and sends it back to our servers for logging - * @param eventType = original event that was fired - * @param exception = {stack:"...",message:"..."}, exception that was triggered - */ -cnvrHelper.sendErrorData = function(eventType, exception) { - if (!cnvrHelper.doSendErrorData) { - logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'Skipping sending error data due to config disabling error logging, error thrown = ' + exception); - return; - } - - let error = { - event: eventType, - siteId: initOptions.site_id, - message: exception.message, - stack: exception.stack, - prebidVersion: '$$REPO_AND_VERSION$$', // testing val sample: prebid_prebid_7.27.0-pre' - userAgent: navigator.userAgent, - url: cnvrHelper.getPageUrl() - }; - - - ajax(ERROR_URL, function () {}, JSON.stringify(error), {contentType: 'text/plain'}); -} - -/** - * Helper function to send data back to server. Need to make sure we don't trigger a CORS preflight by not adding - * extra header params. - * @param payload our JSON payload from either AUCTION END, BID WIN, RENDER FAILED - */ -function sendData(payload) { - ajax(ANALYTICS_URL, function () {}, JSON.stringify(payload), {contentType: 'text/plain'}); -} - -// =============================== BOILERPLATE FOR PRE-BID ANALYTICS SETUP ============================================ -// save the base class function -conversantAnalytics.originEnableAnalytics = conversantAnalytics.enableAnalytics; -conversantAnalytics.originDisableAnalytics = conversantAnalytics.disableAnalytics; - -// override enableAnalytics so we can get access to the config passed in from the page -conversantAnalytics.enableAnalytics = function (config) { - if (!config || !config.options || !config.options.site_id) { - logError(CNVR_CONSTANTS.LOG_PREFIX + 'siteId is required.'); - return; - } - - cacheCleanupInterval = setInterval( - function() { - const currTime = Date.now(); - cnvrHelper.cleanCache(cnvrHelper.adIdLookup, currTime); - cnvrHelper.cleanCache(cnvrHelper.timeoutCache, currTime); - cnvrHelper.cleanCache(cnvrHelper.auctionIdTimestampCache, currTime); - cnvrHelper.cleanCache(cnvrHelper.bidderErrorCache, currTime); - }, - CNVR_CONSTANTS.CACHE_CLEANUP_TIME_IN_MILLIS - ); - - Object.assign(initOptions, config.options); - - initOptions.global_sample_rate = cnvrHelper.getSampleRate(initOptions, 'sampling', 1); - initOptions.cnvr_sample_rate = cnvrHelper.getSampleRate(initOptions, 'cnvr_sampling', CNVR_CONSTANTS.DEFAULT_SAMPLE_RATE); - - logInfo(CNVR_CONSTANTS.LOG_PREFIX + 'Conversant sample rate set to ' + initOptions.cnvr_sample_rate); - logInfo(CNVR_CONSTANTS.LOG_PREFIX + 'Global sample rate set to ' + initOptions.global_sample_rate); - // Math.random() pseudo-random number in the range 0 to less than 1 (inclusive of 0, but not 1) - cnvrHelper.doSample = Math.random() < initOptions.cnvr_sample_rate; - - if (initOptions.send_error_data !== undefined && initOptions.send_error_data !== null) { - cnvrHelper.doSendErrorData = !!initOptions.send_error_data; // Forces data into boolean type - } - - conversantAnalyticsEnabled = true; - conversantAnalytics.originEnableAnalytics(config); // call the base class function -}; - -/** - * Cleanup code for any timers and caches. - */ -conversantAnalytics.disableAnalytics = function () { - if (!conversantAnalyticsEnabled) { - return; - } - - // Cleanup our caches and disable our timer - clearInterval(cacheCleanupInterval); - cnvrHelper.timeoutCache = {}; - cnvrHelper.adIdLookup = {}; - cnvrHelper.auctionIdTimestampCache = {}; - cnvrHelper.bidderErrorCache = {}; - - conversantAnalyticsEnabled = false; - conversantAnalytics.originDisableAnalytics(); -}; -ANALYTICS_ALIASES.forEach(alias => { - adapterManager.registerAnalyticsAdapter({ - adapter: conversantAnalytics, - code: alias, - gvlid: GVLID - }); -}); - -export default conversantAnalytics; diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js deleted file mode 100644 index f46de31b19c..00000000000 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ /dev/null @@ -1,1113 +0,0 @@ -import sinon from 'sinon'; -import { expect } from 'chai'; -import { default as conversantAnalytics, CNVR_CONSTANTS, cnvrHelper } from 'modules/conversantAnalyticsAdapter'; -import * as utils from 'src/utils.js'; -import * as prebidGlobal from 'src/prebidGlobal'; -import { server } from '../../mocks/xhr.js'; - -import {EVENTS} from 'src/constants.js' - -const events = require('src/events'); - -describe('Conversant analytics adapter tests', function() { - let sandbox; // sinon sandbox to make restoring all stubbed objects easier - let clock; // clock stub from sinon to mock our cache cleanup interval - let logInfoStub; - - const PREBID_VERSION = '$prebid.version$'; - const SITE_ID = 108060; - - let requests; - const DATESTAMP = Date.now(); - - const VALID_CONFIGURATION = { - options: { - site_id: SITE_ID, - send_error_data: true - } - }; - - const VALID_ALWAYS_SAMPLE_CONFIG = { - options: { - site_id: SITE_ID, - cnvr_sampling: 1, - send_error_data: true - } - }; - - beforeEach(function () { - requests = server.requests; - sandbox = sinon.sandbox.create(); - sandbox.stub(events, 'getEvents').returns([]); // need to stub this otherwise unwanted events seem to get fired during testing - const getGlobalStub = { - version: PREBID_VERSION, - getUserIds: function() { // userIdTargeting.js init() gets called on AUCTION_END so we need to mock this function. - return {}; - } - }; - sandbox.stub(prebidGlobal, 'getGlobal').returns(getGlobalStub); // getGlobal does not seem to be available in testing so need to mock it - clock = sandbox.useFakeTimers(DATESTAMP); // to use sinon fake timers they MUST be created before the interval/timeout is created in the code you are testing. - - logInfoStub = sandbox.stub(utils, 'logInfo');/* .callsFake((arg, arg1, arg2) => { //debugging stuff - console.log(arg); - if (arg1) console.log(arg1); - if (arg2) console.log(arg2); - }); */ - - conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); - }); - - afterEach(function () { - sandbox.restore(); - conversantAnalytics.disableAnalytics(); - }); - - describe('Initialization Tests', function() { - it('should log error if site id is not passed', function() { - sandbox.stub(utils, 'logError'); - conversantAnalytics.disableAnalytics(); - conversantAnalytics.enableAnalytics(); - expect(utils.logError.calledWith(CNVR_CONSTANTS.LOG_PREFIX + 'siteId is required.')).to.be.true; - }); - - it('should not log error if valid config is passed', function() { - sandbox.stub(utils, 'logError'); - - conversantAnalytics.enableAnalytics(VALID_CONFIGURATION); - expect(utils.logError.called).to.equal(false); - expect(utils.logInfo.called).to.equal(true); - expect( - utils.logInfo.calledWith( - CNVR_CONSTANTS.LOG_PREFIX + 'Conversant sample rate set to ' + CNVR_CONSTANTS.DEFAULT_SAMPLE_RATE - ) - ).to.be.true; - expect( - utils.logInfo.calledWith( - CNVR_CONSTANTS.LOG_PREFIX + 'Global sample rate set to 1' - ) - ).to.be.true; - }); - - it('should sample when sampling set to 1', function() { - sandbox.stub(utils, 'logError'); - conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); - expect(utils.logError.called).to.equal(false); - expect(cnvrHelper.doSample).to.equal(true); - }); - - it('should NOT sample when sampling set to 0', function() { - sandbox.stub(utils, 'logError'); - const NEVER_SAMPLE_CONFIG = utils.deepClone(VALID_ALWAYS_SAMPLE_CONFIG); - NEVER_SAMPLE_CONFIG.options.cnvr_sampling = 0; - conversantAnalytics.disableAnalytics(); - conversantAnalytics.enableAnalytics(NEVER_SAMPLE_CONFIG); - expect(utils.logError.called).to.equal(false); - expect(cnvrHelper.doSample).to.equal(false); - }); - }); - - describe('Helper Function Tests', function() { - it('should cleanup up cache objects', function() { - conversantAnalytics.enableAnalytics(VALID_CONFIGURATION); - - cnvrHelper.adIdLookup.keep = { timeReceived: DATESTAMP + 1 }; - cnvrHelper.adIdLookup.delete = { timeReceived: DATESTAMP - CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE }; - - cnvrHelper.timeoutCache.keep = { timeReceived: DATESTAMP + 1 }; - cnvrHelper.timeoutCache.delete = { timeReceived: DATESTAMP - CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE }; - - cnvrHelper.auctionIdTimestampCache.keep = { timeReceived: DATESTAMP + 1 }; - cnvrHelper.auctionIdTimestampCache.delete = { timeReceived: DATESTAMP - CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE }; - - cnvrHelper.bidderErrorCache.keep = { timeReceived: DATESTAMP + 1, errors: [] }; - cnvrHelper.bidderErrorCache.delete = { timeReceived: DATESTAMP - CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE, errors: [] }; - - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(2); - expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(2); - expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(2); - expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(2); - - clock.tick(CNVR_CONSTANTS.CACHE_CLEANUP_TIME_IN_MILLIS); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); - expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(1); - expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(1); - expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(1); - - conversantAnalytics.disableAnalytics(); - - // After disable we should cleanup the cache - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(0); - }); - - it('createBid() should return correct object', function() { - const EVENT_CODE = 1; - const TIME = 2; - const bid = cnvrHelper.createBid(EVENT_CODE, 2); - expect(bid).to.deep.equal({ eventCodes: [EVENT_CODE], timeToRespond: TIME }); - }); - - it('createAdUnit() should return correct object', function() { - const adUnit = cnvrHelper.createAdUnit(); - expect(adUnit).to.deep.equal({ - sizes: [], - mediaTypes: [], - bids: {} - }); - }); - - it('createAdSize() should return correct object', function() { - let adSize = cnvrHelper.createAdSize(1, 2); - expect(adSize).to.deep.equal({ w: 1, h: 2 }); - - adSize = cnvrHelper.createAdSize(); - expect(adSize).to.deep.equal({ w: -1, h: -1 }); - - adSize = cnvrHelper.createAdSize('foo', 'bar'); - expect(adSize).to.deep.equal({ w: -1, h: -1 }); - }); - - it('getLookupKey() should return correct object', function() { - let key = cnvrHelper.getLookupKey(undefined, undefined, undefined); - expect(key).to.equal('undefined-undefined-undefined'); - - key = cnvrHelper.getLookupKey('foo', 'bar', 'baz'); - expect(key).to.equal('foo-bar-baz'); - }); - - it('createPayload() should return correct object', function() { - const REQUEST_TYPE = 'foo'; - const AUCTION_ID = '124 abc'; - const myDate = Date.now(); - conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); - - const payload = cnvrHelper.createPayload(REQUEST_TYPE, AUCTION_ID, myDate); - expect(payload).to.deep.equal({ - bidderErrors: [], - cnvrSampleRate: 1, - globalSampleRate: 1, - requestType: REQUEST_TYPE, - auction: { - auctionId: AUCTION_ID, - auctionTimestamp: myDate, - sid: VALID_ALWAYS_SAMPLE_CONFIG.options.site_id, - preBidVersion: PREBID_VERSION - }, - adUnits: {} - }); - }); - - it('keyExistsAndIsObject() should return correct data', function() { - const data = { - a: [], - b: 1, - c: 'foo', - d: function () { return true; }, - e: {} - }; - expect(cnvrHelper.keyExistsAndIsObject(data, 'foobar')).to.be.false; - expect(cnvrHelper.keyExistsAndIsObject(data, 'a')).to.be.false; - expect(cnvrHelper.keyExistsAndIsObject(data, 'b')).to.be.false; - expect(cnvrHelper.keyExistsAndIsObject(data, 'c')).to.be.false; - expect(cnvrHelper.keyExistsAndIsObject(data, 'd')).to.be.false; - expect(cnvrHelper.keyExistsAndIsObject(data, 'e')).to.be.true; - }); - - it('deduplicateArray() should return correct data', function () { - const arrayOfObjects = [{ w: 1, h: 2 }, { w: 2, h: 3 }, { w: 1, h: 2 }]; - const array = [3, 2, 1, 1, 2, 3]; - let empty; - const notArray = 3; - const emptyArray = []; - - expect(JSON.stringify(cnvrHelper.deduplicateArray(array))).to.equal(JSON.stringify([3, 2, 1])); - expect(JSON.stringify(cnvrHelper.deduplicateArray(arrayOfObjects))).to.equal(JSON.stringify([{ w: 1, h: 2 }, { w: 2, h: 3 }])); - expect(JSON.stringify(cnvrHelper.deduplicateArray(emptyArray))).to.equal(JSON.stringify([])); - expect(cnvrHelper.deduplicateArray(empty)).to.be.undefined; - expect(cnvrHelper.deduplicateArray(notArray)).to.equal(notArray); - }); - - it('getSampleRate() should return correct data', function () { - const obj = { - sampling: 1, - cnvr_sampling: 0.5, - too_big: 1.2, - too_small: -1, - string: 'foo', - object: {}, - } - const DEFAULT_VAL = 0.11; - expect(cnvrHelper.getSampleRate(obj, 'sampling', DEFAULT_VAL)).to.equal(1); - expect(cnvrHelper.getSampleRate(obj, 'cnvr_sampling', DEFAULT_VAL)).to.equal(0.5); - expect(cnvrHelper.getSampleRate(obj, 'too_big', DEFAULT_VAL)).to.equal(DEFAULT_VAL); - expect(cnvrHelper.getSampleRate(obj, 'string', DEFAULT_VAL)).to.equal(DEFAULT_VAL); - expect(cnvrHelper.getSampleRate(obj, 'object', DEFAULT_VAL)).to.equal(DEFAULT_VAL); - expect(cnvrHelper.getSampleRate(obj, 'not_a_key', DEFAULT_VAL)).to.equal(DEFAULT_VAL); - expect(cnvrHelper.getSampleRate(obj, 'too_small', DEFAULT_VAL)).to.equal(0); - }); - - it('getPageUrl() should return correct data', function() { - const url = cnvrHelper.getPageUrl(); - expect(url.length).to.be.above(1); - }); - - it('sendErrorData() should send data via ajax', function() { - const error = { - stack: 'foobar', - message: 'foobar message' - }; - const eventType = 'fooType'; - - expect(requests).to.have.lengthOf(0); - cnvrHelper.sendErrorData(eventType, error); - expect(requests).to.have.lengthOf(1); - - expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); - const data = JSON.parse(requests[0].requestBody); - expect(data.event).to.be.equal(eventType); - expect(data.siteId).to.be.equal(SITE_ID); - expect(data.message).to.not.be.undefined; - expect(data.prebidVersion).to.not.be.undefined; - expect(data.userAgent).to.not.be.undefined; - expect(data.url).to.not.be.undefined; - }); - - it('Should not send data when error logging disabled', function() { - const error = { - stack: 'foobar', - message: 'foobar message' - }; - const eventType = 'fooType'; - - conversantAnalytics.disableAnalytics(); - conversantAnalytics.enableAnalytics({ - options: { - site_id: SITE_ID, - cnvr_sampling: 1, - send_error_data: false - } - }); - expect(cnvrHelper.doSendErrorData).to.be.false; - - expect(requests).to.have.lengthOf(0); - cnvrHelper.sendErrorData(eventType, error); - expect(requests).to.have.lengthOf(0); - - conversantAnalytics.disableAnalytics(); - conversantAnalytics.enableAnalytics({ - options: { - site_id: SITE_ID, - cnvr_sampling: 1, - send_error_data: 0 - } - }); - expect(cnvrHelper.doSendErrorData).to.be.false; - - expect(requests).to.have.lengthOf(0); - cnvrHelper.sendErrorData(eventType, error); - expect(requests).to.have.lengthOf(0); - }); - }); - - describe('Bid Timeout Event Tests', function() { - const BID_TIMEOUT_PAYLOAD = [{ - bidId: '80882409358b8a8', - bidder: 'conversant', - adUnitCode: 'MedRect', - auctionId: 'afbd6e0b-e45b-46ab-87bf-c0bac0cb8881' - }, { - bidId: '9da4c107a6f24c8', - bidder: 'conversant', - adUnitCode: 'Leaderboard', - auctionId: 'afbd6e0b-e45b-46ab-87bf-c0bac0cb8881' - }]; - - it('should put both items in timeout cache', function() { - expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(0); - events.emit(EVENTS.BID_TIMEOUT, BID_TIMEOUT_PAYLOAD); - expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(2); - - BID_TIMEOUT_PAYLOAD.forEach(timeoutBid => { - const key = cnvrHelper.getLookupKey(timeoutBid.auctionId, timeoutBid.adUnitCode, timeoutBid.bidder); - expect(cnvrHelper.timeoutCache[key].timeReceived).to.not.be.undefined; - }); - expect(requests).to.have.lengthOf(0); - }); - }); - - describe('Render Failed Tests', function() { - const RENDER_FAILED_PAYLOAD = { - reason: 'reason', - message: 'value', - adId: '57e03aeafd83a68' - }; - - const RENDER_FAILED_PAYLOAD_NO_ADID = { - reason: 'reason', - message: 'value' - }; - - it('should empty adIdLookup and send data', function() { - cnvrHelper.adIdLookup[RENDER_FAILED_PAYLOAD.adId] = { - bidderCode: 'bidderCode', - adUnitCode: 'adUnitCode', - auctionId: 'auctionId', - timeReceived: Date.now() - }; - - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); - events.emit(EVENTS.AD_RENDER_FAILED, RENDER_FAILED_PAYLOAD); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); // object should be removed - expect(requests).to.have.lengthOf(1); - const data = JSON.parse(requests[0].requestBody); - - expect(data.auction.auctionId).to.equal('auctionId'); - expect(data.auction.preBidVersion).to.equal(PREBID_VERSION); - expect(data.auction.sid).to.equal(SITE_ID); - expect(data.adUnits.adUnitCode.bids.bidderCode[0].eventCodes.includes(CNVR_CONSTANTS.RENDER_FAILED)).to.be.true; - expect(data.adUnits.adUnitCode.bids.bidderCode[0].message).to.have.lengthOf.above(0); - }); - - it('should not send data if no adId', function() { - cnvrHelper.adIdLookup[RENDER_FAILED_PAYLOAD.adId] = { - bidderCode: 'bidderCode', - adUnitCode: 'adUnitCode', - auctionId: 'auctionId', - timeReceived: Date.now() - }; - - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); - events.emit(EVENTS.AD_RENDER_FAILED, RENDER_FAILED_PAYLOAD_NO_ADID); - expect(requests).to.have.lengthOf(1); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); // same object in cache as before... no change - expect(cnvrHelper.adIdLookup[RENDER_FAILED_PAYLOAD.adId]).to.not.be.undefined; - - expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); - const data = JSON.parse(requests[0].requestBody); - expect(data.event).to.be.equal(EVENTS.AD_RENDER_FAILED); - expect(data.siteId).to.be.equal(SITE_ID); - expect(data.message).to.not.be.undefined; - expect(data.prebidVersion).to.not.be.undefined; - expect(data.userAgent).to.not.be.undefined; - expect(data.url).to.not.be.undefined; - }); - - it('should not send data if bad data in lookup', function() { - cnvrHelper.adIdLookup[RENDER_FAILED_PAYLOAD.adId] = { - bidderCode: 'bidderCode', - auctionId: 'auctionId', - timeReceived: Date.now() - }; - expect(requests).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); - events.emit(EVENTS.AD_RENDER_FAILED, RENDER_FAILED_PAYLOAD); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); // object should be removed but no call made to send data - expect(requests).to.have.lengthOf(1); - - expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); - const data = JSON.parse(requests[0].requestBody); - expect(data.event).to.be.equal(EVENTS.AD_RENDER_FAILED); - expect(data.siteId).to.be.equal(SITE_ID); - expect(data.message).to.not.be.undefined; - expect(data.prebidVersion).to.not.be.undefined; - expect(data.userAgent).to.not.be.undefined; - expect(data.url).to.not.be.undefined; - }); - }); - - describe('Bid Won Tests', function() { - const GOOD_BID_WON_ARGS = { - bidderCode: 'conversant', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '57e03aeafd83a68', - requestId: '2c2a5485a076898', - mediaType: 'banner', - source: 'client', - currency: 'USD', - cpm: 4, - creativeId: '29123_55016759', - ttl: 300, - netRevenue: true, - ad: '', - originalCpm: 0.04, - originalCurrency: 'USD', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - responseTimestamp: 1583851418626, - requestTimestamp: 1583851418292, - bidder: 'conversant', - adUnitCode: 'div-gpt-ad-1460505748561-0', - timeToRespond: 334, - pbLg: '4.00', - pbMg: '4.00', - pbHg: '4.00', - pbAg: '4.00', - pbDg: '4.00', - pbCg: '', - size: '300x250', - adserverTargeting: { - hb_bidder: 'conversant', - hb_adid: '57e03aeafd83a68', - hb_pb: '4.00', - hb_size: '300x250', - hb_source: 'client', - hb_format: 'banner' - }, - status: 'rendered', - params: [ - { - site_id: '108060' - } - ] - }; - - // no adUnitCode, auctionId or bidderCode will cause a failure - const BAD_BID_WON_ARGS = { - bidderCode: 'conversant', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '57e03aeafd83a68', - requestId: '2c2a5485a076898', - mediaType: 'banner', - source: 'client', - currency: 'USD', - cpm: 4, - originalCpm: 0.04, - originalCurrency: 'USD', - bidder: 'conversant', - adUnitCode: 'div-gpt-ad-1460505748561-0', - size: '300x250', - status: 'rendered', - params: [ - { - site_id: '108060' - } - ] - }; - - it('should not send data or put a record in adIdLookup when bad data provided', function() { - expect(requests).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); - events.emit(EVENTS.BID_WON, BAD_BID_WON_ARGS); - expect(requests).to.have.lengthOf(1); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); - - // check for error event - expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); - const data = JSON.parse(requests[0].requestBody); - expect(data.event).to.be.equal(EVENTS.BID_WON); - expect(data.siteId).to.be.equal(SITE_ID); - expect(data.message).to.not.be.undefined; - expect(data.prebidVersion).to.not.be.undefined; - expect(data.userAgent).to.not.be.undefined; - expect(data.url).to.not.be.undefined; - }); - - it('should send data and put a record in adIdLookup', function() { - const myAuctionStart = Date.now(); - cnvrHelper.auctionIdTimestampCache[GOOD_BID_WON_ARGS.auctionId] = { timeReceived: myAuctionStart }; - - expect(requests).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); - events.emit(EVENTS.BID_WON, GOOD_BID_WON_ARGS); - - // Check that adIdLookup was set correctly - expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); - expect(cnvrHelper.adIdLookup[GOOD_BID_WON_ARGS.adId].auctionId).to.equal(GOOD_BID_WON_ARGS.auctionId); - expect(cnvrHelper.adIdLookup[GOOD_BID_WON_ARGS.adId].adUnitCode).to.equal(GOOD_BID_WON_ARGS.adUnitCode); - expect(cnvrHelper.adIdLookup[GOOD_BID_WON_ARGS.adId].bidderCode).to.equal(GOOD_BID_WON_ARGS.bidderCode); - expect(cnvrHelper.adIdLookup[GOOD_BID_WON_ARGS.adId].timeReceived).to.not.be.undefined; - - expect(requests).to.have.lengthOf(1); - const data = JSON.parse(requests[0].requestBody); - expect(data.requestType).to.equal('bid_won'); - expect(data.auction.auctionId).to.equal(GOOD_BID_WON_ARGS.auctionId); - expect(data.auction.preBidVersion).to.equal(PREBID_VERSION); - expect(data.auction.sid).to.equal(VALID_ALWAYS_SAMPLE_CONFIG.options.site_id); - expect(data.auction.auctionTimestamp).to.equal(myAuctionStart); - - expect(typeof data.adUnits).to.equal('object'); - expect(Object.keys(data.adUnits)).to.have.lengthOf(1); - - expect(Object.keys(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids)).to.have.lengthOf(1); - expect(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids[GOOD_BID_WON_ARGS.bidderCode][0].eventCodes.includes(CNVR_CONSTANTS.WIN)).to.be.true; - expect(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids[GOOD_BID_WON_ARGS.bidderCode][0].cpm).to.equal(GOOD_BID_WON_ARGS.cpm); - expect(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids[GOOD_BID_WON_ARGS.bidderCode][0].originalCpm).to.equal(GOOD_BID_WON_ARGS.originalCpm); - expect(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids[GOOD_BID_WON_ARGS.bidderCode][0].currency).to.equal(GOOD_BID_WON_ARGS.currency); - expect(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids[GOOD_BID_WON_ARGS.bidderCode][0].timeToRespond).to.equal(GOOD_BID_WON_ARGS.timeToRespond); - expect(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids[GOOD_BID_WON_ARGS.bidderCode][0].adSize.w).to.equal(GOOD_BID_WON_ARGS.width); - expect(data.adUnits[GOOD_BID_WON_ARGS.adUnitCode].bids[GOOD_BID_WON_ARGS.bidderCode][0].adSize.h).to.equal(GOOD_BID_WON_ARGS.height); - }); - }); - - describe('Auction End Tests', function() { - const AUCTION_END_PAYLOAD = { - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - timestamp: 1583851418288, - auctionEnd: 1583851418628, - auctionStatus: 'completed', - adUnits: [ - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [100, 200] - ] - } - }, - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - } - }, - { - bidder: 'conversant', - params: { - site_id: '108060' - } - } - ], - sizes: [ - [300, 250], - [100, 200] - ], - transactionId: '5fa8a7d7-2a73-4d1c-b73a-ff9f5b53ba17' - }, - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [100, 200] - ] - }, - video: { - playerSize: [ - [300, 250], - [600, 400] - ] - } - }, - sizes: [ - [300, 250], - [100, 200], - [600, 400] - ], - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - } - }, - { - bidder: 'conversant', - params: { - site_id: '108060' - } - } - ], - transactionId: '5fa8a7d7-2a73-4d1c-b73a-ff9f5b53ba18' - }, - { - code: 'div-gpt-ad-1460505748561-1', - mediaTypes: { - native: { - type: 'image' - } - }, - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144371 - } - } - ], - transactionId: '5fa8a7d7-2a73-4d1c-b73a-ff9f5b53ba10' - } - ], - adUnitCodes: [ - 'div-gpt-ad-1460505748561-0' - ], - bidderRequests: [ - { - bidderCode: 'conversant', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - bidderRequestId: '13f16db358d4c58', - bids: [ - { - bidder: 'conversant', - params: { - site_id: '108060' - }, - mediaTypes: { - banner: { - sizes: [ - [ - 300, - 250 - ] - ] - } - }, - adUnitCode: 'div-gpt-ad-1460505748561-0', - transactionId: '5fa8a7d7-2a73-4d1c-b73a-ff9f5b53ba17', - sizes: [ - [ - 300, - 250 - ] - ], - bidId: '2c2a5485a076898', - bidderRequestId: '13f16db358d4c58', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - } - ], - auctionStart: 1583851418288, - timeout: 3000, - refererInfo: { - referer: 'http://localhost:9999/integrationExamples/gpt/hello_analytics1.html', - reachedTop: true, - numIframes: 0, - stack: [ - 'http://localhost:9999/integrationExamples/gpt/hello_analytics1.html' - ] - }, - start: 1583851418292 - }, - { - bidderCode: 'appnexus', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - bidderRequestId: '3e8179f67f31b98', - bids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - }, - mediaTypes: { - banner: { - sizes: [ - [ - 300, - 250 - ] - ] - } - }, - adUnitCode: 'div-gpt-ad-1460505748561-0', - transactionId: '5fa8a7d7-2a73-4d1c-b73a-ff9f5b53ba17', - sizes: [ - [ - 300, - 250 - ] - ], - bidId: '40a1d3ac6b79668', - bidderRequestId: '3e8179f67f31b98', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - }, - { - bidder: 'appnexus', - params: { - placementId: 13144370 - }, - mediaTypes: { - native: { - type: 'image' - } - }, - adUnitCode: 'div-gpt-ad-1460505748561-1', - transactionId: '5fa8a7d7-2a73-4d1c-b73a-ff9f5b53ba17', - sizes: [], - bidId: '40a1d3ac6b79668', - bidderRequestId: '3e8179f67f31b98', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - } - ], - auctionStart: 1583851418288, - timeout: 3000, - refererInfo: { - referer: 'http://localhost:9999/integrationExamples/gpt/hello_analytics1.html', - reachedTop: true, - numIframes: 0, - stack: [ - 'http://localhost:9999/integrationExamples/gpt/hello_analytics1.html' - ] - }, - start: 1583851418295 - } - ], - noBids: [ - { - bidder: 'appnexus', - params: { - placementId: 13144370 - }, - mediaTypes: { - banner: { - sizes: [ - [ - 300, - 250 - ] - ] - } - }, - adUnitCode: 'div-gpt-ad-1460505748561-0', - transactionId: '5fa8a7d7-2a73-4d1c-b73a-ff9f5b53ba17', - sizes: [ - [ - 300, - 250 - ] - ], - bidId: '40a1d3ac6b79668', - bidderRequestId: '3e8179f67f31b98', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - src: 'client', - bidRequestsCount: 1, - bidderRequestsCount: 1, - bidderWinsCount: 0 - } - ], - bidsReceived: [ - { - bidderCode: 'conversant', - width: 300, - height: 250, - statusMessage: 'Bid available', - adId: '57e03aeafd83a68', - requestId: '2c2a5485a076898', - mediaType: 'banner', - source: 'client', - currency: 'USD', - cpm: 4, - creativeId: '29123_55016759', - ttl: 300, - netRevenue: true, - ad: '', - originalCpm: 0.04, - originalCurrency: 'USD', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - responseTimestamp: 1583851418626, - requestTimestamp: 1583851418292, - bidder: 'conversant', - adUnitCode: 'div-gpt-ad-1460505748561-0', - timeToRespond: 334, - pbLg: '4.00', - pbMg: '4.00', - pbHg: '4.00', - pbAg: '4.00', - pbDg: '4.00', - pbCg: '', - size: '300x250', - adserverTargeting: { - hb_bidder: 'conversant', - hb_adid: '57e03aeafd83a68', - hb_pb: '4.00', - hb_size: '300x250', - hb_source: 'client', - hb_format: 'banner' - } - }, { - bidderCode: 'conversant', - height: 100, - statusMessage: 'Bid available', - width: 200, - adId: '57e03aeafd83a68', - requestId: '2c2a5485a076898', - mediaType: 'banner', - source: 'client', - currency: 'USD', - cpm: 4, - creativeId: '29123_55016759', - ttl: 300, - netRevenue: true, - ad: '', - originalCpm: 0.04, - originalCurrency: 'USD', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - responseTimestamp: 1583851418626, - requestTimestamp: 1583851418292, - bidder: 'conversant', - adUnitCode: 'div-gpt-ad-1460505748561-0', - timeToRespond: 334, - pbLg: '4.00', - pbMg: '4.00', - pbHg: '4.00', - pbAg: '4.00', - pbDg: '4.00', - pbCg: '', - size: '100x200', - adserverTargeting: { - hb_bidder: 'conversant', - hb_adid: '57e03aeafd83a68', - hb_pb: '4.00', - hb_size: '300x250', - hb_source: 'client', - hb_format: 'banner' - } - }, { - bidderCode: 'appnexus', - statusMessage: 'Bid available', - adId: '57e03aeafd83a68', - requestId: '2c2a5485a076898', - mediaType: 'native', - source: 'client', - currency: 'USD', - cpm: 4, - creativeId: '29123_55016759', - ttl: 300, - netRevenue: true, - ad: '', - originalCpm: 0.04, - originalCurrency: 'USD', - auctionId: '85e1bf44-4035-4e24-bd3c-b1ba367fe294', - responseTimestamp: 1583851418626, - requestTimestamp: 1583851418292, - bidder: 'appnexus', - adUnitCode: 'div-gpt-ad-1460505748561-1', - timeToRespond: 334, - pbLg: '4.00', - pbMg: '4.00', - pbHg: '4.00', - pbAg: '4.00', - pbDg: '4.00', - pbCg: '', - adserverTargeting: { - hb_bidder: 'appnexus', - hb_adid: '57e03aeafd83a68', - hb_pb: '4.00', - hb_size: '300x250', - hb_source: 'client', - hb_format: 'banner' - } - } - ], - winningBids: [], - timeout: 3000 - }; - - it('should not do anything when auction id doesnt exist', function() { - sandbox.stub(utils, 'logError'); - - const BAD_ARGS = JSON.parse(JSON.stringify(AUCTION_END_PAYLOAD)); - delete BAD_ARGS.auctionId; - expect(requests).to.have.lengthOf(0); - events.emit(EVENTS.AUCTION_END, BAD_ARGS); - expect(requests).to.have.lengthOf(1); - - // check for error event - expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); - const data = JSON.parse(requests[0].requestBody); - expect(data.event).to.be.equal(EVENTS.AUCTION_END); - expect(data.siteId).to.be.equal(SITE_ID); - expect(data.message).to.not.be.undefined; - expect(data.prebidVersion).to.not.be.undefined; - expect(data.userAgent).to.not.be.undefined; - expect(data.url).to.not.be.undefined; - }); - - it('should send the expected data', function() { - sandbox.stub(utils, 'logError'); - sandbox.stub(utils, 'logWarn'); - - expect(requests).to.have.lengthOf(0); - const AUCTION_ID = AUCTION_END_PAYLOAD.auctionId; - const AD_UNIT_CODE = AUCTION_END_PAYLOAD.adUnits[0].code; - const AD_UNIT_CODE_NATIVE = AUCTION_END_PAYLOAD.adUnits[2].code; - const timeoutKey = cnvrHelper.getLookupKey(AUCTION_ID, AD_UNIT_CODE, 'appnexus'); - const URL = 'some url'; - cnvrHelper.bidderErrorCache[AUCTION_ID] = { - errors: [{ - status: 500, - message: 'error msg', - bidderCode: 'bidderCode', - url: URL, - }, { - status: 501, - message: 'error msg1', - bidderCode: 'bidderCode1', - url: URL, - }], - timeReceived: Date.now() - }; - cnvrHelper.timeoutCache[timeoutKey] = { timeReceived: Date.now() }; - expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(1); - expect(utils.logError.called).to.equal(false); - expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(1); - - events.emit(EVENTS.AUCTION_END, AUCTION_END_PAYLOAD); - expect(utils.logError.called).to.equal(false); - expect(requests).to.have.lengthOf(1); - expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(1); - expect(cnvrHelper.auctionIdTimestampCache[AUCTION_END_PAYLOAD.auctionId].timeReceived).to.equal(AUCTION_END_PAYLOAD.timestamp); - - const data = JSON.parse(requests[0].requestBody); - expect(data.requestType).to.equal('auction_end'); - expect(data.auction.auctionId).to.equal(AUCTION_ID); - expect(data.auction.preBidVersion).to.equal(PREBID_VERSION); - expect(data.auction.sid).to.equal(VALID_ALWAYS_SAMPLE_CONFIG.options.site_id); - - expect(Object.keys(data.adUnits)).to.have.lengthOf(2); - - expect(data.adUnits[AD_UNIT_CODE].sizes).to.have.lengthOf(3); - expect(data.adUnits[AD_UNIT_CODE].sizes[0].w).to.equal(300); - expect(data.adUnits[AD_UNIT_CODE].sizes[0].h).to.equal(250); - expect(data.adUnits[AD_UNIT_CODE].sizes[1].w).to.equal(100); - expect(data.adUnits[AD_UNIT_CODE].sizes[1].h).to.equal(200); - expect(data.adUnits[AD_UNIT_CODE].sizes[2].w).to.equal(600); - expect(data.adUnits[AD_UNIT_CODE].sizes[2].h).to.equal(400); - - expect(data.adUnits[AD_UNIT_CODE].mediaTypes).to.have.lengthOf(2); - expect(data.adUnits[AD_UNIT_CODE].mediaTypes[0]).to.equal('banner'); - expect(data.adUnits[AD_UNIT_CODE].mediaTypes[1]).to.equal('video'); - - expect(data.adUnits[AD_UNIT_CODE_NATIVE].mediaTypes).to.have.lengthOf(1); - expect(data.adUnits[AD_UNIT_CODE_NATIVE].mediaTypes[0]).to.equal('native'); - expect(data.adUnits[AD_UNIT_CODE_NATIVE].sizes).to.have.lengthOf(0); - - expect(Object.keys(data.adUnits[AD_UNIT_CODE].bids)).to.have.lengthOf(2); - const cnvrBidsArray = data.adUnits[AD_UNIT_CODE].bids.conversant; - // testing multiple bids from same bidder - expect(cnvrBidsArray).to.have.lengthOf(2); - expect(cnvrBidsArray[0].eventCodes.includes(CNVR_CONSTANTS.BID)).to.be.true; - expect(cnvrBidsArray[0].cpm).to.equal(4); - expect(cnvrBidsArray[0].originalCpm).to.equal(0.04); - expect(cnvrBidsArray[0].currency).to.equal('USD'); - expect(cnvrBidsArray[0].timeToRespond).to.equal(334); - expect(cnvrBidsArray[0].adSize.w).to.equal(300); - expect(cnvrBidsArray[0].adSize.h).to.equal(250); - expect(cnvrBidsArray[0].mediaType).to.equal('banner'); - // 2nd bid different size - expect(cnvrBidsArray[1].eventCodes.includes(CNVR_CONSTANTS.BID)).to.be.true; - expect(cnvrBidsArray[1].cpm).to.equal(4); - expect(cnvrBidsArray[1].originalCpm).to.equal(0.04); - expect(cnvrBidsArray[1].currency).to.equal('USD'); - expect(cnvrBidsArray[1].timeToRespond).to.equal(334); - expect(cnvrBidsArray[1].adSize.w).to.equal(200); - expect(cnvrBidsArray[1].adSize.h).to.equal(100); - expect(cnvrBidsArray[1].mediaType).to.equal('banner'); - - const apnBidsArray = data.adUnits[AD_UNIT_CODE].bids.appnexus; - expect(apnBidsArray).to.have.lengthOf(2); - let apnBid = apnBidsArray[0]; - expect(apnBid.originalCpm).to.be.undefined; - expect(apnBid.eventCodes.includes(CNVR_CONSTANTS.TIMEOUT)).to.be.true; - expect(apnBid.cpm).to.be.undefined; - expect(apnBid.currency).to.be.undefined; - expect(apnBid.timeToRespond).to.equal(3000); - expect(apnBid.adSize).to.be.undefined; - expect(apnBid.mediaType).to.be.undefined; - apnBid = apnBidsArray[1]; - expect(apnBid.originalCpm).to.be.undefined; - expect(apnBid.eventCodes.includes(CNVR_CONSTANTS.NO_BID)).to.be.true; - expect(apnBid.cpm).to.be.undefined; - expect(apnBid.currency).to.be.undefined; - expect(apnBid.timeToRespond).to.equal(0); - expect(apnBid.adSize).to.be.undefined; - expect(apnBid.mediaType).to.be.undefined; - - expect(Object.keys(data.adUnits[AD_UNIT_CODE_NATIVE].bids)).to.have.lengthOf(1); - const apnNativeBidsArray = data.adUnits[AD_UNIT_CODE_NATIVE].bids.appnexus; - expect(apnNativeBidsArray).to.have.lengthOf(1); - const apnNativeBid = apnNativeBidsArray[0]; - expect(apnNativeBid.eventCodes.includes(CNVR_CONSTANTS.BID)).to.be.true; - expect(apnNativeBid.cpm).to.equal(4); - expect(apnNativeBid.originalCpm).to.equal(0.04); - expect(apnNativeBid.currency).to.equal('USD'); - expect(apnNativeBid.timeToRespond).to.equal(334); - expect(apnNativeBid.adSize.w).to.be.undefined; - expect(apnNativeBid.adSize.h).to.be.undefined; - expect(apnNativeBid.mediaType).to.equal('native'); - - expect(Object.keys(data.bidderErrors)).to.have.lengthOf(2); - expect(data.bidderErrors[0].status).to.equal(500); - expect(data.bidderErrors[0].url).to.equal(URL); - expect(data.bidderErrors[0].message).to.not.be.undefined; - expect(data.bidderErrors[0].bidderCode).to.not.be.undefined; - - expect(data.bidderErrors[1].status).to.equal(501); - expect(data.bidderErrors[1].url).to.equal(URL); - expect(data.bidderErrors[1].message).to.not.be.undefined; - expect(data.bidderErrors[1].bidderCode).to.not.be.undefined; - - expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(0); - }); - }); - - describe('Bidder Error Tests', function() { - // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest - const XHR_ERROR_MOCK = { - status: 500, - statusText: 'Internal Server Error' - }; - - // https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error - const MOCK_BID_REQUEST = { - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - auctionStart: 1579746300522, - bidderCode: 'myBidderCode', - bidderRequestId: '15246a574e859f', - bids: [{}], - gdprConsent: { consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', vendorData: {}, gdprApplies: true }, - refererInfo: { - canonicalUrl: null, - page: 'http://mypage.org?pbjs_debug=true', - domain: 'mypage.org', - ref: null, - numIframes: 0, - reachedTop: true, - isAmp: false, - stack: ['http://mypage.org?pbjs_debug=true'] - } - }; - - it('should record error when bidder_error called', function() { - const warnStub = sandbox.stub(utils, 'logWarn'); - expect(requests).to.have.lengthOf(0); - expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(0); - expect(warnStub.calledOnce).to.be.false; - - events.emit(EVENTS.BIDDER_ERROR, { error: XHR_ERROR_MOCK, bidderRequest: MOCK_BID_REQUEST }); - expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(1); - expect(warnStub.calledOnce).to.be.true; - - let errorObj = cnvrHelper.bidderErrorCache[MOCK_BID_REQUEST.auctionId]; - expect(errorObj.errors).to.have.lengthOf(1); - expect(errorObj.errors[0].status).to.equal(XHR_ERROR_MOCK.status); - expect(errorObj.errors[0].message).to.equal(XHR_ERROR_MOCK.statusText); - expect(errorObj.errors[0].bidderCode).to.equal(MOCK_BID_REQUEST.bidderCode); - expect(errorObj.errors[0].url).to.not.be.undefined; - - events.emit(EVENTS.BIDDER_ERROR, { error: XHR_ERROR_MOCK, bidderRequest: MOCK_BID_REQUEST }); - errorObj = cnvrHelper.bidderErrorCache[MOCK_BID_REQUEST.auctionId]; - expect(errorObj.errors).to.have.lengthOf(2); - }); - }); -});