Revert "Revert "Analytics: track generic events through the server (#12735)""

This reverts commit 9d6fb2ca26.
This commit is contained in:
Matteo Pagliazzi 2020-11-09 11:34:20 +01:00
parent 997cc9f3c5
commit 2e59260149
16 changed files with 120 additions and 17 deletions

View file

@ -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();
});
});

View file

@ -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 {

View file

@ -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');

View file

@ -36,7 +36,7 @@ export default {
eventCategory: 'drop-cap-reached',
eventAction: 'click',
eventLabel: 'Drop Cap Reached > Notification Click',
});
}, { trackOnServer: true });
},
},
};

View file

@ -183,7 +183,7 @@ export default {
eventCategory: 'button',
eventAction: 'click',
eventLabel: 'User Dropdown > Subscriptions',
});
}, { trackOnServer: true });
this.$router.push({ name: 'subscription' });
},

View file

@ -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);
}
});
}

View 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);
}

View file

@ -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;

View file

@ -148,6 +148,10 @@ module.exports = {
target: DEV_BASE_URL,
changeOrigin: true,
},
'^/analytics': {
target: DEV_BASE_URL,
changeOrigin: true,
},
},
},
};

View file

@ -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, {});

View file

@ -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);
},
};

View 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;

View file

@ -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) {

View file

@ -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, {});

View file

@ -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) {

View file

@ -225,7 +225,7 @@ function trackCronAnalytics (analytics, user, _progress, options) {
user,
questName: user.party.quest.key,
headers: options.headers,
});
}, true);
}
}