-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hummingbird analytics #12
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
'use strict'; | ||
|
||
import {ajax} from 'src/ajax'; | ||
import adapter from 'src/AnalyticsAdapter'; | ||
import CONSTANTS from 'src/constants.json'; | ||
import adaptermanager from 'src/adapterManager'; | ||
import {logInfo} from '../src/utils'; | ||
|
||
// Standard Analytics Adapter code | ||
const analyticsType = 'endpoint'; | ||
const AUCTION_END = CONSTANTS.EVENTS.AUCTION_END; | ||
const AUCTION_INIT = CONSTANTS.EVENTS.AUCTION_INIT; | ||
|
||
// Internal controls | ||
let hummingbirdUrl = false; | ||
const BATCH_MESSAGE_FREQUENCY = 1000; // Send results batched on a 1s delay | ||
|
||
// Store our auction event data | ||
const currentAuctions = {}; | ||
// Track internal states | ||
let currentBatch = [], | ||
initialized = false, | ||
options, | ||
sampling, | ||
verbose = false; | ||
|
||
|
||
let hummingbirdAnalytics = Object.assign(adapter({hummingbirdUrl, analyticsType}), { | ||
|
||
track({ eventType, args }) { | ||
try { | ||
// We track only two events. | ||
if ([AUCTION_INIT, AUCTION_END].indexOf(eventType) === -1) { | ||
return; | ||
} | ||
if (args && args.auctionId && currentAuctions[args.auctionId] && currentAuctions[args.auctionId].status === 'complete') { | ||
throw new Error('Tracked event received after auction end for', args.auctionId); | ||
} | ||
let id = args && args.auctionId; | ||
let auctionObj; | ||
switch (eventType) { | ||
case AUCTION_INIT: | ||
logMsg('AUCTION STARTED', args, options); | ||
// TODO: A possible improvement would be splitting out the | ||
// page-level values (content item, correlator, country | ||
// code, path, pod, screen size, domain/channel, and | ||
// timeout period), leaving only the auction ID as | ||
// duplicated in our batched values. (NB: We want to push | ||
// this information over the wire, but should be able to | ||
// cut down on bandwidth by sending all this identical | ||
// information only once per batch.) | ||
auctionObj = { | ||
auctionId: id, | ||
contentItem: options.contentItem, | ||
correlator: window.hummingbirdCorrelator, | ||
countryCode: options.countryCode, | ||
mavenChannel: options.mavenChannel, | ||
path: location.pathname, | ||
pod: Number(options.pod), | ||
productionDomain: options.productionDomain, | ||
screenSize: options.screenSize, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggest "device" here to match owl with possible values being 'mobile' (mobile and tablet) and 'desktop' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, after discussion with Ed, we're going to keep this as A/B/C for now -- determining device in Owl is simple, and Ed thinks mingling tablet and phone may skew some of the data. We'll revisit in a future sprint. |
||
status: 'inProgress', | ||
timeoutPeriod: undefined, | ||
}; | ||
// Sanity check on POD | ||
if (isNaN(auctionObj.pod) || auctionObj.pod > 999) { | ||
auctionObj.pod = 0; | ||
}; | ||
currentAuctions[id] = auctionObj; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid an ever-growing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. |
||
break | ||
case AUCTION_END: | ||
if (currentAuctions[id] && currentAuctions[id].status === 'inProgress') { | ||
currentAuctions[id].status = 'complete'; | ||
} else { | ||
throw new Error('Auction end received for unknown auction', id); | ||
} | ||
auctionObj = currentAuctions[id]; | ||
auctionObj.timeoutPeriod = args.timeout; | ||
// Strip down to a set of information we're interested in. | ||
// bidUnit is used for easy generation of our timed-out bid | ||
// list, but does not need to go over the wire. | ||
// | ||
// NOTE: We are skipping sizes (available from | ||
// bidderRequests.bids.foo.sizes) for the initial MVP of | ||
// Hummingbird. | ||
// | ||
// NOTE: A possible future improvement would be gathering | ||
// a deal flag, if present. | ||
// | ||
// ADZONES: unitId => { adzone, index } | ||
// BIDUNIT: unitId => [ bidder1, bidders2, ... ] | ||
// NOBID: unitId => [ bidder1, bidder2, ... ] | ||
// BID: unitId => [{ bidder, cpm, timeToRespond }, ... ] | ||
// TIMEDOUT: unitId => [ bidder1, bidder2, ... ] | ||
['bidUnit', 'nobid', 'bid', 'timedout'].forEach(key => { | ||
auctionObj[key] = {}; | ||
}); | ||
let bidUnits = []; | ||
args.bidderRequests.forEach(request => { | ||
let bidder = request.bidderCode; | ||
request.bids.forEach(bid => { | ||
let auc = bid.adUnitCode; | ||
if (!auctionObj.bidUnit[auc]) { | ||
auctionObj.bidUnit[auc] = []; | ||
} | ||
auctionObj.bidUnit[auc].push(bid.bidder); | ||
auctionObj.nobid[auc] = []; | ||
auctionObj.bid[auc] = []; | ||
auctionObj.timedout[auc] = []; | ||
}); | ||
}); | ||
args.noBids.forEach(noBid => { | ||
let auc = noBid.adUnitCode; | ||
auctionObj.nobid[auc].push(noBid.bidder); | ||
}); | ||
args.bidsReceived.forEach(bid => { | ||
let auc = bid.adUnitCode; | ||
auctionObj.bid[auc].push({ | ||
bidder: bid.bidderCode, | ||
cpm: bid.cpm, | ||
timeToRespond: bid.timeToRespond, | ||
}); | ||
}); | ||
// Zone info for all zones in play. | ||
auctionObj.adzones = {}; | ||
Object.keys(options.zoneMap).map(key => { | ||
if (auctionObj.bidUnit[key]) { | ||
auctionObj.adzones[key] = options.zoneMap[key]; | ||
} | ||
}); | ||
// Figure out which bids have not responded yet. | ||
Object.keys(auctionObj.bidUnit).map(auc => { | ||
let bidders = auctionObj.bid[auc].map(bidInfo => bidInfo.bidder); | ||
let nonbidders = auctionObj.nobid[auc]; | ||
auctionObj.bidUnit[auc].map(bidderCode => { | ||
if (bidders.indexOf(bidderCode) === -1 && nonbidders.indexOf(bidderCode) === -1) { | ||
auctionObj.timedout[auc].push(bidderCode); | ||
} | ||
}); | ||
}); | ||
// bidUnit currently contains no info not found in | ||
// bid/noBid/timedout | ||
delete auctionObj.bidUnit; | ||
// Additionally we don't care about status once this goes | ||
// over the wire; nothing is submitted from incomplete | ||
// auctions. | ||
delete auctionObj.status; | ||
// Now, push it into our batch and set a timer. | ||
currentBatch.push(auctionObj); | ||
setTimeout(() => { sendBatch(); }, BATCH_MESSAGE_FREQUENCY); | ||
break | ||
} | ||
} catch (e) { | ||
// Log error | ||
logInfo('HUMMINGBIRD ADAPTER ERROR', e); | ||
} | ||
}, | ||
}); | ||
|
||
const sendBatch = function() { | ||
if (currentBatch.length === 0) { | ||
return; | ||
} | ||
logMsg('SENDING ANALYTICS', currentBatch); | ||
if (hummingbirdUrl) { | ||
ajax( | ||
hummingbirdUrl, | ||
null, | ||
JSON.stringify({ auctions: currentBatch }), | ||
{ | ||
contentType: 'application/json', | ||
} | ||
); | ||
} | ||
currentBatch = []; | ||
} | ||
|
||
const logMsg = (...args) => { | ||
if (verbose) { | ||
logInfo('HUMMINGBIRD:', ...args); | ||
} | ||
} | ||
|
||
// save the base class function | ||
hummingbirdAnalytics.originEnableAnalytics = hummingbirdAnalytics.enableAnalytics; | ||
|
||
// override enableAnalytics so we can get access to the config passed in from the page | ||
hummingbirdAnalytics.enableAnalytics = function (config) { | ||
if (initialized) { | ||
return; | ||
} | ||
options = config.options; | ||
hummingbirdAnalytics.originEnableAnalytics(config); // call the base class function | ||
initialized = true; | ||
hummingbirdUrl = options.url; | ||
verbose = !!options.verbose; | ||
}; | ||
|
||
adaptermanager.registerAnalyticsAdapter({ | ||
adapter: hummingbirdAnalytics, | ||
code: 'hummingbird' | ||
}); | ||
|
||
export default hummingbirdAnalytics; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Overview | ||
|
||
Module Name: Hummingbird Analytics Adapter | ||
Module Type: Analytics Adapter | ||
Maintainer: scook@maven.io | ||
|
||
# Description | ||
|
||
Proof of concept for Hummingbird. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are both ids right? Should it be contentItemId and correlatorId?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Correlator" is by analogy to GA -- note that it's not the same correlator as GA's -- so I'll leave it named that, but I will switch to contentItemId.