add client and OS info to amplitude events

closes #7865
This commit is contained in:
Phillip Thelen 2016-08-02 16:45:06 +02:00 committed by Blade Barringer
parent e3c40aa142
commit 679378331d
37 changed files with 320 additions and 40 deletions

View file

@ -85,6 +85,7 @@ module.exports = function updateStats (user, stats, req = {}, analytics) {
itemKey: k,
acquireMethod: 'Level Drop',
category: 'behavior',
headers: req.headers,
});
}
user._tmp.drop = {

View file

@ -98,6 +98,7 @@ module.exports = function buyArmoire (user, req = {}, analytics) {
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -56,6 +56,7 @@ module.exports = function buyGear (user, req = {}, analytics) {
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -34,6 +34,7 @@ module.exports = function buyHealthPotion (user, req = {}, analytics) {
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -37,6 +37,7 @@ module.exports = function buyMysterySet (user, req = {}, analytics) {
itemType: 'Subscriber Gear',
acquireMethod: 'Hourglass',
category: 'behavior',
headers: req.headers,
});
}
});

View file

@ -34,6 +34,7 @@ module.exports = function buyQuest (user, req = {}, analytics) {
goldCost: item.goldValue,
acquireMethod: 'Gold',
category: 'behavior',
headers: req.headers,
});
}

View file

@ -49,6 +49,7 @@ module.exports = function changeClass (user, req = {}, analytics) {
acquireMethod: 'Gems',
gemCost: 3,
category: 'behavior',
headers: req.headers,
});
}
} else {

View file

@ -47,6 +47,7 @@ module.exports = function purchaseHourglass (user, req = {}, analytics) {
itemType: type,
acquireMethod: 'Hourglass',
category: 'behavior',
headers: req.headers,
});
}

View file

@ -24,6 +24,7 @@ module.exports = function openMysteryItem (user, req = {}, analytics) {
itemType: 'Subscriber Gear',
acquireMethod: 'Subscriber',
category: 'behavior',
headers: req.headers,
});
}

View file

@ -51,6 +51,7 @@ module.exports = function purchase (user, req = {}, analytics) {
acquireMethod: 'Gold',
goldCost: convRate,
category: 'behavior',
headers: req.headers,
});
}
@ -114,6 +115,7 @@ module.exports = function purchase (user, req = {}, analytics) {
acquireMethod: 'Gems',
gemCost: item.value,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -30,6 +30,7 @@ module.exports = function rebirth (user, tasks = [], req = {}, analytics) {
}
if (analytics) {
analyticsData.headers = req.headers;
analytics.track('Rebirth', analyticsData);
}

View file

@ -22,6 +22,7 @@ module.exports = function releaseBoth (user, req = {}, analytics) {
acquireMethod: 'Gems',
gemCost: 6,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -29,6 +29,7 @@ module.exports = function releaseMounts (user, req = {}, analytics) {
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -27,6 +27,7 @@ module.exports = function releasePets (user, req = {}, analytics) {
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -26,6 +26,7 @@ module.exports = function reroll (user, tasks = [], req = {}, analytics) {
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -93,6 +93,7 @@ module.exports = function revive (user, req = {}, analytics) {
lostItem,
gaLabel: lostItem,
category: 'behavior',
headers: req.headers,
});
}

View file

@ -100,6 +100,7 @@ module.exports = function unlock (user, req = {}, analytics) {
acquireMethod: 'Gems',
gemCost: cost / 0.25,
category: 'behavior',
headers: req.headers,
});
}
}

View file

@ -93,7 +93,8 @@
"validator": "^4.9.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"winston": "^2.1.0"
"winston": "^2.1.0",
"useragent": "2.1.9"
},
"private": true,
"engines": {

View file

@ -27,6 +27,9 @@ describe('analyticsService', () => {
uuid: 'unique-user-id',
resting: true,
cronCount: 5,
headers: {'x-client': 'habitica-web',
'user-agent': '',
},
};
});
@ -50,14 +53,91 @@ describe('analyticsService', () => {
});
});
it('sets platform as server', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*server.*/g, '');
context('platform', () => {
it('logs web platform', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*Web.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
data.headers = {'x-client': 'habitica-web'};
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('logs iOS platform', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*iOS.*/g, '');
data.headers = {'x-client': 'habitica-ios'};
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('logs Android platform', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*Android.*/g, '');
data.headers = {'x-client': 'habitica-android'};
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('logs 3rd Party platform', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*3rd\%20Party.*/g, '');
data.headers = {};
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
});
context('Operating System', () => {
it('sets default', () => {
amplitudeNock
.filteringPath(/httpapi.*os.*name.*Other.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sets iOS', () => {
amplitudeNock
.filteringPath(/httpapi.*os.*name.*iOS.*/g, '');
data.headers = {'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)'};
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sets Android', () => {
amplitudeNock
.filteringPath(/httpapi.*os.*name.*Android.*/g, '');
data.headers = {'x-client': 'habitica-android'};
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
});
it('sends details about event', () => {
@ -205,6 +285,9 @@ describe('analyticsService', () => {
purchaseType: 'checkout',
gift: false,
quantity: 1,
headers: {'x-client': 'habitica-web',
'user-agent': '',
},
};
});
@ -228,14 +311,92 @@ describe('analyticsService', () => {
});
});
it('sets platform as server', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*server.*/g, '');
context('sets platform as', () => {
it('Web', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*Web.*/g, '');
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
data.headers = {'x-client': 'habitica-web'};
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('iOS', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*iOS.*/g, '');
data.headers = {'x-client': 'habitica-ios'};
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('Android', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*Android.*/g, '');
data.headers = {'x-client': 'habitica-android'};
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('3rd Party', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*3rd\%20Party.*/g, '');
data.headers = {};
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
});
context('sets os for', () => {
it('Default', () => {
amplitudeNock
.filteringPath(/httpapi.*os.*name.*Other.*/g, '');
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('iOS', () => {
amplitudeNock
.filteringPath(/httpapi.*os.*name.*iOS.*/g, '');
data.headers = {'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)'};
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('Android', () => {
amplitudeNock
.filteringPath(/httpapi.*os.*name.*Android.*/g, '');
data.headers = {'x-client': 'habitica-android',
'user-agent': ''};
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
});
it('sends details about purchase', () => {

View file

@ -23,6 +23,10 @@ describe('payments/index', () => {
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
plan = {
@ -160,6 +164,10 @@ describe('payments/index', () => {
quantity: 1,
gift: true,
purchaseValue: 15,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
});
@ -227,6 +235,10 @@ describe('payments/index', () => {
quantity: 1,
gift: false,
purchaseValue: 15,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
});
@ -429,6 +441,10 @@ describe('payments/index', () => {
data = {
user,
paymentMethod: 'payment',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
});

View file

@ -19,7 +19,6 @@ angular.module('habitrpg')
if(!err) $scope.registrationInProgress = false;
Analytics.login();
Analytics.updateUser();
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'login'});
$window.location.href = ('/' + window.location.hash);
});
};

View file

@ -34,11 +34,6 @@ habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$r
Groups.Group.create(group)
.then(function (response) {
var createdGroup = response.data.data;
if (createdGroup.privacy == 'public') {
Analytics.track({'hitType':'event', 'eventCategory':'behavior', 'eventAction':'join group', 'owner':true, 'groupType':'guild', 'privacy': createdGroup.privacy, 'groupName':createdGroup.name})
} else {
Analytics.track({'hitType':'event', 'eventCategory':'behavior', 'eventAction':'join group', 'owner':true, 'groupType':'guild', 'privacy': createdGroup.privacy})
}
$rootScope.hardRedirect('/#/options/groups/guilds/' + createdGroup._id);
});
}
@ -59,12 +54,6 @@ habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$r
User.user.guilds.push(joinedGroup._id);
if (joinedGroup.privacy == 'public') {
Analytics.track({'hitType':'event', 'eventCategory':'behavior', 'eventAction':'join group', 'owner':false, 'groupType':'guild','privacy': joinedGroup.privacy, 'groupName': joinedGroup.name})
} else {
Analytics.track({'hitType':'event', 'eventCategory':'behavior', 'eventAction':'join group', 'owner':false, 'groupType':'guild','privacy': joinedGroup.privacy})
}
_.pull(User.user.invitations.guilds, group);
$location.path('/options/groups/guilds/' + joinedGroup._id);

View file

@ -53,7 +53,6 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
if (!group.name) group.name = env.t('possessiveParty', {name: User.user.profile.name});
Groups.Group.create(group)
.then(function(response) {
Analytics.track({'hitType':'event', 'eventCategory':'behavior', 'eventAction':'join group', 'owner':true, 'groupType':'party', 'privacy':'private'});
Analytics.updateUser({'party.id': $scope.group ._id, 'partySize': 1});
$rootScope.hardRedirect('/#/options/groups/party');
});
@ -64,7 +63,6 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
.then(function (response) {
$rootScope.party = $scope.group = response.data.data;
User.sync();
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'join group','owner':false,'groupType':'party','privacy':'private'});
Analytics.updateUser({'partyID': party.id});
$rootScope.hardRedirect('/#/options/groups/party');
});

View file

@ -28,7 +28,6 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
}
User.score({params:{task: task, direction:direction}});
Analytics.updateUser();
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'score task','taskType':task.type,'direction':direction});
};
function addTask(addTo, listDef, tasks) {

View file

@ -97,7 +97,6 @@ angular.module('habitrpg')
function initQuest(key) {
return $q(function(resolve, reject) {
Analytics.track({'hitType':'event', 'eventCategory':'behavior', 'eventAction':'quest', 'owner':true, 'response':'accept', 'questName': key});
Analytics.updateUser({'partyID': party._id, 'partySize': party.memberCount});
Groups.Group.inviteToQuest(party._id, key)
.then(function(response) {

View file

@ -154,6 +154,7 @@ api.registerLocal = {
type: 'local',
gaLabel: 'local',
uuid: savedUser._id,
headers: req.headers,
});
}
@ -213,6 +214,15 @@ api.loginLocal = {
let user = await User.findOne(login, {auth: 1, apiToken: 1}).exec();
let isValidPassword = user && user.auth.local.hashed_password === passwordUtils.encrypt(req.body.password, user.auth.local.salt);
if (!isValidPassword) throw new NotAuthorized(res.t('invalidLoginCredentialsLong'));
res.analytics.track('login', {
category: 'behaviour',
type: 'local',
gaLabel: 'local',
uuid: user._id,
headers: req.headers,
});
return _loginRes(user, ...arguments);
},
};
@ -277,6 +287,7 @@ api.loginSocial = {
type: network,
gaLabel: network,
uuid: savedUser._id,
headers: req.headers,
});
return null;

View file

@ -66,6 +66,23 @@ api.createGroup = {
_id: user._id,
profile: {name: user.profile.name},
};
let analyticsObject = {
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: true,
groupType: savedGroup.type,
privacy: savedGroup.privacy,
headers: req.headers,
};
if (savedGroup.privacy === 'public') {
analyticsObject.groupName = savedGroup.name;
}
res.analytics.track('join group', analyticsObject);
res.respond(201, response); // do not remove chat flags data as we've just created the group
},
};
@ -276,6 +293,23 @@ api.joinGroup = {
if (leader) {
response.leader = leader.toJSON({minimize: true});
}
let analyticsObject = {
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: false,
groupType: group.type,
privacy: group.privacy,
headers: req.headers,
};
if (group.privacy === 'public') {
analyticsObject.groupName = group.name;
}
res.analytics.track('join group', analyticsObject);
res.respond(200, response);
},
};

View file

@ -135,6 +135,7 @@ api.inviteToQuest = {
gaLabel: 'accept',
questName: questKey,
uuid: user._id,
headers: req.headers,
});
},
};
@ -191,6 +192,7 @@ api.acceptQuest = {
gaLabel: 'accept',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
},
};
@ -247,6 +249,7 @@ api.rejectQuest = {
gaLabel: 'reject',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
},
};
@ -300,6 +303,7 @@ api.forceStart = {
gaLabel: 'force-start',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
},
};

View file

@ -451,6 +451,17 @@ api.scoreTask = {
}
}
/*
* TODO: enable score task analytics if desired
res.analytics.track('score task', {
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
direction
});
*/
return null;
},
};

View file

@ -212,6 +212,7 @@ api.subscribe = {
customerId: billingAgreementId,
paymentMethod: 'Amazon Payments',
sub,
headers: req.headers,
});
res.respond(200);

View file

@ -59,6 +59,7 @@ api.iapAndroidVerify = {
user,
paymentMethod: 'IAP GooglePlay',
amount: 5.25,
headers: req.headers,
});
res.respond(200, googleRes);
@ -117,17 +118,17 @@ api.iapiOSVerify = {
switch (purchaseData.productId) {
case 'com.habitrpg.ios.Habitica.4gems':
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1}); // eslint-disable-line babel/no-await-in-loop
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1, headers: req.headers}); // eslint-disable-line babel/no-await-in-loop
break;
case 'com.habitrpg.ios.Habitica.8gems':
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2}); // eslint-disable-line babel/no-await-in-loop
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2, headers: req.headers}); // eslint-disable-line babel/no-await-in-loop
break;
case 'com.habitrpg.ios.Habitica.20gems':
case 'com.habitrpg.ios.Habitica.21gems':
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25}); // eslint-disable-line babel/no-await-in-loop
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25, headers: req.headers}); // eslint-disable-line babel/no-await-in-loop
break;
case 'com.habitrpg.ios.Habitica.42gems':
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5}); // eslint-disable-line babel/no-await-in-loop
await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5, headers: req.headers}); // eslint-disable-line babel/no-await-in-loop
break;
default:
correctReceipt = false;

View file

@ -208,6 +208,7 @@ api.subscribeSuccess = {
customerId: result.id,
paymentMethod: 'Paypal',
sub: block,
headers: req.headers,
});
res.redirect('/');

View file

@ -84,6 +84,7 @@ api.checkout = {
customerId: response.id,
paymentMethod: 'Stripe',
sub,
headers: req.headers,
});
} else {
let method = 'buyGems';

View file

@ -3,6 +3,7 @@ import nconf from 'nconf';
import Amplitude from 'amplitude';
import Bluebird from 'bluebird';
import googleAnalytics from 'universal-analytics';
import useragent from 'useragent';
import {
each,
omit,
@ -13,7 +14,13 @@ const AMPLIUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
const GA_TOKEN = nconf.get('GA_ID');
const GA_POSSIBLE_LABELS = ['gaLabel', 'itemKey'];
const GA_POSSIBLE_VALUES = ['gaValue', 'gemCost', 'goldCost'];
const AMPLITUDE_PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaLabel', 'gaValue'];
const AMPLITUDE_PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaLabel', 'gaValue', 'headers'];
const PLATFORM_MAP = Object.freeze({
'habitica-web': 'Web',
'habitica-ios': 'iOS',
'habitica-android': 'Android',
});
let amplitude = new Amplitude(AMPLIUDE_TOKEN);
let ga = googleAnalytics(GA_TOKEN);
@ -81,13 +88,40 @@ let _formatUserData = (user) => {
return properties;
};
let _formatPlatformForAmplitude = (platform) => {
if (platform in PLATFORM_MAP) {
return PLATFORM_MAP[platform];
}
return '3rd Party';
};
let _formatUserAgentForAmplitude = (platform, agentString) => {
let agent = useragent.lookup(agentString).toJSON();
let formattedAgent = {};
if (platform === 'iOS' || platform === 'Android') {
formattedAgent.name = agent.os.family;
formattedAgent.version = `${agent.os.major}.${agent.os.minor}.${agent.os.patch}`;
if (platform === 'Android' && formattedAgent.name === 'Other') {
formattedAgent.name = 'Android';
}
} else {
formattedAgent.name = agent.family;
formattedAgent.version = agent.major;
}
return formattedAgent;
};
let _formatDataForAmplitude = (data) => {
let event_properties = omit(data, AMPLITUDE_PROPERTIES_TO_SCRUB);
let platform = _formatPlatformForAmplitude(data.headers['x-client']);
let agent = _formatUserAgentForAmplitude(platform, data.headers['user-agent']);
let ampData = {
user_id: data.uuid || 'no-user-id-was-provided',
platform: 'server',
platform,
os_name: agent.name,
os_version: agent.version,
event_properties,
};
@ -100,7 +134,6 @@ let _formatDataForAmplitude = (data) => {
if (itemName) {
event_properties.itemName = itemName;
}
return ampData;
};

View file

@ -326,6 +326,7 @@ export function cron (options = {}) {
cronCount: user.flags.cronCount,
progressUp: _.min([_progress.up, 900]),
progressDown: _progress.down,
headers: options.headers,
});
return _progress;

View file

@ -82,6 +82,7 @@ api.createSubscription = async function createSubscription (data) {
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: block.price,
headers: data.headers,
});
data.user.purchased.txnCount++;
@ -166,6 +167,7 @@ api.buyGems = async function buyGems (data) {
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: amt,
headers: data.headers,
});
if (data.gift) {

View file

@ -133,7 +133,7 @@ async function cronAsync (req, res) {
tasks.forEach(task => tasksByType[`${task.type}s`].push(task));
// Run cron
let progress = cron({user, tasksByType, now, daysMissed, analytics, timezoneOffsetFromUserPrefs});
let progress = cron({user, tasksByType, now, daysMissed, analytics, timezoneOffsetFromUserPrefs, headers: req.headers});
// Clear old completed todos - 30 days for free users, 90 for subscribers
// Do not delete challenges completed todos TODO unless the task is broken?