diff --git a/test/spec/services/analyticsServicesSpec.js b/test/spec/services/analyticsServicesSpec.js index 026ec77664..798e0fab47 100644 --- a/test/spec/services/analyticsServicesSpec.js +++ b/test/spec/services/analyticsServicesSpec.js @@ -1,6 +1,3 @@ -/** - * Created by Sabe on 6/11/2015. - */ 'use strict'; describe('Analytics Service', function () { @@ -8,8 +5,8 @@ describe('Analytics Service', function () { beforeEach(function() { user = specHelper.newUser(); - user.contributor = { level: 1 }; - user.purchased = { plan: true }; + user.contributor = {}; + user.purchased = { plan: {} }; module(function($provide) { $provide.value('User', {user: user}); @@ -20,138 +17,220 @@ describe('Analytics Service', function () { }); }); - context('error handling', function() { + context('functions', function() { - beforeEach(function() { - sandbox.stub(console, 'log'); + describe('register', function() { + + beforeEach(function() { + sandbox.stub(amplitude, 'setUserId'); + sandbox.stub(window, 'ga'); + }); + + it('sets up user with amplitude', function() { + analytics.register(); + expect(amplitude.setUserId).to.have.been.calledOnce; + expect(amplitude.setUserId).to.have.been.calledWith(user._id); + }); + + it('sets up user with google analytics', function() { + analytics.register(); + expect(ga).to.have.been.calledOnce; + expect(ga).to.have.been.calledWith('set', {userId: user._id}); + }); }); - 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); + describe('login', function() { + + beforeEach(function() { + sandbox.stub(amplitude, 'setUserId'); + sandbox.stub(window, 'ga'); + }); + + it('sets up tracking for amplitude', function() { + analytics.login(); + + expect(amplitude.setUserId).to.have.been.calledOnce; + expect(amplitude.setUserId).to.have.been.calledWith(user._id); + }); + + it('sets up tracking for google analytics', function() { + analytics.login(); + + expect(ga).to.have.been.calledOnce; + expect(ga).to.have.been.calledWith('set', {userId: user._id}); + }); }); - 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; - }); - }); + describe('track', function() { - context('Amplitude', function() { + beforeEach(function() { + sandbox.stub(amplitude, 'logEvent'); + sandbox.stub(window, 'ga'); + }); - beforeEach(function() { - sandbox.stub(amplitude, 'setUserId'); - sandbox.stub(amplitude, 'logEvent'); - sandbox.stub(amplitude, 'setUserProperties'); + context('succeful tracking', function() { + + it('tracks a simple user action with amplitude', function() { + var properties = {'hitType':'event','eventCategory':'behavior','eventAction':'cron'}; + analytics.track(properties); + + expect(amplitude.logEvent).to.have.been.calledOnce; + expect(amplitude.logEvent).to.have.been.calledWith('cron', properties); + }); + + it('tracks a simple user action with google analytics', function() { + var properties = {'hitType':'event','eventCategory':'behavior','eventAction':'cron'}; + analytics.track(properties); + + expect(ga).to.have.been.calledOnce; + expect(ga).to.have.been.calledWith('send', properties); + }); + + it('tracks a user action with additional properties in amplitude', function() { + var properties = {'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'}; + analytics.track(properties); + + expect(amplitude.logEvent).to.have.been.calledOnce; + expect(amplitude.logEvent).to.have.been.calledWith('cron', properties); + }); + + it('tracks a user action with additional properties in google analytics', function() { + var properties = {'hitType':'event','eventCategory':'behavior','eventAction':'cron','booleanProperty':true,'numericProperty':17,'stringProperty':'bagel'}; + analytics.track(properties); + + expect(ga).to.have.been.calledOnce; + expect(ga).to.have.been.calledWith('send', properties); + }); + }); + + context('unsuccesful tracking', function() { + + beforeEach(function() { + sandbox.stub(console, 'log'); + }); + + context('events without requird properties', function() { + beforeEach(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'}); + }); + + it('logs errors to console', function() { + expect(console.log.callCount).to.eql(7); + }); + + it('does not call out to amplitude', function() { + expect(amplitude.logEvent).to.not.be.called; + }); + + it('does not call out to google analytics', function() { + expect(ga).to.not.be.called; + }); + }); + + context('incorrect hit type', function() { + beforeEach(function() { + analytics.track({'hitType':'moogly','eventCategory':'green','eventAction':'eat'}); + }); + + it('logs error to console', function () { + expect(console.log).to.have.been.calledOnce; + }); + + it('does not call out to amplitude', function() { + expect(amplitude.logEvent).to.not.be.called; + }); + + it('does not call out to google analytics', function() { + expect(ga).to.not.be.called; + }); + }); + }); }); - it('sets up tracking when user registers', function() { - analytics.register(); - expect(amplitude.setUserId).to.have.been.calledOnce; - }); + describe('updateUser', function() { - it('sets up tracking when user logs in', function() { - analytics.login(); - expect(amplitude.setUserId).to.have.been.calledOnce; - }); + beforeEach(function() { + sandbox.stub(amplitude, 'setUserProperties'); + sandbox.stub(window, 'ga'); + }); - 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'}); - }); + context('properties argument provided', function(){ + var properties = {'userBoolean': false, 'userNumber': -8, 'userString': 'Enlightened'}; + var expectedProperties = _.cloneDeep(properties); + expectedProperties.UUID = 'unique-user-id'; + expectedProperties.Class = 'wizard'; + expectedProperties.Experience = 35; + expectedProperties.Gold = 43; + expectedProperties.Health = 48; + expectedProperties.Level = 24; + expectedProperties.Mana = 41; - 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'}); - }); + beforeEach(function() { + user._id = 'unique-user-id'; + user.stats.class = 'wizard'; + user.stats.exp = 35.7; + user.stats.gp = 43.2; + user.stats.hp = 47.8; + user.stats.lvl = 24; + user.stats.mp = 41; - 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'}); - }); - }); + analytics.updateUser(properties); + }); - context('Google Analytics', function() { + it('calls amplitude with provided properties and select user info', function() { + expect(amplitude.setUserProperties).to.have.been.calledOnce; + expect(amplitude.setUserProperties).to.have.been.calledWith(expectedProperties); + }); - beforeEach(function() { - sandbox.stub(window, 'ga'); - }); + it('calls google analytics with provided properties and select user info', function() { + expect(ga).to.have.been.calledOnce; + expect(ga).to.have.been.calledWith('set', expectedProperties); + }); + }); - it('sets up tracking when user registers', function() { - analytics.register(); - expect(ga).to.have.been.calledOnce; - expect(ga).to.have.been.calledWith('set'); - }); + context('no properties argument provided', function() { + var expectedProperties = { + UUID: 'unique-user-id', + Class: 'wizard', + Experience: 35, + Gold: 43, + Health: 48, + Level: 24, + Mana: 41, + contributorLevel: 1, + subscription: 'unique-plan-id' + }; - it('sets up tracking when user logs in', function() { - analytics.login(); - expect(ga).to.have.been.calledOnce; - expect(ga).to.have.been.calledWith('set'); - }); + beforeEach(function() { + user._id = 'unique-user-id'; + user.stats.class = 'wizard'; + user.stats.exp = 35.7; + user.stats.gp = 43.2; + user.stats.hp = 47.8; + user.stats.lvl = 24; + user.stats.mp = 41; + user.contributor.level = 1; + user.purchased.plan.planId = 'unique-plan-id'; - 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'}); - }); + analytics.updateUser(); + }); - 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('calls amplitude with select user info', function() { + expect(amplitude.setUserProperties).to.have.been.calledOnce; + expect(amplitude.setUserProperties).to.have.been.calledWith(expectedProperties); + }); - 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.skip('Mixpanel', function() { // Mixpanel not currently in use - - beforeEach(function() { - sandbox.stub(mixpanel, 'alias'); - sandbox.stub(mixpanel, 'identify'); - sandbox.stub(mixpanel, 'track'); - sandbox.stub(mixpanel, 'register'); - }); - - 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'}); + it('calls google analytics with select user info', function() { + expect(ga).to.have.been.calledOnce; + expect(ga).to.have.been.calledWith('set', expectedProperties); + }); + }); }); }); }); diff --git a/website/public/js/services/analyticsServices.js b/website/public/js/services/analyticsServices.js index 5e0882368a..a97caba8d9 100644 --- a/website/public/js/services/analyticsServices.js +++ b/website/public/js/services/analyticsServices.js @@ -1,146 +1,114 @@ -/** - * Created by Sabe on 6/15/2015. - */ 'use strict'; -angular - .module('habitrpg') - .factory('Analytics', analyticsFactory); +(function(){ + var REQUIRED_FIELDS = ['hitType','eventCategory','eventAction']; + var ALLOWED_HIT_TYPES = ['pageview','screenview','event','transaction','item','social','exception','timing']; -analyticsFactory.$inject = [ - 'User' -]; + angular + .module('habitrpg') + .factory('Analytics', analyticsFactory); -function analyticsFactory(User) { + analyticsFactory.$inject = [ + 'User' + ]; - var user = User.user; + function analyticsFactory(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); + var user = User.user; - // 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 - BROKEN - (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); + 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 - 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); + // 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 - 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 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); + } + + function register() { + amplitude.setUserId(user._id); + ga('set', {'userId':user._id}); + } + + function login() { + amplitude.setUserId(user._id); + ga('set', {'userId':user._id}); + } + + function track(properties) { + if(_doesNotHaveRequiredFields(properties)) { return false; } + if(_doesNotHaveAllowedHitType(properties)) { return false; } + + amplitude.logEvent(properties.eventAction,properties); + ga('send',properties); + } + + function updateUser(properties) { + properties = properties || {}; + + _gatherUserStats(user, properties); + + amplitude.setUserProperties(properties); + ga('set',properties); + } + + return { + loadScripts: loadScripts, + register: register, + login: login, + track: track, + updateUser: updateUser + }; } - function register() { - amplitude.setUserId(user._id); - ga('set', {'userId':user._id}); - // mixpanel.alias(user._id); + function _gatherUserStats(user, properties) { + if (user._id) properties.UUID = user._id; + if (user.stats.class) properties.Class = user.stats.class; + if (user.stats.exp) properties.Experience = Math.floor(user.stats.exp); + if (user.stats.gp) properties.Gold = Math.floor(user.stats.gp); + if (user.stats.hp) properties.Health = Math.ceil(user.stats.hp); + if (user.stats.lvl) properties.Level = user.stats.lvl; + if (user.stats.mp) properties.Mana = Math.floor(user.stats.mp); + if (user.contributor.level) properties.contributorLevel = user.contributor.level; + if (user.purchased.plan.planId) properties.subscription = user.purchased.plan.planId; } - 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']; + function _doesNotHaveRequiredFields(properties) { if (!_.isEqual(_.keys(_.pick(properties, REQUIRED_FIELDS)), REQUIRED_FIELDS)) { - return console.log('Analytics tracking calls must include the following properties: ' + JSON.stringify(REQUIRED_FIELDS)); + console.log('Analytics tracking calls must include the following properties: ' + JSON.stringify(REQUIRED_FIELDS)); + return true; } + } + + function _doesNotHaveAllowedHitType(properties) { 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)); + console.log('Hit type of Analytics event must be one of the following: ' + JSON.stringify(ALLOWED_HIT_TYPES)); + return true; } - - 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 - }; -}