WIP(analytics): Move client tracking to service

This commit is contained in:
Sabe Jones
2015-06-18 15:43:57 -05:00
parent 07b41d16b6
commit 3c5ae80e0b
5 changed files with 341 additions and 1 deletions

View File

@@ -21,7 +21,8 @@
"NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID", "NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID",
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY", "NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID", "GA_ID": "GA_ID",
"MP_ID": "MP_ID", "MIXPANEL_TOKEN": "MIXPANEL_TOKEN",
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
"FLAG_REPORT_EMAIL": ["email@mod.com"], "FLAG_REPORT_EMAIL": ["email@mod.com"],
"EMAIL_SERVER": { "EMAIL_SERVER": {
"url": "http://example.com", "url": "http://example.com",

View File

@@ -45,6 +45,7 @@ module.exports = function(config) {
"website/public/js/services/notificationServices.js", "website/public/js/services/notificationServices.js",
"common/script/public/userServices.js", "common/script/public/userServices.js",
"common/script/public/directives.js", "common/script/public/directives.js",
"website/public/js/services/analyticsServices.js",
"website/public/js/services/groupServices.js", "website/public/js/services/groupServices.js",
"website/public/js/services/memberServices.js", "website/public/js/services/memberServices.js",
"website/public/js/services/guideServices.js", "website/public/js/services/guideServices.js",

View File

@@ -0,0 +1,191 @@
/**
* Created by Sabe on 6/11/2015.
*/
'use strict';
describe('Analytics Service', function () {
var analytics;
beforeEach(function() {
inject(function(Analytics) {
analytics = Analytics;
});
});
context('error handling', function() {
before(function() {
sinon.stub(console, 'log');
});
afterEach(function() {
console.log.reset();
});
after(function() {
console.log.restore();
});
it('does not accept tracking events without required properties', function() {
analytics.track('action');
analytics.track({'hitType':'pageview','eventCategory':'green'});
analytics.track({'hitType':'pageview','eventAction':'eat'});
analytics.track({'eventCategory':'green','eventAction':'eat'});
analytics.track({'hitType':'pageview'});
analytics.track({'eventCategory':'green'});
analytics.track({'eventAction':'eat'});
expect(console.log.callCount).to.eql(7);
});
it('does not accept tracking events with incorrect hit type', function () {
analytics.track({'hitType':'moogly','eventCategory':'green','eventAction':'eat'});
expect(console.log).to.have.been.calledOnce;
});
});
context('Amplitude', function() {
before(function() {
sinon.stub(amplitude, 'setUserId');
sinon.stub(amplitude, 'logEvent');
sinon.stub(amplitude, 'setUserProperties');
});
afterEach(function() {
amplitude.setUserId.reset();
amplitude.logEvent.reset();
amplitude.setUserProperties.reset();
});
after(function() {
amplitude.setUserId.restore();
amplitude.logEvent.restore();
amplitude.setUserProperties.restore();
});
it('sets up tracking when user registers', function() {
analytics.register();
expect(amplitude.setUserId).to.have.been.calledOnce;
});
it('sets up tracking when user logs in', function() {
analytics.login();
expect(amplitude.setUserId).to.have.been.calledOnce;
});
it('tracks a simple user action', function() {
analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'cron'});
expect(amplitude.logEvent).to.have.been.calledOnce;
expect(amplitude.logEvent).to.have.been.calledWith('cron',{'hitType':'event','eventCategory':'behavior','eventAction':'cron'});
});
it('tracks a user action with additional properties', function() {
analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'});
expect(amplitude.logEvent).to.have.been.calledOnce;
expect(amplitude.logEvent).to.have.been.calledWith('cron',{'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'});
});
it('updates user-level properties', function() {
analytics.updateUser({'userBoolean': false, 'userNumber': -8, 'userString': 'Enlightened'});
expect(amplitude.setUserProperties).to.have.been.calledOnce;
expect(amplitude.setUserProperties).to.have.been.calledWith({'userBoolean': false, 'userNumber': -8, 'userString': 'Enlightened'});
});
});
context('Google Analytics', function() {
before(function() {
sinon.stub(ga);
});
afterEach(function() {
ga.reset();
});
after(function() {
ga.restore();
});
it('sets up tracking when user registers', function() {
analytics.register();
expect(ga).to.have.been.calledOnce;
expect(ga).to.have.been.calledWith('set');
});
it('sets up tracking when user logs in', function() {
analytics.login();
expect(ga).to.have.been.calledOnce;
expect(ga).to.have.been.calledWith('set');
});
it('tracks a simple user action', function() {
analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'cron'});
expect(ga).to.have.been.calledOnce;
expect(ga).to.have.been.calledWith('send',{'hitType':'event','eventCategory':'behavior','eventAction':'cron'});
});
it('tracks a user action with additional properties', function() {
analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'});
expect(ga).to.have.been.calledOnce;
expect(ga).to.have.been.calledWith('send',{'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'});
});
it('updates user-level properties', function() {
analytics.updateUser({'userBoolean': false, 'userNumber': -8, 'userString': 'Enlightened'});
expect(ga).to.have.been.calledOnce;
expect(ga).to.have.been.calledWith('set',{'userBoolean': false, 'userNumber': -8, 'userString': 'Enlightened'});
});
});
context('Mixpanel', function() {
before(function() {
sinon.stub(mixpanel, 'alias');
sinon.stub(mixpanel, 'identify');
sinon.stub(mixpanel, 'track');
sinon.stub(mixpanel, 'register');
});
afterEach(function() {
mixpanel.alias.reset();
mixpanel.identify.reset();
mixpanel.track.reset();
mixpanel.register.reset();
});
after(function() {
mixpanel.alias.restore();
mixpanel.identify.restore();
mixpanel.track.restore();
mixpanel.register.restore();
});
it('sets up tracking when user registers', function() {
analytics.register();
expect(mixpanel.alias).to.have.been.calledOnce;
});
it('sets up tracking when user logs in', function() {
analytics.login();
expect(mixpanel.identify).to.have.been.calledOnce;
});
it('tracks a simple user action', function() {
analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'cron'});
expect(mixpanel.track).to.have.been.calledOnce;
expect(mixpanel.track).to.have.been.calledWith('cron',{'hitType':'event','eventCategory':'behavior','eventAction':'cron'});
});
it('tracks a user action with additional properties', function() {
analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'});
expect(mixpanel.track).to.have.been.calledOnce;
expect(mixpanel.track).to.have.been.calledWith('cron',{'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'});
});
it('updates user-level properties', function() {
analytics.updateUser({'userBoolean': false, 'userNumber': -8, 'userString': 'Enlightened'});
expect(mixpanel.register).to.have.been.calledOnce;
expect(mixpanel.register).to.have.been.calledWith({'userBoolean': false, 'userNumber': -8, 'userString': 'Enlightened'});
});
});
});

View File

@@ -0,0 +1,146 @@
/**
* Created by Sabe on 6/15/2015.
*/
'use strict';
angular
.module('habitrpg')
.factory('Analytics', analyticsFactory);
analyticsFactory.$inject = [
'User'
];
function analyticsFactory(User) {
var user = User.user;
// Amplitude
var r = window.amplitude || {};
r._q = [];
function a(window) {r[window] = function() {r._q.push([window].concat(Array.prototype.slice.call(arguments, 0)));}}
var i = ["init", "logEvent", "logRevenue", "setUserId", "setUserProperties", "setOptOut", "setVersionName", "setDomain", "setDeviceId", "setGlobalUserProperties"];
for (var o = 0; o < i.length; o++) {a(i[o])}
window.amplitude = r;
amplitude.init(window.env.AMPLITUDE_KEY);
// Google Analytics (aka Universal Analytics)
window['GoogleAnalyticsObject'] = 'ga';
window['ga'] = window['ga'] || function() {
(window['ga'].q = window['ga'].q || []).push(arguments)
}, window['ga'].l = 1 * new Date();
ga('create', window.env.GA_ID, 'auto');
// Mixpanel
(function(b) {
if (!b.__SV) {
var i, g;
window.mixpanel = b;
b._i = [];
b.init = function(a, e, d) {
function f(b, h) {
var a = h.split(".");
2 == a.length && (b = b[a[0]], h = a[1]);
b[h] = function() {
b.push([h].concat(Array.prototype.slice.call(arguments, 0)))
}
}
var c = b;
"undefined" !== typeof d ? c = b[d] = [] : d = "mixpanel";
c.people = c.people || [];
c.toString = function(b) {
var a = "mixpanel";
"mixpanel" !== d && (a += "." + d);
b || (a += " (stub)");
return a
};
c.people.toString = function() {
return c.toString(1) + ".people (stub)"
};
i = "disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");
for (g = 0; g < i.length; g++) f(c, i[g]);
b._i.push([a, e, d])
};
b.__SV = 1.2;
}
})(window.mixpanel || []);
mixpanel.init(window.env.MIXPANEL_TOKEN);
function loadScripts() {
// Amplitude
var n = document.createElement("script");
var s = document.getElementsByTagName("script")[0];
n.type = "text/javascript";
n.async = true;
n.src = "https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.2.0-min.gz.js";
s.parentNode.insertBefore(n, s);
// Google Analytics
var a = document.createElement('script');
var m = document.getElementsByTagName('script')[0];
a.async = 1;
a.src = '//www.google-analytics.com/analytics.js';
m.parentNode.insertBefore(a, m);
// Mixpanel
var g = document.createElement("script");
var e = document.getElementsByTagName("script")[0];
g.type = "text/javascript";
g.async = !0;
g.src = "undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL ? MIXPANEL_CUSTOM_LIB_URL : "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";
e.parentNode.insertBefore(g, e);
}
function register() {
amplitude.setUserId(user._id);
ga('set', {'userId':user._id});
mixpanel.alias(user._id);
}
function login() {
amplitude.setUserId(user._id);
ga('set', {'userId':user._id});
mixpanel.identify(user._id);
}
function track(properties) {
var REQUIRED_FIELDS = ['hitType','eventCategory','eventAction'];
var ALLOWED_HIT_TYPES = ['pageview','screenview','event','transaction','item','social','exception','timing'];
if (!_.isEqual(_.keys(_.pick(properties, REQUIRED_FIELDS)), REQUIRED_FIELDS)) {
return console.log('Analytics tracking calls must include the following properties: ' + JSON.stringify(REQUIRED_FIELDS));
}
if (!_.contains(ALLOWED_HIT_TYPES, properties.hitType)) {
return console.log('Hit type of Analytics event must be one of the following: ' + JSON.stringify(ALLOWED_HIT_TYPES));
}
amplitude.logEvent(properties.eventAction,properties);
mixpanel.track(properties.eventAction,properties);
ga('send',properties);
}
function updateUser(properties) {
if (typeof properties === 'undefined') properties = {};
if (typeof user._id !== 'undefined') properties.UUID = user._id;
if (typeof user.stats.class !== 'undefined') properties.Class = user.stats.class;
if (typeof user.stats.exp !== 'undefined') properties.Experience = Math.floor(user.stats.exp);
if (typeof user.stats.gp !== 'undefined') properties.Gold = Math.floor(user.stats.gp);
if (typeof user.stats.hp !== 'undefined') properties.Health = Math.ceil(user.stats.hp);
if (typeof user.stats.lvl !== 'undefined') properties.Level = user.stats.lvl;
if (typeof user.stats.mp !== 'undefined') properties.Mana = Math.floor(user.stats.mp);
if (typeof user.contributor.level !== 'undefined') properties.contributorLevel = user.contributor.level;
if (typeof user.purchased.plan.planId !== 'undefined') properties.subscription = user.purchased.plan.planId;
amplitude.setUserProperties(properties);
ga('set',properties);
mixpanel.register(properties);
}
return {
loadScripts: loadScripts,
register: register,
login: login,
track: track,
updateUser: updateUser
};
}

View File

@@ -43,6 +43,7 @@
"js/services/notificationServices.js", "js/services/notificationServices.js",
"common/script/public/userServices.js", "common/script/public/userServices.js",
"common/script/public/directives.js", "common/script/public/directives.js",
"js/services/analyticsServices.js",
"js/services/groupServices.js", "js/services/groupServices.js",
"js/services/memberServices.js", "js/services/memberServices.js",
"js/services/guideServices.js", "js/services/guideServices.js",