diff --git a/common/script/fns/updateStats.js b/common/script/fns/updateStats.js index 8b9920f791..258f46f32f 100644 --- a/common/script/fns/updateStats.js +++ b/common/script/fns/updateStats.js @@ -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 = { diff --git a/common/script/ops/buyArmoire.js b/common/script/ops/buyArmoire.js index e183c06984..5dd76b4974 100644 --- a/common/script/ops/buyArmoire.js +++ b/common/script/ops/buyArmoire.js @@ -98,6 +98,7 @@ module.exports = function buyArmoire (user, req = {}, analytics) { acquireMethod: 'Gold', goldCost: item.value, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/buyGear.js b/common/script/ops/buyGear.js index e4f4eb3b68..197c9932f5 100644 --- a/common/script/ops/buyGear.js +++ b/common/script/ops/buyGear.js @@ -56,6 +56,7 @@ module.exports = function buyGear (user, req = {}, analytics) { acquireMethod: 'Gold', goldCost: item.value, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/buyHealthPotion.js b/common/script/ops/buyHealthPotion.js index 1a6c8b0e18..ab2d0101e4 100644 --- a/common/script/ops/buyHealthPotion.js +++ b/common/script/ops/buyHealthPotion.js @@ -34,6 +34,7 @@ module.exports = function buyHealthPotion (user, req = {}, analytics) { acquireMethod: 'Gold', goldCost: item.value, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/buyMysterySet.js b/common/script/ops/buyMysterySet.js index acf0014279..b0ade9926a 100644 --- a/common/script/ops/buyMysterySet.js +++ b/common/script/ops/buyMysterySet.js @@ -37,6 +37,7 @@ module.exports = function buyMysterySet (user, req = {}, analytics) { itemType: 'Subscriber Gear', acquireMethod: 'Hourglass', category: 'behavior', + headers: req.headers, }); } }); diff --git a/common/script/ops/buyQuest.js b/common/script/ops/buyQuest.js index af7c384419..bd03abfe0f 100644 --- a/common/script/ops/buyQuest.js +++ b/common/script/ops/buyQuest.js @@ -34,6 +34,7 @@ module.exports = function buyQuest (user, req = {}, analytics) { goldCost: item.goldValue, acquireMethod: 'Gold', category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/changeClass.js b/common/script/ops/changeClass.js index 4b3eb6b289..67615958ba 100644 --- a/common/script/ops/changeClass.js +++ b/common/script/ops/changeClass.js @@ -49,6 +49,7 @@ module.exports = function changeClass (user, req = {}, analytics) { acquireMethod: 'Gems', gemCost: 3, category: 'behavior', + headers: req.headers, }); } } else { diff --git a/common/script/ops/hourglassPurchase.js b/common/script/ops/hourglassPurchase.js index e1d07bb482..8367df776a 100644 --- a/common/script/ops/hourglassPurchase.js +++ b/common/script/ops/hourglassPurchase.js @@ -47,6 +47,7 @@ module.exports = function purchaseHourglass (user, req = {}, analytics) { itemType: type, acquireMethod: 'Hourglass', category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/openMysteryItem.js b/common/script/ops/openMysteryItem.js index 9728bc9eb9..9f6765ecfe 100644 --- a/common/script/ops/openMysteryItem.js +++ b/common/script/ops/openMysteryItem.js @@ -24,6 +24,7 @@ module.exports = function openMysteryItem (user, req = {}, analytics) { itemType: 'Subscriber Gear', acquireMethod: 'Subscriber', category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/purchase.js b/common/script/ops/purchase.js index 79eb0475d4..fac9ee5e0e 100644 --- a/common/script/ops/purchase.js +++ b/common/script/ops/purchase.js @@ -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, }); } diff --git a/common/script/ops/rebirth.js b/common/script/ops/rebirth.js index e1fab0b68b..7937f295ad 100644 --- a/common/script/ops/rebirth.js +++ b/common/script/ops/rebirth.js @@ -30,6 +30,7 @@ module.exports = function rebirth (user, tasks = [], req = {}, analytics) { } if (analytics) { + analyticsData.headers = req.headers; analytics.track('Rebirth', analyticsData); } diff --git a/common/script/ops/releaseBoth.js b/common/script/ops/releaseBoth.js index cf7d2267ca..7acec6b579 100644 --- a/common/script/ops/releaseBoth.js +++ b/common/script/ops/releaseBoth.js @@ -22,6 +22,7 @@ module.exports = function releaseBoth (user, req = {}, analytics) { acquireMethod: 'Gems', gemCost: 6, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/releaseMounts.js b/common/script/ops/releaseMounts.js index d8b8dde659..bc2406f422 100644 --- a/common/script/ops/releaseMounts.js +++ b/common/script/ops/releaseMounts.js @@ -29,6 +29,7 @@ module.exports = function releaseMounts (user, req = {}, analytics) { acquireMethod: 'Gems', gemCost: 4, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/releasePets.js b/common/script/ops/releasePets.js index 9466e1ccda..499876925d 100644 --- a/common/script/ops/releasePets.js +++ b/common/script/ops/releasePets.js @@ -27,6 +27,7 @@ module.exports = function releasePets (user, req = {}, analytics) { acquireMethod: 'Gems', gemCost: 4, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/reroll.js b/common/script/ops/reroll.js index 3087845509..aabcce7e55 100644 --- a/common/script/ops/reroll.js +++ b/common/script/ops/reroll.js @@ -26,6 +26,7 @@ module.exports = function reroll (user, tasks = [], req = {}, analytics) { acquireMethod: 'Gems', gemCost: 4, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/revive.js b/common/script/ops/revive.js index 30b29a78fc..3112cee510 100644 --- a/common/script/ops/revive.js +++ b/common/script/ops/revive.js @@ -93,6 +93,7 @@ module.exports = function revive (user, req = {}, analytics) { lostItem, gaLabel: lostItem, category: 'behavior', + headers: req.headers, }); } diff --git a/common/script/ops/unlock.js b/common/script/ops/unlock.js index 62f1420e2a..d9f4ec812c 100644 --- a/common/script/ops/unlock.js +++ b/common/script/ops/unlock.js @@ -100,6 +100,7 @@ module.exports = function unlock (user, req = {}, analytics) { acquireMethod: 'Gems', gemCost: cost / 0.25, category: 'behavior', + headers: req.headers, }); } } diff --git a/package.json b/package.json index 215799d273..c68d97d59a 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/test/api/v3/unit/libs/analyticsService.test.js b/test/api/v3/unit/libs/analyticsService.test.js index 4e9e491fca..8eb7f9c05e 100644 --- a/test/api/v3/unit/libs/analyticsService.test.js +++ b/test/api/v3/unit/libs/analyticsService.test.js @@ -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', () => { diff --git a/test/api/v3/unit/libs/payments.test.js b/test/api/v3/unit/libs/payments.test.js index 33ba5d8f65..8d3df651a5 100644 --- a/test/api/v3/unit/libs/payments.test.js +++ b/test/api/v3/unit/libs/payments.test.js @@ -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': '', + }, }; }); diff --git a/website/client/js/controllers/authCtrl.js b/website/client/js/controllers/authCtrl.js index 6a380fe9c7..4bd7528548 100644 --- a/website/client/js/controllers/authCtrl.js +++ b/website/client/js/controllers/authCtrl.js @@ -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); }); }; diff --git a/website/client/js/controllers/guildsCtrl.js b/website/client/js/controllers/guildsCtrl.js index ec548cb35f..f2cb14520a 100644 --- a/website/client/js/controllers/guildsCtrl.js +++ b/website/client/js/controllers/guildsCtrl.js @@ -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); diff --git a/website/client/js/controllers/partyCtrl.js b/website/client/js/controllers/partyCtrl.js index 6f2974703a..32d0d1226a 100644 --- a/website/client/js/controllers/partyCtrl.js +++ b/website/client/js/controllers/partyCtrl.js @@ -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'); }); diff --git a/website/client/js/controllers/tasksCtrl.js b/website/client/js/controllers/tasksCtrl.js index b0d680fb9d..6606b6f91d 100644 --- a/website/client/js/controllers/tasksCtrl.js +++ b/website/client/js/controllers/tasksCtrl.js @@ -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) { diff --git a/website/client/js/services/questServices.js b/website/client/js/services/questServices.js index 5033bad33d..4c07ae9a40 100644 --- a/website/client/js/services/questServices.js +++ b/website/client/js/services/questServices.js @@ -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) { diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js index eae17136ed..7863a9f384 100644 --- a/website/server/controllers/api-v3/auth.js +++ b/website/server/controllers/api-v3/auth.js @@ -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; diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 80e803a116..15cf34cba8 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -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); }, }; diff --git a/website/server/controllers/api-v3/quests.js b/website/server/controllers/api-v3/quests.js index d06638c9ce..180646e619 100644 --- a/website/server/controllers/api-v3/quests.js +++ b/website/server/controllers/api-v3/quests.js @@ -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, }); }, }; diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 280e13f001..3191662623 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -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; }, }; diff --git a/website/server/controllers/top-level/payments/amazon.js b/website/server/controllers/top-level/payments/amazon.js index 413fdc8e5c..a0522b2b32 100644 --- a/website/server/controllers/top-level/payments/amazon.js +++ b/website/server/controllers/top-level/payments/amazon.js @@ -212,6 +212,7 @@ api.subscribe = { customerId: billingAgreementId, paymentMethod: 'Amazon Payments', sub, + headers: req.headers, }); res.respond(200); diff --git a/website/server/controllers/top-level/payments/iap.js b/website/server/controllers/top-level/payments/iap.js index ee013e9e2e..f26314945e 100644 --- a/website/server/controllers/top-level/payments/iap.js +++ b/website/server/controllers/top-level/payments/iap.js @@ -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; diff --git a/website/server/controllers/top-level/payments/paypal.js b/website/server/controllers/top-level/payments/paypal.js index c20260b1ea..feb565f280 100644 --- a/website/server/controllers/top-level/payments/paypal.js +++ b/website/server/controllers/top-level/payments/paypal.js @@ -208,6 +208,7 @@ api.subscribeSuccess = { customerId: result.id, paymentMethod: 'Paypal', sub: block, + headers: req.headers, }); res.redirect('/'); diff --git a/website/server/controllers/top-level/payments/stripe.js b/website/server/controllers/top-level/payments/stripe.js index eb2c27f1e0..e76c6667ab 100644 --- a/website/server/controllers/top-level/payments/stripe.js +++ b/website/server/controllers/top-level/payments/stripe.js @@ -84,6 +84,7 @@ api.checkout = { customerId: response.id, paymentMethod: 'Stripe', sub, + headers: req.headers, }); } else { let method = 'buyGems'; diff --git a/website/server/libs/analyticsService.js b/website/server/libs/analyticsService.js index 664194c916..6996d565c0 100644 --- a/website/server/libs/analyticsService.js +++ b/website/server/libs/analyticsService.js @@ -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; }; diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index 814ffe9a07..704adcba02 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -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; diff --git a/website/server/libs/payments.js b/website/server/libs/payments.js index 4ae9fcb72d..3f5b1c5e3b 100644 --- a/website/server/libs/payments.js +++ b/website/server/libs/payments.js @@ -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) { diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js index 9ab349a4b5..59fccd0c34 100644 --- a/website/server/middlewares/cron.js +++ b/website/server/middlewares/cron.js @@ -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?