diff --git a/modules/liveIntentAnalyticsAdapter.js b/modules/liveIntentAnalyticsAdapter.js new file mode 100644 index 00000000000..b702e4a319a --- /dev/null +++ b/modules/liveIntentAnalyticsAdapter.js @@ -0,0 +1,147 @@ +import {ajax} from '../src/ajax.js'; +import { generateUUID, logInfo, logWarn } from '../src/utils.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import { auctionManager } from '../src/auctionManager.js'; + +const ANALYTICS_TYPE = 'endpoint'; +const URL = 'https://wba.liadm.com/analytic-events'; +const GVL_ID = 148; +const ADAPTER_CODE = 'liveintent'; +const DEFAULT_SAMPLING = 0.1; +const DEFAULT_BID_WON_TIMEOUT = 2000; +const { EVENTS: { AUCTION_END } } = CONSTANTS; +let initOptions = {}; +let isSampled; +let bidWonTimeout; + +function handleAuctionEnd(args) { + setTimeout(() => { + const auction = auctionManager.index.getAuction(args.auctionId); + const winningBids = (auction) ? auction.getWinningBids() : []; + const data = createAnalyticsEvent(args, winningBids); + sendAnalyticsEvent(data); + }, bidWonTimeout); +} + +function getAnalyticsEventBids(bidsReceived) { + return bidsReceived.map(bid => { + return { + adUnitCode: bid.adUnitCode, + timeToRespond: bid.timeToRespond, + cpm: bid.cpm, + currency: bid.currency, + ttl: bid.ttl, + bidder: bid.bidder + }; + }); +} + +function getBannerSizes(banner) { + if (banner && banner.sizes) { + return banner.sizes.map(size => { + const [width, height] = size; + return {w: width, h: height}; + }); + } else return []; +} + +function getUniqueBy(arr, key) { + return [...new Map(arr.map(item => [item[key], item])).values()] +} + +function createAnalyticsEvent(args, winningBids) { + let payload = { + instanceId: generateUUID(), + url: window.location.href, + bidsReceived: getAnalyticsEventBids(args.bidsReceived), + auctionStart: args.timestamp, + auctionEnd: args.auctionEnd, + adUnits: [], + userIds: [], + bidders: [] + } + let allUserIds = []; + + if (args.adUnits) { + args.adUnits.forEach(unit => { + if (unit.mediaTypes && unit.mediaTypes.banner) { + payload['adUnits'].push({ + code: unit.code, + mediaType: 'banner', + sizes: getBannerSizes(unit.mediaTypes.banner), + ortb2Imp: unit.ortb2Imp + }); + } + if (unit.bids) { + let userIds = unit.bids.flatMap(getAnalyticsEventUserIds); + allUserIds.push(...userIds); + let bidders = unit.bids.map(({bidder, params}) => { + return { bidder, params } + }); + + payload['bidders'].push(...bidders); + } + }) + let uniqueUserIds = getUniqueBy(allUserIds, 'source'); + payload['userIds'] = uniqueUserIds; + } + payload['winningBids'] = getAnalyticsEventBids(winningBids); + payload['auctionId'] = args.auctionId; + return payload; +} + +function getAnalyticsEventUserIds(bid) { + if (bid && bid.userIdAsEids) { + return bid.userIdAsEids.map(({source, uids, ext}) => { + let analyticsEventUserId = {source, uids, ext}; + return ignoreUndefined(analyticsEventUserId) + }); + } else { return []; } +} + +function sendAnalyticsEvent(data) { + ajax(URL, { + success: function () { + logInfo('LiveIntent Prebid Analytics: send data success'); + }, + error: function (e) { + logWarn('LiveIntent Prebid Analytics: send data error' + e); + } + }, JSON.stringify(data), { + contentType: 'application/json', + method: 'POST' + }) +} + +function ignoreUndefined(data) { + const filteredData = Object.entries(data).filter(([key, value]) => value) + return Object.fromEntries(filteredData) +} + +let liAnalytics = Object.assign(adapter({URL, ANALYTICS_TYPE}), { + track({ eventType, args }) { + if (eventType == AUCTION_END && args && isSampled) { handleAuctionEnd(args); } + } +}); + +// save the base class function +liAnalytics.originEnableAnalytics = liAnalytics.enableAnalytics; + +// override enableAnalytics so we can get access to the config passed in from the page +liAnalytics.enableAnalytics = function (config) { + initOptions = config.options; + const sampling = (initOptions && initOptions.sampling) || DEFAULT_SAMPLING; + isSampled = Math.random() < parseFloat(sampling); + bidWonTimeout = (initOptions && initOptions.bidWonTimeout) || DEFAULT_BID_WON_TIMEOUT; + liAnalytics.originEnableAnalytics(config); // call the base class function +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: liAnalytics, + code: ADAPTER_CODE, + gvlid: GVL_ID +}); + +export default liAnalytics; diff --git a/modules/liveIntentAnalyticsAdapter.md b/modules/liveIntentAnalyticsAdapter.md new file mode 100644 index 00000000000..15f51006134 --- /dev/null +++ b/modules/liveIntentAnalyticsAdapter.md @@ -0,0 +1,22 @@ +# Overview +Module Name: LiveIntent Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: product@liveintent.com + +# Description + +Analytics adapter for [LiveIntent](https://www.liveintent.com/). Contact product@liveintent.com for information. + +# Test Parameters + +``` +{ + provider: 'liveintent', + options: { + bidWonTimeout: 2000, + sampling: 0.5 // the tracked event percentage, a number between 0 to 1 + } +} +``` diff --git a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..413bb7ec03f --- /dev/null +++ b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js @@ -0,0 +1,295 @@ +import liAnalytics from 'modules/liveintentAnalyticsAdapter'; +import { expect } from 'chai'; +import { server } from 'test/mocks/xhr.js'; +import { auctionManager } from 'src/auctionManager.js'; + +let utils = require('src/utils'); +let instanceId = '77abbc81-c1f1-41cd-8f25-f7149244c800'; +let url = window.location.href; +let sandbox; +let clock; +let now = new Date(); + +let events = require('src/events'); +let constants = require('src/constants.json'); +let auctionId = '99abbc81-c1f1-41cd-8f25-f7149244c897' + +const config = { + provider: 'liveintent', + options: { + bidWonTimeout: 2000, + sampling: 1 + } +} + +let args = { + auctionId: auctionId, + timestamp: 1660915379703, + auctionEnd: 1660915381635, + adUnits: [ + { + code: 'ID_Bot100AdJ1', + mediaTypes: { + banner: { + sizes: [ + [ + 300, + 250 + ], + [ + 320, + 50 + ] + ] + } + }, + ortb2Imp: { + gpid: '/777/test/home/ID_Bot100AdJ1', + ext: { + data: { + aupName: '/777/test/home/ID_Bot100AdJ1', + adserver: { + name: 'gam', + adslot: '/777/test/home/ID_Bot100AdJ1' + }, + pbadslot: '/777/test/home/ID_Bot100AdJ1' + }, + gpid: '/777/test/home/ID_Bot100AdJ1' + } + }, + bids: [ + { + bidder: 'testBidder', + params: { + siteId: 321218, + zoneId: 1732558, + position: 'bug', + accountId: 10777 + }, + userIdAsEids: [ + { + source: 'source1.com', + uids: [ + { + id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', + atype: 1, + ext: { + linkType: 2 + } + } + ] + }, + { + source: 'source2.com', + uids: [ + { + id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', + atype: 1, + ext: { + linkType: 2 + } + } + ] + } + ] + }, + { + bidder: 'testBidder2', + params: { + adSlot: '2926251', + publisherId: '159423' + }, + userIdAsEids: [ + { + source: 'source1.com', + uids: [ + { + id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', + atype: 1, + ext: { + linkType: 2 + } + } + ] + } + ] + } + ] + } + ], + bidderRequests: [ + { + bidderCode: 'tripl_ss1', + auctionId: '8e5a5eda-a7dc-49a3-bc7f-654fc', + bidderRequestId: '953fe1ee8a1645', + uniquePbsTid: '0da1f980-8351-415d-860d-ebbdb4274179', + auctionStart: 1660915379703 + }, + { + bidderCode: 'tripl_ss2', + auctionId: '8e5a5eda-a7dc-49a3-bc7f-6ca682ae893c', + bidderRequestId: '953fe1ee8a164e', + uniquePbsTid: '0da1f980-8351-415d-860d-ebbdb4274180', + auctionStart: 1660915379703 + } + ], + bidsReceived: [ + { + adUnitCode: 'ID_Bot100AdJ1', + timeToRespond: 824, + cpm: 0.447, + currency: 'USD', + ttl: 300, + bidder: 'testBidder' + } + ], + winningBids: [] +} + +let winningBids = [ + { + adUnitCode: 'ID_Bot100AdJ1', + timeToRespond: 824, + cpm: 0.447, + currency: 'USD', + ttl: 300, + bidder: 'testBidder' + } +]; + +let expectedEvent = { + instanceId: instanceId, + url: url, + bidsReceived: [ + { + adUnitCode: 'ID_Bot100AdJ1', + timeToRespond: 824, + cpm: 0.447, + currency: 'USD', + ttl: 300, + bidder: 'testBidder' + } + ], + auctionStart: 1660915379703, + auctionEnd: 1660915381635, + adUnits: [ + { + code: 'ID_Bot100AdJ1', + mediaType: 'banner', + sizes: [ + { + w: 300, + h: 250 + }, + { + w: 320, + h: 50 + } + ], + ortb2Imp: { + gpid: '/777/test/home/ID_Bot100AdJ1', + ext: { + data: { + aupName: '/777/test/home/ID_Bot100AdJ1', + adserver: { + name: 'gam', + adslot: '/777/test/home/ID_Bot100AdJ1' + }, + pbadslot: '/777/test/home/ID_Bot100AdJ1' + }, + gpid: '/777/test/home/ID_Bot100AdJ1' + } + } + } + ], + winningBids: [ + { + adUnitCode: 'ID_Bot100AdJ1', + timeToRespond: 824, + cpm: 0.447, + currency: 'USD', + ttl: 300, + bidder: 'testBidder' + } + ], + auctionId: auctionId, + userIds: [ + { + source: 'source1.com', + uids: [ + { + id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', + atype: 1, + ext: { + linkType: 2 + } + } + ] + }, + { + source: 'source2.com', + uids: [ + { + id: 'ID5*yO-L9xRugTx4mkIJ9z99eYva6CZQhz8B70QOkLLSEEQWowsxvVQqMaZOt4qpBTYAFqR3y6ZtZ8qLJJBAsRqnRRalTfy8iZszQavAAkZcAjkWpxp6DnOSkF3R5LafC10OFqhwcxH699dDc_fI6RVEGBasN6zrJwgqCGelgfQLtQwWrikWRyi0l3ICFj9JUiVGFrCF8SAFaqJD9A0_I07a8xa0-jADtEj1T8w30oX--sMWvTK_I5_3zA5f3z0OMoxbFsCMFdhfGRDuw5GrpI475g', + atype: 1, + ext: { + linkType: 2 + } + } + ] + } + ], + bidders: [ + { + bidder: 'testBidder', + params: { + siteId: 321218, + zoneId: 1732558, + position: 'bug', + accountId: 10777 + } + }, + { + bidder: 'testBidder2', + params: { + adSlot: '2926251', + publisherId: '159423' + } + } + ] +}; + +describe('LiveIntent Analytics Adapter ', () => { + beforeEach(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(events, 'getEvents').returns([]); + clock = sandbox.useFakeTimers(now.getTime()); + }); + afterEach(function () { + liAnalytics.disableAnalytics(); + sandbox.restore(); + clock.restore(); + }); + + it('request is computed and sent correctly', () => { + liAnalytics.enableAnalytics(config); + sandbox.stub(utils, 'generateUUID').returns(instanceId); + sandbox.stub(auctionManager.index, 'getAuction').withArgs(auctionId).returns({ getWinningBids: () => winningBids }); + events.emit(constants.EVENTS.AUCTION_END, args); + clock.tick(2000); + expect(server.requests.length).to.equal(1); + + let requestBody = JSON.parse(server.requests[0].requestBody); + expect(requestBody).to.deep.equal(expectedEvent); + }); + + it('track is called', () => { + liAnalytics.enableAnalytics(config); + sandbox.stub(liAnalytics, 'track'); + events.emit(constants.EVENTS.AUCTION_END, args); + events.emit(constants.EVENTS.AUCTION_END, args); + events.emit(constants.EVENTS.AUCTION_END, args); + clock.tick(6000); + sinon.assert.callCount(liAnalytics.track, 3) + }) +});