Skip to content
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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions modules/hummingbirdAnalyticsAdapter.js
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,

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?

Copy link
Author

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.

countryCode: options.countryCode,
mavenChannel: options.mavenChannel,
path: location.pathname,
pod: Number(options.pod),
productionDomain: options.productionDomain,
screenSize: options.screenSize,

Choose a reason for hiding this comment

The 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'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Author

Choose a reason for hiding this comment

The 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid an ever-growing currentAuctions object, I'd delete from it when you receive AUCTION_END. No reason to hold onto it at that point, is there? The code only errors if there's a subsequent init or end event for that auction id...

Copy link
Author

Choose a reason for hiding this comment

The 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;

10 changes: 10 additions & 0 deletions modules/hummingbirdAnalyticsAdapter.md
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.