Compare commits

...

21 Commits

Author SHA1 Message Date
Phillip Thelen
40d5172972 fix anonymizing properties 2025-11-24 15:55:07 +01:00
Phillip Thelen
ebb58e4470 send anonymized data if opted out 2025-09-19 11:52:57 +02:00
Phillip Thelen
c3ef26b2f3 remove invitee field from analytics 2025-09-18 11:22:41 +02:00
Phillip Thelen
108bd59296 call correct method to update user data 2025-09-18 11:15:33 +02:00
Kalista Payne
5228ed40d1 fix(lint): remove undef function call 2025-09-17 15:58:44 -05:00
Kalista Payne
becb6e49f0 fix(merge): put back settings mixins 2025-09-17 15:51:43 -05:00
Kalista Payne
aa8f0f0c4e chore(subproj): merge habitica-images 2025-09-17 15:47:04 -05:00
Kalista Payne
d1891f4c43 Merge branch 'develop' into phillip/server-analytics 2025-09-17 15:45:45 -05:00
Phillip Thelen
2381be8c46 remove old import 2025-09-10 17:50:33 +02:00
Phillip Thelen
5c7545f32a cleanup 2025-09-10 17:38:52 +02:00
Phillip Thelen
ffed5a9a97 update package locks 2025-09-10 12:58:39 +02:00
Phillip Thelen
cd58ce2233 remove google analytics 2025-09-10 12:50:41 +02:00
Phillip Thelen
a2b5e3621e refactor amplitude event properties 2025-09-10 12:44:17 +02:00
Phillip Thelen
a06dfc9ed8 allow mobile to send analytics calls 2025-09-05 16:12:23 +02:00
Phillip Thelen
58b0e323a3 anonymize all uuids 2025-09-05 16:10:41 +02:00
Phillip Thelen
9ca60d7551 anonymize user data if they didn’t consent to analytics 2025-09-05 12:55:56 +02:00
Phillip Thelen
6f63583a12 use ip-lookup-api to determine users country 2025-09-05 12:51:54 +02:00
Phillip Thelen
d952239d35 fix imports 2025-09-04 14:37:42 +02:00
Phillip Thelen
2c7f6fd9e3 properly update user properties in events 2025-09-04 12:57:35 +02:00
Phillip Thelen
ddba450630 add new api call to allow client to update amplitude events 2025-09-04 12:57:18 +02:00
Phillip Thelen
187238d39a remove amplitude from client 2025-09-04 12:56:57 +02:00
58 changed files with 8249 additions and 5648 deletions

13394
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.15",
"@google-analytics/data": "^4.12.1",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.2.3",
"@slack/webhook": "^6.1.0",
@@ -43,6 +42,7 @@
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
"in-app-purchase": "^1.11.3",
"ip-location-api": "^4.0.0",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^2.1.5",

View File

@@ -12,7 +12,6 @@
"@froxz/vite-plugin-s3": "^1.6.0",
"@vitejs/plugin-vue2": "^2.3.3",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"assert": "^2.1.0",
"autoprefixer": "^10.4.20",
"axios": "^0.28.0",
@@ -68,49 +67,6 @@
"node": ">=0.10.0"
}
},
"node_modules/@amplitude/analytics-connector": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz",
"integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g=="
},
"node_modules/@amplitude/types": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.2.tgz",
"integrity": "sha512-I8qenRI7uU6wKNb9LiZrAosSHVoNHziXouKY81CrqxH9xhVTEIJFXeuCV0hbtBr0Al/8ejnGjQRx+S2SvU/pPg==",
"engines": {
"node": ">=10"
}
},
"node_modules/@amplitude/ua-parser-js": {
"version": "0.7.33",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
"integrity": "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/@amplitude/utils": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.2.tgz",
"integrity": "sha512-tVsHXu61jITEtRjB7NugQ5cVDd4QDzne8T3ifmZye7TiJeUfVRvqe44gDtf55A+7VqhDhyEIIXTA1iVcDGqlEw==",
"dependencies": {
"@amplitude/types": "^1.10.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@@ -1231,6 +1187,7 @@
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz",
"integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -3050,19 +3007,6 @@
"ajv": "^6.9.1"
}
},
"node_modules/amplitude-js": {
"version": "8.21.9",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
"integrity": "sha512-d0jJH00wbXu7sxKtVwkdSXtVffjqdUrxuACKlnzP7jU5qt9wriXXMgHifdH5Oq+buKmyF8wKL9S02gAykysURA==",
"dependencies": {
"@amplitude/analytics-connector": "^1.4.6",
"@amplitude/ua-parser-js": "0.7.33",
"@amplitude/utils": "^1.10.2",
"@babel/runtime": "^7.21.0",
"blueimp-md5": "^2.19.0",
"query-string": "8.1.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -3351,11 +3295,6 @@
"node": ">=8"
}
},
"node_modules/blueimp-md5": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
},
"node_modules/bootstrap": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
@@ -3768,14 +3707,6 @@
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"dev": true
},
"node_modules/decode-uri-component": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
"integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
"engines": {
"node": ">=14.16"
}
},
"node_modules/deep-eql": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
@@ -4987,17 +4918,6 @@
"node": ">=8"
}
},
"node_modules/filter-obj": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
"integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -7283,22 +7203,6 @@
"node": ">=6"
}
},
"node_modules/query-string": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz",
"integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==",
"dependencies": {
"decode-uri-component": "^0.4.1",
"filter-obj": "^5.1.0",
"split-on-first": "^3.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ramda": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
@@ -7344,7 +7248,8 @@
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
@@ -7858,17 +7763,6 @@
"source-map": "^0.6.0"
}
},
"node_modules/split-on-first": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz",
"integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",

View File

@@ -16,7 +16,6 @@
"@froxz/vite-plugin-s3": "^1.6.0",
"@vitejs/plugin-vue2": "^2.3.3",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"assert": "^2.1.0",
"autoprefixer": "^10.4.20",
"axios": "^0.28.0",

View File

@@ -445,7 +445,7 @@ export default {
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);

View File

@@ -1,8 +1,8 @@
<template>
<div
class="banner d-flex align-items-center justify-content-between py-3 px-4"
id="privacy-banner"
v-if="!hidden"
id="privacy-banner"
class="banner d-flex align-items-center justify-content-between py-3 px-4"
>
<p
class="mr-3 mb-0"

View File

@@ -546,7 +546,7 @@ export default {
eventCategory: 'behavior',
demographics: this.upgradedGroup.demographics,
type: this.paymentData.group.type,
}, { trackOnClient: true });
});
}
this.paymentData = {};
this.$root.$emit('bv::hide::modal', 'payments-success-modal');

View File

@@ -64,9 +64,11 @@
<li>sexual orientation; and</li>
<li>information collected from a known child.</li>
</ul>
<p><strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong></p>
<p>
<strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong>
</p>
<h3 id="section_1_1">
1.1 Information You Provide Directly
</h3>
@@ -617,7 +619,7 @@
7. General Audience Services
</h2>
<p>
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>, and we will delete that information from our databases.
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>, and we will delete that information from our databases.
</p>
<h2 id="section_8">
@@ -708,7 +710,7 @@
<p><strong><u>Nevada Residents</u></strong></p>
<p>
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>.
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>.
</p>
<p><strong><u>Notice to United Kingdom/European/Switzerland Residents.</u></strong></p>
<p>

View File

@@ -15,8 +15,8 @@
<router-view />
</div>
<div
id="bottom-background"
v-if="loginFlow"
id="bottom-background"
class="bg-purple-300"
>
<div class="seamless_mountains_demo_repeat"></div>
@@ -31,7 +31,10 @@
id="bottom-wrap"
class="purple-4"
>
<div id="bottom-background" v-if="!loginFlow">
<div
v-if="!loginFlow"
id="bottom-background"
>
<div class="seamless_mountains_demo_repeat"></div>
<div class="midground_foreground_extended2"></div>
</div>

View File

@@ -158,7 +158,7 @@
BY PURCHASING PREMIUM YOU EXPRESSLY UNDERSTAND AND AGREE TO OUR REFUND POLICY:
</p>
<p>
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href='mailto:admin@habitica.com'>ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href="mailto:admin@habitica.com">ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
</p>
<p>
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a

View File

@@ -25,8 +25,8 @@
type="checkbox"
:checked="isChecked"
:value="value"
@change="handleChange"
:disabled="disabled"
@change="handleChange"
>
<label
class="toggle-switch-label"

View File

@@ -1,21 +1,11 @@
import forEach from 'lodash/forEach';
import isEqual from 'lodash/isEqual';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import amplitude from 'amplitude-js';
import { gtag, install } from 'ga-gtag';
import Vue from 'vue';
import getStore from '@/store';
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const DEBUG_ENABLED = import.meta.env.DEBUG_ENABLED === 'true';
const GA_ID = import.meta.env.GA_ID;
const IS_PRODUCTION = import.meta.env.NODE_ENV === 'production';
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
let analyticsLoading = false;
let analyticsReady = false;
function _getConsentedUser () {
const store = getStore();
const user = store.state.user.data;
@@ -66,49 +56,24 @@ function _gatherUserStats (properties) {
if (user.purchased.plan.planId) properties.subscription = user.purchased.plan.planId;
}
export function safeSetup (userId) {
if (analyticsLoading || analyticsReady) return;
analyticsLoading = true;
install(GA_ID, {
debug_mode: DEBUG_ENABLED || !IS_PRODUCTION,
user_id: userId,
});
amplitude.getInstance().init(AMPLITUDE_KEY, userId);
analyticsReady = true;
analyticsLoading = false;
}
export function track (properties, options = {}) {
export function track (properties) {
const user = _getConsentedUser();
if (!user) return;
safeSetup(user._id);
// Use nextTick to avoid blocking the UI
Vue.nextTick(() => {
if (_doesNotHaveRequiredFields(properties)) return;
const trackOnClient = options && options.trackOnClient === true;
// Track events on the server by default
if (trackOnClient === true) {
amplitude.getInstance().logEvent(properties.eventAction, properties);
gtag('event', properties.eventAction, properties);
} else {
const store = getStore();
store.dispatch('analytics:trackEvent', properties);
}
const store = getStore();
store.dispatch('analytics:trackEvent', properties);
});
}
export function updateUser (properties = {}) {
const user = _getConsentedUser();
if (!user) return;
safeSetup(user._id);
// Use nextTick to avoid blocking the UI
Vue.nextTick(() => {
_gatherUserStats(properties);
gtag('set', 'user_properties', properties);
forEach(properties, (value, key) => {
const identify = new amplitude.Identify().set(key, value);
amplitude.getInstance().identify(identify);
});
const store = getStore();
store.dispatch('analytics:updateUserProperties', properties);
});
}

View File

@@ -215,7 +215,7 @@ export default {
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

View File

@@ -66,7 +66,7 @@ export default {
uuid: user._id,
taskType: task.type,
direction,
}, { trackOnClient: true });
});
if (!tasksScoredCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
} else {

View File

@@ -1,7 +1,8 @@
<template>
<tr>
<td colspan="3"
<td
v-if="!mixinData.inlineSettingMixin.modalVisible"
colspan="3"
>
<div class="d-flex justify-content-between align-items-center">
<h3
@@ -18,8 +19,9 @@
</a>
</div>
</td>
<td colspan="3"
<td
v-if="mixinData.inlineSettingMixin.modalVisible"
colspan="3"
>
<h3
v-once
@@ -59,8 +61,8 @@
{{ $t('performanceAnalytics') }}
</label>
<toggle-switch
class="mb-auto"
v-model="user.preferences.analyticsConsent"
class="mb-auto"
@change="prefToggled()"
/>
</div>

View File

@@ -128,7 +128,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';
@@ -280,7 +279,6 @@ export default {
return null;
}
}
Analytics.updateUser();
return axios.get(
'/api/v4/i18n/browser-script',
{

View File

@@ -320,7 +320,7 @@ router.beforeEach(async (to, from, next) => {
eventName: 'View Find Members',
eventAction: 'View Find Members',
eventCategory: 'behavior',
}, { trackOnClient: true });
});
}
// Redirect old guild urls

View File

@@ -2,6 +2,10 @@ import axios from 'axios';
export async function trackEvent (store, params) {
const url = `/analytics/track/${params.eventAction}`;
await axios.post(url, params);
}
export async function updateUserProperties (store, params) {
const url = '/analytics/update';
await axios.post(url, params);
}

View File

@@ -1,8 +1,6 @@
import axios from 'axios';
import { authAsCredentialsState, LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
const GA_ID = import.meta.env.GA_ID;
function saveLocalDataAuth (store, apiId, apiToken) {
const credentialsObj = {
auth: {
@@ -123,9 +121,6 @@ export async function appleAuth (store, params) {
export function logout (store, options = {}) {
localStorage.clear();
sessionStorage.clear();
if (window.gtag) {
window.gtag('config', GA_ID, { user_id: null });
}
const query = options.redirectToLogin === true ? '?redirectToLogin=true' : '';
window.location.href = `/logout-server${query}`;
}

View File

@@ -120,7 +120,7 @@ export async function create (store, createdTask) {
hitType: 'event',
uuid,
taskType: taskRes.type,
}, { trackOnClient: true });
});
if (!tasksCreatedCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
} else {

View File

@@ -26,11 +26,9 @@ const envVars = [
'EMAILS_COMMUNITY_MANAGER_EMAIL',
'EMAILS_TECH_ASSISTANCE_EMAIL',
'EMAILS_PRESS_ENQUIRY_EMAIL',
'GA_ID',
'STRIPE_PUB_KEY',
'GOOGLE_CLIENT_ID',
'APPLE_AUTH_CLIENT_ID',
'AMPLITUDE_KEY',
'LOGGLY_CLIENT_TOKEN',
'TRUSTED_DOMAINS',
'TIME_TRAVEL_ENABLED',

View File

@@ -3,7 +3,6 @@ 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';
@@ -160,7 +159,7 @@ export default function randomDrop (user, options, req = {}, analytics) {
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('dropped item', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
itemKey: drop.key,
category: 'behavior',

View File

@@ -1,5 +1,3 @@
import pick from 'lodash/pick';
export function hasCompletedOnboarding (user) {
return (
user.achievements.createdTask === true
@@ -21,7 +19,7 @@ export function checkOnboardingStatus (user, req, analytics) {
user.addNotification('ONBOARDING_COMPLETE');
if (analytics) {
analytics.track('onboarding complete', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

View File

@@ -1,7 +1,6 @@
/* 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,
@@ -114,7 +113,7 @@ export class AbstractBuyOperation {
sendToAnalytics (additionalData = {}) {
// spread-operator produces an "unexpected token" error
const analyticsData = merge(additionalData, {
user: pick(this.user, ['preferences', 'registeredThrough']),
user: this.user,
uuid: this.user._id,
category: 'behavior',
headers: this.req.headers,

View File

@@ -73,7 +73,7 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-d
this.analytics.track(
'Enchanted Armoire',
{
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
itemKey: key,
category: 'behavior',

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 {
@@ -37,7 +36,7 @@ export default async function buyMysterySet (user, req = {}, analytics) {
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
itemKey: mysterySet.key,
itemType: 'Subscriber Gear',

View File

@@ -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 {
@@ -96,7 +95,7 @@ export default async function purchaseHourglass (user, req = {}, analytics, quan
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
itemKey: key,
itemType: type,

View File

@@ -132,7 +132,7 @@ export default async function purchase (user, req = {}, analytics) {
/* eslint-enable no-await-in-loop */
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
itemKey: key,
itemType: type,

View File

@@ -70,7 +70,7 @@ export default async function changeClass (user, req = {}, analytics) {
if (analytics) {
analytics.track('change class', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
class: klass,
currency: balanceRemoved === 0 ? 'Free' : 'Gems',

View File

@@ -2,7 +2,6 @@ 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';
@@ -143,7 +142,7 @@ 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']),
user,
uuid: user._id,
foodKey: food.key,
petKey: pet.key,

View File

@@ -2,7 +2,6 @@ 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';
@@ -154,7 +153,7 @@ 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']),
user,
uuid: user._id,
petKey: pet,
category: 'behavior',

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 { MAX_LEVEL } from '../constants';
@@ -23,7 +22,7 @@ export default async function rebirth (user, tasks = [], req = {}, analytics) {
const analyticsData = {
uuid: user._id,
user: pick(user, ['preferences', 'registeredThrough']),
user,
category: 'behavior',
};

View File

@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import content from '../content/index';
import { mountMasterProgress } from '../count';
import i18n from '../i18n';
@@ -44,7 +43,7 @@ export default async function releaseMounts (user, req = {}, analytics) {
if (analytics) {
analytics.track('release mounts', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
currency: 'Gems',
gemCost: 4,

View File

@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import content from '../content/index';
import { beastMasterProgress } from '../count';
import i18n from '../i18n';
@@ -44,7 +43,7 @@ export default function releasePets (user, req = {}, analytics) {
if (analytics) {
analytics.track('release pets', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
currency: 'Gems',
gemCost: 4,

View File

@@ -1,5 +1,4 @@
import each from 'lodash/each';
import pick from 'lodash/pick';
import i18n from '../i18n';
import {
NotAuthorized,
@@ -24,7 +23,7 @@ export default async function reroll (user, tasks = [], req = {}, analytics) {
if (analytics) {
analytics.track('Fortify Potion', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
currency: 'Gems',
gemCost: 4,

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';
@@ -112,7 +111,7 @@ export default function revive (user, req = {}, analytics) {
if (analytics) {
analytics.track('Death', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
lostItem,
category: 'behavior',

View File

@@ -1,11 +1,9 @@
import pick from 'lodash/pick';
export function sleep (user, req = {}, analytics) {
user.preferences.sleep = !user.preferences.sleep;
if (analytics) {
analytics.track('sleep', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
status: user.preferences.sleep,
category: 'behavior',

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';
@@ -318,7 +317,7 @@ export default async function unlock (user, req = {}, analytics) {
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
itemKey: path,
itemType: 'customization',

View File

@@ -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 {
@@ -128,7 +127,7 @@ api.loginLocal = {
await user.save();
res.analytics.track('login', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
category: 'behavior',
type: 'local',
uuid: user._id,

View File

@@ -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';
@@ -291,7 +290,7 @@ api.createChallenge = {
response.group = getChallengeGroupResponse(group);
res.analytics.track('challenge create', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -360,7 +359,7 @@ api.joinChallenge = {
response.leader = chalLeader ? chalLeader.toJSON({ minimize: true }) : null;
res.analytics.track('challenge join', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -411,7 +410,7 @@ api.leaveChallenge = {
await challenge.unlinkTasks(user, keep);
res.analytics.track('challenge leave', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -896,7 +895,7 @@ api.deleteChallenge = {
await challenge.closeChal({ broken: 'CHALLENGE_DELETED' });
res.analytics.track('challenge delete', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -957,7 +956,7 @@ api.selectChallengeWinner = {
await challenge.closeChal({ broken: 'CHALLENGE_CLOSED', winner });
res.analytics.track('challenge close', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

View File

@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import moment from 'moment';
import nconf from 'nconf';
import { authWithHeaders } from '../../middlewares/auth';
@@ -187,7 +186,7 @@ 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']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -239,7 +238,7 @@ api.postChat = {
await Promise.all(toSave);
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

View File

@@ -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';
@@ -169,7 +168,7 @@ api.createGroup = {
};
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -220,7 +219,7 @@ api.createGroupPlan = {
const savedGroup = results[1];
res.analytics.track('join group', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -705,7 +704,7 @@ api.joinGroup = {
promises.push(group.save());
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

View File

@@ -1,5 +1,4 @@
import escapeRegExp from 'lodash/escapeRegExp';
import pick from 'lodash/pick';
import { authWithHeaders } from '../../middlewares/auth';
import {
model as User,
@@ -737,7 +736,7 @@ api.transferGems = {
if (res.analytics) {
res.analytics.track('transfer gems', {
user: pick(sender, ['preferences', 'registeredThrough']),
user: sender,
uuid: sender._id,
hitType: 'event',
category: 'behavior',

View File

@@ -1,7 +1,6 @@
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 {
@@ -168,7 +167,7 @@ api.inviteToQuest = {
// track that the inviting user has accepted the quest
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
category: 'behavior',
headers: req.headers,
@@ -233,7 +232,7 @@ api.acceptQuest = {
// track that a user has accepted the quest
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
category: 'behavior',
owner: false,
response: 'accept',
@@ -298,7 +297,7 @@ api.rejectQuest = {
res.respond(200, savedGroup.quest);
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
category: 'behavior',
owner: false,
response: 'reject',
@@ -362,7 +361,7 @@ api.forceStart = {
res.respond(200, savedGroup.quest);
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
category: 'behavior',
owner: user._id === group.quest.leader,
response: 'force-start',

View File

@@ -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 {
@@ -333,7 +332,7 @@ api.createChallengeTasks = {
tasks.forEach(task => {
res.analytics.track('challenge task created', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -703,7 +702,7 @@ api.updateTask = {
if (group) {
res.analytics.track('task edit', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

View File

@@ -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';
@@ -64,7 +63,7 @@ api.createGroupTasks = {
tasks.forEach(task => {
res.analytics.track('team task created', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -253,7 +252,7 @@ api.assignTask = {
res.respond(200, task);
res.analytics.track('task assign', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

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';
@@ -326,7 +325,7 @@ api.deleteUser = {
}
res.analytics.track('account delete', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

View File

@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import {
NotAuthorized,
} from '../../libs/errors';
@@ -22,16 +21,17 @@ api.trackEvent = {
// 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.');
if (req.headers['x-client'] !== 'habitica-web'
&& req.headers['x-client'] !== 'habitica-ios'
&& req.headers['x-client'] !== 'habitica-android') {
throw new NotAuthorized('Only official clients are allowed to track analytics events.');
}
const { user } = res.locals;
const eventProperties = req.body;
res.analytics.track(req.params.eventName, {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
headers: req.headers,
category: 'behavior',
@@ -44,4 +44,31 @@ api.trackEvent = {
},
};
api.updateUserProperties = {
method: 'POST',
url: '/analytics/update',
// we authenticate these requests to make sure they actually came from a real user
middlewares: [authWithHeaders()],
async handler (req, res) {
if (req.headers['x-client'] !== 'habitica-web'
&& req.headers['x-client'] !== 'habitica-ios'
&& req.headers['x-client'] !== 'habitica-android') {
throw new NotAuthorized('Only official clients are allowed to track analytics events.');
}
const { user } = res.locals;
const properties = req.body;
res.analytics.updateUserData({
user,
uuid: user._id,
properties,
});
// 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

@@ -2,6 +2,9 @@
import nconf from 'nconf';
import Amplitude from 'amplitude';
import useragent from 'useragent';
import validator from 'validator';
import { lookup } from 'ip-location-api';
import { createHash } from 'crypto';
import {
omit,
toArray,
@@ -27,6 +30,36 @@ if (AMPLITUDE_TOKEN) amplitude = new Amplitude(AMPLITUDE_TOKEN);
const Content = common.content;
function _hashUUID (uuid) {
return createHash('sha256').update(uuid).digest('hex');
}
function _anonymizeProperties (properties) {
if (Array.isArray(properties)) {
return properties.map(userProp => {
if (typeof userProp === 'string' && validator.isEmail(userProp)) {
return _hashUUID(userProp);
}
return userProp;
});
}
if (typeof properties === 'object' && properties !== null) {
const anonymizedProps = {};
Object.keys(properties).forEach(key => {
const value = properties[key];
if (typeof value === 'string' && validator.isEmail(value)) {
anonymizedProps[key] = _hashUUID(value);
} else if (typeof value === 'object' && value !== null) {
anonymizedProps[key] = _anonymizeProperties(value);
} else {
anonymizedProps[key] = value;
}
});
return anonymizedProps;
}
return properties;
}
function _lookUpItemName (itemKey) {
if (!itemKey) return null;
@@ -56,7 +89,7 @@ function _lookUpItemName (itemKey) {
return itemName;
}
function _formatUserData (user) {
function _formatUserData (user, ipaddress, anonymize = false) {
const properties = {};
if (user.stats) {
@@ -72,7 +105,7 @@ function _formatUserData (user) {
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) {
if (properties.verifiedUsername && user.auth && user.auth.local && !anonymize) {
properties.username = user.auth.local.lowerCaseUsername;
}
@@ -89,10 +122,12 @@ function _formatUserData (user) {
properties.contributorLevel = user.contributor.level;
}
if (user.purchased && user.purchased.plan.planId) {
properties.subscription = user.purchased.plan.planId;
} else {
properties.subscription = null;
if (!anonymize) {
if (user.purchased && user.purchased.plan.planId) {
properties.subscription = user.purchased.plan.planId;
} else {
properties.subscription = null;
}
}
if (user._ABtests) {
@@ -103,6 +138,16 @@ function _formatUserData (user) {
properties.loginIncentives = user.loginIncentives;
}
if (ipaddress) {
const location = lookup(ipaddress);
properties.country = location.country;
properties.region = location.region1;
}
if (anonymize) {
return _anonymizeProperties(properties);
}
return properties;
}
@@ -139,16 +184,20 @@ function _formatUserAgentForAmplitude (platform, agentString) {
return formattedAgent;
}
function _formatUUIDForAmplitude (uuid) {
function _formatUUIDForAmplitude (uuid, anonymize = false) {
if (anonymize) {
return _hashUUID(uuid);
}
return uuid || 'no-user-id-was-provided';
}
function _formatDataForAmplitude (data) {
const consented = data.user && data.user.preferences && data.user.preferences.analyticsConsent;
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),
user_id: _formatUUIDForAmplitude(data.uuid, !consented),
platform,
os_name: agent.name,
os_version: agent.version,
@@ -156,7 +205,12 @@ function _formatDataForAmplitude (data) {
};
if (data.user) {
ampData.user_properties = _formatUserData(data.user);
const ipaddress = data.ipaddress || (data.headers && data.headers['x-forwarded-for']);
ampData.user_properties = _formatUserData(data.user, ipaddress, !consented);
}
if (!consented) {
ampData.event_properties = _anonymizeProperties(ampData.event_properties);
}
const itemName = _lookUpItemName(data.itemKey);
@@ -215,12 +269,18 @@ function _setOnce (dataToSetOnce, uuid) {
.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}
function _updateProperties (properties, uuid) {
return amplitude
.identify({
user_id: _formatUUIDForAmplitude(uuid),
user_properties: properties,
})
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
// There's no error handling directly here because it's handled inside _sendDataToAmplitude
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),
];
@@ -234,29 +294,37 @@ async function track (eventType, data, loggerOnly = false) {
}
// There's no error handling directly here because
// it's handled inside _sendPurchaseDataTo{Amplitude|Google}
// it's handled inside _sendPurchaseDataToAmplitude
async function trackPurchase (data) {
const { user } = data;
if (!user || !user.preferences || !user.preferences.analyticsConsent) {
return null;
}
return Promise.all([
_sendPurchaseDataToAmplitude(data),
]);
}
async function updateUserData (data) {
const { user, properties } = data;
const toUpdate = {
..._formatUserData(user, data.ipaddress),
...properties,
};
return _updateProperties(toUpdate, user._id);
}
// Stub for non-prod environments
const mockAnalyticsService = {
track: () => { },
trackPurchase: () => { },
updateUserData: () => { },
};
// 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'))) {
if (nconf.get('IS_PROD') || nconf.get('USE_PROD_ANALYTICS') || (nconf.get('DEBUG_ENABLED') && !nconf.get('BASE_URL').includes('localhost'))) {
return {
track,
trackPurchase,
updateUserData,
};
}
return mockAnalyticsService;

View File

@@ -1,5 +1,4 @@
import moment from 'moment';
import pick from 'lodash/pick';
import {
BadRequest,
NotAuthorized,
@@ -219,7 +218,7 @@ async function registerLocal (req, res, { isV3 = false }) {
if (!existingUser) {
res.analytics.track('register', {
user: pick(savedUser, ['preferences', 'registeredThrough']),
user: savedUser,
category: 'acquisition',
type: 'local',
uuid: savedUser._id,

View File

@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import passport from 'passport';
import common from '../../../common';
import { BadRequest, NotAuthorized, NotFound } from '../errors';
@@ -158,7 +157,7 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
if (!existingUser) {
res.analytics.track('register', {
user: pick(savedUser, ['preferences', 'registeredThrough']),
user: savedUser,
uuid: savedUser._id,
category: 'acquisition',
type: network,

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';
@@ -104,7 +103,7 @@ function trackCronAnalytics (analytics, user, _progress, options) {
analytics.track('Cron', {
category: 'behavior',
uuid: user._id,
user: pick(user, ['preferences', 'registeredThrough']),
user,
resting: user.preferences.sleep,
cronCount: user.flags.cronCount,
progressUp: Math.min(_progress.up, 900),

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';
@@ -144,11 +143,10 @@ async function inviteByUUID (uuid, group, inviter, req, res) {
}
const analyticsObject = {
user: pick(inviter, ['preferences', 'registeredThrough']),
user: inviter,
uuid: inviter._id,
hitType: 'event',
category: 'behavior',
invitee: uuid,
groupId: group._id,
groupType: group.type,
headers: req.headers,
@@ -209,11 +207,10 @@ async function inviteByEmail (invite, group, inviter, req, res) {
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
const analyticsObject = {
user: pick(inviter, ['preferences', 'registeredThrough']),
user: inviter,
uuid: inviter._id,
hitType: 'event',
category: 'behavior',
invitee: 'email',
groupId: group._id,
groupType: group.type,
headers: req.headers,
@@ -247,11 +244,10 @@ async function inviteByUserName (username, group, inviter, req, res) {
}
const analyticsObject = {
user: pick(inviter, ['preferences', 'registeredThrough']),
user: inviter,
uuid: inviter._id,
hitType: 'event',
category: 'behavior',
invitee: userToInvite._id,
groupId: group._id,
groupType: group.type,
headers: req.headers,

View File

@@ -1,5 +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
@@ -115,7 +114,7 @@ export async function buyGems (data) {
if (!data.gift) txnEmail(data.user, 'donation');
analytics.trackPurchase({
user: pick(data.user, ['preferences', 'registeredThrough']),
user: data.user,
uuid: data.user._id,
itemPurchased: 'Gems',
sku: `${data.paymentMethod.toLowerCase()}-checkout`,

View File

@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import moment from 'moment';
import {
BadRequest,
@@ -32,7 +31,7 @@ async function buyGryphatrice (data) {
data.user.purchased.txnCount += 1;
analytics.trackPurchase({
user: pick(data.user, ['preferences', 'registeredThrough']),
user: data.user,
uuid: data.user._id,
itemPurchased: 'Gryphatrice',
sku: `${data.paymentMethod.toLowerCase()}-checkout`,

View File

@@ -3,7 +3,6 @@
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';
@@ -465,7 +464,7 @@ async function cancelSubscription (data) {
analytics.track(cancelType, {
uuid: data.user._id,
user: pick(data.user, ['preferences', 'registeredThrough']),
user: data.user,
groupId,
paymentMethod: data.paymentMethod,
headers: data.headers,

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 {
@@ -516,7 +515,7 @@ async function scoreTask (user, task, direction, req, res) {
role = 'member';
}
res.analytics.track('team task scored', {
user: pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',

View File

@@ -117,7 +117,7 @@ export async function update (req, res, { isV3 = false }) {
user.invitations.party = {};
user.invitations.parties = [];
res.analytics.track('Starts Looking for Party', {
user: _.pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -201,7 +201,7 @@ 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']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',
@@ -292,7 +292,7 @@ export async function reset (req, res, { isV3 = false }) {
]);
res.analytics.track('account reset', {
user: _.pick(user, ['preferences', 'registeredThrough']),
user,
uuid: user._id,
hitType: 'event',
category: 'behavior',