feat(analytics): initial Habitica-owned solution

This commit is contained in:
Phillip Thelen
2026-04-07 15:33:57 -05:00
committed by Kalista Payne
parent 746fcfff49
commit e6ffd69148
90 changed files with 762 additions and 2479 deletions
@@ -198,7 +198,6 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
import todoIcon from '@/assets/svg/todo.svg?raw';
import rewardIcon from '@/assets/svg/reward.svg?raw';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
export default {
@@ -438,14 +437,6 @@ export default {
return false;
},
changeMirrorPreference (newVal) {
Analytics.track({
eventName: 'mirror tasks',
eventAction: 'mirror tasks',
eventCategory: 'behavior',
hitType: 'event',
mirror: newVal,
group: this.group._id,
}, { trackOnClient: true });
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id);
@@ -240,7 +240,6 @@
<script>
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX';
@@ -276,11 +275,6 @@ export default {
this.$store.state.party.data = party;
this.user.party._id = party._id;
Analytics.updateUser({
partyID: party._id,
partySize: 1,
});
this.$root.$emit('bv::hide::modal', 'create-party-modal');
await this.$router.push('/party');
},
@@ -314,7 +314,6 @@ import extend from 'lodash/extend';
import groupUtilities from '@/mixins/groupsUtilities';
import styleHelper from '@/mixins/styleHelper';
import { mapGetters } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal';
import groupGemsModal from '@/components/groups/groupGemsModal';
@@ -560,7 +559,6 @@ export default {
if (this.isParty) {
data.type = 'party';
Analytics.updateUser({ partySize: null, partyID: null });
this.$store.state.partyMembers = [];
}
@@ -334,7 +334,6 @@ import orderBy from 'lodash/orderBy';
import * as quests from '@/../../common/script/content/quests';
import getItemInfo from '@/../../common/script/libs/getItemInfo';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import navigationBack from '@/assets/svg/navigation_back.svg?raw';
import questDialogContent from '../shops/quests/questDialogContent';
@@ -421,11 +420,6 @@ export default {
async questInit () {
this.loading = true;
Analytics.updateUser({
partyID: this.group._id,
partySize: this.group.memberCount,
});
const groupId = this.group._id || this.user.party._id;
const key = this.selectedQuest;
@@ -123,7 +123,6 @@
<script>
import orderBy from 'lodash/orderBy';
import * as Analytics from '@/libs/analytics';
import { mapGetters, mapActions } from '@/libs/store';
import MemberDetails from '../memberDetails';
import createPartyModal from '../groups/createPartyModal';
@@ -236,22 +235,8 @@ export default {
},
async createOrInviteParty () {
if (this.user.party._id) {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Find Party Members',
});
this.$router.push('/looking-for-party');
} else {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Get Started',
});
this.$root.$emit('bv::show::modal', 'create-party-modal');
}
},
@@ -114,7 +114,6 @@ import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
import yesterdailyModal from './tasks/yesterdailyModal';
import newStuff from './news/modal';
@@ -648,15 +647,6 @@ export default {
// Reset daily analytics actions
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
} else {
// Note a failed cron event, for our records and investigation
Analytics.track({
eventName: 'cron failed',
eventAction: 'cron failed',
eventCategory: 'behavior',
hitType: 'event',
responseCode: response.status,
}, { trackOnClient: true });
}
// Sync
@@ -433,9 +433,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import notificationsMixin from '@/mixins/notifications';
import paymentsMixin from '@/mixins/payments';
// analytics
import * as Analytics from '@/libs/analytics';
export default {
components: {
selectTranslatedArray,
@@ -536,16 +533,6 @@ export default {
this.close();
},
submit () {
if (this.paymentData.group && !this.paymentData.newGroup) {
Analytics.track({
hitType: 'event',
eventName: 'group plan upgrade',
eventAction: 'group plan upgrade',
eventCategory: 'behavior',
demographics: this.upgradedGroup.demographics,
type: this.paymentData.group.type,
}, { trackOnClient: true });
}
this.paymentData = {};
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
},
-11
View File
@@ -6,7 +6,6 @@ import { mapState } from '@/libs/store';
import encodeParams from '@/libs/encodeParams';
import notificationsMixin from '@/mixins/notifications';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
const { STRIPE_PUB_KEY } = import.meta.env;
@@ -207,16 +206,6 @@ export default {
alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`);
throw checkoutSessionResult.error;
}
if (paymentType === 'groupPlan') {
Analytics.track({
hitType: 'event',
eventName: 'group plan create',
eventAction: 'group plan create',
eventCategory: 'behavior',
demographics: appState.newGroup.demographics,
type: appState.newGroup.type,
}, { trackOnClient: true });
}
} catch (err) {
console.error('Error while redirecting to Stripe', err); // eslint-disable-line
alert(`Error while redirecting to Stripe: ${err.message}`);
-10
View File
@@ -3,7 +3,6 @@ import Vue from 'vue';
import scoreTask from '@/../../common/script/ops/scoreTask';
import notifications from './notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export default {
@@ -58,15 +57,6 @@ export default {
const tasksScoredCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT);
if (!tasksScoredCount || tasksScoredCount < 2) {
Analytics.track({
eventName: 'task scored',
eventAction: 'task scored',
eventCategory: 'behavior',
hitType: 'event',
uuid: user._id,
taskType: task.type,
direction,
}, { trackOnClient: true });
if (!tasksScoredCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
} else {
-2
View File
@@ -130,7 +130,6 @@ import PrivacyBanner from '@/components/header/banners/privacy';
import AppFooter from '@/components/appFooter';
import notificationsDisplay from '@/components/notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import BuyModal from '@/components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications';
@@ -276,7 +275,6 @@ export default {
}
}
Analytics.updateUser();
return this.loadAllTranslations();
}).then(() => {
this.$store.state.isUserLoaded = true;
-10
View File
@@ -1,6 +1,5 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import * as Analytics from '@/libs/analytics';
import getStore from '@/store';
import handleRedirect from './handleRedirect';
@@ -318,15 +317,6 @@ router.beforeEach(async (to, from, next) => {
router.app.$root.$emit('update-party');
}
if (to.name === 'lookingForParty') {
Analytics.track({
hitType: 'event',
eventName: 'View Find Members',
eventAction: 'View Find Members',
eventCategory: 'behavior',
}, { trackOnClient: true });
}
// Redirect old guild urls
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
const splits = to.hash.split('/');
-8
View File
@@ -1,6 +1,5 @@
import axios from 'axios';
import Vue from 'vue';
import * as Analytics from '@/libs/analytics';
export async function getChat (store, payload) {
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
@@ -17,13 +16,6 @@ export async function postChat (store, payload) {
url += `?previousMsg=${payload.previousMsg}`;
}
if (group.type === 'party') {
Analytics.updateUser({
partyID: group.id,
partySize: group.memberCount,
});
}
const response = await axios.post(url, {
message: payload.message,
});
@@ -1,7 +1,6 @@
import axios from 'axios';
import omit from 'lodash/omit';
import findIndex from 'lodash/findIndex';
import * as Analytics from '@/libs/analytics';
import { loadAsyncResource } from '@/libs/asyncResource';
export async function getPublicGuilds (store, payload) {
@@ -74,7 +73,6 @@ export async function join (store, payload) {
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
user.party._id = groupId;
Analytics.updateUser({ partyID: groupId });
// load the party members so that they get shown in the header
store.dispatch('party:getMembers');
}
@@ -18,7 +18,6 @@ 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';
import * as faq from './faq';
import * as blockers from './blockers';
@@ -44,7 +43,6 @@ const actions = flattenAndNamespace({
snackbars,
worldState,
news,
analytics,
faq,
blockers,
});
@@ -1,26 +1,6 @@
import axios from 'axios';
import * as Analytics from '@/libs/analytics';
// export async function initQuest (store) {
// }
export async function sendAction (store, payload) { // eslint-disable-line import/prefer-default-export, max-len
// @TODO: Maybe move this to server
let partyData = {};
if (store.state.party && store.state.party.data) {
partyData = {
partyID: store.state.party.data._id,
partySize: store.state.party.data.memberCount,
};
} else {
partyData = {
partyID: store.state.user.data.party._id,
partySize: store.state.partyMembers.data.length,
};
}
Analytics.updateUser(partyData);
const response = await axios.post(`/api/v4/groups/${payload.groupId}/${payload.action}`);
// @TODO: Update user?
-10
View File
@@ -3,7 +3,6 @@ import Vue from 'vue';
import compact from 'lodash/compact';
import omit from 'lodash/omit';
import { loadAsyncResource } from '@/libs/asyncResource';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export function fetchUserTasks (store, options = {}) {
@@ -112,15 +111,6 @@ export async function create (store, createdTask) {
}
const tasksCreatedCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT);
if (!tasksCreatedCount || tasksCreatedCount < 2) {
const uuid = store.state.user.data._id;
Analytics.track({
eventName: 'task created',
eventAction: 'task created',
eventCategory: 'behavior',
hitType: 'event',
uuid,
taskType: taskRes.type,
}, { trackOnClient: true });
if (!tasksCreatedCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
} else {
-4
View File
@@ -159,10 +159,6 @@ export default defineConfig({
target: DEV_BASE_URL,
changeOrigin: true,
},
'^/analytics': {
target: DEV_BASE_URL,
changeOrigin: true,
},
}
}
})
+1 -13
View File
@@ -3,10 +3,8 @@ import isFunction from 'lodash/isFunction';
import min from 'lodash/min';
import reduce from 'lodash/reduce';
import filter from 'lodash/filter';
import pick from 'lodash/pick';
import pickBy from 'lodash/pickBy';
import size from 'lodash/size';
import moment from 'moment';
import content from '../content/index';
import i18n from '../i18n';
import { daysSince } from '../cron';
@@ -28,7 +26,7 @@ function trueRandom () {
return Math.random();
}
export default function randomDrop (user, options, req = {}, analytics) {
export default function randomDrop (user, options, req = {}) {
let acceptableDrops;
let drop;
let dropMultiplier;
@@ -157,15 +155,5 @@ export default function randomDrop (user, options, req = {}, analytics) {
user._tmp.drop = drop;
user.items.lastDrop.date = Number(new Date());
user.items.lastDrop.count += 1;
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('dropped item', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: drop.key,
category: 'behavior',
headers: req.headers,
});
}
}
}
+1 -12
View File
@@ -1,5 +1,3 @@
import pick from 'lodash/pick';
export function hasCompletedOnboarding (user) {
return (
user.achievements.createdTask === true
@@ -16,18 +14,9 @@ export function onOnboardingComplete (user) {
}
// Add notification and awards (server)
export function checkOnboardingStatus (user, req, analytics) {
export function checkOnboardingStatus (user) {
if (hasCompletedOnboarding(user) && user.addNotification) {
user.addNotification('ONBOARDING_COMPLETE');
if (analytics) {
analytics.track('onboarding complete', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
}
onOnboardingComplete(user);
}
}
@@ -1,7 +1,5 @@
/* eslint-disable max-classes-per-file */
import get from 'lodash/get';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import i18n from '../../i18n';
import {
NotAuthorized,
@@ -15,12 +13,10 @@ export class AbstractBuyOperation {
/**
* @param {User} user - the User-Object
* @param {Request} req - the Request-Object
* @param {analytics} analytics
*/
constructor (user, req, analytics) {
constructor (user, req) {
this.user = user;
this.req = req || {};
this.analytics = analytics;
const quantity = get(req, 'quantity');
@@ -87,10 +83,6 @@ export class AbstractBuyOperation {
throw new NotImplementedError('executeChanges');
}
analyticsData () { // eslint-disable-line class-methods-use-this
throw new NotImplementedError('sendToAnalytics');
}
async purchase () {
if (!this.multiplePurchaseAllowed() && this.quantity > 1) {
throw new NotAuthorized(this.i18n('messageNotAbleToBuyInBulk'));
@@ -98,34 +90,10 @@ export class AbstractBuyOperation {
this.extractAndValidateParams(this.user, this.req);
const resultObj = await this.executeChanges(this.user, this.item, this.req, this.analytics);
if (this.analytics) {
this.sendToAnalytics(this.analyticsData());
}
const resultObj = await this.executeChanges(this.user, this.item, this.req);
return resultObj;
}
analyticsLabel () { // eslint-disable-line class-methods-use-this
return 'buy';
}
sendToAnalytics (additionalData = {}) {
// spread-operator produces an "unexpected token" error
const analyticsData = merge(additionalData, {
user: pick(this.user, ['preferences', 'registeredThrough']),
uuid: this.user._id,
category: 'behavior',
headers: this.req.headers,
});
if (this.multiplePurchaseAllowed()) {
analyticsData.quantityPurchased = this.quantity;
}
this.analytics.track(this.analyticsLabel(), analyticsData);
}
}
export class AbstractGoldItemOperation extends AbstractBuyOperation {
@@ -149,15 +117,6 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
user.stats.gp -= itemValue * this.quantity;
}
analyticsData () {
return {
itemKey: this.getItemKey(this.item),
itemType: this.getItemType(this.item),
currency: 'Gold',
goldCost: this.getItemValue(this.item),
};
}
}
export class AbstractGemItemOperation extends AbstractBuyOperation {
@@ -179,15 +138,6 @@ export class AbstractGemItemOperation extends AbstractBuyOperation {
await updateUserBalance(user, -(itemValue * this.quantity), 'spend', item.key, item.text());
}
analyticsData () {
return {
itemKey: this.getItemKey(this.item),
itemType: this.getItemType(this.item),
currency: 'Gems',
gemCost: this.getItemValue(this.item) * 4,
};
}
}
export class AbstractHourglassItemOperation extends AbstractBuyOperation {
@@ -202,11 +152,4 @@ export class AbstractHourglassItemOperation extends AbstractBuyOperation {
async subtractCurrency (user, item) { // eslint-disable-line class-methods-use-this
await updateUserHourglasses(user, -1, 'spend', item.key);
}
analyticsData () {
return {
itemKey: this.item.key,
currency: 'Hourglass',
};
}
}
+14 -15
View File
@@ -24,7 +24,6 @@ import { BuyHourglassMountOperation } from './buyMount';
export default async function buy (
user,
req = {},
analytics,
options = { quantity: 1, hourglass: false },
) {
const key = get(req, 'params.key');
@@ -42,35 +41,35 @@ export default async function buy (
switch (type) {
case 'armoire': {
const buyOp = new BuyArmoireOperation(user, req, analytics);
const buyOp = new BuyArmoireOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'backgrounds':
if (!hourglass) throw new BadRequest(errorMessage('useUnlockForCosmetics'));
buyRes = await hourglassPurchase(user, req, analytics);
buyRes = await hourglassPurchase(user, req);
break;
case 'mystery':
buyRes = await buyMysterySet(user, req, analytics);
buyRes = await buyMysterySet(user, req);
break;
case 'potion': {
const buyOp = new BuyHealthPotionOperation(user, req, analytics);
const buyOp = new BuyHealthPotionOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'gems': {
const buyOp = new BuyGemOperation(user, req, analytics);
const buyOp = new BuyGemOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'quests': {
if (hourglass) {
buyRes = await hourglassPurchase(user, req, analytics, quantity);
buyRes = await hourglassPurchase(user, req, quantity);
} else {
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
const buyOp = new BuyQuestWithGemOperation(user, req);
buyRes = await buyOp.purchase();
}
@@ -81,36 +80,36 @@ export default async function buy (
case 'food':
case 'gear':
case 'bundles':
buyRes = await purchaseOp(user, req, analytics);
buyRes = await purchaseOp(user, req);
break;
case 'mounts': {
const buyOp = new BuyHourglassMountOperation(user, req, analytics);
const buyOp = new BuyHourglassMountOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'pets':
if (key === 'Gryphatrice-Jubilant') {
const buyOp = new BuyPetWithGemOperation(user, req, analytics);
const buyOp = new BuyPetWithGemOperation(user, req);
buyRes = await buyOp.purchase();
} else {
buyRes = hourglassPurchase(user, req, analytics);
buyRes = hourglassPurchase(user, req);
}
break;
case 'quest': {
const buyOp = new BuyQuestWithGoldOperation(user, req, analytics);
const buyOp = new BuyQuestWithGoldOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'special': {
const buyOp = new BuySpellOperation(user, req, analytics);
const buyOp = new BuySpellOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
default: {
const buyOp = new BuyMarketGearOperation(user, req, analytics);
const buyOp = new BuyMarketGearOperation(user, req);
buyRes = await buyOp.purchase();
break;
@@ -69,19 +69,6 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-d
];
}
_trackDropAnalytics (user, key) {
this.analytics.track(
'Enchanted Armoire',
{
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: key,
category: 'behavior',
headers: this.req.headers,
},
);
}
_gearResult (user, eligibleEquipment) {
const emptied = eligibleEquipment.length === 1;
eligibleEquipment.sort();
@@ -105,10 +92,6 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-d
removeItemByPath(user, `gear.flat.${drop.key}`);
if (this.analytics) {
this._trackDropAnalytics(user, drop.key);
}
const armoireResp = {
type: 'gear',
dropKey: drop.key,
@@ -134,9 +117,6 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-d
user.items.food[drop.key] += 1;
if (user.markModified) user.markModified('items.food');
if (this.analytics) {
this._trackDropAnalytics(user, drop.key);
}
return {
message: this.i18n('armoireFood', {
image: `<span class="Pet_Food_${drop.key} pull-left"></span>`,
-4
View File
@@ -70,8 +70,4 @@ export class BuyGemOperation extends AbstractGoldItemOperation { // eslint-disab
this.i18n('plusGem', { count: this.quantity }),
];
}
analyticsLabel () { // eslint-disable-line class-methods-use-this
return 'purchase gems';
}
}
@@ -60,7 +60,7 @@ export class BuyMarketGearOperation extends AbstractGoldItemOperation { // eslin
}
}
executeChanges (user, item, req, analytics) {
executeChanges (user, item, req) {
let message;
if (user.preferences.autoEquip) {
@@ -70,7 +70,7 @@ export class BuyMarketGearOperation extends AbstractGoldItemOperation { // eslin
if (!user.achievements.purchasedEquipment && user.addAchievement) {
user.addAchievement('purchasedEquipment');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
@@ -49,10 +49,4 @@ export class BuyHourglassMountOperation extends AbstractHourglassItemOperation {
message,
];
}
analyticsData () {
const data = super.analyticsData();
data.itemType = 'mounts';
return data;
}
}
+1 -14
View File
@@ -1,6 +1,5 @@
import get from 'lodash/get';
import each from 'lodash/each';
import pick from 'lodash/pick';
import i18n from '../../i18n';
import content from '../../content/index';
import {
@@ -13,7 +12,7 @@ import updateUserHourglasses from '../updateUserHourglasses';
import { removeItemByPath } from '../pinnedGearUtils';
import getItemInfo from '../../libs/getItemInfo';
export default async function buyMysterySet (user, req = {}, analytics) {
export default async function buyMysterySet (user, req = {}) {
const key = get(req, 'params.key');
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
@@ -35,18 +34,6 @@ export default async function buyMysterySet (user, req = {}, analytics) {
const itemInfo = getItemInfo(user, 'mystery_set', mysterySet);
removeItemByPath(user, itemInfo.path);
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: mysterySet.key,
itemType: 'Subscriber Gear',
currency: 'Hourglass',
category: 'behavior',
headers: req.headers,
});
}
// Here we need to trigger vue reactivity through reassign object
user.items.gear.owned = {
...user.items.gear.owned,
@@ -1,7 +1,6 @@
import get from 'lodash/get';
import includes from 'lodash/includes';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import i18n from '../../i18n';
import content from '../../content/index';
import {
@@ -13,7 +12,7 @@ import getItemInfo from '../../libs/getItemInfo';
import { removeItemByPath } from '../pinnedGearUtils';
import updateUserHourglasses from '../updateUserHourglasses';
export default async function purchaseHourglass (user, req = {}, analytics, quantity = 1) {
export default async function purchaseHourglass (user, req = {}, quantity = 1) {
const key = get(req, 'params.key');
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
@@ -94,18 +93,6 @@ export default async function purchaseHourglass (user, req = {}, analytics, quan
}
}
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: key,
itemType: type,
currency: 'Hourglass',
category: 'behavior',
headers: req.headers,
});
}
return [
{ items: user.items, purchasedPlanConsecutive: user.purchased.plan.consecutive },
i18n.t('hourglassPurchase', req.language),
+1 -14
View File
@@ -76,7 +76,7 @@ async function purchaseItem (user, item, price, type, key) {
const acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'gear', 'bundles'];
const singlePurchaseTypes = ['gear'];
export default async function purchase (user, req = {}, analytics) {
export default async function purchase (user, req = {}) {
const type = get(req.params, 'type');
const key = get(req.params, 'key');
@@ -130,19 +130,6 @@ export default async function purchase (user, req = {}, analytics) {
await purchaseItem(user, item, price, type, key);
}
/* eslint-enable no-await-in-loop */
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: key,
itemType: type,
currency: 'Gems',
gemCost: price * 4,
quantityPurchased: quantity,
category: 'behavior',
headers: req.headers,
});
}
return [
pick(user, splitWhitespace('items balance')),
+3 -15
View File
@@ -34,19 +34,18 @@ async function resetClass (user, req = {}) {
return balanceRemoved;
}
export default async function changeClass (user, req = {}, analytics) {
export default async function changeClass (user, req = {}) {
const klass = get(req, 'query.class');
let balanceRemoved = 0;
// user.flags.classSelected is set to false after the user paid the 3 gems
if (user.stats.lvl < 10) {
throw new NotAuthorized(i18n.t('lvl10ChangeClass', req.language));
} else if (!klass) {
// if no class is specified, reset points and set user.flags.classSelected to false.
// User will have paid 3 gems and will be prompted to select class.
balanceRemoved = await resetClass(user, req);
await resetClass(user, req);
} else if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') {
if (user.flags.classSelected) {
balanceRemoved = await resetClass(user, req);
await resetClass(user, req);
}
user.stats.class = klass;
@@ -67,17 +66,6 @@ export default async function changeClass (user, req = {}, analytics) {
if (user.markModified) user.markModified('items.gear.owned');
removePinnedItemsByOwnedGear(user);
if (analytics) {
analytics.track('change class', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
class: klass,
currency: balanceRemoved === 0 ? 'Free' : 'Gems',
category: 'behavior',
headers: req.headers,
});
}
} else {
// if invalid class is specified, throw an error.
throw new BadRequest(i18n.t('invalidClass', req.language));
+2 -15
View File
@@ -2,9 +2,7 @@ import forEach from 'lodash/forEach';
import findIndex from 'lodash/findIndex';
import get from 'lodash/get';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import upperFirst from 'lodash/upperFirst';
import moment from 'moment';
import i18n from '../i18n';
import content from '../content/index';
import {
@@ -36,7 +34,7 @@ function evolve (user, pet, req) {
}, req.language);
}
export default function feed (user, req = {}, analytics) {
export default function feed (user, req = {}) {
let pet = get(req, 'params.pet');
const foodK = get(req, 'params.food');
let amount = Number(get(req.query, 'amount', 1));
@@ -116,7 +114,7 @@ export default function feed (user, req = {}, analytics) {
if (!user.achievements.fedPet && user.addAchievement) {
user.addAchievement('fedPet');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
}
@@ -141,17 +139,6 @@ export default function feed (user, req = {}, analytics) {
}
});
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('pet feed', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
foodKey: food.key,
petKey: pet.key,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items.pets[pet.key],
message,
+2 -14
View File
@@ -2,9 +2,7 @@ import findIndex from 'lodash/findIndex';
import forEach from 'lodash/forEach';
import get from 'lodash/get';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import upperFirst from 'lodash/upperFirst';
import moment from 'moment';
import i18n from '../i18n';
import content from '../content/index';
import {
@@ -15,7 +13,7 @@ import {
import { errorMessage } from '../libs/errorMessage';
import { checkOnboardingStatus } from '../libs/onboarding';
export default function hatch (user, req = {}, analytics) {
export default function hatch (user, req = {}) {
const egg = get(req, 'params.egg');
const hatchingPotion = get(req, 'params.hatchingPotion');
@@ -57,7 +55,7 @@ export default function hatch (user, req = {}, analytics) {
if (!user.achievements.hatchedPet && user.addAchievement) {
user.addAchievement('hatchedPet');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
if (content.dropEggs[egg]) {
@@ -152,16 +150,6 @@ export default function hatch (user, req = {}, analytics) {
});
}
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('pet hatch', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
petKey: pet,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items,
i18n.t('messageHatched', req.language),
+1 -18
View File
@@ -1,5 +1,4 @@
import each from 'lodash/each';
import pick from 'lodash/pick';
import i18n from '../i18n';
import { capByLevel } from '../statHelpers';
import {
@@ -13,31 +12,15 @@ import updateUserBalance from './updateUserBalance';
const USERSTATSLIST = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'];
export default async function rebirth (user, tasks = [], req = {}, analytics) {
export default async function rebirth (user, tasks = [], req = {}) {
const notFree = !isFreeRebirth(user);
if (user.balance < 1.5 && notFree) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
const analyticsData = {
uuid: user._id,
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
};
if (notFree) {
await updateUserBalance(user, -1.5, 'rebirth');
analyticsData.currency = 'Gems';
analyticsData.gemCost = 6;
} else {
analyticsData.currency = 'Free';
analyticsData.gemCost = 0;
}
if (analytics) {
analyticsData.headers = req.headers;
analytics.track('Rebirth', analyticsData);
}
const lvl = capByLevel(user.stats.lvl);
+1 -13
View File
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import content from '../content/index';
import { mountMasterProgress } from '../count';
import i18n from '../i18n';
@@ -7,7 +6,7 @@ import {
} from '../libs/errors';
import updateUserBalance from './updateUserBalance';
export default async function releaseMounts (user, req = {}, analytics) {
export default async function releaseMounts (user, req = {}) {
if (user.balance < 1) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
@@ -42,17 +41,6 @@ export default async function releaseMounts (user, req = {}, analytics) {
user.achievements.mountMasterCount += 1;
}
if (analytics) {
analytics.track('release mounts', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
currency: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items.mounts,
i18n.t('mountsReleased'),
+1 -13
View File
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import content from '../content/index';
import { beastMasterProgress } from '../count';
import i18n from '../i18n';
@@ -7,7 +6,7 @@ import {
} from '../libs/errors';
import updateUserBalance from './updateUserBalance';
export default function releasePets (user, req = {}, analytics) {
export default function releasePets (user, req = {}) {
if (user.balance < 1) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
@@ -42,17 +41,6 @@ export default function releasePets (user, req = {}, analytics) {
user.achievements.beastMasterCount += 1;
}
if (analytics) {
analytics.track('release pets', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
currency: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items.pets,
i18n.t('petsReleased'),
+1 -13
View File
@@ -1,12 +1,11 @@
import each from 'lodash/each';
import pick from 'lodash/pick';
import i18n from '../i18n';
import {
NotAuthorized,
} from '../libs/errors';
import updateUserBalance from './updateUserBalance';
export default async function reroll (user, tasks = [], req = {}, analytics) {
export default async function reroll (user, tasks = [], req = {}) {
if (user.balance < 1) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
@@ -22,17 +21,6 @@ export default async function reroll (user, tasks = [], req = {}, analytics) {
}
});
if (analytics) {
analytics.track('Fortify Potion', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
currency: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}
return [
{ user, tasks },
i18n.t('fortifyComplete'),
+1 -12
View File
@@ -1,5 +1,4 @@
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import reduce from 'lodash/reduce';
import each from 'lodash/each';
import i18n from '../i18n';
@@ -13,7 +12,7 @@ import predictableRandom from '../fns/predictableRandom';
import { removePinnedGearByClass, addPinnedGearByClass, addPinnedGear } from './pinnedGearUtils';
import getItemInfo from '../libs/getItemInfo';
export default function revive (user, req = {}, analytics) {
export default function revive (user, req = {}) {
if (user.stats.hp > 0) {
throw new NotAuthorized(i18n.t('cannotRevive', req.language));
}
@@ -110,16 +109,6 @@ export default function revive (user, req = {}, analytics) {
message = i18n.t('messageLostItem', { itemText: item.text(req.language) }, req.language);
}
if (analytics) {
analytics.track('Death', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
lostItem,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items,
message,
+2 -2
View File
@@ -225,7 +225,7 @@ function _updateLastHistoryEntry (lastHistoryEntry, task, direction, times) {
}
}
export default function scoreTask (options = {}, req = {}, analytics) {
export default function scoreTask (options = {}, req = {}) {
const {
user, task, direction, times = 1, cron = false,
} = options;
@@ -425,7 +425,7 @@ export default function scoreTask (options = {}, req = {}, analytics) {
if (!user.achievements.completedTask && cron === false && direction === 'up' && user.addAchievement) {
user.addAchievement('completedTask');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
return delta;
+1 -14
View File
@@ -1,17 +1,4 @@
import pick from 'lodash/pick';
export function sleep (user, req = {}, analytics) {
export function sleep (user) {
user.preferences.sleep = !user.preferences.sleep;
if (analytics) {
analytics.track('sleep', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
status: user.preferences.sleep,
category: 'behavior',
headers: req.headers,
});
}
return [user.preferences.sleep];
}
+1 -15
View File
@@ -1,5 +1,4 @@
import get from 'lodash/get';
import pick from 'lodash/pick';
import setWith from 'lodash/setWith';
import i18n from '../i18n';
import { NotAuthorized, BadRequest } from '../libs/errors';
@@ -208,7 +207,7 @@ function buildResponse ({ purchased, preference, items }, ownsAlready, language)
// If item is already purchased -> equip it
// Otherwise unlock it
// @TODO refactor and take as parameter the set name, for single items use the buy ops
export default async function unlock (user, req = {}, analytics) {
export default async function unlock (user, req = {}) {
const path = get(req.query, 'path');
if (!path) {
@@ -319,19 +318,6 @@ export default async function unlock (user, req = {}, analytics) {
if (!unlockedAlready) {
await updateUserBalance(user, -cost, 'spend', path);
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: path,
itemType: 'customization',
currency: 'Gems',
gemCost: cost / 0.25,
category: 'behavior',
headers: req.headers,
});
}
}
return buildResponse(user, unlockedAlready, req.language);
@@ -1,6 +1,5 @@
import validator from 'validator';
import moment from 'moment';
import pick from 'lodash/pick';
import sortBy from 'lodash/sortBy';
import nconf from 'nconf';
import {
@@ -127,14 +126,6 @@ api.loginLocal = {
user.auth.timestamps.updated = new Date();
await user.save();
res.analytics.track('login', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
type: 'local',
uuid: user._id,
headers: req.headers,
});
return loginRes(user, req, res);
},
};
@@ -1,7 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import escapeRegExp from 'lodash/escapeRegExp';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import reduce from 'lodash/reduce';
import times from 'lodash/times';
import { authWithHeaders, authWithSession } from '../../middlewares/auth';
@@ -290,19 +289,6 @@ api.createChallenge = {
};
response.group = getChallengeGroupResponse(group);
res.analytics.track('challenge create', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: response._id,
groupID: group._id,
groupName: group.privacy === 'private' ? null : group.name,
groupType: group._id === TAVERN_ID ? 'tavern' : group.type,
prize: response.prize,
headers: req.headers,
});
res.respond(201, response);
},
};
@@ -359,18 +345,6 @@ api.joinChallenge = {
const chalLeader = await User.findById(response.leader).select(nameFields).exec();
response.leader = chalLeader ? chalLeader.toJSON({ minimize: true }) : null;
res.analytics.track('challenge join', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
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);
},
};
@@ -410,18 +384,6 @@ api.leaveChallenge = {
// Unlink challenge's tasks from user's tasks and save the challenge
await challenge.unlinkTasks(user, keep);
res.analytics.track('challenge leave', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
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, {});
},
};
@@ -895,19 +857,6 @@ api.deleteChallenge = {
// Close channel in background, some ops are run in the background without `await`ing
await challenge.closeChal({ broken: 'CHALLENGE_DELETED' });
res.analytics.track('challenge delete', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
groupID: challenge.group._id,
groupName: challenge.group.privacy === 'private' ? null : challenge.group.name,
groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type,
prize: challenge.prize,
headers: req.headers,
});
res.respond(200, {});
},
};
@@ -956,20 +905,6 @@ api.selectChallengeWinner = {
// Close channel in background, some ops are run in the background without `await`ing
await challenge.closeChal({ broken: 'CHALLENGE_CLOSED', winner });
res.analytics.track('challenge close', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
challengeWinnerID: winner._id,
groupID: challenge.group._id,
groupName: challenge.group.privacy === 'private' ? null : challenge.group.name,
groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type,
prize: challenge.prize,
headers: req.headers,
});
res.respond(200, {});
},
};
-32
View File
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import moment from 'moment';
import nconf from 'nconf';
import { authWithHeaders, chatPrivilegesRequired } from '../../middlewares/auth';
@@ -23,9 +22,6 @@ import { getMatchesByWordArray } from '../../libs/stringUtils';
import bannedSlurs from '../../libs/bannedSlurs';
import { apiError } from '../../libs/apiError';
import highlightMentions from '../../libs/highlightMentions';
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
const analytics = getAnalyticsServiceByEnvironment();
const ACCOUNT_MIN_CHAT_AGE = Number(nconf.get('ACCOUNT_MIN_CHAT_AGE'));
@@ -187,13 +183,6 @@ api.postChat = {
// Check if account is newer than the minimum age for chat participation
if (moment().diff(user.auth.timestamps.created, 'minutes') < ACCOUNT_MIN_CHAT_AGE) {
analytics.track('chat age error', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
throw new BadRequest(res.t('chatTemporarilyUnavailable'));
}
@@ -239,27 +228,6 @@ api.postChat = {
await Promise.all(toSave);
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
groupType: group.type,
privacy: group.privacy,
headers: req.headers,
};
if (mentions) {
analyticsObject.mentionsCount = mentions.length;
} else {
analyticsObject.mentionsCount = 0;
}
if (group.privacy === 'public') {
analyticsObject.groupName = group.name;
}
res.analytics.track('group chat', analyticsObject);
if (chatUpdated) {
res.respond(200, { chat: chatRes.chat });
} else {
@@ -5,7 +5,6 @@ import findIndex from 'lodash/findIndex';
import includes from 'lodash/includes';
import isArray from 'lodash/isArray';
import mergeWith from 'lodash/mergeWith';
import pick from 'lodash/pick';
import uniqBy from 'lodash/uniqBy';
import nconf from 'nconf';
import moment from 'moment';
@@ -166,25 +165,6 @@ api.createGroup = {
profile: { name: user.profile.name },
};
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: true,
groupId: savedGroup._id,
groupType: savedGroup.type,
privacy: savedGroup.privacy,
headers: req.headers,
invited: false,
};
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
},
};
@@ -217,19 +197,6 @@ api.createGroupPlan = {
const results = await Promise.all([user.save(), group.save()]);
const savedGroup = results[1];
res.analytics.track('join group', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: true,
groupId: savedGroup._id,
groupType: savedGroup.type,
privacy: savedGroup.privacy,
headers: req.headers,
invited: false,
});
// do not remove chat flags data as we've just created the group
const groupResponse = savedGroup.toJSON();
// the leader is the authenticated user
@@ -585,7 +552,6 @@ api.joinGroup = {
if (!group) throw new NotFound(res.t('groupNotFound'));
let isUserInvited = false;
const seekingParty = Boolean(user.party.seeking);
if (group.type === 'party') {
// Check if was invited to party
@@ -710,20 +676,6 @@ api.joinGroup = {
promises.push(group.save());
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: false,
groupId: group._id,
groupType: group.type,
privacy: group.privacy,
headers: req.headers,
invited: isUserInvited,
seekingParty: group.type === 'party' ? seekingParty : null,
};
promises = await Promise.all(promises);
if (group.hasNotCancelled()) {
@@ -737,8 +689,6 @@ api.joinGroup = {
response.leader = leader.toJSON({ minimize: true });
}
res.analytics.track('join group', analyticsObject);
res.respond(200, response);
},
};
@@ -1,5 +1,4 @@
import escapeRegExp from 'lodash/escapeRegExp';
import pick from 'lodash/pick';
import { authWithHeaders } from '../../middlewares/auth';
import {
model as User,
@@ -734,17 +733,6 @@ api.transferGems = {
}
res.respond(200, {});
if (res.analytics) {
res.analytics.track('transfer gems', {
user: pick(sender, ['preferences', 'registeredThrough']),
uuid: sender._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
quantity: gemAmount,
});
}
},
};
@@ -1,9 +1,7 @@
import each from 'lodash/each';
import every from 'lodash/every';
import isBoolean from 'lodash/isBoolean';
import pick from 'lodash/pick';
import { authWithHeaders } from '../../middlewares/auth';
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
import {
model as Group,
basicFields as basicGroupFields,
@@ -24,8 +22,6 @@ import { apiError } from '../../libs/apiError';
import { questActivityWebhook } from '../../libs/webhook';
import { model as UserHistory } from '../../models/userHistory';
const analytics = getAnalyticsServiceByEnvironment();
const questScrolls = common.content.quests;
function canStartQuestAutomatically (group) {
@@ -166,17 +162,6 @@ api.inviteToQuest = {
quest,
});
// track that the inviting user has accepted the quest
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
category: 'behavior',
headers: req.headers,
owner: true,
questName: questKey,
response: 'accept',
});
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withQuestInviteResponse(group.quest.key, 'invite')
.commit();
@@ -231,17 +216,6 @@ api.acceptQuest = {
res.respond(200, savedGroup.quest);
// track that a user has accepted the quest
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
owner: false,
response: 'accept',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withQuestInviteResponse(group.quest.key, 'accept')
.commit();
@@ -297,16 +271,6 @@ api.rejectQuest = {
res.respond(200, savedGroup.quest);
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
owner: false,
response: 'reject',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withQuestInviteResponse(group.quest.key, 'reject')
.commit();
@@ -360,16 +324,6 @@ api.forceStart = {
]);
res.respond(200, savedGroup.quest);
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
owner: user._id === group.quest.leader,
response: 'force-start',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
},
};
@@ -1,7 +1,6 @@
import assign from 'lodash/assign';
import find from 'lodash/find';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import moment from 'moment';
import { authWithHeaders } from '../../middlewares/auth';
import {
@@ -330,17 +329,6 @@ api.createChallengeTasks = {
// If adding tasks to a challenge -> sync users
if (challenge) challenge.addTasks(tasks);
tasks.forEach(task => {
res.analytics.track('challenge task created', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
challengeID: challenge._id,
});
});
},
};
@@ -700,17 +688,6 @@ api.updateTask = {
task: savedTask,
});
}
if (group) {
res.analytics.track('task edit', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
groupID: group._id,
});
}
},
};
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import isUUID from 'validator/lib/isUUID';
import { authWithHeaders } from '../../../middlewares/auth';
import * as Tasks from '../../../models/task';
@@ -61,18 +60,6 @@ api.createGroupTasks = {
const tasks = await createTasks(req, res, { user, group });
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
tasks.forEach(task => {
res.analytics.track('team task created', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
groupID: group._id,
headers: req.headers,
});
});
},
};
@@ -251,16 +238,6 @@ api.assignTask = {
await Promise.all(promises);
res.respond(200, task);
res.analytics.track('task assign', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
groupID: group._id,
headers: req.headers,
});
},
};
+17 -33
View File
@@ -1,7 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import forEach from 'lodash/forEach';
import isFunction from 'lodash/isFunction';
import pick from 'lodash/pick';
import nconf from 'nconf';
import get from 'lodash/get';
import { authWithHeaders } from '../../middlewares/auth';
@@ -27,7 +26,6 @@ import * as inboxLib from '../../libs/inbox';
import * as userLib from '../../libs/user';
import { model as UserHistory } from '../../models/userHistory';
const OFFICIAL_PLATFORMS = ['habitica-web', 'habitica-ios', 'habitica-android'];
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
const DELETE_CONFIRMATION = 'DELETE';
@@ -325,13 +323,6 @@ api.deleteUser = {
]);
}
res.analytics.track('account delete', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
});
res.respond(200, {});
},
};
@@ -441,7 +432,7 @@ api.sleep = {
url: '/user/sleep',
async handler (req, res) {
const { user } = res.locals;
const sleepRes = common.ops.sleep(user, req, res.analytics);
const sleepRes = common.ops.sleep(user, req);
await user.save();
res.respond(200, ...sleepRes);
},
@@ -500,10 +491,7 @@ api.buy = {
let quantity = 1;
if (req.body.quantity) quantity = req.body.quantity;
req.quantity = quantity;
if (OFFICIAL_PLATFORMS.indexOf(req.headers['x-client']) === -1) {
res.analytics = undefined;
}
const buyRes = await common.ops.buy(user, req, res.analytics);
const buyRes = await common.ops.buy(user, req);
await user.save();
@@ -558,7 +546,7 @@ api.buyGear = {
url: '/user/buy-gear/:key',
async handler (req, res) {
const { user } = res.locals;
const buyGearRes = await common.ops.buy(user, req, res.analytics);
const buyGearRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyGearRes);
},
@@ -600,10 +588,7 @@ api.buyArmoire = {
const { user } = res.locals;
req.type = 'armoire';
req.params.key = 'armoire';
if (OFFICIAL_PLATFORMS.indexOf(req.headers['x-client']) === -1) {
res.analytics = undefined;
}
const buyArmoireResponse = await common.ops.buy(user, req, res.analytics);
const buyArmoireResponse = await common.ops.buy(user, req);
await user.save();
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withArmoire(buyArmoireResponse[0].armoire.dropKey || 'experience')
@@ -646,7 +631,7 @@ api.buyHealthPotion = {
const { user } = res.locals;
req.type = 'potion';
req.params.key = 'potion';
const buyHealthPotionResponse = await common.ops.buy(user, req, res.analytics);
const buyHealthPotionResponse = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyHealthPotionResponse);
},
@@ -688,7 +673,7 @@ api.buyMysterySet = {
async handler (req, res) {
const { user } = res.locals;
req.type = 'mystery';
const buyMysterySetRes = await common.ops.buy(user, req, res.analytics);
const buyMysterySetRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyMysterySetRes);
},
@@ -731,7 +716,7 @@ api.buyQuest = {
async handler (req, res) {
const { user } = res.locals;
req.type = 'quest';
const buyQuestRes = await common.ops.buy(user, req, res.analytics);
const buyQuestRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyQuestRes);
},
@@ -818,7 +803,7 @@ api.hatch = {
url: '/user/hatch/:egg/:hatchingPotion',
async handler (req, res) {
const { user } = res.locals;
const hatchRes = common.ops.hatch(user, req, res.analytics);
const hatchRes = common.ops.hatch(user, req);
await user.save();
@@ -916,7 +901,7 @@ api.feed = {
url: '/user/feed/:pet/:food',
async handler (req, res) {
const { user } = res.locals;
const feedRes = common.ops.feed(user, req, res.analytics);
const feedRes = common.ops.feed(user, req);
await user.save();
@@ -964,7 +949,7 @@ api.changeClass = {
url: '/user/change-class',
async handler (req, res) {
const { user } = res.locals;
const changeClassRes = await common.ops.changeClass(user, req, res.analytics);
const changeClassRes = await common.ops.changeClass(user, req);
await user.save();
res.respond(200, ...changeClassRes);
},
@@ -1040,7 +1025,7 @@ api.purchase = {
if (req.body.quantity) quantity = req.body.quantity;
req.quantity = quantity;
const purchaseRes = await common.ops.buy(user, req, res.analytics);
const purchaseRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...purchaseRes);
},
@@ -1083,7 +1068,6 @@ api.userPurchaseHourglass = {
const purchaseHourglassRes = await common.ops.buy(
user,
req,
res.analytics,
{ quantity, hourglass: true },
);
await user.save();
@@ -1180,7 +1164,7 @@ api.userOpenMysteryItem = {
url: '/user/open-mystery-item',
async handler (req, res) {
const { user } = res.locals;
const openMysteryItemRes = common.ops.openMysteryItem(user, req, res.analytics);
const openMysteryItemRes = common.ops.openMysteryItem(user, req);
await user.save();
res.respond(200, ...openMysteryItemRes);
},
@@ -1212,7 +1196,7 @@ api.userReleasePets = {
url: '/user/release-pets',
async handler (req, res) {
const { user } = res.locals;
const releasePetsRes = await common.ops.releasePets(user, req, res.analytics);
const releasePetsRes = await common.ops.releasePets(user, req);
await user.save();
res.respond(200, ...releasePetsRes);
},
@@ -1261,7 +1245,7 @@ api.userReleaseBoth = {
url: '/user/release-both',
async handler (req, res) {
const { user } = res.locals;
const releaseBothRes = common.ops.releaseBoth(user, req, res.analytics);
const releaseBothRes = common.ops.releaseBoth(user, req);
await user.save();
res.respond(200, ...releaseBothRes);
},
@@ -1297,7 +1281,7 @@ api.userReleaseMounts = {
url: '/user/release-mounts',
async handler (req, res) {
const { user } = res.locals;
const releaseMountsRes = await common.ops.releaseMounts(user, req, res.analytics);
const releaseMountsRes = await common.ops.releaseMounts(user, req);
await user.save();
res.respond(200, ...releaseMountsRes);
},
@@ -1373,7 +1357,7 @@ api.userUnlock = {
url: '/user/unlock',
async handler (req, res) {
const { user } = res.locals;
const unlockRes = await common.ops.unlock(user, req, res.analytics);
const unlockRes = await common.ops.unlock(user, req);
await user.save();
res.respond(200, ...unlockRes);
},
@@ -1399,7 +1383,7 @@ api.userRevive = {
url: '/user/revive',
async handler (req, res) {
const { user } = res.locals;
const reviveRes = common.ops.revive(user, req, res.analytics);
const reviveRes = common.ops.revive(user, req);
await user.save();
res.respond(200, ...reviveRes);
},
@@ -1,47 +0,0 @@
import pick from 'lodash/pick';
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, {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
headers: req.headers,
category: 'behavior',
...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;
-270
View File
@@ -1,270 +0,0 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import Amplitude from 'amplitude';
import useragent from 'useragent';
import {
omit,
toArray,
} from 'lodash';
import common from '../../common';
import logger from './logger';
const LOG_AMPLITUDE_EVENTS = nconf.get('LOG_AMPLITUDE_EVENTS') === 'true';
const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
const AMPLITUDE_PROPERTIES_TO_SCRUB = [
'uuid', 'user', 'purchaseValue',
'headers', 'registeredThrough',
];
const PLATFORM_MAP = Object.freeze({
'habitica-web': 'Web',
'habitica-ios': 'iOS',
'habitica-android': 'Android',
});
let amplitude;
if (AMPLITUDE_TOKEN) amplitude = new Amplitude(AMPLITUDE_TOKEN);
const Content = common.content;
function _lookUpItemName (itemKey) {
if (!itemKey) return null;
const gear = Content.gear.flat[itemKey];
const egg = Content.eggs[itemKey];
const food = Content.food[itemKey];
const hatchingPotion = Content.hatchingPotions[itemKey];
const quest = Content.quests[itemKey];
const spell = Content.special[itemKey];
let itemName;
if (gear) {
itemName = gear.text();
} else if (egg) {
itemName = `${egg.text()} Egg`;
} else if (food) {
itemName = food.text();
} else if (hatchingPotion) {
itemName = `${hatchingPotion.text()} Hatching Potion`;
} else if (quest) {
itemName = quest.text();
} else if (spell) {
itemName = spell.text();
}
return itemName;
}
function _formatUserData (user) {
const properties = {};
if (user.stats) {
properties.Class = user.stats.class;
properties.Experience = Math.floor(user.stats.exp);
properties.Gold = Math.floor(user.stats.gp);
properties.Health = Math.ceil(user.stats.hp);
properties.Level = user.stats.lvl;
properties.Mana = Math.floor(user.stats.mp);
}
properties.balance = user.balance;
properties.balanceGemAmount = properties.balance * 4;
properties.tutorialComplete = user.flags && user.flags.tour && user.flags.tour.intro === -2;
properties.verifiedUsername = user.flags && user.flags.verifiedUsername;
if (properties.verifiedUsername && user.auth && user.auth.local) {
properties.username = user.auth.local.lowerCaseUsername;
}
if (user.habits && user.dailys && user.todos && user.rewards) {
properties['Number Of Tasks'] = {
habits: user.habits.length,
dailys: user.dailys.length,
todos: user.todos.length,
rewards: user.rewards.length,
};
}
if (user.contributor && user.contributor.level) {
properties.contributorLevel = user.contributor.level;
}
if (user.purchased && user.purchased.plan.planId) {
properties.subscription = user.purchased.plan.planId;
} else {
properties.subscription = null;
}
if (user._ABtests) {
properties.ABtests = toArray(user._ABtests);
}
if (user.loginIncentives) {
properties.loginIncentives = user.loginIncentives;
}
return properties;
}
function _formatPlatformForAmplitude (platform) {
if (!platform) {
return 'Unknown';
}
if (platform in PLATFORM_MAP) {
return PLATFORM_MAP[platform];
}
return '3rd Party';
}
function _formatUserAgentForAmplitude (platform, agentString) {
if (!agentString) {
return 'Unknown';
}
const agent = useragent.lookup(agentString).toJSON();
const 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;
}
function _formatUUIDForAmplitude (uuid) {
return uuid || 'no-user-id-was-provided';
}
function _formatDataForAmplitude (data) {
const event_properties = omit(data, AMPLITUDE_PROPERTIES_TO_SCRUB);
const platform = _formatPlatformForAmplitude(data.headers && data.headers['x-client']);
const agent = _formatUserAgentForAmplitude(platform, data.headers && data.headers['user-agent']);
const ampData = {
user_id: _formatUUIDForAmplitude(data.uuid),
platform,
os_name: agent.name,
os_version: agent.version,
event_properties,
};
if (data.user) {
ampData.user_properties = _formatUserData(data.user);
}
const itemName = _lookUpItemName(data.itemKey);
if (itemName) {
ampData.event_properties.itemName = itemName;
}
return ampData;
}
function _sendDataToAmplitude (eventType, data, loggerOnly) {
const amplitudeData = _formatDataForAmplitude(data);
amplitudeData.event_type = eventType;
if (LOG_AMPLITUDE_EVENTS) {
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.'));
}
function _sendPurchaseDataToAmplitude (data) {
const amplitudeData = _formatDataForAmplitude(data);
// Stripe transactions come via webhook. We can log these as Web events
if (data.paymentMethod === 'Stripe' && amplitudeData.platform === 'Unknown') {
amplitudeData.platform = 'Web';
}
amplitudeData.event_type = 'purchase';
amplitudeData.revenue = data.purchaseValue;
amplitudeData.productId = data.itemPurchased;
if (LOG_AMPLITUDE_EVENTS) {
logger.info('Amplitude Purchase Event', amplitudeData);
}
return amplitude
.track(amplitudeData)
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
function _setOnce (dataToSetOnce, uuid) {
return amplitude
.identify({
user_id: _formatUUIDForAmplitude(uuid),
user_properties: {
$setOnce: dataToSetOnce,
},
})
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
// There's no error handling directly here because it's handled inside _sendDataTo{Amplitude|Google}
async function track (eventType, data, loggerOnly = false) {
const { user } = data;
if (!user || !user.preferences || !user.preferences.analyticsConsent) {
return null;
}
const promises = [
_sendDataToAmplitude(eventType, data, loggerOnly),
];
if (user.registeredThrough) {
promises.push(_setOnce({
registeredPlatform: user.registeredThrough,
}, data.uuid || user._id));
}
return Promise.all(promises);
}
// There's no error handling directly here because
// it's handled inside _sendPurchaseDataTo{Amplitude|Google}
async function trackPurchase (data) {
const { user } = data;
if (!user || !user.preferences || !user.preferences.analyticsConsent) {
return null;
}
return Promise.all([
_sendPurchaseDataToAmplitude(data),
]);
}
// Stub for non-prod environments
const mockAnalyticsService = {
track: () => { },
trackPurchase: () => { },
};
// Return the production or mock service based on the current environment
function getServiceByEnvironment () {
if (nconf.get('IS_PROD') || (nconf.get('DEBUG_ENABLED') && !nconf.get('BASE_URL').includes('localhost'))) {
return {
track,
trackPurchase,
};
}
return mockAnalyticsService;
}
export {
track,
trackPurchase,
mockAnalyticsService,
getServiceByEnvironment as getAnalyticsServiceByEnvironment,
};
+2 -11
View File
@@ -1,5 +1,4 @@
import moment from 'moment';
import pick from 'lodash/pick';
import {
BadRequest,
NotAuthorized,
@@ -19,6 +18,7 @@ import {
} from './social';
import { loginRes } from './utils';
import { verifyUsername } from '../user/validation';
import { trackRegistrationEvent } from '../localAnalytics';
const USERNAME_LENGTH_MIN = 1;
const USERNAME_LENGTH_MAX = 20;
@@ -180,6 +180,7 @@ async function registerLocal (req, res, { isV3 = false }) {
} else {
newUser = new User(newUser);
newUser.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
trackRegistrationEvent({ user: newUser, method: 'local', ipAddress: req.ip });
}
// we check for partyInvite for backward compatibility
@@ -217,16 +218,6 @@ async function registerLocal (req, res, { isV3 = false }) {
})
.catch(err => logger.error(err));
if (!existingUser) {
res.analytics.track('register', {
user: pick(savedUser, ['preferences', 'registeredThrough']),
category: 'acquisition',
type: 'local',
uuid: savedUser._id,
headers: req.headers,
});
}
return null;
}
+2 -11
View File
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import passport from 'passport';
import common from '../../../common';
import { verifyUsername } from '../user/validation';
@@ -13,6 +12,7 @@ import { model as User } from '../../models/user';
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
import { sendTxn as sendTxnEmail } from '../email';
import { apiError } from '../apiError';
import { trackRegistrationEvent } from '../localAnalytics';
function _passportProfile (network, accessToken) {
return new Promise((resolve, reject) => {
@@ -145,6 +145,7 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
};
user = new User(user);
user.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
trackRegistrationEvent({ user, method: network, ipAddress: req.ip });
}
const savedUser = await user.save();
@@ -172,15 +173,5 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
.catch(err => logger.error(err)); // eslint-disable-line max-nested-callbacks
}
if (!existingUser) {
res.analytics.track('register', {
user: pick(savedUser, ['preferences', 'registeredThrough']),
uuid: savedUser._id,
category: 'acquisition',
type: network,
headers: req.headers,
});
}
return response;
}
+1 -20
View File
@@ -1,6 +1,5 @@
import moment from 'moment';
import mongoose from 'mongoose';
import pick from 'lodash/pick';
import nconf from 'nconf';
import { model as User } from '../models/user';
import * as Tasks from '../models/task';
@@ -100,20 +99,6 @@ function processHabits (user, habits, now, daysMissed) {
});
}
function trackCronAnalytics (analytics, user, _progress, options) {
analytics.track('Cron', {
category: 'behavior',
uuid: user._id,
user: pick(user, ['preferences', 'registeredThrough']),
resting: user.preferences.sleep,
cronCount: user.flags.cronCount,
progressUp: Math.min(_progress.up, 900),
progressDown: _progress.down,
headers: options.headers,
loginIncentives: user.loginIncentives,
});
}
function awardLoginIncentives (user) {
if (user.loginIncentives > MAX_INCENTIVES) return;
@@ -165,7 +150,7 @@ function awardLoginIncentives (user) {
// Perform various beginning-of-day reset actions.
export async function cron (options = {}) {
const {
user, tasksByType, analytics, now = new Date(), daysMissed, timezoneUtcOffsetFromUserPrefs,
user, tasksByType, now = new Date(), daysMissed, timezoneUtcOffsetFromUserPrefs,
} = options;
let _progress = { down: 0, up: 0, collectedItems: 0 };
@@ -392,9 +377,7 @@ export async function cron (options = {}) {
user.pinnedItems = common.cleanupPinnedItems(user);
}
// Analytics
user.flags.cronCount += 1;
trackCronAnalytics(analytics, user, _progress, options);
await UserHistory.beginUserHistoryUpdate(user._id, options.headers)
.withCron(user.flags.cronCount)
@@ -438,7 +421,6 @@ export async function cronWrapper (req, res) {
const { user } = res.locals;
if (!user) return null; // User might not be available when authentication is not mandatory
const { analytics } = res;
const now = new Date();
let session;
@@ -488,7 +470,6 @@ export async function cronWrapper (req, res) {
tasksByType,
now,
daysMissed,
analytics,
timezoneUtcOffsetFromUserPrefs,
headers: req.headers,
});
-49
View File
@@ -1,6 +1,5 @@
import find from 'lodash/find';
import includes from 'lodash/includes';
import pick from 'lodash/pick';
import { encrypt } from '../encryption';
import { sendNotification as sendPushNotification } from '../pushNotifications';
@@ -143,23 +142,6 @@ async function inviteByUUID (uuid, group, inviter, req, res) {
));
}
const analyticsObject = {
user: pick(inviter, ['preferences', 'registeredThrough']),
uuid: inviter._id,
hitType: 'event',
category: 'behavior',
invitee: uuid,
groupId: group._id,
groupType: group.type,
headers: req.headers,
};
if (group.type === 'party') {
analyticsObject.seekingParty = Boolean(userToInvite.party.seeking);
}
res.analytics.track('group invite', analyticsObject);
return addInvitationToUser(userToInvite, group, inviter, res);
}
@@ -207,19 +189,6 @@ async function inviteByEmail (invite, group, inviter, req, res) {
const userIsUnsubscribed = await EmailUnsubscription.findOne({ email: invite.email }).exec();
const groupLabel = group.type === 'guild' ? '-guild' : '';
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
const analyticsObject = {
user: pick(inviter, ['preferences', 'registeredThrough']),
uuid: inviter._id,
hitType: 'event',
category: 'behavior',
invitee: 'email',
groupId: group._id,
groupType: group.type,
headers: req.headers,
};
res.analytics.track('group invite', analyticsObject);
}
return userReturnInfo;
@@ -245,24 +214,6 @@ async function inviteByUserName (username, group, inviter, req, res) {
{ userId: userToInvite._id, username: userToInvite.profile.name },
));
}
const analyticsObject = {
user: pick(inviter, ['preferences', 'registeredThrough']),
uuid: inviter._id,
hitType: 'event',
category: 'behavior',
invitee: userToInvite._id,
groupId: group._id,
groupType: group.type,
headers: req.headers,
};
if (group.type === 'party') {
analyticsObject.seekingParty = Boolean(userToInvite.party.seeking);
}
res.analytics.track('group invite', analyticsObject);
return addInvitationToUser(userToInvite, group, inviter, res);
}
+50
View File
@@ -0,0 +1,50 @@
import nconf from 'nconf';
import { RegistrationEventModel } from '../models/analytics/registrationEvent';
import { SubscriptionEventModel } from '../models/analytics/subscriptionEvent';
const LOCAL_ANALYTICS = !nconf.get('DISABLE_LOCAL_ANALYTICS');
function getAuthenticationMethod (user) {
if (user.auth.google && user.auth.google.id) return 'google';
if (user.auth.facebook && user.auth.facebook.id) return 'facebook';
if (user.auth.apple && user.auth.apple.id) return 'apple';
return 'local';
}
export async function trackRegistrationEvent (eventData) {
if (!LOCAL_ANALYTICS) return null;
const { user, ipAddress, method } = eventData;
const registrationEvent = new RegistrationEventModel({
userId: user._id,
ipAddress,
authenticationMethod: method || getAuthenticationMethod(user),
platform: user.registeredThrough,
language: user.preferences.language,
});
return registrationEvent.save();
}
export async function trackSubscriptionEvent (eventData) {
if (!LOCAL_ANALYTICS) return null;
const {
eventType,
user,
paymentMethod,
customerId,
planId,
cancellationReason,
} = eventData;
const subscriptionEvent = new SubscriptionEventModel({
userId: user._id,
eventType,
paymentMethod,
customerId,
planId,
cancellationReason,
});
return subscriptionEvent.save();
}
+14
View File
@@ -38,3 +38,17 @@ export default async function connectToMongoDB () {
}
return null;
}
let analyticsDb;
export function getAnalyticsDatabase () {
if (!analyticsDb) {
const analyticsDbName = nconf.get('ANALYTICS_DB');
const analyticsDbUri = nconf.get('ANALYTICS_DB_URI') || connectionUrl;
analyticsDb = mongoose.createConnection(analyticsDbUri, {
...mongooseOptions,
dbName: analyticsDbName,
});
}
return analyticsDb;
}
-18
View File
@@ -1,6 +1,4 @@
import find from 'lodash/find';
import pick from 'lodash/pick';
import { getAnalyticsServiceByEnvironment } from '../analyticsService';
import { getCurrentEventList } from '../worldState'; // eslint-disable-line import/no-cycle
import { // eslint-disable-line import/no-cycle
getUserInfo,
@@ -13,8 +11,6 @@ import {
} from '../errors';
import { apiError } from '../apiError';
const analytics = getAnalyticsServiceByEnvironment();
function getGiftMessage (data, byUsername, gemAmount, language) {
const senderMsg = shared.i18n.t('giftedGemsFull', {
username: data.gift.member.profile.name,
@@ -114,20 +110,6 @@ export async function buyGems (data) {
if (!data.gift) txnEmail(data.user, 'donation');
analytics.trackPurchase({
user: pick(data.user, ['preferences', 'registeredThrough']),
uuid: data.user._id,
itemPurchased: 'Gems',
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
purchaseType: 'checkout',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: amt,
headers: data.headers,
firstPurchase: data.user.purchased.txnCount === 1,
});
if (data.gift) await buyGemGift(data);
await data.user.save();
-17
View File
@@ -1,14 +1,10 @@
import pick from 'lodash/pick';
import moment from 'moment';
import {
BadRequest,
} from '../errors';
import shared from '../../../common';
import { getAnalyticsServiceByEnvironment } from '../analyticsService';
import { getGemsBlock, buyGems } from './gems'; // eslint-disable-line import/no-cycle
const analytics = getAnalyticsServiceByEnvironment();
const RESPONSE_INVALID_ITEM = 'INVALID_ITEM_PURCHASED';
const EVENTS = {
@@ -31,19 +27,6 @@ async function buyGryphatrice (data) {
data.user.items.pets[key] = 5;
data.user.purchased.txnCount += 1;
analytics.trackPurchase({
user: pick(data.user, ['preferences', 'registeredThrough']),
uuid: data.user._id,
itemPurchased: 'Gryphatrice',
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
purchaseType: 'checkout',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: 10,
headers: data.headers,
firstPurchase: data.user.purchased.txnCount === 1,
});
if (data.user.markModified) data.user.markModified('items.pets');
await data.user.save();
}
+28 -42
View File
@@ -3,10 +3,7 @@
import defaults from 'lodash/defaults';
import each from 'lodash/each';
import find from 'lodash/find';
import pick from 'lodash/pick';
import moment from 'moment';
import { getAnalyticsServiceByEnvironment } from '../analyticsService';
import * as slack from '../slack'; // eslint-disable-line import/no-cycle
import { // eslint-disable-line import/no-cycle
getUserInfo,
@@ -26,10 +23,10 @@ import calculateSubscriptionTerminationDate from './calculateSubscriptionTermina
import { getCurrentEventList } from '../worldState'; // eslint-disable-line import/no-cycle
import { paymentConstants } from './constants';
import { addSubscriptionToGroupUsers, cancelGroupUsersSubscription } from './groupPayments'; // eslint-disable-line import/no-cycle
import { trackSubscriptionEvent } from '../localAnalytics';
// @TODO: Abstract to shared/constant
const JOINED_GROUP_PLAN = 'joined group plan';
const analytics = getAnalyticsServiceByEnvironment();
function _findMysteryItems (user, dateMoment) {
const pushedItems = [];
@@ -81,6 +78,14 @@ async function prepareSubscriptionValues (data) {
? shared.content.subscriptionBlocks[data.updatedFrom.key]
: undefined;
let months;
let subscriptionEventType = 'subscribed';
if (updatedFrom) {
if (Number(updatedFrom.months) > Number(block.months)) {
subscriptionEventType = 'downgraded';
} else {
subscriptionEventType = 'upgraded';
}
}
if (updatedFrom && Number(updatedFrom.months) !== 1) {
if (Number(updatedFrom.months) > Number(block.months)) {
months = 0;
@@ -126,13 +131,6 @@ async function prepareSubscriptionValues (data) {
user: data.user, groupId: data.groupId, populateLeader: false, groupFields,
});
if (group) {
analytics.track(
data.groupID,
data.demographics,
);
}
if (!group) {
throw new NotFound(shared.i18n.t('groupNotFound'));
}
@@ -230,6 +228,7 @@ async function prepareSubscriptionValues (data) {
purchaseType,
emailType,
isNewSubscription,
subscriptionEventType,
};
}
@@ -242,10 +241,9 @@ async function createSubscription (data) {
autoRenews,
group,
groupId,
itemPurchased,
purchaseType,
emailType,
isNewSubscription,
subscriptionEventType,
} = await prepareSubscriptionValues(data);
if (recipient !== group) {
recipient.items.pets['Jackalope-RoyalPurple'] = 5;
@@ -277,22 +275,6 @@ async function createSubscription (data) {
if (!group && !data.promo) data.user.purchased.txnCount += 1;
if (!data.promo) {
analytics.trackPurchase({
uuid: data.user._id,
groupId,
itemPurchased,
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
purchaseType,
paymentMethod: data.paymentMethod,
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: block.price,
headers: data.headers || { 'x-client': 'habitica-web' },
firstPurchase: !group && data.user.purchased.txnCount === 1,
});
}
if (data.gift) {
const byUserName = getUserInfo(data.user, ['name']).name;
@@ -381,6 +363,16 @@ async function createSubscription (data) {
if (data.user && data.user.isModified()) await data.user.save();
if (data.gift) await data.gift.member.save();
await trackSubscriptionEvent({
eventType: subscriptionEventType,
user: data.gift ? data.gift.member : data.user,
gifted: data.gift !== undefined,
autoRenews,
paymentMethod: data.paymentMethod,
planId: block.key,
customerId: plan.customerId,
});
slack.sendSubscriptionNotification({
buyer: {
id: data.user._id,
@@ -403,8 +395,6 @@ async function createSubscription (data) {
async function cancelSubscription (data) {
let plan;
let group;
let cancelType = 'unsubscribe';
let groupId;
let emailType;
const emailMergeData = [];
let sendEmail = true;
@@ -462,17 +452,13 @@ async function cancelSubscription (data) {
txnEmail(data.user, emailType, emailMergeData);
}
if (group) {
cancelType = 'group-unsubscribe';
groupId = group._id;
}
analytics.track(cancelType, {
uuid: data.user._id,
user: pick(data.user, ['preferences', 'registeredThrough']),
groupId,
paymentMethod: data.paymentMethod,
headers: data.headers,
await trackSubscriptionEvent({
eventType: 'cancelled',
user: data.user,
cancellationReason: data.cancellationReason,
paymentMethod: plan.paymentMethod,
planId: plan.planId,
customerId: plan.customerId,
});
}
+4 -27
View File
@@ -3,7 +3,6 @@ import cloneDeep from 'lodash/cloneDeep';
import compact from 'lodash/compact';
import forEach from 'lodash/forEach';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import remove from 'lodash/remove';
import validator from 'validator';
import {
@@ -77,7 +76,7 @@ async function createTasks (req, res, options = {}) {
// are the onboarding ones
if (!user.achievements.createdTask && user.flags.welcomed) {
user.addAchievement('createdTask');
shared.onboarding.checkOnboardingStatus(user, req, res.analytics);
shared.onboarding.checkOnboardingStatus(user, req);
}
}
@@ -462,14 +461,14 @@ async function scoreTask (user, task, direction, req, res) {
task,
user: rollbackUser,
direction,
}, req, res.analytics);
}, req);
await rollbackUser.save();
} else {
delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics);
delta = shared.ops.scoreTask({ task, user, direction }, req);
}
// Drop system (don't run on the client,
// as it would only be discarded since ops are sent to the API, not the results)
if (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task, delta }, req, res.analytics);
if (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task, delta }, req);
// If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list
// TODO move to common code?
@@ -506,28 +505,6 @@ async function scoreTask (user, task, direction, req, res) {
user,
});
if (group) {
let role;
if (group.leader === user._id) {
role = 'leader';
} else if (group.managers[user._id]) {
role = 'manager';
} else {
role = 'member';
}
res.analytics.track('team task scored', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
direction,
headers: req.headers,
groupID: group._id,
role,
});
}
return {
task,
delta,
+2 -24
View File
@@ -116,13 +116,6 @@ export async function update (req, res, { isV3 = false }) {
if (req.body['party.seeking'] !== undefined && req.body['party.seeking'] !== null) {
user.invitations.party = {};
user.invitations.parties = [];
res.analytics.track('Starts Looking for Party', {
user: _.pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
}
let slurWasUsed = false;
@@ -200,13 +193,6 @@ export async function update (req, res, { isV3 = false }) {
if (key === 'party.seeking' && val === null) {
user.party.seeking = undefined;
res.analytics.track('Leaves Looking for Party', {
user: _.pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
} else if (key === 'tags') {
if (!Array.isArray(val)) throw new BadRequest('Tag list must be an array.');
@@ -291,14 +277,6 @@ export async function reset (req, res, { isV3 = false }) {
user.save(),
]);
res.analytics.track('account reset', {
user: _.pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
res.respond(200, ...resetRes);
}
@@ -310,7 +288,7 @@ export async function reroll (req, res, { isV3 = false }) {
...Tasks.taskIsGroupOrChallengeQuery,
};
const tasks = await Tasks.Task.find(query).exec();
const rerollRes = await common.ops.reroll(user, tasks, req, res.analytics);
const rerollRes = await common.ops.reroll(user, tasks, req);
if (isV3) {
rerollRes[0].user = await rerollRes[0].user.toJSONWithInbox();
}
@@ -331,7 +309,7 @@ export async function rebirth (req, res, { isV3 = false }) {
...Tasks.taskIsGroupOrChallengeQuery,
}).exec();
const rebirthRes = await common.ops.rebirth(user, tasks, req, res.analytics);
const rebirthRes = await common.ops.rebirth(user, tasks, req);
if (isV3) {
rebirthRes[0].user = await rebirthRes[0].user.toJSONWithInbox();
}
-11
View File
@@ -1,11 +0,0 @@
import {
getAnalyticsServiceByEnvironment,
} from '../libs/analyticsService';
const service = getAnalyticsServiceByEnvironment();
export default function attachAnalytics (req, res, next) {
res.analytics = service;
next();
}
-2
View File
@@ -1,7 +1,6 @@
import express from 'express';
import expressValidator from 'express-validator';
import path from 'path';
import analytics from './analytics';
import setupBody from './setupBody';
import rateLimiter from './rateLimiter';
import setupExpress from '../libs/setupExpress';
@@ -17,7 +16,6 @@ const app = express();
setupExpress(app);
app.use(expressValidator());
app.use(analytics);
app.use(setupBody);
const topLevelRouter = express.Router(); // eslint-disable-line new-cap
@@ -0,0 +1,32 @@
import mongoose from 'mongoose';
import validator from 'validator';
import baseModel from '../../libs/baseModel';
import { getAnalyticsDatabase } from '../../libs/mongoose';
const { Schema } = mongoose;
export const schema = new Schema({
userId: {
$type: String, ref: 'User', required: true, validate: [v => validator.isUUID(v), 'Invalid uuid for user.'],
},
ipAddress: { $type: String },
platform: { $type: String },
authenticationMethod: { $type: String },
language: { $type: String },
}, {
strict: true,
typeKey: '$type',
});
schema.plugin(baseModel, {
noSet: [
'id',
'_id',
'userId',
'platform',
'authenticationMethod',
], // Nothing can be set from the client
timestamps: true,
});
export const RegistrationEventModel = getAnalyticsDatabase().model('RegistrationEvent', schema);
@@ -0,0 +1,38 @@
import mongoose from 'mongoose';
import validator from 'validator';
import baseModel from '../../libs/baseModel';
import { getAnalyticsDatabase } from '../../libs/mongoose';
const { Schema } = mongoose;
const eventTypes = ['subscribed', 'cancelled', 'resubscribed', 'upgraded', 'downgraded'];
export const schema = new Schema({
userId: {
$type: String, required: true, validate: [v => validator.isUUID(v), 'Invalid uuid for user.'],
},
ipAddress: { $type: String },
eventType: { $type: String, enum: eventTypes, required: true },
paymentMethod: { $type: String },
customerId: { $type: String },
planId: { $type: String },
cancellationReason: { $type: String },
}, {
strict: true,
typeKey: '$type',
});
schema.plugin(baseModel, {
noSet: [
'id',
'_id',
'userId',
'eventType',
'paymentMethod',
'customerId',
'planId',
'cancellationReason',
], // Nothing can be set from the client
timestamps: true,
});
export const SubscriptionEventModel = getAnalyticsDatabase().model('SubscriptionEvent', schema);
+3 -1
View File
@@ -31,7 +31,9 @@ process.on('SIGTERM', async () => {
console.log('SIGTERM signal received: closing HTTP server');
server.close(async () => {
await mongoose.disconnect();
await redis.quit();
if (redis.quit) {
await redis.quit();
}
process.exit(0);
});
});