/* eslint-disable camelcase */ import nconf from 'nconf'; import Amplitude from 'amplitude'; import { Visitor } from 'universal-analytics'; import * as analyticsService from '../../../../website/server/libs/analyticsService'; describe('analyticsService', () => { beforeEach(() => { sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve()); sandbox.stub(Visitor.prototype, 'event'); sandbox.stub(Visitor.prototype, 'transaction'); }); afterEach(() => { sandbox.restore(); }); describe('#getServiceByEnvironment', () => { it('returns mock methods when not in production', () => { sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false); expect(analyticsService.getAnalyticsServiceByEnvironment()) .to.equal(analyticsService.mockAnalyticsService); }); it('returns real methods when in production', () => { sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); expect(analyticsService.getAnalyticsServiceByEnvironment().track) .to.equal(analyticsService.track); expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase) .to.equal(analyticsService.trackPurchase); }); }); describe('#track', () => { let eventType; let data; beforeEach(() => { Visitor.prototype.event.yields(); eventType = 'Cron'; data = { category: 'behavior', uuid: 'unique-user-id', resting: true, cronCount: 5, headers: { 'x-client': 'habitica-web', 'user-agent': '', }, }; }); context('Amplitude', () => { it('calls out to amplitude', () => analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledOnce; })); it('uses a dummy user id if none is provided', () => { delete data.uuid; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ user_id: 'no-user-id-was-provided', }); }); }); context('platform', () => { it('logs web platform', () => { data.headers = { 'x-client': 'habitica-web' }; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'Web', }); }); }); it('logs iOS platform', () => { data.headers = { 'x-client': 'habitica-ios' }; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'iOS', }); }); }); it('logs Android platform', () => { data.headers = { 'x-client': 'habitica-android' }; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'Android', }); }); }); it('logs 3rd Party platform', () => { data.headers = { 'x-client': 'some-third-party' }; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: '3rd Party', }); }); }); it('logs unknown if headers are not passed in', () => { delete data.headers; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'Unknown', }); }); }); }); context('Operating System', () => { it('sets default', () => { data.headers = { 'x-client': 'third-party', 'user-agent': 'foo', }; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: 'Other', os_version: '0', }); }); }); it('sets iOS', () => { data.headers = { 'x-client': 'habitica-ios', 'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)', }; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: 'iOS', os_version: '9.3.0', }); }); }); it('sets Android', () => { data.headers = { 'x-client': 'habitica-android', 'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19', }; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: 'Android', os_version: '4.0.4', }); }); }); it('sets Unknown if headers are not passed in', () => { delete data.headers; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: undefined, os_version: undefined, }); }); }); }); it('sends details about event', () => analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { category: 'behavior', resting: true, cronCount: 5, }, }); })); it('sends english item name for gear if itemKey is provided', () => { data.itemKey = 'headAccessory_special_foxEars'; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { itemKey: data.itemKey, itemName: 'Fox Ears', }, }); }); }); it('sends english item name for egg if itemKey is provided', () => { data.itemKey = 'Wolf'; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { itemKey: data.itemKey, itemName: 'Wolf Egg', }, }); }); }); it('sends english item name for food if itemKey is provided', () => { data.itemKey = 'Cake_Skeleton'; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { itemKey: data.itemKey, itemName: 'Bare Bones Cake', }, }); }); }); it('sends english item name for hatching potion if itemKey is provided', () => { data.itemKey = 'Golden'; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { itemKey: data.itemKey, itemName: 'Golden Hatching Potion', }, }); }); }); it('sends english item name for quest if itemKey is provided', () => { data.itemKey = 'atom1'; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { itemKey: data.itemKey, itemName: 'Attack of the Mundane, Part 1: Dish Disaster!', }, }); }); }); it('sends english item name for purchased spell if itemKey is provided', () => { data.itemKey = 'seafoam'; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { itemKey: data.itemKey, itemName: 'Seafoam', }, }); }); }); it('sends user data if provided', () => { const stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30, }; const user = { stats, contributor: { level: 1 }, purchased: { plan: { planId: 'foo-plan' } }, flags: { tour: { intro: -2 } }, habits: [{ _id: 'habit' }], dailys: [{ _id: 'daily' }], todos: [{ _id: 'todo' }], rewards: [{ _id: 'reward' }], balance: 12, loginIncentives: 1, }; data.user = user; return analyticsService.track(eventType, data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ user_properties: { Class: 'wizard', Experience: 5, Gold: 23, Health: 10, Level: 4, Mana: 30, tutorialComplete: true, 'Number Of Tasks': { habits: 1, dailys: 1, todos: 1, rewards: 1, }, contributorLevel: 1, subscription: 'foo-plan', balance: 12, balanceGemAmount: 48, loginIncentives: 1, }, }); }); }); }); context('GA', () => { it('calls out to GA', () => analyticsService.track(eventType, data) .then(() => { expect(Visitor.prototype.event).to.be.calledOnce; })); it('sends details about event', () => analyticsService.track(eventType, data) .then(() => { expect(Visitor.prototype.event).to.be.calledWith({ ea: 'Cron', ec: 'behavior', }); })); }); }); describe('#trackPurchase', () => { let data; let itemSpy; beforeEach(() => { Visitor.prototype.event.yields(); itemSpy = sandbox.stub().returnsThis(); Visitor.prototype.transaction.returns({ item: itemSpy, send: sandbox.stub().yields(), }); data = { uuid: 'user-id', sku: 'paypal-checkout', paymentMethod: 'PayPal', itemPurchased: 'Gems', purchaseValue: 8, purchaseType: 'checkout', gift: false, quantity: 1, headers: { 'x-client': 'habitica-web', 'user-agent': '', }, }; }); context('Amplitude', () => { it('calls out to amplitude', () => analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledOnce; })); it('uses a dummy user id if none is provided', () => { delete data.uuid; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ user_id: 'no-user-id-was-provided', }); }); }); context('platform', () => { it('logs web platform', () => { data.headers = { 'x-client': 'habitica-web' }; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'Web', }); }); }); it('logs iOS platform', () => { data.headers = { 'x-client': 'habitica-ios' }; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'iOS', }); }); }); it('logs Android platform', () => { data.headers = { 'x-client': 'habitica-android' }; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'Android', }); }); }); it('logs 3rd Party platform', () => { data.headers = { 'x-client': 'some-third-party' }; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: '3rd Party', }); }); }); it('logs unknown if headers are not passed in', () => { delete data.headers; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ platform: 'Unknown', }); }); }); }); context('Operating System', () => { it('sets default', () => { data.headers = { 'x-client': 'third-party', 'user-agent': 'foo', }; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: 'Other', os_version: '0', }); }); }); it('sets iOS', () => { data.headers = { 'x-client': 'habitica-ios', 'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)', }; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: 'iOS', os_version: '9.3.0', }); }); }); it('sets Android', () => { data.headers = { 'x-client': 'habitica-android', 'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19', }; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: 'Android', os_version: '4.0.4', }); }); }); it('sets Unknown if headers are not passed in', () => { delete data.headers; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ os_name: undefined, os_version: undefined, }); }); }); }); it('sends details about purchase', () => analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ event_properties: { gift: false, itemPurchased: 'Gems', paymentMethod: 'PayPal', purchaseType: 'checkout', quantity: 1, sku: 'paypal-checkout', }, }); })); it('sends user data if provided', () => { const stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30, }; const user = { stats, contributor: { level: 1 }, purchased: { plan: { planId: 'foo-plan' } }, flags: { tour: { intro: -2 } }, habits: [{ _id: 'habit' }], dailys: [{ _id: 'daily' }], todos: [{ _id: 'todo' }], rewards: [{ _id: 'reward' }], }; data.user = user; return analyticsService.trackPurchase(data) .then(() => { expect(Amplitude.prototype.track).to.be.calledWithMatch({ user_properties: { Class: 'wizard', Experience: 5, Gold: 23, Health: 10, Level: 4, Mana: 30, tutorialComplete: true, 'Number Of Tasks': { habits: 1, dailys: 1, todos: 1, rewards: 1, }, contributorLevel: 1, subscription: 'foo-plan', }, }); }); }); }); context('GA', () => { it('calls out to GA', () => analyticsService.trackPurchase(data) .then(() => { expect(Visitor.prototype.event).to.be.calledOnce; expect(Visitor.prototype.transaction).to.be.calledOnce; })); it('sends details about purchase', () => analyticsService.trackPurchase(data) .then(() => { expect(Visitor.prototype.event).to.be.calledWith({ ea: 'checkout', ec: 'commerce', el: 'PayPal', ev: 8, }); expect(Visitor.prototype.transaction).to.be.calledWith('user-id', 8); expect(itemSpy).to.be.calledWith(8, 1, 'paypal-checkout', 'Gems', 'checkout'); })); }); }); describe('mockAnalyticsService', () => { it('has stubbed track method', () => { expect(analyticsService.mockAnalyticsService).to.respondTo('track'); }); it('has stubbed trackPurchase method', () => { expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase'); }); }); });