From 0ac76cfe34b3a0094290d18b4f043afc7fd1d7e4 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Mon, 22 Jun 2015 20:13:50 -0500 Subject: [PATCH 01/31] Remove mixpanel mock --- test/spec/mocks/mixpanelMock.js | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 test/spec/mocks/mixpanelMock.js diff --git a/test/spec/mocks/mixpanelMock.js b/test/spec/mocks/mixpanelMock.js deleted file mode 100644 index 2cb7300f73..0000000000 --- a/test/spec/mocks/mixpanelMock.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' -//Adapted from http://stackoverflow.com/questions/23785603/angularjs-testing-with-jasmine-and-mixpanel -// @TODO: replace with an injectable mixpanel instance for testing - -var MixpanelMock; - -MixpanelMock = (function() { - function MixpanelMock() {} - - MixpanelMock.prototype.track = function() { - return console.log("mixpanel.track", arguments); - }; - - MixpanelMock.prototype.register_once = function() { - return console.log("mixpanel.register_once", arguments); - }; - - MixpanelMock.prototype.identify = function() { - return console.log("mixpanel.identify", arguments); - }; - - MixpanelMock.prototype.register = function() { - return console.log("mixpanel.register", arguments); - }; - - return MixpanelMock; - -})(); - -window.mixpanel = new MixpanelMock(); From e6f0acba4cb836f72b58541256126f35a1c6c3d3 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 1 Jul 2015 18:36:43 -0500 Subject: [PATCH 02/31] Add initial server side analytics service --- package.json | 1 + test/server_side/analytics.test.js | 78 ++++++++++++++++++++++++++++++ website/src/analytics.js | 22 +++++++++ 3 files changed, 101 insertions(+) create mode 100644 test/server_side/analytics.test.js create mode 100644 website/src/analytics.js diff --git a/package.json b/package.json index 621a00ce75..ef419e25cd 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "./website/src/server.js", "dependencies": { "amazon-payments": "0.0.4", + "amplitude": "^1.0.3", "async": "~0.9.0", "aws-sdk": "^2.0.25", "babel": "^5.5.4", diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js new file mode 100644 index 0000000000..fa28579ab2 --- /dev/null +++ b/test/server_side/analytics.test.js @@ -0,0 +1,78 @@ +var sinon = require('sinon'); +var chai = require("chai") +chai.use(require("sinon-chai")) +var expect = chai.expect +var rewire = require('rewire'); + +describe('analytics', function() { + var amplitudeMock = sinon.stub(); + + describe('init', function() { + var analytics = rewire('../../website/src/analytics'); + + beforeEach(function() { + analytics.__set__('Amplitude', amplitudeMock); + }); + + afterEach(function(){ + amplitudeMock.reset(); + }); + + it('throws an error if no options are passed in', function() { + expect(analytics.init).to.throw('No options provided'); + }); + + it('registers amplitude with token', function() { + var options = { + amplitudeToken: 'token', + uuid: 'user-id' + }; + analytics.init(options); + + expect(amplitudeMock).to.be.calledOnce; + expect(amplitudeMock).to.be.calledWith('token', 'user-id'); + }); + + it('does not register amplitude without token', function() { + var options = { uuid: 'user-id' }; + analytics.init(options); + + expect(amplitudeMock).to.not.be.called; + }); + }); + + describe('track', function() { + var analytics = rewire('../../website/src/analytics'); + + context('amplitude not initialized', function() { + it('throws error', function() { + expect(analytics.track).to.throw('Amplitude not initialized'); + }); + }); + + context('amplitude initialized', function() { + var amplitudeTrack = sinon.stub(); + + beforeEach(function() { + analytics.__set__('Amplitude', amplitudeMock); + analytics.init({amplitudeToken: 'token', uuid: 'user-id'}); + analytics.__set__('amplitude.track', amplitudeTrack); + }); + + afterEach(function(){ + amplitudeMock.reset(); + }); + + it('tracks event in amplitude', function() { + var data = { + foo: 'bar' + }; + + analytics.track(data); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith(data); + }); + }); + }); +}); diff --git a/website/src/analytics.js b/website/src/analytics.js new file mode 100644 index 0000000000..0fac3db680 --- /dev/null +++ b/website/src/analytics.js @@ -0,0 +1,22 @@ +var Amplitude = require('amplitude'); +var amplitude; + +var analytics = { + init: init, + track: track +} + +function init(options) { + if(!options) { throw 'No options provided' } + + if(options.amplitudeToken) { + amplitude = new Amplitude(options.amplitudeToken, options.uuid); + } +} + +function track(data) { + if(!amplitude) throw 'Amplitude not initialized'; + amplitude.track(data); +} + +module.exports = analytics; From e6ee4d303cb2cecaf02597234a78191e69366a45 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Mon, 6 Jul 2015 08:11:31 -0500 Subject: [PATCH 03/31] Set up purchase tracking --- package.json | 2 +- test/server_side/analytics.test.js | 168 +++++++++++++++++++++-------- website/src/analytics.js | 68 ++++++++++-- 3 files changed, 184 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index ef419e25cd..19022c9826 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "./website/src/server.js", "dependencies": { "amazon-payments": "0.0.4", - "amplitude": "^1.0.3", + "amplitude": "1.0.4", "async": "~0.9.0", "aws-sdk": "^2.0.25", "babel": "^5.5.4", diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index fa28579ab2..794777e35b 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -6,73 +6,151 @@ var rewire = require('rewire'); describe('analytics', function() { var amplitudeMock = sinon.stub(); + var googleAnalyticsMock = sinon.stub(); describe('init', function() { var analytics = rewire('../../website/src/analytics'); + afterEach(function(){ + amplitudeMock.reset(); + googleAnalyticsMock.reset(); + }); + + it('throws an error if no options are passed in', function() { + expect(analytics).to.throw('No options provided'); + }); + + it('registers amplitude with token', function() { + analytics.__set__('Amplitude', amplitudeMock); + var options = { + amplitudeToken: 'token' + }; + analytics(options); + + expect(amplitudeMock).to.be.calledOnce; + expect(amplitudeMock).to.be.calledWith('token'); + }); + + it('registers google analytics with token', function() { + analytics.__set__('googleAnalytics', googleAnalyticsMock); + var options = { + googleAnalytics: 'token' + }; + analytics(options); + + expect(googleAnalyticsMock).to.be.calledOnce; + expect(googleAnalyticsMock).to.be.calledWith('token'); + }); + }); + + describe('trackPurchase', function() { + + var purchaseData = { + uuid: 'user-id', + sku: 'paypal-checkout', + paymentMethod: 'PayPal', + itemPurchased: 'Gems', + purchaseValue: 8, + purchaseType: 'checkout', + gift: false, + quantity: 1 + } + + var analytics = rewire('../../website/src/analytics'); + var amplitudeTrack = sinon.stub(); + var googleEvent = sinon.stub().returns({ + send: function() { return true } + }); + var googleItem = sinon.stub().returns({ + send: function() {} + }); + var googleTransaction = sinon.stub().returns({ + item: googleItem + }); + var initializedAnalytics; + beforeEach(function() { analytics.__set__('Amplitude', amplitudeMock); + initializedAnalytics = analytics({amplitudeToken: 'token', googleAnalytics: 'token'}); + analytics.__set__('amplitude.track', amplitudeTrack); + analytics.__set__('ga.event', googleEvent); + analytics.__set__('ga.transaction', googleTransaction); }); afterEach(function(){ amplitudeMock.reset(); + googleEvent.reset(); + googleTransaction.reset(); + googleItem.reset(); }); - it('throws an error if no options are passed in', function() { - expect(analytics.init).to.throw('No options provided'); - }); + it('calls amplitude.track', function() { - it('registers amplitude with token', function() { - var options = { - amplitudeToken: 'token', - uuid: 'user-id' - }; - analytics.init(options); + initializedAnalytics.trackPurchase(purchaseData); - expect(amplitudeMock).to.be.calledOnce; - expect(amplitudeMock).to.be.calledWith('token', 'user-id'); - }); - - it('does not register amplitude without token', function() { - var options = { uuid: 'user-id' }; - analytics.init(options); - - expect(amplitudeMock).to.not.be.called; - }); - }); - - describe('track', function() { - var analytics = rewire('../../website/src/analytics'); - - context('amplitude not initialized', function() { - it('throws error', function() { - expect(analytics.track).to.throw('Amplitude not initialized'); + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'purchase', + user_id: 'user-id', + event_properties: { + paymentMethod: 'PayPal', + sku: 'paypal-checkout', + gift: false, + itemPurchased: 'Gems', + purchaseType: 'checkout', + quantity: 1 + }, + revenue: 8 }); }); - context('amplitude initialized', function() { - var amplitudeTrack = sinon.stub(); + it('calls ga.event', function() { - beforeEach(function() { - analytics.__set__('Amplitude', amplitudeMock); - analytics.init({amplitudeToken: 'token', uuid: 'user-id'}); - analytics.__set__('amplitude.track', amplitudeTrack); - }); + initializedAnalytics.trackPurchase(purchaseData); - afterEach(function(){ - amplitudeMock.reset(); - }); + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'commerce', + 'checkout', + 'PayPal', + 8 + ); + }); - it('tracks event in amplitude', function() { - var data = { - foo: 'bar' - }; + it('calls ga.transaction', function() { - analytics.track(data); + initializedAnalytics.trackPurchase(purchaseData); - expect(amplitudeTrack).to.be.calledOnce; - expect(amplitudeTrack).to.be.calledWith(data); - }); + expect(googleTransaction).to.be.calledOnce; + expect(googleTransaction).to.be.calledWith( + 'user-id', + 8 + ); + expect(googleItem).to.be.calledOnce; + expect(googleItem).to.be.calledWith( + 8, + 1, + 'paypal-checkout', + 'Gems', + 'checkout' + ); + }); + + it('appends gift to variation of ga.transaction.item if gift is true', function() { + + var purchaseDataWithGift = _.clone(purchaseData); + purchaseDataWithGift.gift = true; + + initializedAnalytics.trackPurchase(purchaseDataWithGift); + + expect(googleItem).to.be.calledOnce; + expect(googleItem).to.be.calledWith( + 8, + 1, + 'paypal-checkout', + 'Gems', + 'checkout - Gift' + ); }); }); }); diff --git a/website/src/analytics.js b/website/src/analytics.js index 0fac3db680..e2216a7993 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -1,22 +1,74 @@ +var _ = require('lodash'); var Amplitude = require('amplitude'); +var googleAnalytics = require('universal-analytics'); + +var ga; var amplitude; var analytics = { - init: init, - track: track + trackPurchase: trackPurchase } function init(options) { if(!options) { throw 'No options provided' } - if(options.amplitudeToken) { - amplitude = new Amplitude(options.amplitudeToken, options.uuid); + amplitude = new Amplitude(options.amplitudeToken); + ga = googleAnalytics(options.googleAnalytics); + + return analytics; +} + +function trackPurchase(data) { + _sendPurchaseDataToAmplitude(data); + _sendPurchaseDataToGoogle(data); +} + +function _sendPurchaseDataToAmplitude(data) { + var amplitudeData = _formatDataForAmplitude(data); + amplitudeData.event_type = 'purchase'; + amplitudeData.revenue = data.purchaseValue; + + amplitude.track(amplitudeData) +} + +function _formatDataForAmplitude(data) { + var PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue']; + var event_properties = _.omit(data, PROPERTIES_TO_SCRUB); + + var ampData = { + user_id: data.uuid, + event_properties: event_properties } + + if(data.user) { + ampData.user_properties = _formatUserData(data.user); + } + + return ampData; } -function track(data) { - if(!amplitude) throw 'Amplitude not initialized'; - amplitude.track(data); +function _formatUserData(user) { + var data = {}; + + return data; } -module.exports = analytics; +function _sendPurchaseDataToGoogle(data) { + var label = data.paymentMethod; + var type = data.purchaseType; + var price = data.purchaseValue; + var qty = data.quantity; + var sku = data.sku; + var itemName = data.itemPurchased; + var variation = type; + if(data.gift) variation += ' - Gift'; + + ga.event('commerce', type, label, price) + .send(); + + ga.transaction(data.uuid, price) + .item(price, qty, sku, itemName, variation) + .send(); +} + +module.exports = init; From d2b74f11dd67789ce3e3b786a44bbc4035ea747a Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Tue, 7 Jul 2015 18:00:41 -0500 Subject: [PATCH 04/31] Create analytics.track --- test/server_side/analytics.test.js | 89 ++++++++++++++++++++++-------- website/src/analytics.js | 22 +++++++- 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index 794777e35b..c803c46cd4 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -5,17 +5,31 @@ var expect = chai.expect var rewire = require('rewire'); describe('analytics', function() { + // Mocks var amplitudeMock = sinon.stub(); var googleAnalyticsMock = sinon.stub(); + var amplitudeTrack = sinon.stub(); + var googleEvent = sinon.stub().returns({ + send: function() { } + }); + var googleItem = sinon.stub().returns({ + send: function() { } + }); + var googleTransaction = sinon.stub().returns({ + item: googleItem + }); + + afterEach(function(){ + amplitudeMock.reset(); + amplitudeTrack.reset(); + googleEvent.reset(); + googleTransaction.reset(); + googleItem.reset(); + }); describe('init', function() { var analytics = rewire('../../website/src/analytics'); - afterEach(function(){ - amplitudeMock.reset(); - googleAnalyticsMock.reset(); - }); - it('throws an error if no options are passed in', function() { expect(analytics).to.throw('No options provided'); }); @@ -43,6 +57,54 @@ describe('analytics', function() { }); }); + describe('track', function() { + + var event_type = 'Cron'; + var analyticsData = { + gaCategory: 'behavior', + gaLabel: 'Ga Label', + uuid: 'unique-user-id', + resting: true, + cronCount: 5 + } + + var analytics = rewire('../../website/src/analytics'); + var initializedAnalytics; + + beforeEach(function() { + analytics.__set__('Amplitude', amplitudeMock); + initializedAnalytics = analytics({amplitudeToken: 'token'}); + analytics.__set__('amplitude.track', amplitudeTrack); + analytics.__set__('ga.event', googleEvent); + }); + + it('tracks event in amplitude', function() { + + initializedAnalytics.track(event_type, analyticsData); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + event_properties: { + resting: true, + cronCount: 5 + } + }); + }); + + it('tracks event in google analytics', function() { + initializedAnalytics.track(event_type, analyticsData); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'behavior', + 'Cron', + 'Ga Label' + ); + }); + }); + describe('trackPurchase', function() { var purchaseData = { @@ -57,16 +119,6 @@ describe('analytics', function() { } var analytics = rewire('../../website/src/analytics'); - var amplitudeTrack = sinon.stub(); - var googleEvent = sinon.stub().returns({ - send: function() { return true } - }); - var googleItem = sinon.stub().returns({ - send: function() {} - }); - var googleTransaction = sinon.stub().returns({ - item: googleItem - }); var initializedAnalytics; beforeEach(function() { @@ -77,13 +129,6 @@ describe('analytics', function() { analytics.__set__('ga.transaction', googleTransaction); }); - afterEach(function(){ - amplitudeMock.reset(); - googleEvent.reset(); - googleTransaction.reset(); - googleItem.reset(); - }); - it('calls amplitude.track', function() { initializedAnalytics.trackPurchase(purchaseData); diff --git a/website/src/analytics.js b/website/src/analytics.js index e2216a7993..90252c33e1 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -6,7 +6,8 @@ var ga; var amplitude; var analytics = { - trackPurchase: trackPurchase + trackPurchase: trackPurchase, + track: track } function init(options) { @@ -18,6 +19,23 @@ function init(options) { return analytics; } +function track(eventType, data) { + _sendDataToAmplitude(eventType, data); + _sendDataToGoogle(eventType, data); +} + +function _sendDataToAmplitude(eventType, data) { + var amplitudeData = _formatDataForAmplitude(data); + amplitudeData.event_type = eventType; + amplitude.track(amplitudeData); +} + +function _sendDataToGoogle(eventType, data) { + var category = data.gaCategory; + var label = data.gaLabel; + ga.event(category, eventType, label).send(); +} + function trackPurchase(data) { _sendPurchaseDataToAmplitude(data); _sendPurchaseDataToGoogle(data); @@ -32,7 +50,7 @@ function _sendPurchaseDataToAmplitude(data) { } function _formatDataForAmplitude(data) { - var PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue']; + var PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaCategory', 'gaLabel']; var event_properties = _.omit(data, PROPERTIES_TO_SCRUB); var ampData = { From c4c4f4a3a5d67b935fa82f19643c6ec549b91de4 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 8 Jul 2015 07:47:52 -0500 Subject: [PATCH 05/31] Add analytics to utils --- website/src/utils.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/website/src/utils.js b/website/src/utils.js index 96f5f04645..0614486415 100644 --- a/website/src/utils.js +++ b/website/src/utils.js @@ -7,8 +7,6 @@ var request = require('request'); // Set when utils.setupConfig is run var isProd, baseUrl; -module.exports.ga = undefined; // set Google Analytics on nconf init - module.exports.sendEmail = function(mailData) { var smtpTransport = nodemailer.createTransport("SMTP",{ service: nconf.get('SMTP_SERVICE'), @@ -179,11 +177,16 @@ module.exports.setupConfig = function(){ baseUrl = nconf.get('BASE_URL'); module.exports.ga = require('universal-analytics')(nconf.get('GA_ID')); + var analytics = isProd && require('./analytics'); + var analytics = require('./analytics'); + var analyticsTokens = { + amplitudeToken: nconf.get('AMPLITUDE_KEY'), + googleAnalytics: nconf.get('GA_ID') + } - var mixpanel = isProd && require('mixpanel'); - module.exports.mixpanel = mixpanel - ? mixpanel.init(nconf.get('MP_ID')) - : { track: function() {} }; + module.exports.analytics = analytics + ? analytics(analyticsTokens) + : { track: function() { }, trackPurchase: function() { } }; }; var algorithm = 'aes-256-ctr'; From 73b2d816d1283fd5c5620e43561fb59c26da3b49 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 8 Jul 2015 08:08:45 -0500 Subject: [PATCH 06/31] Track cron with analytics service --- common/script/index.coffee | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/common/script/index.coffee b/common/script/index.coffee index 43628c20fe..0a6425c837 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -1692,8 +1692,16 @@ api.wrap = (user, main=true) -> # Analytics user.flags.cronCount?=0 user.flags.cronCount++ - options.mixpanel?.track('Cron',{'distinct_id':user._id,'resting':user.preferences.sleep}) - options.ga?.event('behavior', 'cron', 'cron', user.flags.cronCount).send(); #TODO userId for cohort + + analyticsData = { + gaCategory: 'behavior', + gaLabel: user.flags.cronCount, + uuid: user._id, + user: user, + resting: user.preferences.sleep, + cronCount: user.flags.cronCount + } + options.analytics?.track('Cron', analyticsData) # After all is said and done, progress up user's effect on quest, return those values & reset the user's progress = user.party.quest.progress; _progress = _.cloneDeep progress From 1c2ec66c93ae9602bc04a1d770acbb3b106e4ec0 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 8 Jul 2015 08:09:43 -0500 Subject: [PATCH 07/31] Add format user data function to analytics server --- test/server_side/analytics.test.js | 74 ++++++++++++++++++++++-------- website/src/analytics.js | 21 ++++++++- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index c803c46cd4..eb80cb5a49 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -78,30 +78,68 @@ describe('analytics', function() { analytics.__set__('ga.event', googleEvent); }); - it('tracks event in amplitude', function() { + context('Amplitude', function() { + it('tracks event in amplitude', function() { - initializedAnalytics.track(event_type, analyticsData); + initializedAnalytics.track(event_type, analyticsData); - expect(amplitudeTrack).to.be.calledOnce; - expect(amplitudeTrack).to.be.calledWith({ - event_type: 'Cron', - user_id: 'unique-user-id', - event_properties: { - resting: true, - cronCount: 5 - } + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + event_properties: { + resting: true, + cronCount: 5 + } + }); + }); + + it('sends user data if provided', function() { + var stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 }; + var user = { + stats: stats, + contributor: { level: 1 }, + purchased: { plan: { planId: 'foo-plan' } } + }; + + var analyticsDataWithUser = _.cloneDeep(analyticsData); + analyticsDataWithUser.user = user; + + initializedAnalytics.track(event_type, analyticsDataWithUser); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + event_properties: { + resting: true, + cronCount: 5 + }, + user_properties: { + Class: 'wizard', + Experience: 5, + Gold: 23, + Health: 10, + Level: 4, + Mana: 30, + contributorLevel: 1, + subscription: 'foo-plan' + } + }); }); }); - it('tracks event in google analytics', function() { - initializedAnalytics.track(event_type, analyticsData); + context('Google Analytics', function() { + it('tracks event in google analytics', function() { + initializedAnalytics.track(event_type, analyticsData); - expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'behavior', - 'Cron', - 'Ga Label' - ); + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'behavior', + 'Cron', + 'Ga Label' + ); + }); }); }); diff --git a/website/src/analytics.js b/website/src/analytics.js index 90252c33e1..d3debe4fcc 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -66,9 +66,26 @@ function _formatDataForAmplitude(data) { } function _formatUserData(user) { - var data = {}; + var properties = {}; - return data; + if (user.stats) { + properties.Class = user.stats.class; + properties.Experience = Math.floor(user.stats.exp); + properties.Gold = Math.floor(user.stats.gp); + properties.Health = Math.ceil(user.stats.hp); + properties.Level = user.stats.lvl; + properties.Mana = Math.floor(user.stats.mp); + } + + if (user.contributor && user.contributor.level) { + properties.contributorLevel = user.contributor.level; + } + + if (user.purchased && user.purchased.plan.planId) { + properties.subscription = user.purchased.plan.planId; + } + + return properties; } function _sendPurchaseDataToGoogle(data) { From f58dbb2a682cc3a5abe2efc21ff2c41a9d471677 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 8 Jul 2015 08:10:48 -0500 Subject: [PATCH 08/31] Send only analytics for use in cron --- website/src/controllers/user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/controllers/user.js b/website/src/controllers/user.js index df14373818..adb35d9328 100644 --- a/website/src/controllers/user.js +++ b/website/src/controllers/user.js @@ -102,7 +102,7 @@ api.score = function(req, res, next) { if (task.type === 'daily' || task.type === 'todo') task.completed = direction === 'up'; - + task = user.ops.addTask({body:task}); } var delta = user.ops.score({params:{id:task.id, direction:direction}, language: req.language}); @@ -336,7 +336,7 @@ api.update = function(req, res, next) { api.cron = function(req, res, next) { var user = res.locals.user, - progress = user.fns.cron({ga:ga, mixpanel:utils.mixpanel}), + progress = user.fns.cron({analytics:utils.analytics}), ranCron = user.isModified(), quest = shared.content.quests[user.party.quest.key]; From c92cf58acb2af345b4ea118c6606b9beef1328ae Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 8 Jul 2015 08:11:27 -0500 Subject: [PATCH 09/31] Track purchases with analytics service --- website/src/controllers/payments/index.js | 32 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/website/src/controllers/payments/index.js b/website/src/controllers/payments/index.js index 8f1144cbcc..d9ce943250 100644 --- a/website/src/controllers/payments/index.js +++ b/website/src/controllers/payments/index.js @@ -74,9 +74,18 @@ exports.createSubscription = function(data, cb) { revealMysteryItems(recipient); if(isProduction) { if (!data.gift) utils.txnEmail(data.user, 'subscription-begins'); - utils.ga.event('commerce', 'subscribe', data.paymentMethod, block.price).send(); - utils.ga.transaction(data.user._id, block.price).item(block.price, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod).send(); - utils.mixpanel.track('purchase',{'distinct_id':data.user._id,'itemPurchased':block.key,'purchaseValue':block.price}) + + var analyticsData = { + uuid: data.user._id, + itemPurchased: 'Subscription', + sku: data.paymentMethod.toLowerCase() + '-subscription', + purchaseType: 'subscribe', + paymentMethod: data.paymentMethod, + quantity: 1, + gift: !!data.gift, // coerced into a boolean + purchaseValue: block.price + } + utils.analytics.trackPurchase(analyticsData); } data.user.purchased.txnCount++; if (data.gift){ @@ -127,11 +136,20 @@ exports.buyGems = function(data, cb) { data.user.purchased.txnCount++; if(isProduction) { if (!data.gift) utils.txnEmail(data.user, 'donation'); - utils.ga.event('commerce', 'checkout', data.paymentMethod, amt).send(); - utils.mixpanel.track('purchase',{'distinct_id':data.user._id,'itemPurchased':'Gems','purchaseValue':amt}) - //TODO ga.transaction to reflect whether this is gift or self-purchase - utils.ga.transaction(data.user._id, amt).item(amt, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); + + var analyticsData = { + uuid: data.user._id, + itemPurchased: 'Gems', + sku: data.paymentMethod.toLowerCase() + '-checkout', + purchaseType: 'checkout', + paymentMethod: data.paymentMethod, + quantity: 1, + gift: !!data.gift, // coerced into a boolean + purchaseValue: amt + } + utils.analytics.trackPurchase(analyticsData); } + if (data.gift){ var byUsername = utils.getUserInfo(data.user, ['name']).name; var gemAmount = data.gift.gems.amount || 20; From 7bc74f2c11304efc5f790fd842d3f68391fb60bd Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 8 Jul 2015 21:12:32 -0500 Subject: [PATCH 10/31] Track unsubscription --- test/server_side/analytics.test.js | 115 +++++++++++----------- website/src/controllers/payments/index.js | 8 +- 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index eb80cb5a49..73c6f60560 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -167,73 +167,78 @@ describe('analytics', function() { analytics.__set__('ga.transaction', googleTransaction); }); - it('calls amplitude.track', function() { + context('Amplitude', function() { - initializedAnalytics.trackPurchase(purchaseData); + it('calls amplitude.track', function() { + var data = _.cloneDeep(purchaseData); + initializedAnalytics.trackPurchase(data); - expect(amplitudeTrack).to.be.calledOnce; - expect(amplitudeTrack).to.be.calledWith({ - event_type: 'purchase', - user_id: 'user-id', - event_properties: { - paymentMethod: 'PayPal', - sku: 'paypal-checkout', - gift: false, - itemPurchased: 'Gems', - purchaseType: 'checkout', - quantity: 1 - }, - revenue: 8 + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'purchase', + user_id: 'user-id', + event_properties: { + paymentMethod: 'PayPal', + sku: 'paypal-checkout', + gift: false, + itemPurchased: 'Gems', + purchaseType: 'checkout', + quantity: 1 + }, + revenue: 8 + }); }); }); - it('calls ga.event', function() { + context('Google Analytics', function() { - initializedAnalytics.trackPurchase(purchaseData); + it('calls ga.event', function() { + var data = _.cloneDeep(purchaseData); + initializedAnalytics.trackPurchase(data); - expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'commerce', - 'checkout', - 'PayPal', - 8 - ); - }); + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'commerce', + 'checkout', + 'PayPal', + 8 + ); + }); - it('calls ga.transaction', function() { + it('calls ga.transaction', function() { + var data = _.cloneDeep(purchaseData); + initializedAnalytics.trackPurchase(data); - initializedAnalytics.trackPurchase(purchaseData); + expect(googleTransaction).to.be.calledOnce; + expect(googleTransaction).to.be.calledWith( + 'user-id', + 8 + ); + expect(googleItem).to.be.calledOnce; + expect(googleItem).to.be.calledWith( + 8, + 1, + 'paypal-checkout', + 'Gems', + 'checkout' + ); + }); - expect(googleTransaction).to.be.calledOnce; - expect(googleTransaction).to.be.calledWith( - 'user-id', - 8 - ); - expect(googleItem).to.be.calledOnce; - expect(googleItem).to.be.calledWith( - 8, - 1, - 'paypal-checkout', - 'Gems', - 'checkout' - ); - }); + it('appends gift to variation of ga.transaction.item if gift is true', function() { - it('appends gift to variation of ga.transaction.item if gift is true', function() { + var data = _.cloneDeep(purchaseData); + data.gift = true; + initializedAnalytics.trackPurchase(data); - var purchaseDataWithGift = _.clone(purchaseData); - purchaseDataWithGift.gift = true; - - initializedAnalytics.trackPurchase(purchaseDataWithGift); - - expect(googleItem).to.be.calledOnce; - expect(googleItem).to.be.calledWith( - 8, - 1, - 'paypal-checkout', - 'Gems', - 'checkout - Gift' - ); + expect(googleItem).to.be.calledOnce; + expect(googleItem).to.be.calledWith( + 8, + 1, + 'paypal-checkout', + 'Gems', + 'checkout - Gift' + ); + }); }); }); }); diff --git a/website/src/controllers/payments/index.js b/website/src/controllers/payments/index.js index d9ce943250..b876954d62 100644 --- a/website/src/controllers/payments/index.js +++ b/website/src/controllers/payments/index.js @@ -127,7 +127,13 @@ exports.cancelSubscription = function(data, cb) { data.user.save(cb); utils.txnEmail(data.user, 'cancel-subscription'); - utils.ga.event('commerce', 'unsubscribe', data.paymentMethod).send(); + var analyticsData = { + uuid: data.user._id, + gaCategory: 'commerce', + gaLabel: data.paymentMethod, + paymentMethod: data.paymentMethod + } + utils.analytics.track('unsubscribe', analyticsData); } exports.buyGems = function(data, cb) { From d6444f922b304ee78bb59804c763a92ffe60d900 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Thu, 9 Jul 2015 22:16:50 -0500 Subject: [PATCH 11/31] Remove double instantiation --- website/src/utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/utils.js b/website/src/utils.js index 0614486415..bfcbdfc6ac 100644 --- a/website/src/utils.js +++ b/website/src/utils.js @@ -178,7 +178,6 @@ module.exports.setupConfig = function(){ module.exports.ga = require('universal-analytics')(nconf.get('GA_ID')); var analytics = isProd && require('./analytics'); - var analytics = require('./analytics'); var analyticsTokens = { amplitudeToken: nconf.get('AMPLITUDE_KEY'), googleAnalytics: nconf.get('GA_ID') From a6d162bfc08535b0eebf8d8d28642d1a64a7fe38 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Thu, 9 Jul 2015 22:17:27 -0500 Subject: [PATCH 12/31] Update how info is sent to google --- test/server_side/analytics.test.js | 68 ++++++++++++++++++++++++++++-- website/src/analytics.js | 21 +++++++-- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index 73c6f60560..6c17f66531 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -61,8 +61,7 @@ describe('analytics', function() { var event_type = 'Cron'; var analyticsData = { - gaCategory: 'behavior', - gaLabel: 'Ga Label', + category: 'behavior', uuid: 'unique-user-id', resting: true, cronCount: 5 @@ -88,6 +87,7 @@ describe('analytics', function() { event_type: 'Cron', user_id: 'unique-user-id', event_properties: { + category: 'behavior', resting: true, cronCount: 5 } @@ -112,6 +112,7 @@ describe('analytics', function() { event_type: 'Cron', user_id: 'unique-user-id', event_properties: { + category: 'behavior', resting: true, cronCount: 5 }, @@ -137,7 +138,68 @@ describe('analytics', function() { expect(googleEvent).to.be.calledWith( 'behavior', 'Cron', - 'Ga Label' + 'Label Not Specified' + ); + }); + + it('if goldCost property is provided, use as label', function() { + var data = _.cloneDeep(analyticsData); + data.goldCost = 4; + + initializedAnalytics.track(event_type, data); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'behavior', + 'Cron', + 4 + ); + }); + + it('if gemCost property is provided, use as label (overrides goldCost)', function() { + var data = _.cloneDeep(analyticsData); + data.goldCost = 10; + data.itemName = 50; + + initializedAnalytics.track(event_type, data); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'behavior', + 'Cron', + 50 + ); + }); + + it('if itemName property is provided, use as label (overrides gem/goldCost)', function() { + var data = _.cloneDeep(analyticsData); + data.goldCost = 5; + data.gemCost = 50; + data.itemName = 'some item'; + + initializedAnalytics.track(event_type, data); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'behavior', + 'Cron', + 'some item' + ); + }); + + it('if gaLabel property is provided, use as label (overrides itemName)', function() { + var data = _.cloneDeep(analyticsData); + data.value = 'some value'; + data.itemName = 'some item'; + data.gaLabel = 'some label'; + + initializedAnalytics.track(event_type, data); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith( + 'behavior', + 'Cron', + 'some label' ); }); }); diff --git a/website/src/analytics.js b/website/src/analytics.js index d3debe4fcc..cf97ce2adc 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -31,11 +31,26 @@ function _sendDataToAmplitude(eventType, data) { } function _sendDataToGoogle(eventType, data) { - var category = data.gaCategory; - var label = data.gaLabel; + var category = data.category; + var label = _generateLabelForGoogleAnalytics(data); + ga.event(category, eventType, label).send(); } +function _generateLabelForGoogleAnalytics(data) { + var label = 'Label Not Specified'; + var POSSIBLE_LABELS = ['gaLabel', 'itemName', 'gemCost', 'goldCost']; + + _(POSSIBLE_LABELS).each(function(key) { + if(data[key]) { + label = data[key]; + return false; // exit _.each early + } + }); + + return label; +} + function trackPurchase(data) { _sendPurchaseDataToAmplitude(data); _sendPurchaseDataToGoogle(data); @@ -50,7 +65,7 @@ function _sendPurchaseDataToAmplitude(data) { } function _formatDataForAmplitude(data) { - var PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaCategory', 'gaLabel']; + var PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaLabel']; var event_properties = _.omit(data, PROPERTIES_TO_SCRUB); var ampData = { From 52620bb69d9e463dda184d1d74790fbbd453d5f5 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Thu, 9 Jul 2015 22:18:37 -0500 Subject: [PATCH 13/31] Impliment analytics in user actions; remove mixpanel --- common/script/index.coffee | 182 ++++++++++++++++++++++++++------ package.json | 1 - website/src/controllers/user.js | 4 +- 3 files changed, 150 insertions(+), 37 deletions(-) diff --git a/common/script/index.coffee b/common/script/index.coffee index 0a6425c837..6c32f2777b 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -455,7 +455,7 @@ api.wrap = (user, main=true) -> user.preferences.sleep = !user.preferences.sleep cb? null, {} - revive: (req, cb) -> + revive: (req, cb, analytics) -> return cb?({code:400, message: "Cannot revive if not dead"}) unless user.stats.hp <= 0 # Reset stats after death @@ -484,7 +484,15 @@ api.wrap = (user, main=true) -> user.items.gear.equipped[item.type] = "#{item.type}_base_0" if user.items.gear.equipped[item.type] is lostItem user.items.gear.costume[item.type] = "#{item.type}_base_0" if user.items.gear.costume[item.type] is lostItem user.markModified? 'items.gear' - mixpanel?.track('Death',{'lostItem':lostItem}) + + analyticsData = { + uuid: user._id, + lostItem: lostItem, + gaLabel: lostItem, + category: 'behavior' + } + analytics?.track('Death', analyticsData) + cb? (if item then {code:200,message: i18n.t('messageLostItem', {itemText: item.text(req.language)}, req.language)} else null), user reset: (req, cb) -> @@ -510,7 +518,7 @@ api.wrap = (user, main=true) -> user.preferences.costume = false cb? null, user - reroll: (req, cb, ga) -> + reroll: (req, cb, analytics) -> if user.balance < 1 return cb? {code:401,message: i18n.t('notEnoughGems', req.language)} user.balance-- @@ -518,19 +526,37 @@ api.wrap = (user, main=true) -> unless task.type is 'reward' task.value = 0 user.stats.hp = 50 - cb? null, user - mixpanel?.track("Acquire Item",{'itemName':'Fortify','acquireMethod':'Gems','gemCost':4}) - ga?.event('behavior', 'gems', 'reroll').send() - rebirth: (req, cb, ga) -> + analyticsData = { + uuid: user._id, + acquireMethod: 'Gems', + gemCost: 4, + category: 'behavior' + } + analytics?.track('Fortify Potion', analyticsData) + + cb? null, user + + rebirth: (req, cb, analytics) -> # Cost is 8 Gems ($2) if (user.balance < 2 && user.stats.lvl < api.maxLevel) return cb? {code:401,message: i18n.t('notEnoughGems', req.language)} + + analyticsData = { + uuid: user._id, + category: 'behavior' + } # only charge people if they are under the max level - ryan if user.stats.lvl < api.maxLevel user.balance -= 2 - mixpanel?.track("Acquire Item",{'itemName':'Rebirth','acquireMethod':'Gems','gemCost':8}) - ga?.event('behavior', 'gems', 'rebirth').send() + analyticsData.acquireMethod = 'Gems' + analyticsData.gemCost = 8 + else + analyticsData.gemCost = 0 + analyticsData.acquireMethod = '> 100' + + analytics?.track('Rebirth', analyticsData) + # Save off user's level, for calculating achievement eligibility later lvl = api.capByLevel(user.stats.lvl) # Turn tasks yellow, zero out streaks @@ -779,7 +805,7 @@ api.wrap = (user, main=true) -> cb? {code:200,message}, _.pick(user,$w 'items stats') # buy is for using Gold, purchase is for Gems (I know, I know...) - purchase: (req, cb, ga) -> + purchase: (req, cb, analytics) -> {type,key} = req.params if type is 'gems' and key is 'gem' @@ -791,7 +817,16 @@ api.wrap = (user, main=true) -> user.balance += .25 user.purchased.plan.gemsBought++ user.stats.gp -= convRate - mixpanel?.track("Acquire Item",{'itemName':key,'acquireMethod':'Gold','goldCost':convRate}) + + analyticsData = { + uuid: user._id, + itemName: key, + acquireMethod: 'Gold', + goldCost: convRate, + category: 'behavior' + } + analytics?.track('purchase gems', analyticsData) + return cb? {code:200,message:"+1 Gem"}, _.pick(user,$w 'stats balance') return cb?({code:404,message:":type must be in [eggs,hatchingPotions,food,quests,gear]"},req) unless type in ['eggs','hatchingPotions','food','quests','gear'] @@ -809,11 +844,19 @@ api.wrap = (user, main=true) -> else user.items[type][key] = 0 unless user.items[type][key] > 0 user.items[type][key]++ - mixpanel?.track("Acquire Item",{'itemName':key,'acquireMethod':'Gems','gemCost':item.value}) - cb? null, _.pick(user,$w 'items balance') - ga?.event('behavior', 'gems', key).send() - releasePets: (req, cb) -> + analyticsData = { + uuid: user._id, + itemName: key, + acquireMethod: 'Gems', + gemCost: item.value, + category: 'behavior' + } + analytics?.track('acquire item', analyticsData) + + cb? null, _.pick(user,$w 'items balance') + + releasePets: (req, cb, analytics) -> if user.balance < 1 return cb? {code:401,message: i18n.t('notEnoughGems', req.language)} else @@ -824,10 +867,17 @@ api.wrap = (user, main=true) -> user.achievements.beastMasterCount = 0 user.achievements.beastMasterCount++ user.items.currentPet = "" - cb? null, user - mixpanel?.track("Acquire Item",{'itemName':'Kennel Key','acquireMethod':'Gems','gemCost':4}) - releaseMounts: (req, cb) -> + analyticsData = { + uuid: user._id, + acquireMethod: 'Gems', + gemCost: 4, + category: 'behavior' + } + analytics?.track('release pets', analyticsData) + cb? null, user + + releaseMounts: (req, cb, analytics) -> if user.balance < 1 return cb? {code:401,message: i18n.t('notEnoughGems', req.language)} else @@ -838,8 +888,16 @@ api.wrap = (user, main=true) -> if not user.achievements.mountMasterCount user.achievements.mountMasterCount = 0 user.achievements.mountMasterCount++ + + analyticsData = { + uuid: user._id, + acquireMethod: 'Gems', + gemCost: 4, + category: 'behavior' + } + analytics?.track('release mounts', analyticsData) + cb? null, user - mixpanel?.track("Acquire Item",{'itemName':'Kennel Key','acquireMethod':'Gems','gemCost':4}) releaseBoth: (req, cb) -> if user.balance < 1.5 and not user.achievements.triadBingo @@ -847,7 +905,13 @@ api.wrap = (user, main=true) -> else giveTriadBingo = true if not user.achievements.triadBingo - mixpanel?.track("Acquire Item",{'itemName':'Kennel Key','acquireMethod':'Gems','gemCost':6}) + analyticsData = { + uuid: user._id, + acquireMethod: 'Gems', + gemCost: 6, + category: 'behavior' + } + analytics?.track('release pets & mounts', analyticsData) user.balance -= 1.5 user.items.currentMount = "" user.items.currentPet = "" @@ -868,7 +932,7 @@ api.wrap = (user, main=true) -> cb? null, user # buy is for gear, purchase is for gem-purchaseables (i know, I know...) - buy: (req, cb) -> + buy: (req, cb, analytics) -> {key} = req.params item = if key is 'potion' then content.potion @@ -909,10 +973,19 @@ api.wrap = (user, main=true) -> message ?= i18n.t('messageBought', {itemText: item.text(req.language)}, req.language) if item.last then user.fns.ultimateGear() user.stats.gp -= item.value - mixpanel?.track("Acquire Item",{'itemName':key,'acquireMethod':'Gold','goldCost':item.value}) + + analyticsData = { + uuid: user._id, + itemName: key, + acquireMethod: 'Gold', + goldCost: item.value, + category: 'behavior' + } + analytics?.track('acquire item', analyticsData) + cb? {code:200, message}, _.pick(user,$w 'items achievements stats flags') - buyMysterySet: (req, cb)-> + buyMysterySet: (req, cb, analytics)-> return cb?({code:401, message:"You don't have enough Mystic Hourglasses"}) unless user.purchased.plan.consecutive.trinkets>0 mysterySet = content.timeTravelerStore(user.items.gear.owned)?[req.params.key] if window?.confirm? @@ -920,7 +993,14 @@ api.wrap = (user, main=true) -> return cb?({code:404, message:"Mystery set not found, or set already owned"}) unless mysterySet _.each mysterySet.items, (i)-> user.items.gear.owned[i.key]=true - mixpanel?.track("Acquire Item",{'itemName':i.key,'acquireMethod':'Hourglass'}) + analyticsData = { + uuid: user._id, + itemName: i.key, + acquireMethod: 'Hourglass', + category: 'behavior' + } + analytics?.track('acquire item', analyticsData) + user.purchased.plan.consecutive.trinkets-- cb? null, _.pick(user,$w 'items purchased.plan.consecutive') @@ -964,7 +1044,7 @@ api.wrap = (user, main=true) -> user.items.hatchingPotions[hatchingPotion]-- cb? {code:200, message:i18n.t('messageHatched', req.language)}, user.items - unlock: (req, cb, ga) -> + unlock: (req, cb, analytics) -> {path} = req.query fullSet = ~path.indexOf(",") cost = @@ -991,17 +1071,36 @@ api.wrap = (user, main=true) -> user.fns.dotSet "purchased." + path, true user.balance -= cost if ~path.indexOf('gear.') then user.markModified? 'gear.owned' else user.markModified? 'purchased' + + analyticsData = { + uuid: user._id, + itemName: path, + itemType: 'customization', + acquireMethod: 'Gems', + gemCost: (cost/.25), + category: 'behavior' + } + analytics?.track('acquire item', analyticsData) + cb? null, _.pick(user,$w 'purchased preferences items') - mixpanel?.track("Acquire Item",{'itemName':'Customizations','acquireMethod':'Gems','gemCost':(cost / .25)}) - ga?.event('behavior', 'gems', path).send() # ------ # Classes # ------ - changeClass: (req, cb, ga) -> + changeClass: (req, cb, analytics) -> klass = req.query?.class if klass in ['warrior','rogue','wizard','healer'] + + analyticsData = { + uuid: user._id, + class: klass, + acquireMethod: 'Gems', + gemCost: 3, + category: 'behavior' + } + analytics?.track('change class', analyticsData) + user.stats.class = klass user.flags.classSelected = true # Clear their gear and equip their new class's gear (can still equip old gear from inventory) @@ -1032,8 +1131,6 @@ api.wrap = (user, main=true) -> user.balance -= .75 _.merge user.stats, {str: 0, con: 0, per: 0, int: 0, points: api.capByLevel(user.stats.lvl)} user.flags.classSelected = false - mixpanel?.track("Acquire Item",{'itemName':klass,'acquireMethod':'Gems','gemCost':3}) - ga?.event('behavior', 'gems', 'changeClass').send() #'stats.points': this is handled on the server cb? null, _.pick(user,$w 'stats flags items preferences') @@ -1059,7 +1156,7 @@ api.wrap = (user, main=true) -> user.markModified? 'items.special.valentineReceived' cb? null, 'items.special' - openMysteryItem: (req,cb,ga) -> + openMysteryItem: (req,cb,analytics) -> item = user.purchased.plan?.mysteryItems?.shift() return cb?(code:400,message:"Empty") unless item item = content.gear.flat[item] @@ -1068,6 +1165,15 @@ api.wrap = (user, main=true) -> # Could show {code:200} message, but it's yellow with no icon. This is round-about, but prettier. FIXME (user._tmp?={}).drop = {type: 'gear', dialog: "#{item.text(req.language)} inside!"} if typeof window != 'undefined' #cb? {code:200, message:"#{item.text} inside!"}, user.items.gear.owned + + analyticsData = { + uuid: user._id, + itemName: item, + acquireMethod: 'Subscriber', + category: 'behavior' + } + analytics?.track('open mystery item', analyticsData) + cb? null, user.items.gear.owned readNYE: (req,cb) -> @@ -1462,7 +1568,7 @@ api.wrap = (user, main=true) -> else "str" # if all else fails, dump into STR )()]++ - updateStats: (stats, req) -> + updateStats: (stats, req, analytics) -> # Game Over (death) return user.stats.hp=0 if stats.hp <= 0 @@ -1516,7 +1622,15 @@ api.wrap = (user, main=true) -> user.items.quests[k]++ (user.flags.levelDrops ?= {})[k] = true user.markModified? 'flags.levelDrops' - mixpanel?.track("Acquire Item",{'itemName':k,'acquireMethod':'Drop'}) + + analyticsData = { + uuid: user._id, + itemName: 'quest drop: ' + k, + acquireMethod: 'Drop', + category: 'behavior' + } + analytics?.track('acquire item', analyticsData) + user._tmp.drop = _.defaults content.quests[k], type: 'Quest' dialog: i18n.t('messageFoundQuest', {questText: content.quests[k].text(req.language)}, req.language) @@ -1694,7 +1808,7 @@ api.wrap = (user, main=true) -> user.flags.cronCount++ analyticsData = { - gaCategory: 'behavior', + category: 'behavior', gaLabel: user.flags.cronCount, uuid: user._id, user: user, diff --git a/package.json b/package.json index 19022c9826..1e29c6c579 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "lodash": "~2.4.1", "loggly": "~1.0.8", "method-override": "~2.2.0", - "mixpanel": "^0.2.1", "moment": "~2.8.3", "mongoose": "~3.8.23", "mongoose-id-autoinc": "~2013.7.14-4", diff --git a/website/src/controllers/user.js b/website/src/controllers/user.js index adb35d9328..7f39d2af79 100644 --- a/website/src/controllers/user.js +++ b/website/src/controllers/user.js @@ -8,7 +8,7 @@ var async = require('async'); var shared = require('../../../common'); var User = require('./../models/user').model; var utils = require('./../utils'); -var ga = utils.ga; +var analytics = utils.analytics; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; var moment = require('moment'); @@ -535,7 +535,7 @@ _.each(shared.wrap({}).ops, function(op,k){ if (err) return next(err); res.json(200,response); }) - }, ga); + }, analytics); } } }) From ac02ad3aa2ddc65dfa5de30c2a7d1d36a8218f57 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 10 Jul 2015 07:35:45 -0500 Subject: [PATCH 14/31] Update amplitude version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e29c6c579..29e69518ac 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "./website/src/server.js", "dependencies": { "amazon-payments": "0.0.4", - "amplitude": "1.0.4", + "amplitude": "^1.0.5", "async": "~0.9.0", "aws-sdk": "^2.0.25", "babel": "^5.5.4", From 2e6725997d28d231df816c6591d228e3da359e61 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 10 Jul 2015 07:40:01 -0500 Subject: [PATCH 15/31] Swap out ga for analytis service in auth controller --- website/src/controllers/auth.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js index e861daab19..0b1c1348bd 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/auth.js @@ -8,7 +8,7 @@ var nconf = require('nconf'); var request = require('request'); var User = require('../models/user').model; var EmailUnsubscription = require('../models/emailUnsubscription').model; -var ga = require('./../utils').ga; +var analytics = utils.analytics; var i18n = require('./../i18n'); var isProd = nconf.get('NODE_ENV') === 'production'; @@ -110,7 +110,14 @@ api.registerUser = function(req, res, next) { newUser.preferences = newUser.preferences || {}; newUser.preferences.language = req.language; // User language detected from browser, not saved var user = new User(newUser); - ga.event('acquisition', 'register', 'local').send(); + + var analyticsData = { + category: 'acquisition', + type: 'local', + gaLabel: 'local' + }; + var analytics.track('register', analyticsData) + user.save(function(err, savedUser){ // Clean previous email preferences EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){ @@ -194,7 +201,12 @@ api.loginSocial = function(req, res, next) { cb.apply(cb, arguments); }); - ga.event('acquisition', 'register', network).send(); + var analyticsData = { + category: 'acquisition', + type: network, + gaLabel: network + }; + var analytics.track('register', analyticsData) }] }, function(err, results){ if (err) return res.json(401, {err: err.toString ? err.toString() : err}); From d9f8e54d1a75d192b7bb6fb531cfb09fe87982bc Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 10 Jul 2015 07:41:07 -0500 Subject: [PATCH 16/31] Remove gogle analytics from export --- website/src/utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website/src/utils.js b/website/src/utils.js index bfcbdfc6ac..5d1e4ac2ba 100644 --- a/website/src/utils.js +++ b/website/src/utils.js @@ -176,7 +176,6 @@ module.exports.setupConfig = function(){ isProd = nconf.get('NODE_ENV') === 'production'; baseUrl = nconf.get('BASE_URL'); - module.exports.ga = require('universal-analytics')(nconf.get('GA_ID')); var analytics = isProd && require('./analytics'); var analyticsTokens = { amplitudeToken: nconf.get('AMPLITUDE_KEY'), From 2f1d3400bc81b7c31733a0d0b3cbcc200cff09f1 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 10 Jul 2015 07:47:52 -0500 Subject: [PATCH 17/31] Added additional analytics info --- common/script/index.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/script/index.coffee b/common/script/index.coffee index 6c32f2777b..9393f06ead 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -848,6 +848,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, itemName: key, + itemType: 'Market', acquireMethod: 'Gems', gemCost: item.value, category: 'behavior' @@ -996,6 +997,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, itemName: i.key, + itemType: 'Subscriber Gear', acquireMethod: 'Hourglass', category: 'behavior' } @@ -1169,6 +1171,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, itemName: item, + itemType: 'Subscriber Gear', acquireMethod: 'Subscriber', category: 'behavior' } From acb0793bafeaca1532c81c0ab77c88b9e119d732 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 10 Jul 2015 07:49:22 -0500 Subject: [PATCH 18/31] Correct indentation --- website/src/controllers/auth.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js index 0b1c1348bd..443fcded16 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/auth.js @@ -201,12 +201,12 @@ api.loginSocial = function(req, res, next) { cb.apply(cb, arguments); }); - var analyticsData = { - category: 'acquisition', - type: network, - gaLabel: network - }; - var analytics.track('register', analyticsData) + var analyticsData = { + category: 'acquisition', + type: network, + gaLabel: network + }; + var analytics.track('register', analyticsData) }] }, function(err, results){ if (err) return res.json(401, {err: err.toString ? err.toString() : err}); From 8d1231a1eedc9cd642b20d8a62cf7817156f503c Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 10 Jul 2015 09:14:22 -0500 Subject: [PATCH 19/31] Remove erroneous var --- website/src/controllers/auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js index 443fcded16..fc5e5dafbe 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/auth.js @@ -116,7 +116,7 @@ api.registerUser = function(req, res, next) { type: 'local', gaLabel: 'local' }; - var analytics.track('register', analyticsData) + analytics.track('register', analyticsData) user.save(function(err, savedUser){ // Clean previous email preferences @@ -206,7 +206,7 @@ api.loginSocial = function(req, res, next) { type: network, gaLabel: network }; - var analytics.track('register', analyticsData) + analytics.track('register', analyticsData) }] }, function(err, results){ if (err) return res.json(401, {err: err.toString ? err.toString() : err}); From 5e89ae200da6ddf30eae7ca8163cb614ea1ec887 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 10 Jul 2015 13:47:03 -0500 Subject: [PATCH 20/31] feat(analytics): Server tweaks Remove duplicate browser-side tracking for new user registration. Move quest tracking to server side and expand to include all quest accept/reject actions. --- website/public/js/controllers/authCtrl.js | 9 -------- .../public/js/controllers/inventoryCtrl.js | 1 - website/src/controllers/groups.js | 22 +++++++++++++++++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/website/public/js/controllers/authCtrl.js b/website/public/js/controllers/authCtrl.js index 89be71c6af..728ec6cd4e 100644 --- a/website/public/js/controllers/authCtrl.js +++ b/website/public/js/controllers/authCtrl.js @@ -50,15 +50,6 @@ angular.module('habitrpg') if($rootScope.selectedLanguage) url = url + '?lang=' + $rootScope.selectedLanguage.code; $http.post(url, scope.registerVals).success(function(data, status, headers, config) { runAuth(data.id, data.apiToken); - if (status == 200) { - if (data.auth.facebook) { - Analytics.updateUser({'email':data.auth.facebook._json.email,'language':data.preferences.language}); - Analytics.track({'hitType':'event','eventCategory':'acquisition','eventAction':'register','authType':'facebook'}); - } else { - Analytics.updateUser({'email':data.auth.local.email,'language':data.preferences.language}); - Analytics.track({'hitType':'event','eventCategory':'acquisition','eventAction':'register','authType':'email'}); - } - } }).error(errorAlert); }; diff --git a/website/public/js/controllers/inventoryCtrl.js b/website/public/js/controllers/inventoryCtrl.js index afd85b625b..5a313b67f6 100644 --- a/website/public/js/controllers/inventoryCtrl.js +++ b/website/public/js/controllers/inventoryCtrl.js @@ -180,7 +180,6 @@ habitrpg.controller("InventoryCtrl", $rootScope.selectedQuest = undefined; } $scope.questInit = function(){ - Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'quest','owner':true,'response':'accept','questName':$scope.selectedQuest.key}); $rootScope.party.$questAccept({key:$scope.selectedQuest.key}, function(){ $rootScope.party.$get(); }); diff --git a/website/src/controllers/groups.js b/website/src/controllers/groups.js index 9eac2c8af4..a5006bbd3b 100644 --- a/website/src/controllers/groups.js +++ b/website/src/controllers/groups.js @@ -17,6 +17,7 @@ var EmailUnsubscription = require('./../models/emailUnsubscription').model; var isProd = nconf.get('NODE_ENV') === 'production'; var api = module.exports; var pushNotify = require('./pushNotifications'); +var analytics = utils.analytics; /* ------------------------------------------------------------------------ @@ -919,6 +920,13 @@ api.questAccept = function(req, res, next) { // or everyone has either accepted/rejected, then we store quest key in user object. _.each(group.members, function(m){ if (m == user._id) { + var analyticsData = { + category: 'behavior', + owner: true, + response: 'accept', + questName: key + }; + analytics.track('quest',analyticsData); group.quest.members[m] = true; group.quest.leader = user._id; } else { @@ -957,6 +965,13 @@ api.questAccept = function(req, res, next) { // Party member accepting the invitation } else { if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'}); + var analyticsData = { + category: 'behavior', + owner: false, + response: 'accept', + questName: group.quest.key + }; + analytics.track('quest',analyticsData); group.quest.members[user._id] = true; User.update({_id:user._id}, {$set: {'party.quest.RSVPNeeded': false}}).exec(); questStart(req,res,next); @@ -968,6 +983,13 @@ api.questReject = function(req, res, next) { var user = res.locals.user; if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'}); + var analyticsData = { + category: 'behavior', + owner: false, + response: 'reject', + questName: key + }; + analytics.track('quest',analyticsData); group.quest.members[user._id] = false; User.update({_id:user._id}, {$set: {'party.quest.RSVPNeeded': false, 'party.quest.key': null}}).exec(); questStart(req,res,next); From 175b4fa6e029c5d4744885e323cab2ebf9040bc6 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 10 Jul 2015 13:51:27 -0500 Subject: [PATCH 21/31] fix(analytics): Correct quest key --- website/src/controllers/groups.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/controllers/groups.js b/website/src/controllers/groups.js index a5006bbd3b..26e33fab39 100644 --- a/website/src/controllers/groups.js +++ b/website/src/controllers/groups.js @@ -987,7 +987,7 @@ api.questReject = function(req, res, next) { category: 'behavior', owner: false, response: 'reject', - questName: key + questName: group.quest.key }; analytics.track('quest',analyticsData); group.quest.members[user._id] = false; From 3bbde2a548fda3b84b1601c422ce947960aa7376 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 10 Jul 2015 13:57:46 -0500 Subject: [PATCH 22/31] fix(analytics): Include GA label for quest actions --- website/src/controllers/groups.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/src/controllers/groups.js b/website/src/controllers/groups.js index 26e33fab39..77487ab292 100644 --- a/website/src/controllers/groups.js +++ b/website/src/controllers/groups.js @@ -924,6 +924,7 @@ api.questAccept = function(req, res, next) { category: 'behavior', owner: true, response: 'accept', + gaLabel: 'accept', questName: key }; analytics.track('quest',analyticsData); @@ -969,6 +970,7 @@ api.questAccept = function(req, res, next) { category: 'behavior', owner: false, response: 'accept', + gaLabel: 'accept', questName: group.quest.key }; analytics.track('quest',analyticsData); @@ -987,6 +989,7 @@ api.questReject = function(req, res, next) { category: 'behavior', owner: false, response: 'reject', + gaLabel: 'reject', questName: group.quest.key }; analytics.track('quest',analyticsData); From a767cf9a2131e0cc18906aaadf95388f53895b88 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 12 Jul 2015 08:49:57 -0500 Subject: [PATCH 23/31] Add server_side tests to gulp --- tasks/gulp-tests.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tasks/gulp-tests.js b/tasks/gulp-tests.js index 14953c8559..bf81b34838 100644 --- a/tasks/gulp-tests.js +++ b/tasks/gulp-tests.js @@ -70,6 +70,31 @@ gulp.task('test:common:safe', ['test:prepare:build'], (cb) => { pipe(runner); }); +gulp.task('test:server_side', ['test:prepare:build'], (cb) => { + let runner = exec( + testBin('mocha test/server_side'), + (err, stdout, stderr) => { + cb(err); + } + ); + pipe(runner); +}); + +gulp.task('test:server_side:safe', ['test:prepare:build'], (cb) => { + let runner = exec( + testBin('mocha test/server_side'), + (err, stdout, stderr) => { + testResults.push({ + suite: 'Servser Side Specs\t', + pass: testCount(stdout, /(\d+) passing/), + fail: testCount(stderr, /(\d+) failing/), + pend: testCount(stdout, /(\d+) pending/) + }); + cb(); + } + ); + pipe(runner); +}); gulp.task('test:api', ['test:prepare:mongo'], (cb) => { let runner = exec( @@ -193,6 +218,7 @@ gulp.task('test:e2e:safe', ['test:prepare'], (cb) => { gulp.task('test', [ 'test:common:safe', + 'test:server_side:safe', 'test:karma:safe', 'test:api:safe', 'test:e2e:safe' From 284fa03223b4a4b740d7c454a3c90bc6c7397725 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 12 Jul 2015 10:03:41 -0500 Subject: [PATCH 24/31] Add platform, change itemName to itemKey --- common/script/index.coffee | 14 +++++++------- test/server_side/analytics.test.js | 13 ++++++++----- website/src/analytics.js | 7 ++++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/common/script/index.coffee b/common/script/index.coffee index 9393f06ead..8f508f8a72 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -820,7 +820,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, - itemName: key, + itemKey: key, acquireMethod: 'Gold', goldCost: convRate, category: 'behavior' @@ -847,7 +847,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, - itemName: key, + itemKey: key, itemType: 'Market', acquireMethod: 'Gems', gemCost: item.value, @@ -977,7 +977,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, - itemName: key, + itemKey: key, acquireMethod: 'Gold', goldCost: item.value, category: 'behavior' @@ -996,7 +996,7 @@ api.wrap = (user, main=true) -> user.items.gear.owned[i.key]=true analyticsData = { uuid: user._id, - itemName: i.key, + itemKey: i.key, itemType: 'Subscriber Gear', acquireMethod: 'Hourglass', category: 'behavior' @@ -1076,7 +1076,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, - itemName: path, + itemKey: path, itemType: 'customization', acquireMethod: 'Gems', gemCost: (cost/.25), @@ -1170,7 +1170,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, - itemName: item, + itemKey: item, itemType: 'Subscriber Gear', acquireMethod: 'Subscriber', category: 'behavior' @@ -1628,7 +1628,7 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, - itemName: 'quest drop: ' + k, + itemKey: 'quest drop: ' + k, acquireMethod: 'Drop', category: 'behavior' } diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index 6c17f66531..e48acec47d 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -86,6 +86,7 @@ describe('analytics', function() { expect(amplitudeTrack).to.be.calledWith({ event_type: 'Cron', user_id: 'unique-user-id', + platform: 'server', event_properties: { category: 'behavior', resting: true, @@ -111,6 +112,7 @@ describe('analytics', function() { expect(amplitudeTrack).to.be.calledWith({ event_type: 'Cron', user_id: 'unique-user-id', + platform: 'server', event_properties: { category: 'behavior', resting: true, @@ -159,7 +161,7 @@ describe('analytics', function() { it('if gemCost property is provided, use as label (overrides goldCost)', function() { var data = _.cloneDeep(analyticsData); data.goldCost = 10; - data.itemName = 50; + data.itemKey = 50; initializedAnalytics.track(event_type, data); @@ -171,11 +173,11 @@ describe('analytics', function() { ); }); - it('if itemName property is provided, use as label (overrides gem/goldCost)', function() { + it('if itemKey property is provided, use as label (overrides gem/goldCost)', function() { var data = _.cloneDeep(analyticsData); data.goldCost = 5; data.gemCost = 50; - data.itemName = 'some item'; + data.itemKey = 'some item'; initializedAnalytics.track(event_type, data); @@ -187,10 +189,10 @@ describe('analytics', function() { ); }); - it('if gaLabel property is provided, use as label (overrides itemName)', function() { + it('if gaLabel property is provided, use as label (overrides itemKey)', function() { var data = _.cloneDeep(analyticsData); data.value = 'some value'; - data.itemName = 'some item'; + data.itemKey = 'some item'; data.gaLabel = 'some label'; initializedAnalytics.track(event_type, data); @@ -239,6 +241,7 @@ describe('analytics', function() { expect(amplitudeTrack).to.be.calledWith({ event_type: 'purchase', user_id: 'user-id', + platform: 'server', event_properties: { paymentMethod: 'PayPal', sku: 'paypal-checkout', diff --git a/website/src/analytics.js b/website/src/analytics.js index cf97ce2adc..df6bf4f9ae 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -39,7 +39,7 @@ function _sendDataToGoogle(eventType, data) { function _generateLabelForGoogleAnalytics(data) { var label = 'Label Not Specified'; - var POSSIBLE_LABELS = ['gaLabel', 'itemName', 'gemCost', 'goldCost']; + var POSSIBLE_LABELS = ['gaLabel', 'itemKey', 'gemCost', 'goldCost']; _(POSSIBLE_LABELS).each(function(key) { if(data[key]) { @@ -70,6 +70,7 @@ function _formatDataForAmplitude(data) { var ampData = { user_id: data.uuid, + platform: 'server', event_properties: event_properties } @@ -109,7 +110,7 @@ function _sendPurchaseDataToGoogle(data) { var price = data.purchaseValue; var qty = data.quantity; var sku = data.sku; - var itemName = data.itemPurchased; + var itemKey = data.itemPurchased; var variation = type; if(data.gift) variation += ' - Gift'; @@ -117,7 +118,7 @@ function _sendPurchaseDataToGoogle(data) { .send(); ga.transaction(data.uuid, price) - .item(price, qty, sku, itemName, variation) + .item(price, qty, sku, itemKey, variation) .send(); } From 74de248109b8bb32bd65b91428c7ac9148848b5a Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 12 Jul 2015 10:05:00 -0500 Subject: [PATCH 25/31] Adjust how quest drops are sent --- common/script/index.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/script/index.coffee b/common/script/index.coffee index 8f508f8a72..cb85388429 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -1628,8 +1628,8 @@ api.wrap = (user, main=true) -> analyticsData = { uuid: user._id, - itemKey: 'quest drop: ' + k, - acquireMethod: 'Drop', + itemKey: k, + acquireMethod: 'Level Drop', category: 'behavior' } analytics?.track('acquire item', analyticsData) From deac9619bc764b312536b31cf14868f8674e5a1e Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 19 Jul 2015 08:27:16 -0500 Subject: [PATCH 26/31] Refactor to use object method of sending google data --- test/server_side/analytics.test.js | 147 +++++++++++------------------ website/src/analytics.js | 25 +++-- 2 files changed, 74 insertions(+), 98 deletions(-) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index e48acec47d..c054ee08b9 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -59,14 +59,7 @@ describe('analytics', function() { describe('track', function() { - var event_type = 'Cron'; - var analyticsData = { - category: 'behavior', - uuid: 'unique-user-id', - resting: true, - cronCount: 5 - } - + var analyticsData, event_type; var analytics = rewire('../../website/src/analytics'); var initializedAnalytics; @@ -75,6 +68,14 @@ describe('analytics', function() { initializedAnalytics = analytics({amplitudeToken: 'token'}); analytics.__set__('amplitude.track', amplitudeTrack); analytics.__set__('ga.event', googleEvent); + + event_type = 'Cron'; + analyticsData = { + category: 'behavior', + uuid: 'unique-user-id', + resting: true, + cronCount: 5 + } }); context('Amplitude', function() { @@ -103,10 +104,9 @@ describe('analytics', function() { purchased: { plan: { planId: 'foo-plan' } } }; - var analyticsDataWithUser = _.cloneDeep(analyticsData); - analyticsDataWithUser.user = user; + analyticsData.user = user; - initializedAnalytics.track(event_type, analyticsDataWithUser); + initializedAnalytics.track(event_type, analyticsData); expect(amplitudeTrack).to.be.calledOnce; expect(amplitudeTrack).to.be.calledWith({ @@ -137,88 +137,45 @@ describe('analytics', function() { initializedAnalytics.track(event_type, analyticsData); expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'behavior', - 'Cron', - 'Label Not Specified' - ); + expect(googleEvent).to.be.calledWith({ + ec: 'behavior', + ea: 'Cron' + }); }); - it('if goldCost property is provided, use as label', function() { - var data = _.cloneDeep(analyticsData); - data.goldCost = 4; + it('if itemKey property is provided, use as label', function() { + analyticsData.itemKey = 'some item'; - initializedAnalytics.track(event_type, data); + initializedAnalytics.track(event_type, analyticsData); expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'behavior', - 'Cron', - 4 - ); - }); - - it('if gemCost property is provided, use as label (overrides goldCost)', function() { - var data = _.cloneDeep(analyticsData); - data.goldCost = 10; - data.itemKey = 50; - - initializedAnalytics.track(event_type, data); - - expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'behavior', - 'Cron', - 50 - ); - }); - - it('if itemKey property is provided, use as label (overrides gem/goldCost)', function() { - var data = _.cloneDeep(analyticsData); - data.goldCost = 5; - data.gemCost = 50; - data.itemKey = 'some item'; - - initializedAnalytics.track(event_type, data); - - expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'behavior', - 'Cron', - 'some item' - ); + expect(googleEvent).to.be.calledWith({ + ec: 'behavior', + ea: 'Cron', + el: 'some item' + }); }); it('if gaLabel property is provided, use as label (overrides itemKey)', function() { - var data = _.cloneDeep(analyticsData); - data.value = 'some value'; - data.itemKey = 'some item'; - data.gaLabel = 'some label'; + analyticsData.value = 'some value'; + analyticsData.itemKey = 'some item'; + analyticsData.gaLabel = 'some label'; - initializedAnalytics.track(event_type, data); + initializedAnalytics.track(event_type, analyticsData); expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'behavior', - 'Cron', - 'some label' - ); + expect(googleEvent).to.be.calledWith({ + ec: 'behavior', + ea: 'Cron', + el: 'some label' + }); }); }); }); describe('trackPurchase', function() { - var purchaseData = { - uuid: 'user-id', - sku: 'paypal-checkout', - paymentMethod: 'PayPal', - itemPurchased: 'Gems', - purchaseValue: 8, - purchaseType: 'checkout', - gift: false, - quantity: 1 - } + var purchaseData; var analytics = rewire('../../website/src/analytics'); var initializedAnalytics; @@ -229,13 +186,24 @@ describe('analytics', function() { analytics.__set__('amplitude.track', amplitudeTrack); analytics.__set__('ga.event', googleEvent); analytics.__set__('ga.transaction', googleTransaction); + + purchaseData = { + uuid: 'user-id', + sku: 'paypal-checkout', + paymentMethod: 'PayPal', + itemPurchased: 'Gems', + purchaseValue: 8, + purchaseType: 'checkout', + gift: false, + quantity: 1 + } + }); context('Amplitude', function() { it('calls amplitude.track', function() { - var data = _.cloneDeep(purchaseData); - initializedAnalytics.trackPurchase(data); + initializedAnalytics.trackPurchase(purchaseData); expect(amplitudeTrack).to.be.calledOnce; expect(amplitudeTrack).to.be.calledWith({ @@ -258,21 +226,19 @@ describe('analytics', function() { context('Google Analytics', function() { it('calls ga.event', function() { - var data = _.cloneDeep(purchaseData); - initializedAnalytics.trackPurchase(data); + initializedAnalytics.trackPurchase(purchaseData); expect(googleEvent).to.be.calledOnce; - expect(googleEvent).to.be.calledWith( - 'commerce', - 'checkout', - 'PayPal', - 8 - ); + expect(googleEvent).to.be.calledWith({ + ec: 'commerce', + ea: 'checkout', + el: 'PayPal', + ev: 8 + }); }); it('calls ga.transaction', function() { - var data = _.cloneDeep(purchaseData); - initializedAnalytics.trackPurchase(data); + initializedAnalytics.trackPurchase(purchaseData); expect(googleTransaction).to.be.calledOnce; expect(googleTransaction).to.be.calledWith( @@ -291,9 +257,8 @@ describe('analytics', function() { it('appends gift to variation of ga.transaction.item if gift is true', function() { - var data = _.cloneDeep(purchaseData); - data.gift = true; - initializedAnalytics.trackPurchase(data); + purchaseData.gift = true; + initializedAnalytics.trackPurchase(purchaseData); expect(googleItem).to.be.calledOnce; expect(googleItem).to.be.calledWith( diff --git a/website/src/analytics.js b/website/src/analytics.js index df6bf4f9ae..2e20da99da 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -31,15 +31,20 @@ function _sendDataToAmplitude(eventType, data) { } function _sendDataToGoogle(eventType, data) { - var category = data.category; - var label = _generateLabelForGoogleAnalytics(data); + var eventData = { + ec: data.category, + ea: eventType + } - ga.event(category, eventType, label).send(); + var label = _generateLabelForGoogleAnalytics(data); + if(label) { eventData.el = label; } + + ga.event(eventData).send(); } function _generateLabelForGoogleAnalytics(data) { - var label = 'Label Not Specified'; - var POSSIBLE_LABELS = ['gaLabel', 'itemKey', 'gemCost', 'goldCost']; + var label; + var POSSIBLE_LABELS = ['gaLabel', 'itemKey']; _(POSSIBLE_LABELS).each(function(key) { if(data[key]) { @@ -114,8 +119,14 @@ function _sendPurchaseDataToGoogle(data) { var variation = type; if(data.gift) variation += ' - Gift'; - ga.event('commerce', type, label, price) - .send(); + var eventData = { + ec: 'commerce', + ea: type, + el: label, + ev: price + }; + + ga.event(eventData).send(); ga.transaction(data.uuid, price) .item(price, qty, sku, itemKey, variation) From b49995c676d319b82ab174d46c593e6a0ffd8716 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 19 Jul 2015 08:33:44 -0500 Subject: [PATCH 27/31] Allow optional value to be passed in --- test/server_side/analytics.test.js | 41 ++++++++++++++++++++++++++++++ website/src/analytics.js | 19 +++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index c054ee08b9..a67e73b2b7 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -170,6 +170,47 @@ describe('analytics', function() { el: 'some label' }); }); + + it('if goldCost property is provided, use as value', function() { + analyticsData.goldCost = 5; + + initializedAnalytics.track(event_type, analyticsData); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith({ + ec: 'behavior', + ea: 'Cron', + ev: 5 + }); + }); + + it('if gemCost property is provided, use as value (overrides goldCost)', function() { + analyticsData.gemCost = 7; + analyticsData.goldCost = 5; + + initializedAnalytics.track(event_type, analyticsData); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith({ + ec: 'behavior', + ea: 'Cron', + ev: 7 + }); + }); + + it('if gaValue property is provided, use as value (overrides gemCost)', function() { + analyticsData.gemCost = 7; + analyticsData.gaValue = 5; + + initializedAnalytics.track(event_type, analyticsData); + + expect(googleEvent).to.be.calledOnce; + expect(googleEvent).to.be.calledWith({ + ec: 'behavior', + ea: 'Cron', + ev: 5 + }); + }); }); }); diff --git a/website/src/analytics.js b/website/src/analytics.js index 2e20da99da..ff2a350219 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -39,6 +39,9 @@ function _sendDataToGoogle(eventType, data) { var label = _generateLabelForGoogleAnalytics(data); if(label) { eventData.el = label; } + var value = _generateValueForGoogleAnalytics(data); + if(value) { eventData.ev = value; } + ga.event(eventData).send(); } @@ -56,6 +59,20 @@ function _generateLabelForGoogleAnalytics(data) { return label; } +function _generateValueForGoogleAnalytics(data) { + var value; + var POSSIBLE_VALUES = ['gaValue', 'gemCost', 'goldCost']; + + _(POSSIBLE_VALUES).each(function(key) { + if(data[key]) { + value = data[key]; + return false; // exit _.each early + } + }); + + return value; +} + function trackPurchase(data) { _sendPurchaseDataToAmplitude(data); _sendPurchaseDataToGoogle(data); @@ -70,7 +87,7 @@ function _sendPurchaseDataToAmplitude(data) { } function _formatDataForAmplitude(data) { - var PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaLabel']; + var PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaLabel', 'gaValue']; var event_properties = _.omit(data, PROPERTIES_TO_SCRUB); var ampData = { From a950339f01c6e2b60406d0bcee63edcc7e6097ee Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 19 Jul 2015 08:41:45 -0500 Subject: [PATCH 28/31] Clarify cron --- common/script/index.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/script/index.coffee b/common/script/index.coffee index cb85388429..e4605a1a07 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -1812,7 +1812,8 @@ api.wrap = (user, main=true) -> analyticsData = { category: 'behavior', - gaLabel: user.flags.cronCount, + gaLabel: 'Cron Count', + gaValue: user.flags.cronCount, uuid: user._id, user: user, resting: user.preferences.sleep, From 0ce118365231b0dc74718c4634830997a636d34c Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 19 Jul 2015 10:06:50 -0500 Subject: [PATCH 29/31] Automatically sends item name when item is purchased --- test/server_side/analytics.test.js | 120 +++++++++++++++++++++++++++++ website/src/analytics.js | 37 +++++++++ 2 files changed, 157 insertions(+) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index a67e73b2b7..bfcc2b6d8c 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -96,6 +96,126 @@ describe('analytics', function() { }); }); + it('sends english item name for gear if itemKey is provided', function() { + analyticsData.itemKey = 'headAccessory_special_foxEars' + + initializedAnalytics.track(event_type, analyticsData); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + platform: 'server', + event_properties: { + itemKey: 'headAccessory_special_foxEars', + itemName: 'Fox Ears', + category: 'behavior', + resting: true, + cronCount: 5 + } + }); + }); + + it('sends english item name for egg if itemKey is provided', function() { + analyticsData.itemKey = 'Wolf' + + initializedAnalytics.track(event_type, analyticsData); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + platform: 'server', + event_properties: { + itemKey: 'Wolf', + itemName: 'Wolf Egg', + category: 'behavior', + resting: true, + cronCount: 5 + } + }); + }); + + it('sends english item name for food if itemKey is provided', function() { + analyticsData.itemKey = 'Cake_Skeleton' + + initializedAnalytics.track(event_type, analyticsData); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + platform: 'server', + event_properties: { + itemKey: 'Cake_Skeleton', + itemName: 'Bare Bones Cake', + category: 'behavior', + resting: true, + cronCount: 5 + } + }); + }); + + it('sends english item name for hatching potion if itemKey is provided', function() { + analyticsData.itemKey = 'Golden' + + initializedAnalytics.track(event_type, analyticsData); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + platform: 'server', + event_properties: { + itemKey: 'Golden', + itemName: 'Golden Hatching Potion', + category: 'behavior', + resting: true, + cronCount: 5 + } + }); + }); + + it('sends english item name for quest if itemKey is provided', function() { + analyticsData.itemKey = 'atom1' + + initializedAnalytics.track(event_type, analyticsData); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + platform: 'server', + event_properties: { + itemKey: 'atom1', + itemName: 'Attack of the Mundane Questline, Pt. 1: Dish Disaster!', + category: 'behavior', + resting: true, + cronCount: 5 + } + }); + }); + + it('sends english item name for purchased spell if itemKey is provided', function() { + analyticsData.itemKey = 'seafoam' + + initializedAnalytics.track(event_type, analyticsData); + + expect(amplitudeTrack).to.be.calledOnce; + expect(amplitudeTrack).to.be.calledWith({ + event_type: 'Cron', + user_id: 'unique-user-id', + platform: 'server', + event_properties: { + itemKey: 'seafoam', + itemName: 'Seafoam', + category: 'behavior', + resting: true, + cronCount: 5 + } + }); + }); + it('sends user data if provided', function() { var stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 }; var user = { diff --git a/website/src/analytics.js b/website/src/analytics.js index ff2a350219..ecae3768d5 100644 --- a/website/src/analytics.js +++ b/website/src/analytics.js @@ -1,4 +1,7 @@ var _ = require('lodash'); +require('coffee-script'); // remove this once we've fully converted over +var i18n = require('./i18n'); +var Content = require('../../common/script/content'); var Amplitude = require('amplitude'); var googleAnalytics = require('universal-analytics'); @@ -100,9 +103,43 @@ function _formatDataForAmplitude(data) { ampData.user_properties = _formatUserData(data.user); } + var itemName = _lookUpItemName(data.itemKey); + if(itemName) { + event_properties.itemName = itemName; + } + return ampData; } +function _lookUpItemName(itemKey) { + if (!itemKey) return; + + var gear = Content.gear.flat[itemKey]; + var egg = Content.eggs[itemKey]; + var food = Content.food[itemKey]; + var hatchingPotion = Content.hatchingPotions[itemKey]; + var quest = Content.quests[itemKey]; + var spell = Content.special[itemKey]; + + var itemName; + + if (gear) { + itemName = gear.text(); + } else if (egg) { + itemName = egg.text() + ' Egg'; + } else if (food) { + itemName = food.text(); + } else if (hatchingPotion) { + itemName = hatchingPotion.text() + " Hatching Potion"; + } else if (quest) { + itemName = quest.text(); + } else if (spell) { + itemName = spell.text(); + } + + return itemName; +} + function _formatUserData(user) { var properties = {}; From 16c6647b3488f66fe515b305d2f520893bc1942e Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 19 Jul 2015 10:09:00 -0500 Subject: [PATCH 30/31] Bump version number to resolve error message --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29e69518ac..5dcaeb875a 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "./website/src/server.js", "dependencies": { "amazon-payments": "0.0.4", - "amplitude": "^1.0.5", + "amplitude": "^1.0.6", "async": "~0.9.0", "aws-sdk": "^2.0.25", "babel": "^5.5.4", From 1fb068d06d0fbd64d5ce1bfcf6c98395f519c760 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 19 Jul 2015 10:17:03 -0500 Subject: [PATCH 31/31] Update test for new quest name --- test/server_side/analytics.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server_side/analytics.test.js b/test/server_side/analytics.test.js index bfcc2b6d8c..2c421f9dab 100644 --- a/test/server_side/analytics.test.js +++ b/test/server_side/analytics.test.js @@ -188,7 +188,7 @@ describe('analytics', function() { platform: 'server', event_properties: { itemKey: 'atom1', - itemName: 'Attack of the Mundane Questline, Pt. 1: Dish Disaster!', + itemName: 'Attack of the Mundane, Part 1: Dish Disaster!', category: 'behavior', resting: true, cronCount: 5