From 92f283f6a212d692cdc4c059eb70bbe157bc9643 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 22 Oct 2020 13:42:55 -0500 Subject: [PATCH 1/6] 4.166.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7cec6fdcd5..392394d4b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.166.0", + "version": "4.166.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 90ced55a7b..281be579b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.166.0", + "version": "4.166.1", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.12.3", From b15462596beca597d4a65475aa870d29f0e789ce Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 28 Oct 2020 22:38:59 +0100 Subject: [PATCH 2/6] 4.166.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 392394d4b9..8724ffa99b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.166.1", + "version": "4.166.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 281be579b1..4381e27b0f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.166.1", + "version": "4.166.2", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.12.3", From 48dbe547c0640a39fd350239b4a4691267cfbade Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 28 Oct 2020 22:39:19 +0100 Subject: [PATCH 3/6] Analytics: track generic events through the server (#12735) * Loggly-only user support events (#12676) * feat(analytics): Loggly-only user support events * fix(analytics): clean up more Unknowns * wip: allow tracking events from the server * analytics: allow tracking generic events on the server * remove console.logs * remove console.log (client) * add integration test Co-authored-by: Sabe Jones --- .../POST-analytics_track_eventName.test.js | 28 +++++++++++ test/helpers/api-integration/requester.js | 1 + .../achievements/dropCapReached.vue | 4 +- .../header/notifications/dropCapReached.vue | 2 +- .../src/components/header/userDropdown.vue | 2 +- website/client/src/libs/analytics.js | 13 +++-- website/client/src/store/actions/analytics.js | 7 +++ website/client/src/store/actions/index.js | 2 + website/client/vue.config.js | 4 ++ .../server/controllers/api-v3/challenges.js | 5 ++ website/server/controllers/api-v3/quests.js | 8 ++-- .../server/controllers/top-level/analytics.js | 47 +++++++++++++++++++ .../controllers/top-level/payments/paypal.js | 2 +- .../controllers/top-level/payments/stripe.js | 2 +- website/server/libs/analyticsService.js | 8 ++-- website/server/libs/cron.js | 2 +- 16 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 test/api/v3/integration/analytics/POST-analytics_track_eventName.test.js create mode 100644 website/client/src/store/actions/analytics.js create mode 100644 website/server/controllers/top-level/analytics.js diff --git a/test/api/v3/integration/analytics/POST-analytics_track_eventName.test.js b/test/api/v3/integration/analytics/POST-analytics_track_eventName.test.js new file mode 100644 index 0000000000..eb00ee8927 --- /dev/null +++ b/test/api/v3/integration/analytics/POST-analytics_track_eventName.test.js @@ -0,0 +1,28 @@ +import { + generateUser, + requester, + translate as t, +} from '../../../../helpers/api-integration/v3'; +import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService'; + +describe('POST /analytics/track/:eventName', () => { + it('requires authentication', async () => { + await expect(requester().post('/analytics/track/event')).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('missingAuthHeaders'), + }); + }); + + it('calls res.analytics', async () => { + const user = await generateUser(); + sandbox.spy(analytics, 'track'); + + const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' }); + await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' }); + expect(analytics.track).to.be.calledOnce; + expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' })); + + sandbox.restore(); + }); +}); diff --git a/test/helpers/api-integration/requester.js b/test/helpers/api-integration/requester.js index a01aa1e227..f5c63b9d14 100644 --- a/test/helpers/api-integration/requester.js +++ b/test/helpers/api-integration/requester.js @@ -41,6 +41,7 @@ function _requestMaker (user, method, additionalSets = {}) { || route.indexOf('/amazon') === 0 || route.indexOf('/stripe') === 0 || route.indexOf('/qr-code') === 0 + || route.indexOf('/analytics') === 0 ) { url += `${route}`; } else { diff --git a/website/client/src/components/achievements/dropCapReached.vue b/website/client/src/components/achievements/dropCapReached.vue index a804fd78fa..0c01057752 100644 --- a/website/client/src/components/achievements/dropCapReached.vue +++ b/website/client/src/components/achievements/dropCapReached.vue @@ -232,7 +232,7 @@ export default { eventCategory: 'drop-cap-reached', eventAction: 'click', eventLabel: 'Drop Cap Reached > Modal > Wiki', - }); + }, { trackOnServer: true }); }, toLearnMore () { Analytics.track({ @@ -240,7 +240,7 @@ export default { eventCategory: 'drop-cap-reached', eventAction: 'click', eventLabel: 'Drop Cap Reached > Modal > Subscriptions', - }); + }, { trackOnServer: true }); this.close(); this.$router.push('/user/settings/subscription'); diff --git a/website/client/src/components/header/notifications/dropCapReached.vue b/website/client/src/components/header/notifications/dropCapReached.vue index 34a133b64e..6fcab511d2 100644 --- a/website/client/src/components/header/notifications/dropCapReached.vue +++ b/website/client/src/components/header/notifications/dropCapReached.vue @@ -36,7 +36,7 @@ export default { eventCategory: 'drop-cap-reached', eventAction: 'click', eventLabel: 'Drop Cap Reached > Notification Click', - }); + }, { trackOnServer: true }); }, }, }; diff --git a/website/client/src/components/header/userDropdown.vue b/website/client/src/components/header/userDropdown.vue index 8434defbef..cb058247b0 100644 --- a/website/client/src/components/header/userDropdown.vue +++ b/website/client/src/components/header/userDropdown.vue @@ -183,7 +183,7 @@ export default { eventCategory: 'button', eventAction: 'click', eventLabel: 'User Dropdown > Subscriptions', - }); + }, { trackOnServer: true }); this.$router.push({ name: 'subscription' }); }, diff --git a/website/client/src/libs/analytics.js b/website/client/src/libs/analytics.js index d7af60b8a1..3b2bed28ec 100644 --- a/website/client/src/libs/analytics.js +++ b/website/client/src/libs/analytics.js @@ -82,14 +82,21 @@ export function setUser () { window.ga('set', { userId: user._id }); } -export function track (properties) { +export function track (properties, options = {}) { // Use nextTick to avoid blocking the UI Vue.nextTick(() => { if (_doesNotHaveRequiredFields(properties)) return; if (_doesNotHaveAllowedHitType(properties)) return; - amplitude.getInstance().logEvent(properties.eventAction, properties); - window.ga('send', properties); + const trackOnServer = options && options.trackOnServer === true; + if (trackOnServer === true) { + // Track an event on the server + const store = getStore(); + store.dispatch('analytics:trackEvent', properties); + } else { + amplitude.getInstance().logEvent(properties.eventAction, properties); + window.ga('send', properties); + } }); } diff --git a/website/client/src/store/actions/analytics.js b/website/client/src/store/actions/analytics.js new file mode 100644 index 0000000000..1d104d749c --- /dev/null +++ b/website/client/src/store/actions/analytics.js @@ -0,0 +1,7 @@ +import axios from 'axios'; + +export async function trackEvent (store, params) { + const url = `/analytics/track/${params.eventAction}`; + + await axios.post(url, params); +} diff --git a/website/client/src/store/actions/index.js b/website/client/src/store/actions/index.js index 947be42878..adc7a38fb2 100644 --- a/website/client/src/store/actions/index.js +++ b/website/client/src/store/actions/index.js @@ -17,6 +17,7 @@ import * as shops from './shops'; import * as snackbars from './snackbars'; import * as worldState from './worldState'; import * as news from './news'; +import * as analytics from './analytics'; // Actions should be named as 'actionName' and can be accessed as 'namespace:actionName' // Example: fetch in user.js -> 'user:fetch' @@ -39,6 +40,7 @@ const actions = flattenAndNamespace({ snackbars, worldState, news, + analytics, }); export default actions; diff --git a/website/client/vue.config.js b/website/client/vue.config.js index aae8725c33..6d446cb1f6 100644 --- a/website/client/vue.config.js +++ b/website/client/vue.config.js @@ -148,6 +148,10 @@ module.exports = { target: DEV_BASE_URL, changeOrigin: true, }, + '^/analytics': { + target: DEV_BASE_URL, + changeOrigin: true, + }, }, }, }; diff --git a/website/server/controllers/api-v3/challenges.js b/website/server/controllers/api-v3/challenges.js index 3e8bea0a6c..84801e28a5 100644 --- a/website/server/controllers/api-v3/challenges.js +++ b/website/server/controllers/api-v3/challenges.js @@ -221,6 +221,7 @@ api.createChallenge = { groupID: group._id, groupName: group.privacy === 'private' ? null : group.name, groupType: group._id === TAVERN_ID ? 'tavern' : group.type, + headers: req.headers, }); res.respond(201, response); @@ -286,6 +287,7 @@ api.joinChallenge = { groupID: group._id, groupName: group.privacy === 'private' ? null : group.name, groupType: group._id === TAVERN_ID ? 'tavern' : group.type, + headers: req.headers, }); res.respond(200, response); @@ -335,6 +337,7 @@ api.leaveChallenge = { groupID: challenge.group._id, groupName: challenge.group.privacy === 'private' ? null : challenge.group.name, groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type, + headers: req.headers, }); res.respond(200, {}); @@ -748,6 +751,7 @@ api.deleteChallenge = { groupID: challenge.group._id, groupName: challenge.group.privacy === 'private' ? null : challenge.group.name, groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type, + headers: req.headers, }); res.respond(200, {}); @@ -798,6 +802,7 @@ api.selectChallengeWinner = { groupID: challenge.group._id, groupName: challenge.group.privacy === 'private' ? null : challenge.group.name, groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type, + headers: req.headers, }); res.respond(200, {}); diff --git a/website/server/controllers/api-v3/quests.js b/website/server/controllers/api-v3/quests.js index 264069bab5..5c10b67cab 100644 --- a/website/server/controllers/api-v3/quests.js +++ b/website/server/controllers/api-v3/quests.js @@ -157,7 +157,7 @@ api.inviteToQuest = { questName: questKey, uuid: user._id, headers: req.headers, - }); + }, true); }, }; @@ -218,7 +218,7 @@ api.acceptQuest = { questName: group.quest.key, uuid: user._id, headers: req.headers, - }); + }, true); }, }; @@ -279,7 +279,7 @@ api.rejectQuest = { questName: group.quest.key, uuid: user._id, headers: req.headers, - }); + }, true); }, }; @@ -339,7 +339,7 @@ api.forceStart = { questName: group.quest.key, uuid: user._id, headers: req.headers, - }); + }, true); }, }; diff --git a/website/server/controllers/top-level/analytics.js b/website/server/controllers/top-level/analytics.js new file mode 100644 index 0000000000..f663a10dcb --- /dev/null +++ b/website/server/controllers/top-level/analytics.js @@ -0,0 +1,47 @@ +import { + NotAuthorized, +} from '../../libs/errors'; +import { + authWithHeaders, +} from '../../middlewares/auth'; + +const api = {}; + +/** + * @apiIgnore Analytics are considered part of the private API + * @api {post} /analytics/track/:eventName Track a generic analytics event + * @apiName AnalyticsTrack + * @apiGroup Analytics + * + * @apiSuccess {Object} data An empty object + * */ +api.trackEvent = { + method: 'POST', + url: '/analytics/track/:eventName', + // we authenticate these requests to make sure they actually came from a real user + middlewares: [authWithHeaders()], + async handler (req, res) { + // As of now only web can track events using this route + if (req.headers['x-client'] !== 'habitica-web') { + throw new NotAuthorized('Only habitica.com is allowed to track analytics events.'); + } + + const { user } = res.locals; + const eventProperties = req.body; + + res.analytics.track(req.params.eventName, { + uuid: user._id, + headers: req.headers, + category: 'behaviour', + gaLabel: 'local', + // hitType: 'event', sent from the client + ...eventProperties, + }); + + // not using res.respond + // because we don't want to send back notifications and other user-related data + res.status(200).send({}); + }, +}; + +export default api; diff --git a/website/server/controllers/top-level/payments/paypal.js b/website/server/controllers/top-level/payments/paypal.js index ba3eaf64b3..1432ceea90 100644 --- a/website/server/controllers/top-level/payments/paypal.js +++ b/website/server/controllers/top-level/payments/paypal.js @@ -63,7 +63,7 @@ api.checkoutSuccess = { if (!customerId) throw new BadRequest(apiError('missingCustomerId')); await paypalPayments.checkoutSuccess({ - user, gemsBlock, gift, paymentId, customerId, + user, gemsBlock, gift, paymentId, customerId, headers: req.headers, }); if (req.query.noRedirect) { diff --git a/website/server/controllers/top-level/payments/stripe.js b/website/server/controllers/top-level/payments/stripe.js index 4a01108306..fec9230524 100644 --- a/website/server/controllers/top-level/payments/stripe.js +++ b/website/server/controllers/top-level/payments/stripe.js @@ -35,7 +35,7 @@ api.checkout = { const { groupId, coupon, gemsBlock } = req.query; await stripePayments.checkout({ - token, user, gemsBlock, gift, sub, groupId, coupon, + token, user, gemsBlock, gift, sub, groupId, coupon, headers: req.headers, }); res.respond(200, {}); diff --git a/website/server/libs/analyticsService.js b/website/server/libs/analyticsService.js index 45de7fef6c..f990b60a9d 100644 --- a/website/server/libs/analyticsService.js +++ b/website/server/libs/analyticsService.js @@ -174,7 +174,7 @@ function _formatDataForAmplitude (data) { return ampData; } -function _sendDataToAmplitude (eventType, data) { +function _sendDataToAmplitude (eventType, data, loggerOnly) { const amplitudeData = _formatDataForAmplitude(data); amplitudeData.event_type = eventType; @@ -183,6 +183,8 @@ function _sendDataToAmplitude (eventType, data) { logger.info('Amplitude Event', amplitudeData); } + if (loggerOnly) return Promise.resolve(null); + return amplitude .track(amplitudeData) .catch(err => logger.error(err, 'Error while sending data to Amplitude.')); @@ -312,9 +314,9 @@ function _setOnce (dataToSetOnce, uuid) { } // There's no error handling directly here because it's handled inside _sendDataTo{Amplitude|Google} -async function track (eventType, data) { +async function track (eventType, data, loggerOnly = false) { const promises = [ - _sendDataToAmplitude(eventType, data), + _sendDataToAmplitude(eventType, data, loggerOnly), _sendDataToGoogle(eventType, data), ]; if (data.user && data.user.registeredThrough) { diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index 67804c705e..f8755f7337 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -225,7 +225,7 @@ function trackCronAnalytics (analytics, user, _progress, options) { user, questName: user.party.quest.key, headers: options.headers, - }); + }, true); } } From f257488b3960d8fc2e151a8b75cb4af47bcf485d Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 28 Oct 2020 22:39:55 +0100 Subject: [PATCH 4/6] 4.166.3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8724ffa99b..85a68589d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.166.2", + "version": "4.166.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4381e27b0f..0108385754 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.166.2", + "version": "4.166.3", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.12.3", From cc901fe0855bc150f098e38ee36c4a7b63c6c6b9 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 28 Oct 2020 22:43:32 +0100 Subject: [PATCH 5/6] fix erranous upgrade --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85a68589d8..392394d4b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.166.3", + "version": "4.166.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0108385754..281be579b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.166.3", + "version": "4.166.1", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.12.3", From f196ff2e24ddf33ae5aa4188e9f5840896a384c2 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 28 Oct 2020 22:43:39 +0100 Subject: [PATCH 6/6] 4.166.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 392394d4b9..8724ffa99b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.166.1", + "version": "4.166.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 281be579b1..4381e27b0f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.166.1", + "version": "4.166.2", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.12.3",