diff --git a/modules/liveyieldAnalyticsAdapter.js b/modules/liveyieldAnalyticsAdapter.js new file mode 100644 index 00000000000..a6ac9765957 --- /dev/null +++ b/modules/liveyieldAnalyticsAdapter.js @@ -0,0 +1,212 @@ +import adapter from '../src/AnalyticsAdapter'; +import adapterManager from '../src/adapterManager'; +import CONSTANTS from '../src/constants.json'; +import * as utils from '../src/utils'; + +const { + EVENTS: { BID_REQUESTED, BID_TIMEOUT, BID_RESPONSE, BID_WON } +} = CONSTANTS; + +const prebidVersion = '$prebid.version$'; + +const adapterConfig = { + /** Name of the `rta` function, override only when instructed. */ + rtaFunctionName: 'rta', + + /** This is optional but highly recommended. The value returned by the + * function will be used as ad impression ad unit attribute value. + * + * As such if you have placement (10293845) or ad unit codes + * (div-gpt-ad-124984-0) but you want these to be translated to meaningful + * values like 'SIDEBAR-AD-01-MOBILE' then this function shall express this + * mapping. + */ + getAdUnitName: function(placementOrAdUnitCode) { + return placementOrAdUnitCode; + }, + + /** + * Function used to extract placement/adUnitCode (depending on prebid version). + * + * The extracted value will be passed to the `getAdUnitName()` for mapping into + * human friendly value. + */ + getPlacementOrAdUnitCode: function(bid, version) { + return version[0] === '0' ? bid.placementCode : bid.adUnitCode; + } +}; + +const cpmToMicroUSD = v => (isNaN(v) ? 0 : Math.round(v * 1000)); + +const liveyield = Object.assign(adapter({ analyticsType: 'bundle' }), { + track({ eventType, args }) { + switch (eventType) { + case BID_REQUESTED: + args.bids.forEach(function(b) { + try { + window[adapterConfig.rtaFunctionName]( + 'bidRequested', + adapterConfig.getAdUnitName( + adapterConfig.getPlacementOrAdUnitCode(b, prebidVersion) + ), + args.bidderCode + ); + } catch (e) { + utils.logError(e); + } + }); + break; + case BID_RESPONSE: + var cpm = args.statusMessage === 'Bid available' ? args.cpm : null; + try { + window[adapterConfig.rtaFunctionName]( + 'addBid', + adapterConfig.getAdUnitName( + adapterConfig.getPlacementOrAdUnitCode(args, prebidVersion) + ), + args.bidder || 'unknown', + cpmToMicroUSD(cpm), + typeof args.bidder === 'undefined', + args.statusMessage !== 'Bid available' + ) + } catch (e) { + utils.logError(e); + } + break; + case BID_TIMEOUT: + window[adapterConfig.rtaFunctionName]('biddersTimeout', args); + break; + case BID_WON: + try { + const ad = adapterConfig.getAdUnitName( + adapterConfig.getPlacementOrAdUnitCode(args, prebidVersion) + ); + if (!ad) { + utils.logError('Cannot find ad by unit name: ' + + adapterConfig.getAdUnitName( + adapterConfig.getPlacementOrAdUnitCode(args, prebidVersion) + )); + break; + } + if (!args.bidderCode || !args.cpm) { + utils.logError('Bidder code or cpm is not valid'); + break; + } + window[adapterConfig.rtaFunctionName]( + 'resolveSlot', + adapterConfig.getAdUnitName( + adapterConfig.getPlacementOrAdUnitCode(args, prebidVersion) + ), + { + prebidWon: true, + prebidPartner: args.bidderCode, + prebidValue: cpmToMicroUSD(args.cpm) + } + ) + } catch (e) { + utils.logError(e); + } + break; + } + } +}); + +liveyield.originEnableAnalytics = liveyield.enableAnalytics; + +/** + * Minimal valid config: + * + * ``` + * { + * provider: 'liveyield', + * options: { + * // will be provided by the LiveYield team + * customerId: 'UUID', + * // will be provided by the LiveYield team, + * customerName: 'Customer Name', + * // do NOT use window.location.host, use constant value + * customerSite: 'Fixed Site Name', + * // this is used to be inline with GA 'sessionizer' which closes the session on midnight (EST-time). + * sessionTimezoneOffset: '-300' + * } + * } + * ``` + */ +liveyield.enableAnalytics = function(config) { + if (!config || !config.provider || config.provider !== 'liveyield') { + utils.logError('expected config.provider to equal liveyield'); + return; + } + if (!config.options) { + utils.logError('options must be defined'); + return; + } + if (!config.options.customerId) { + utils.logError('options.customerId is required'); + return; + } + if (!config.options.customerName) { + utils.logError('options.customerName is required'); + return; + } + if (!config.options.customerSite) { + utils.logError('options.customerSite is required'); + return; + } + if (!config.options.sessionTimezoneOffset) { + utils.logError('options.sessionTimezoneOffset is required'); + return; + } + Object.assign(adapterConfig, config.options); + if (typeof window[adapterConfig.rtaFunctionName] !== 'function') { + utils.logError(`Function ${adapterConfig.rtaFunctionName} is not defined.` + + `Make sure that LiveYield snippet in included before the Prebid Analytics configuration.`); + return; + } + + const additionalParams = { + customerTimezone: config.options.customerTimezone, + contentId: config.options.contentId, + contentPart: config.options.contentPart, + contentAuthor: config.options.contentAuthor, + contentTitle: config.options.contentTitle, + contentCategory: config.options.contentCategory, + contentLayout: config.options.contentLayout, + contentVariants: config.options.contentVariants, + contentTimezone: config.options.contentTimezone, + cstringDim1: config.options.cstringDim1, + cstringDim2: config.options.cstringDim2, + cintDim1: config.options.cintDim1, + cintDim2: config.options.cintDim2, + cintArrayDim1: config.options.cintArrayDim1, + cintArrayDim2: config.options.cintArrayDim2, + cuniqueStringMet1: config.options.cuniqueStringMet1, + cuniqueStringMet2: config.options.cuniqueStringMet2, + cavgIntMet1: config.options.cavgIntMet1, + cavgIntMet2: config.options.cavgIntMet2, + csumIntMet1: config.options.csumIntMet1, + csumIntMet2: config.options.csumIntMet2 + }; + + Object.keys(additionalParams).forEach( + key => additionalParams[key] == null && delete additionalParams[key] + ); + + window[adapterConfig.rtaFunctionName]( + 'create', + config.options.customerId, + config.options.customerName, + config.options.customerSite, + config.options.sessionTimezoneOffset, + additionalParams + ); + + liveyield.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: liveyield, + code: 'liveyield' +}); + +export default liveyield; diff --git a/modules/liveyieldAnalyticsAdapter.md b/modules/liveyieldAnalyticsAdapter.md new file mode 100644 index 00000000000..a5e602361a1 --- /dev/null +++ b/modules/liveyieldAnalyticsAdapter.md @@ -0,0 +1,45 @@ +# Overview + +Module Name: LiveYield Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: liveyield@pubocean.com + +# Description + +To install the LiveYield Tracker following snippet shall be added at the top of +the page. + +``` +(function(i,s,o,g,r,a,m,z){i['RTAAnalyticsObject']=r;i[r]=i[r]||function(){ +z=Array.prototype.slice.call(arguments);z.unshift(+new Date()); +(i[r].q=i[r].q||[]).push(z)},i[r].t=1,i[r].l=1*new Date();a=s.createElement(o), +m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) +})(window,document,'script','https://rta.pubocean.com/lib/pubocean-tracker.min.js','rta'); +``` + +# Test Parameters + +The LiveYield team will provide you configurations for each of your sites, it +will be similar to: + +``` +{ + provider: 'liveyield', + options: { + // will be provided by the LiveYield team + customerId: 'UUID', + // will be provided by the LiveYield team, + customerName: 'Customer Name', + // do NOT use window.location.host, use constant value + customerSite: 'Fixed Site Name', + // this is used to be inline with GA 'sessionizer' which closes the session on midnight (EST-time). + sessionTimezoneOffset: '-300' + } +} +``` + +Additional documentation and support will be provided by the LiveYield team as +part of the onboarding process. + diff --git a/test/spec/modules/liveyieldAnalyticsAdapter_spec.js b/test/spec/modules/liveyieldAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..61d9d9e5de0 --- /dev/null +++ b/test/spec/modules/liveyieldAnalyticsAdapter_spec.js @@ -0,0 +1,399 @@ +import CONSTANTS from 'src/constants.json'; +import liveyield from 'modules/liveyieldAnalyticsAdapter'; +import { expect } from 'chai'; +const events = require('src/events'); + +const { + EVENTS: { BID_REQUESTED, BID_TIMEOUT, BID_RESPONSE, BID_WON } +} = CONSTANTS; + +describe('liveyield analytics adapter', function() { + const rtaCalls = []; + + window.rta = function() { + rtaCalls.push({ callArgs: arguments }); + }; + + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + }); + afterEach(function() { + events.getEvents.restore(); + }); + describe('initialization', function() { + afterEach(function() { + rtaCalls.length = 0; + }); + it('it should require provider', function() { + liveyield.enableAnalytics({}); + expect(rtaCalls).to.be.empty; + }); + it('should require config.options', function() { + liveyield.enableAnalytics({ provider: 'liveyield' }); + expect(rtaCalls).to.be.empty; + }); + it('should require options.customerId', function() { + liveyield.enableAnalytics({ provider: 'liveyield', options: {} }); + expect(rtaCalls).to.be.empty; + }); + it('should require options.customerName', function() { + liveyield.enableAnalytics({ + provider: 'liveyield', + options: { + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa' + } + }) + expect(rtaCalls).to.be.empty; + }); + it('should require options.customerSite', function() { + liveyield.enableAnalytics({ + provider: 'liveyield', + options: { + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa', + customerName: 'pubocean' + } + }); + expect(rtaCalls).to.be.empty; + }); + it('should require options.sessionTimezoneOffset', function() { + liveyield.enableAnalytics({ + provider: 'liveyield', + options: { + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa', + customerName: 'pubocean', + customerSite: 'scribol.com' + } + }); + expect(rtaCalls).to.be.empty; + }); + it("should throw error, when 'rta' function is not defined ", function() { + const keepMe = window.rta; + + delete window.rta; + + liveyield.enableAnalytics({ + provider: 'liveyield', + options: { + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa', + customerName: 'pubocean', + customerSite: 'scribol.com', + sessionTimezoneOffset: 12 + } + }); + expect(rtaCalls).to.be.empty; + + window.rta = keepMe; + }); + it('should initialize when all required parameters are passed', function() { + liveyield.enableAnalytics({ + provider: 'liveyield', + options: { + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa', + customerName: 'pubocean', + customerSite: 'scribol.com', + sessionTimezoneOffset: 12 + } + }); + expect(rtaCalls[0].callArgs['0']).to.match(/create/); + expect(rtaCalls[0].callArgs['1']).to.match( + /d6a6f8da-190f-47d6-ae11-f1a4469083fa/ + ); + expect(rtaCalls[0].callArgs['2']).to.match(/pubocean/); + expect(rtaCalls[0].callArgs['4']).to.match(/12/); + liveyield.disableAnalytics(); + }); + it('should allow to redefine rta function name', function() { + const keepMe = window.rta; + window.abc = keepMe; + delete window.rta; + liveyield.enableAnalytics({ + provider: 'liveyield', + options: { + rtaFunctionName: 'abc', + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa', + customerName: 'test', + customerSite: 'scribol.com', + sessionTimezoneOffset: 25 + } + }); + + liveyield.disableAnalytics(); + expect(rtaCalls[0].callArgs['0']).to.match(/create/); + expect(rtaCalls[0].callArgs['1']).to.match( + /d6a6f8da-190f-47d6-ae11-f1a4469083fa/ + ); + expect(rtaCalls[0].callArgs['2']).to.match(/test/); + expect(rtaCalls[0].callArgs['4']).to.match(/25/); + + window.rta = keepMe; + liveyield.disableAnalytics(); + }); + it('should handle custom parameters', function() { + liveyield.enableAnalytics({ + provider: 'liveyield', + options: { + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa', + customerName: 'test2', + customerSite: 'scribol.com', + sessionTimezoneOffset: 38, + contentTitle: 'testTitle', + contentAuthor: 'testAuthor', + contentCategory: 'testCategory' + } + }); + + liveyield.disableAnalytics(); + expect(rtaCalls[0].callArgs['0']).to.match(/create/); + expect(rtaCalls[0].callArgs['2']).to.match(/test2/); + expect(rtaCalls[0].callArgs['4']).to.match(/38/); + expect(rtaCalls[0].callArgs['5'].contentTitle).to.match(/testTitle/); + expect(rtaCalls[0].callArgs['5'].contentAuthor).to.match(/testAuthor/); + expect(rtaCalls[0].callArgs['5'].contentCategory).to.match( + /testCategory/ + ); + liveyield.disableAnalytics(); + }); + }); + + describe('handling events', function() { + const options = { + provider: 'liveyield', + options: { + customerId: 'd6a6f8da-190f-47d6-ae11-f1a4469083fa', + customerName: 'pubocean', + customerSite: 'scribol.com', + sessionTimezoneOffset: 12 + } + }; + beforeEach(function() { + rtaCalls.length = 0; + liveyield.enableAnalytics(options); + }); + afterEach(function() { + liveyield.disableAnalytics(); + }); + it('should handle BID_REQUESTED event', function() { + const bidRequest = { + bidderCode: 'appnexus', + bids: [ + { + params: { + placementId: '10433394' + }, + adUnitCode: 'div-gpt-ad-1438287399331-0', + transactionId: '2f481ff1-8d20-4c28-8e36-e384e9e3eec6', + sizes: '300x250,300x600', + bidId: '2eddfdc0c791dc', + auctionId: 'a5b849e5-87d7-4205-8300-d063084fcfb7' + } + ] + }; + + events.emit(BID_REQUESTED, bidRequest); + expect(rtaCalls[1].callArgs['0']).to.equal('bidRequested'); + expect(rtaCalls[1].callArgs['1']).to.equal('div-gpt-ad-1438287399331-0'); + expect(rtaCalls[1].callArgs['2']).to.equal('appnexus'); + }); + it('should handle BID_REQUESTED event with invalid args', function() { + const bidRequest = { + bids: [ + { + params: { + placementId: '10433394' + }, + transactionId: '2f481ff1-8d20-4c28-8e36-e384e9e3eec6', + sizes: '300x250,300x600', + bidId: '2eddfdc0c791dc', + auctionId: 'a5b849e5-87d7-4205-8300-d063084fcf' + }, + { + params: { + placementId: '31034023' + }, + transactionId: '2f481ff1-8d20-4c28-8e36-e384e9e3eec6', + sizes: '300x250,300x600', + bidId: '3dkg0404fmd0', + auctionId: 'a5b849e5-87d7-4205-8300-d063084fcf' + } + ] + }; + events.emit(BID_REQUESTED, bidRequest); + expect(rtaCalls[1].callArgs['0']).to.equal('bidRequested'); + expect(rtaCalls[1].callArgs['1']).to.equal(undefined); + expect(rtaCalls[1].callArgs['2']).to.equal(undefined); + expect(rtaCalls[1].callArgs['0']).to.equal('bidRequested'); + }); + it('should handle BID_RESPONSE event', function() { + const bidResponse = { + height: 250, + statusMessage: 'Bid available', + adId: '2eddfdc0c791dc', + mediaType: 'banner', + source: 'client', + requestId: '2eddfdc0c791dc', + cpm: 0.5, + creativeId: 29681110, + currency: 'USD', + netRevenue: true, + ttl: 300, + auctionId: 'a5b849e5-87d7-4205-8300-d063084fcfb7', + responseTimestamp: 1522265866110, + requestTimestamp: 1522265863600, + bidder: 'appnexus', + adUnitCode: 'div-gpt-ad-1438287399331-0', + timeToRespond: 2510, + size: '300x250' + }; + + events.emit(BID_RESPONSE, bidResponse); + expect(rtaCalls[1].callArgs['0']).to.equal('addBid'); + expect(rtaCalls[1].callArgs['1']).to.equal('div-gpt-ad-1438287399331-0'); + expect(rtaCalls[1].callArgs['2']).to.equal('appnexus'); + expect(rtaCalls[1].callArgs['3']).to.equal(500); + expect(rtaCalls[1].callArgs['4']).to.equal(false); + expect(rtaCalls[1].callArgs['5']).to.equal(false); + }); + it('should handle BID_RESPONSE event with undefined bidder and cpm', function() { + const bidResponse = { + height: 250, + statusMessage: 'Bid available', + adId: '2eddfdc0c791dc', + mediaType: 'banner', + source: 'client', + requestId: '2eddfdc0c791dc', + creativeId: 29681110, + currency: 'USD', + netRevenue: true, + ttl: 300, + auctionId: 'a5b849e5-87d7-4205-8300-d063084fcfb7', + responseTimestamp: 1522265866110, + requestTimestamp: 1522265863600, + adUnitCode: 'div-gpt-ad-1438287399331-0', + timeToRespond: 2510, + size: '300x250' + }; + events.emit(BID_RESPONSE, bidResponse); + expect(rtaCalls[1].callArgs['0']).to.equal('addBid'); + expect(rtaCalls[1].callArgs['2']).to.equal('unknown'); + expect(rtaCalls[1].callArgs['3']).to.equal(0); + expect(rtaCalls[1].callArgs['4']).to.equal(true); + }); + it('should handle BID_RESPONSE event with undefined status message and adUnitCode', function() { + const bidResponse = { + height: 250, + adId: '2eddfdc0c791dc', + mediaType: 'banner', + source: 'client', + requestId: '2eddfdc0c791dc', + cpm: 0.5, + creativeId: 29681110, + currency: 'USD', + netRevenue: true, + ttl: 300, + auctionId: 'a5b849e5-87d7-4205-8300-d063084fcfb7', + responseTimestamp: 1522265866110, + requestTimestamp: 1522265863600, + bidder: 'appnexus', + timeToRespond: 2510, + size: '300x250' + }; + events.emit(BID_RESPONSE, bidResponse); + expect(rtaCalls[1].callArgs['0']).to.equal('addBid'); + expect(rtaCalls[1].callArgs['1']).to.equal(undefined); + expect(rtaCalls[1].callArgs['3']).to.equal(0); + expect(rtaCalls[1].callArgs['5']).to.equal(true); + }); + it('should handle BID_TIMEOUT', function() { + const bidTimeout = [ + { + bidId: '2baa51527bd015', + bidder: 'bidderOne', + adUnitCode: '/19968336/header-bid-tag-0', + auctionId: '66529d4c-8998-47c2-ab3e-5b953490b98f' + }, + { + bidId: '6fe3b4c2c23092', + bidder: 'bidderTwo', + adUnitCode: '/19968336/header-bid-tag-0', + auctionId: '66529d4c-8998-47c2-ab3e-5b953490b98f' + } + ]; + events.emit(BID_TIMEOUT, bidTimeout); + expect(rtaCalls[1].callArgs['0']).to.equal('biddersTimeout'); + expect(rtaCalls[1].callArgs['1'].length).to.equal(2); + }); + it('should handle BID_WON event', function() { + const bidWon = { + adId: '4587fec4900b81', + mediaType: 'banner', + requestId: '4587fec4900b81', + cpm: 1.962, + creativeId: 2126, + currency: 'EUR', + netRevenue: true, + ttl: 302, + auctionId: '914bedad-b145-4e46-ba58-51365faea6cb', + statusMessage: 'Bid available', + responseTimestamp: 1530628534437, + requestTimestamp: 1530628534219, + bidderCode: 'testbidder4', + adUnitCode: 'div-gpt-ad-1438287399331-0', + timeToRespond: 218, + size: '300x250', + status: 'rendered' + }; + events.emit(BID_WON, bidWon); + expect(rtaCalls[1].callArgs['0']).to.equal('resolveSlot'); + expect(rtaCalls[1].callArgs['1']).to.equal( + 'div-gpt-ad-1438287399331-0' + ); + expect(rtaCalls[1].callArgs['2'].prebidWon).to.equal(true); + expect(rtaCalls[1].callArgs['2'].prebidPartner).to.equal('testbidder4'); + expect(rtaCalls[1].callArgs['2'].prebidValue).to.equal(1962); + }); + it('should throw error, invoking BID_WON event without adUnitCode', function() { + const bidWon = { + adId: '4587fec4900b81', + mediaType: 'banner', + requestId: '4587fec4900b81', + cpm: 1.962, + creativeId: 2126, + currency: 'EUR', + netRevenue: true, + ttl: 302, + auctionId: '914bedad-b145-4e46-ba58-51365faea6cb', + statusMessage: 'Bid available', + responseTimestamp: 1530628534437, + requestTimestamp: 1530628534219, + timeToRespond: 218, + bidderCode: 'testbidder4', + size: '300x250', + status: 'rendered' + }; + events.emit(BID_WON, bidWon); + expect(rtaCalls[1]).to.be.undefined; + }); + it('should throw error, invoking BID_WON event without bidderCode', function() { + const bidWon = { + adId: '4587fec4900b81', + mediaType: 'banner', + requestId: '4587fec4900b81', + cpm: 1.962, + creativeId: 2126, + currency: 'EUR', + netRevenue: true, + ttl: 302, + auctionId: '914bedad-b145-4e46-ba58-51365faea6cb', + statusMessage: 'Bid available', + responseTimestamp: 1530628534437, + requestTimestamp: 1530628534219, + adUnitCode: 'div-gpt-ad-1438287399331-0', + timeToRespond: 218, + size: '300x250', + status: 'rendered' + }; + events.emit(BID_WON, bidWon); + expect(rtaCalls[1]).to.be.undefined; + }); + }); +});