mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-14 19:47:03 +00:00
Revert "Revert "Analytics: track generic events through the server (#12735)""
This reverts commit 9d6fb2ca26.
This commit is contained in:
parent
997cc9f3c5
commit
2e59260149
16 changed files with 120 additions and 17 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
eventCategory: 'drop-cap-reached',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Drop Cap Reached > Notification Click',
|
||||
});
|
||||
}, { trackOnServer: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ export default {
|
|||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'User Dropdown > Subscriptions',
|
||||
});
|
||||
}, { trackOnServer: true });
|
||||
|
||||
this.$router.push({ name: 'subscription' });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
7
website/client/src/store/actions/analytics.js
Normal file
7
website/client/src/store/actions/analytics.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -148,6 +148,10 @@ module.exports = {
|
|||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/analytics': {
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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, {});
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ api.inviteToQuest = {
|
|||
questName: questKey,
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
});
|
||||
}, true);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ api.acceptQuest = {
|
|||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
});
|
||||
}, true);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ api.rejectQuest = {
|
|||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
});
|
||||
}, true);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -338,7 +338,7 @@ api.forceStart = {
|
|||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
});
|
||||
}, true);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
47
website/server/controllers/top-level/analytics.js
Normal file
47
website/server/controllers/top-level/analytics.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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, {});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ function trackCronAnalytics (analytics, user, _progress, options) {
|
|||
user,
|
||||
questName: user.party.quest.key,
|
||||
headers: options.headers,
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue