From e6ee4d303cb2cecaf02597234a78191e69366a45 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Mon, 6 Jul 2015 08:11:31 -0500 Subject: [PATCH] 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;