mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-28 11:42:29 +01:00
579 lines
17 KiB
JavaScript
579 lines
17 KiB
JavaScript
/* eslint-disable camelcase */
|
|
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('#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 Unkown 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 Unkown 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');
|
|
});
|
|
});
|
|
});
|