forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from guardian/kc-analytics
Create analytics adapter to send data to data lake (eventually)
- Loading branch information
Showing
6 changed files
with
370 additions
and
10 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
// see http://prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html | ||
import adapter from 'src/AnalyticsAdapter'; | ||
import CONSTANTS from 'src/constants.json'; | ||
import * as adaptermanager from 'src/adaptermanager'; | ||
import {ajax} from 'src/ajax'; | ||
|
||
const analyticsType = 'endpoint'; | ||
const QUEUE_TIMEOUT = 4000; | ||
|
||
let analyticsAdapter = Object.assign(adapter({analyticsType}), | ||
{ | ||
track({eventType, args}) { | ||
if (!analyticsAdapter.context) { | ||
return; | ||
} | ||
let handler = null; | ||
switch (eventType) { | ||
case CONSTANTS.EVENTS.AUCTION_INIT: | ||
if (analyticsAdapter.context.queue) { | ||
analyticsAdapter.context.queue.init(); | ||
} | ||
handler = trackAuctionInit; | ||
break; | ||
case CONSTANTS.EVENTS.BID_REQUESTED: | ||
handler = trackBidRequest; | ||
break; | ||
case CONSTANTS.EVENTS.BID_RESPONSE: | ||
handler = trackBidResponse; | ||
break; | ||
case CONSTANTS.EVENTS.BID_TIMEOUT: | ||
handler = trackBidTimeout; | ||
break; | ||
case CONSTANTS.EVENTS.AUCTION_END: | ||
handler = trackAuctionEnd; | ||
break; | ||
} | ||
if (handler) { | ||
let events = handler(args); | ||
if (analyticsAdapter.context.queue) { | ||
analyticsAdapter.context.queue.push(events); | ||
} | ||
if (eventType === CONSTANTS.EVENTS.AUCTION_END) { | ||
sendAll(); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
function buildRequestTemplate(options) { | ||
return { | ||
pv: options.pv | ||
} | ||
} | ||
|
||
function sendAll() { | ||
let events = analyticsAdapter.context.queue.popAll(); | ||
if (events.length !== 0) { | ||
let req = Object.assign({}, analyticsAdapter.context.requestTemplate, {hb_ev: events}); | ||
ajax( | ||
`//${analyticsAdapter.context.host}/commercial/api/hb`, | ||
() => { | ||
}, | ||
JSON.stringify(req), | ||
{ | ||
method: 'PUT', | ||
contentType: 'application/json; charset=utf-8' | ||
} | ||
); | ||
} | ||
} | ||
|
||
function trackAuctionInit(args) { | ||
analyticsAdapter.context.auctionTimeStart = Date.now(); | ||
const event = createHbEvent(undefined, 'init', undefined, args.auctionId); | ||
return [event]; | ||
} | ||
|
||
function trackBidRequest(args) { | ||
return args.bids.map(bid => | ||
createHbEvent(args.bidderCode, 'request', bid.adUnitCode)); | ||
} | ||
|
||
function trackBidResponse(args) { | ||
const event = createHbEvent(args.bidderCode, 'response', args.adUnitCode, undefined, args.timeToRespond); | ||
return [event]; | ||
} | ||
|
||
function trackBidTimeout(args) { | ||
const timeToRespond = Date.now() - analyticsAdapter.context.auctionTimeStart; | ||
return args.map(bid => createHbEvent(bid.bidder, 'timeout', bid.adUnitCode, undefined, timeToRespond)); | ||
} | ||
|
||
function trackAuctionEnd(args) { | ||
const duration = Date.now() - analyticsAdapter.context.auctionTimeStart; | ||
const event = createHbEvent(undefined, 'end', undefined, args.auctionId, duration); | ||
return [event]; | ||
} | ||
|
||
function createHbEvent(bidder, event, slotId, auctionId, timeToRespond, args) { | ||
let ev = {ev: event}; | ||
if (bidder) { | ||
ev.n = bidder | ||
} | ||
if (slotId) { | ||
ev.sid = slotId; | ||
} | ||
if (auctionId) { | ||
ev.aid = auctionId; | ||
} | ||
if (timeToRespond) { | ||
ev.ttr = timeToRespond; | ||
} | ||
if (args) { | ||
ev.args = args; | ||
} | ||
return ev; | ||
} | ||
|
||
export function ExpiringQueue(callback, ttl) { | ||
let queue = []; | ||
let timeoutId; | ||
|
||
this.push = (event) => { | ||
if (event instanceof Array) { | ||
queue.push.apply(queue, event); | ||
} else { | ||
queue.push(event); | ||
} | ||
reset(); | ||
}; | ||
|
||
this.popAll = () => { | ||
let result = queue; | ||
queue = []; | ||
reset(); | ||
return result; | ||
}; | ||
|
||
/** | ||
* For test/debug purposes only | ||
* @return {Array} | ||
*/ | ||
this.peekAll = () => { | ||
return queue; | ||
}; | ||
|
||
this.init = reset; | ||
|
||
function reset() { | ||
if (timeoutId) { | ||
clearTimeout(timeoutId); | ||
} | ||
timeoutId = setTimeout(() => { | ||
if (queue.length) { | ||
callback(); | ||
} | ||
}, ttl); | ||
} | ||
} | ||
|
||
analyticsAdapter.context = {}; | ||
|
||
analyticsAdapter.originEnableAnalytics = analyticsAdapter.enableAnalytics; | ||
|
||
analyticsAdapter.enableAnalytics = (config) => { | ||
if (!config.options.host) { | ||
utils.logError('host is not defined. Analytics won\'t work'); | ||
return; | ||
} | ||
if (!config.options.pv) { | ||
utils.logError('pv is not defined. Analytics won\'t work'); | ||
return; | ||
} | ||
analyticsAdapter.context = { | ||
host: config.options.host, | ||
pv: config.options.pv, | ||
requestTemplate: buildRequestTemplate(config.options), | ||
queue: new ExpiringQueue(sendAll, QUEUE_TIMEOUT) | ||
}; | ||
analyticsAdapter.originEnableAnalytics(config); | ||
}; | ||
|
||
adaptermanager.registerAnalyticsAdapter({ | ||
adapter: analyticsAdapter, | ||
code: 'gu' | ||
}); | ||
|
||
export default analyticsAdapter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import analyticsAdapter from 'modules/guAnalyticsAdapter'; | ||
import {expect} from 'chai'; | ||
import adaptermanager from 'src/adaptermanager'; | ||
import * as ajax from 'src/ajax'; | ||
import CONSTANTS from 'src/constants.json'; | ||
|
||
const events = require('../../../src/events'); | ||
|
||
describe('Gu analytics adapter', () => { | ||
let sandbox; | ||
let timer; | ||
|
||
const REQUEST1 = { | ||
bidderCode: 'b1', | ||
auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', | ||
bidderRequestId: '1a6fc81528d0f7', | ||
bids: [{ | ||
bidder: 'b1', | ||
params: {}, | ||
adUnitCode: 'slot-1', | ||
transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', | ||
sizes: [[300, 250]], | ||
bidId: '208750227436c1', | ||
bidderRequestId: '1a6fc81528d0f7', | ||
auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' | ||
}], | ||
auctionStart: 1509369418387, | ||
timeout: 3000, | ||
start: 1509369418389 | ||
}; | ||
|
||
const REQUEST2 = { | ||
bidderCode: 'b2', | ||
auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', | ||
bidderRequestId: '1a6fc81528d0f6', | ||
bids: [{ | ||
bidder: 'b2', | ||
params: {}, | ||
adUnitCode: 'slot-1', | ||
transactionId: 'de90df62-7fd0-4fbc-8787-92d133a7dc06', | ||
sizes: [[300, 250]], | ||
bidId: '208750227436c2', | ||
bidderRequestId: '1a6fc81528d0f6', | ||
auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' | ||
}], | ||
auctionStart: 1509369418387, | ||
timeout: 3000, | ||
start: 1509369418389 | ||
}; | ||
|
||
const RESPONSE = { | ||
bidderCode: 'b1', | ||
width: 300, | ||
height: 250, | ||
statusMessage: 'Bid available', | ||
adId: '208750227436c1', | ||
mediaType: 'banner', | ||
cpm: 0.015, | ||
ad: '<!-- tag goes here -->', | ||
auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', | ||
responseTimestamp: 1509369418832, | ||
requestTimestamp: 1509369418389, | ||
bidder: 'adapter', | ||
adUnitCode: 'slot-1', | ||
timeToRespond: 443, | ||
size: '300x250' | ||
}; | ||
|
||
before(() => { | ||
sandbox = sinon.sandbox.create(); | ||
timer = sandbox.useFakeTimers(0); | ||
}); | ||
|
||
after(() => { | ||
timer.restore(); | ||
sandbox.restore(); | ||
analyticsAdapter.disableAnalytics(); | ||
}); | ||
|
||
beforeEach(() => { | ||
sandbox.stub(events, 'getEvents').callsFake(() => { | ||
return [] | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
events.getEvents.restore(); | ||
}); | ||
|
||
it('should be configurable', () => { | ||
adaptermanager.registerAnalyticsAdapter({ | ||
code: 'gu', | ||
adapter: analyticsAdapter | ||
}); | ||
|
||
adaptermanager.enableAnalytics({ | ||
provider: 'gu', | ||
options: { | ||
host: 'localhost:9000', | ||
pv: 'pv1234567' | ||
} | ||
}); | ||
|
||
expect(analyticsAdapter.context).to.have.property('host', 'localhost:9000'); | ||
expect(analyticsAdapter.context).to.have.property('pv', 'pv1234567'); | ||
}); | ||
|
||
it('should handle auction init event', () => { | ||
events.emit(CONSTANTS.EVENTS.AUCTION_INIT, { | ||
auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', | ||
config: {}, | ||
timeout: 3000 | ||
}); | ||
const ev = analyticsAdapter.context.queue.peekAll(); | ||
expect(ev).to.have.length(1); | ||
expect(ev[0]).to.be.eql({ev: 'init', aid: '5018eb39-f900-4370-b71e-3bb5b48d324f'}); | ||
}); | ||
|
||
it('should handle bid request events', () => { | ||
events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST1); | ||
events.emit(CONSTANTS.EVENTS.BID_REQUESTED, REQUEST2); | ||
const ev = analyticsAdapter.context.queue.peekAll(); | ||
expect(ev).to.have.length(3); | ||
expect(ev[1]).to.be.eql({ev: 'request', n: 'b1', sid: 'slot-1'}); | ||
expect(ev[2]).to.be.eql({ev: 'request', n: 'b2', sid: 'slot-1'}); | ||
}); | ||
|
||
it('should handle bid response event', () => { | ||
events.emit(CONSTANTS.EVENTS.BID_RESPONSE, RESPONSE); | ||
const ev = analyticsAdapter.context.queue.peekAll(); | ||
expect(ev).to.have.length(4); | ||
expect(ev[3]).to.be.eql({ | ||
ev: 'response', | ||
n: 'b1', | ||
sid: 'slot-1', | ||
ttr: 443 | ||
}); | ||
}); | ||
|
||
it('should handle bid timeout event', () => { | ||
timer.tick(444); | ||
events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, [{ | ||
bidId: "208750227436c1", | ||
bidder: "b2", | ||
adUnitCode: "slot-1", | ||
auctionId: "5018eb39-f900-4370-b71e-3bb5b48d324f" | ||
}]); | ||
const ev = analyticsAdapter.context.queue.peekAll(); | ||
expect(ev).to.have.length(5); | ||
expect(ev[4]).to.be.eql({ | ||
ev: 'timeout', | ||
n: 'b2', | ||
sid: 'slot-1', | ||
ttr: 444 | ||
}); | ||
}); | ||
|
||
it('should handle auction end event', () => { | ||
timer.tick(3); | ||
const ajaxStub = sandbox.stub(ajax, 'ajax'); | ||
events.emit(CONSTANTS.EVENTS.AUCTION_END, RESPONSE); | ||
let ev = analyticsAdapter.context.queue.peekAll(); | ||
expect(ev).to.have.length(0); | ||
expect(ajaxStub.called).to.be.equal(true); | ||
ev = JSON.parse(ajaxStub.secondCall.args[2]).hb_ev; | ||
expect(ev[5]).to.be.eql({ev: 'end', aid: '5018eb39-f900-4370-b71e-3bb5b48d324f', ttr: 447}); | ||
}); | ||
}); |