mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
Client Analytics (#9023)
* start to refactor analytics and some mixins * wip * wip * wip * more analytics * more analytics * more anlytics * fix analytics module * finish analytics * fix env * vue casing
This commit is contained in:
@@ -69,6 +69,7 @@ import AppFooter from './components/appFooter';
|
||||
import notificationsDisplay from './components/notifications';
|
||||
import snackbars from './components/snackbars/notifications';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import BuyModal from './components/shops/buyModal.vue';
|
||||
import SelectMembersModal from 'client/components/selectMembersModal.vue';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
@@ -158,14 +159,27 @@ export default {
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.isUserLoaded = true;
|
||||
Analytics.setUser();
|
||||
Analytics.updateUser();
|
||||
}).catch((err) => {
|
||||
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
||||
// Manage modals
|
||||
this.$root.$on('show::modal', (modalId, data) => {
|
||||
if (data && data.fromRoot) return;
|
||||
this.$root.$on('show::modal', (modalId, data = {}) => {
|
||||
if (data.fromRoot) return;
|
||||
|
||||
// Track opening of gems modal unless it's been already tracked
|
||||
// For example the gems button in the menu already tracks the event by itself
|
||||
if (modalId === 'buy-gems' && data.alreadyTracked !== true) {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > Wallet',
|
||||
});
|
||||
}
|
||||
|
||||
// Get last modal on stack and hide
|
||||
let modalStackLength = this.$store.state.modalStack.length;
|
||||
|
||||
@@ -60,6 +60,7 @@ import bModal from 'bootstrap-vue/lib/components/modal';
|
||||
|
||||
import Avatar from '../avatar';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import percent from '../../../common/script/libs/percent';
|
||||
import {maxHealth} from '../../../common/script/index';
|
||||
|
||||
@@ -84,6 +85,14 @@ export default {
|
||||
return `${Math.ceil(this.user.stats.hp)} / ${this.maxHealth}`;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Health Warning',
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('hide::modal', 'low-health');
|
||||
|
||||
@@ -219,6 +219,7 @@
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
import gryphon from 'assets/svg/gryphon.svg';
|
||||
import twitter from 'assets/svg/twitter.svg';
|
||||
@@ -331,6 +332,12 @@ export default {
|
||||
this.$root.$emit('show::modal', 'modify-inventory');
|
||||
},
|
||||
donate () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > Donate',
|
||||
});
|
||||
this.$root.$emit('show::modal', 'buy-gems');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -224,6 +224,7 @@ div
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import goldIcon from 'assets/svg/gold.svg';
|
||||
import userIcon from 'assets/svg/user.svg';
|
||||
@@ -289,6 +290,12 @@ export default {
|
||||
this.$root.$emit('show::modal', 'create-party-modal');
|
||||
},
|
||||
showBuyGemsModal () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > Toolbar',
|
||||
});
|
||||
this.$root.$emit('show::modal', 'buy-gems');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -290,6 +290,9 @@ export default {
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
});
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
window.location.href = '/';
|
||||
},
|
||||
async login () {
|
||||
@@ -299,6 +302,9 @@ export default {
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
window.location.href = '/';
|
||||
},
|
||||
async socialAuth (network) {
|
||||
@@ -314,6 +320,9 @@ export default {
|
||||
auth,
|
||||
});
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
window.location.href = '/';
|
||||
},
|
||||
handleSubmit () {
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
.col-12.text-center.submit-button-wrapper
|
||||
.alert.alert-warning(v-if='insufficientGemsForTavernChallenge')
|
||||
You do not have enough gems to create a Tavern challenge
|
||||
// @TODO if buy gems button is added, add analytics tracking to it
|
||||
// see https://github.com/HabitRPG/habitica/blob/develop/website/views/options/social/challenges.jade#L134
|
||||
button.btn.btn-primary(v-once, v-if='creating', @click='createChallenge()') {{$t('createChallengeAddTasks')}}
|
||||
button.btn.btn-primary(v-once, v-if='!creating', @click='updateChallenge()') {{$t('updateChallenge')}}
|
||||
.col-12.text-center
|
||||
|
||||
@@ -17,7 +17,7 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
|
||||
.join-party
|
||||
h3(v-once) {{$t('wantToJoinPartyTitle')}}
|
||||
p(v-once) {{$t('wantToJoinPartyDescription')}}
|
||||
button.btn.btn-primary(v-once, @click='shareUserIdShown = !shareUserIdShown') {{$t('shartUserId')}}
|
||||
button.btn.btn-primary(v-once, @click='shareUserId()') {{$t('shartUserId')}}
|
||||
.share-userid-options(v-if="shareUserIdShown")
|
||||
.option-item(v-once)
|
||||
.svg-icon(v-html="icons.copy")
|
||||
@@ -137,6 +137,7 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
import bModal from 'bootstrap-vue/lib/components/modal';
|
||||
|
||||
@@ -166,6 +167,15 @@ export default {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
shareUserId () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Health Warning',
|
||||
});
|
||||
this.shareUserIdShown = !this.shareUserIdShown;
|
||||
},
|
||||
async createParty () {
|
||||
let group = {
|
||||
type: 'party',
|
||||
@@ -174,9 +184,14 @@ export default {
|
||||
let party = await this.$store.dispatch('guilds:create', {group});
|
||||
this.$store.state.party.data = party;
|
||||
this.user.party._id = party._id;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: party._id,
|
||||
partySize: 1,
|
||||
});
|
||||
|
||||
this.$root.$emit('hide::modal', 'create-party-modal');
|
||||
this.$router.push('/party');
|
||||
// @TODO: Analytics.updateUser({'partyID': $scope.group ._id, 'partySize': 1});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -409,6 +409,7 @@ import extend from 'lodash/extend';
|
||||
import groupUtilities from 'client/mixins/groupsUtilities';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import membersModal from './membersModal';
|
||||
import startQuestModal from './startQuestModal';
|
||||
import quests from 'common/script/content/quests';
|
||||
@@ -636,7 +637,7 @@ export default {
|
||||
},
|
||||
async sendMessage () {
|
||||
let response = await this.$store.dispatch('chat:postChat', {
|
||||
groupId: this.group._id,
|
||||
group: this.group._id,
|
||||
message: this.newMessage,
|
||||
});
|
||||
this.group.chat.unshift(response.message);
|
||||
@@ -699,7 +700,13 @@ export default {
|
||||
this.user.guilds.push(this.group._id);
|
||||
},
|
||||
clickLeave () {
|
||||
// Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Leave Party'});
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Leave Party',
|
||||
});
|
||||
|
||||
// @TODO: Get challenges and ask to keep or remove
|
||||
if (!confirm('Are you sure you want to leave?')) return;
|
||||
let keep = true;
|
||||
@@ -714,12 +721,14 @@ export default {
|
||||
keepChallenges,
|
||||
};
|
||||
|
||||
if (this.isParty) data.type = 'party';
|
||||
if (this.isParty) {
|
||||
data.type = 'party';
|
||||
Analytics.updateUser({partySize: null, partyID: null});
|
||||
}
|
||||
|
||||
await this.$store.dispatch('guilds:leave', data);
|
||||
|
||||
// @TODO: Implement
|
||||
// Analytics.updateUser({'partySize':null,'partyID':null});
|
||||
// User.sync().then(function () {
|
||||
// $rootScope.hardRedirect('/#/options/groups/party');
|
||||
// });
|
||||
@@ -743,7 +752,13 @@ export default {
|
||||
await this.$store.dispatch('guilds:join', {groupId: this.group._id});
|
||||
},
|
||||
clickStartQuest () {
|
||||
// Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Start a Quest'});
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Start a Quest',
|
||||
});
|
||||
|
||||
let hasQuests = find(this.user.items.quests, (quest) => {
|
||||
return quest > 0;
|
||||
});
|
||||
|
||||
@@ -375,7 +375,14 @@ export default {
|
||||
// @TODO: Add proper notifications
|
||||
alert('Not enough gems');
|
||||
return;
|
||||
// @TODO return $rootScope.openModal('buyGems', {track:"Gems > Create Group"});
|
||||
// @TODO return $rootScope.openModal('buyGems', {track:"Gems > Gems > Create Group"});
|
||||
// @TODO when modal is implemented, enable analytics
|
||||
/* Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Health Warning',
|
||||
}); */
|
||||
}
|
||||
|
||||
if (!this.workingGroup.name || !this.workingGroup.description) {
|
||||
|
||||
@@ -55,8 +55,12 @@ import filter from 'lodash/filter';
|
||||
import map from 'lodash/map';
|
||||
import bModal from 'bootstrap-vue/lib/components/modal';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
bModal,
|
||||
},
|
||||
mixins: [notifications],
|
||||
props: ['group'],
|
||||
data () {
|
||||
@@ -65,8 +69,13 @@ export default {
|
||||
emails: [],
|
||||
};
|
||||
},
|
||||
components: {
|
||||
bModal,
|
||||
mounted () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Invite Friends',
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import bModal from 'bootstrap-vue/lib/components/modal';
|
||||
|
||||
import quests from 'common/script/content/quests';
|
||||
@@ -208,10 +209,15 @@ export default {
|
||||
this.selectedQuest = quest;
|
||||
},
|
||||
async questInit () {
|
||||
let key = this.selectedQuest;
|
||||
// Analytics.updateUser({'partyID': party._id, 'partySize': party.memberCount});
|
||||
let response = await this.$store.dispatch('guilds:inviteToQuest', {groupId: this.group._id, key});
|
||||
let quest = response.data.data;
|
||||
Analytics.updateUser({
|
||||
partyID: this.group._id,
|
||||
partySize: this.group.memberCount,
|
||||
});
|
||||
|
||||
const key = this.selectedQuest;
|
||||
const response = await this.$store.dispatch('guilds:inviteToQuest', {groupId: this.group._id, key});
|
||||
const quest = response.data.data;
|
||||
|
||||
this.$store.party.quest = quest;
|
||||
this.$root.$emit('hide::modal', 'start-quest-modal');
|
||||
},
|
||||
|
||||
@@ -137,6 +137,7 @@ import isEmpty from 'lodash/isEmpty';
|
||||
import map from 'lodash/map';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import quests from 'common/script/content/quests';
|
||||
import notificationsIcon from 'assets/svg/notifications.svg';
|
||||
|
||||
@@ -299,7 +300,10 @@ export default {
|
||||
|
||||
if (type === 'party') {
|
||||
// @TODO: pretty sure mutability is wrong. Need to check React docs
|
||||
// @TODO mutation to store data should only happen through actions
|
||||
this.user.invitations.parties.splice(index, 1);
|
||||
|
||||
Analytics.updateUser({partyID: group.id});
|
||||
} else {
|
||||
this.user.invitations.guilds.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
|
||||
<script>
|
||||
import bModal from 'bootstrap-vue/lib/components/modal';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
import svgClose from 'assets/svg/close.svg';
|
||||
import svgGold from 'assets/svg/gold.svg';
|
||||
@@ -266,6 +267,14 @@
|
||||
this.hideDialog();
|
||||
},
|
||||
purchaseGems () {
|
||||
if (this.item.key === 'rebirth_orb') {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > Rebirth',
|
||||
});
|
||||
}
|
||||
this.$root.$emit('show::modal', 'buy-gems');
|
||||
},
|
||||
togglePinned () {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
.row.row-margin(style="font-size: 2rem;")
|
||||
span {{ $t('enterprisePlansDescription') }}
|
||||
.row.row-margin
|
||||
// TODO
|
||||
a.btn.btn-primary.btn-lg.btn-block(:href="'mailto:vicky@habitica.com?subject=' + $t('enterprisePlansEmailSubject')") {{ $t('enterprisePlansButton') }}
|
||||
|
||||
br
|
||||
@@ -28,3 +29,22 @@
|
||||
.row.row-margin
|
||||
a.btn.btn-primary.btn-lg.btn-block(href="https://docs.google.com/forms/d/e/1FAIpQLSerMKkaCg3UcgpcMvBJtlNgnF9DNY8sxCebpAT-GHeDAQASPQ/viewform?usp=sf_link") {{ $t('familyPlansButton') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
contactUs () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Contact Us (Plans)',
|
||||
});
|
||||
|
||||
window.location.href = `mailto:vicky@habitica.com?subject=${this.$t('enterprisePlansEmailSubject')}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -91,20 +91,29 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import logo from 'assets/svg/logo.svg';
|
||||
import logo from 'assets/svg/logo.svg';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
logo,
|
||||
}),
|
||||
};
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
logo,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
playButtonClick () {
|
||||
// @TODO duplicate of code in home.vue
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Play',
|
||||
});
|
||||
|
||||
this.$router.push('/register');
|
||||
},
|
||||
methods: {
|
||||
playButtonClick () {
|
||||
this.$router.push('/register');
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -602,6 +602,7 @@
|
||||
<script>
|
||||
import AppFooter from 'client/components/appFooter';
|
||||
import StaticHeader from './header.vue';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -614,10 +615,21 @@
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
// Analytics.track({"hitType":"pageview","eventCategory":"page","eventAction":"landing page","page":"/home"});
|
||||
Analytics.track({
|
||||
hitType: 'pageview',
|
||||
eventCategory: 'page',
|
||||
eventAction: 'landing page',
|
||||
page: '/home',
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
playButtonClick () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Play',
|
||||
});
|
||||
this.$router.push('/register');
|
||||
},
|
||||
},
|
||||
@@ -298,6 +298,7 @@ import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
import scoreTask from 'common/script/ops/scoreTask';
|
||||
import Vue from 'vue';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
import negativeIcon from 'assets/svg/negative.svg';
|
||||
@@ -435,6 +436,7 @@ export default {
|
||||
|
||||
if (task.group.approval.required) task.group.approval.requested = true;
|
||||
|
||||
Analytics.updateUser();
|
||||
const response = await axios.post(`/api/v3/tasks/${task._id}/score/${direction}`);
|
||||
const tmp = response.data.data._tmp || {}; // used to notify drops, critical hits and other bonuses
|
||||
const crit = tmp.crit;
|
||||
|
||||
143
website/client/libs/analytics.js
Normal file
143
website/client/libs/analytics.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import keys from 'lodash/keys';
|
||||
import pick from 'lodash/pick';
|
||||
import includes from 'lodash/includes';
|
||||
import getStore from 'client/store';
|
||||
import Vue from 'vue';
|
||||
|
||||
let REQUIRED_FIELDS = ['hitType', 'eventCategory', 'eventAction'];
|
||||
let ALLOWED_HIT_TYPES = [
|
||||
'pageview',
|
||||
'screenview',
|
||||
'event',
|
||||
'transaction',
|
||||
'item',
|
||||
'social',
|
||||
'exception',
|
||||
'timing',
|
||||
];
|
||||
|
||||
const store = getStore();
|
||||
|
||||
function _doesNotHaveRequiredFields (properties) {
|
||||
if (!isEqual(keys(pick(properties, REQUIRED_FIELDS)), REQUIRED_FIELDS)) {
|
||||
// @TODO: Log with Winston?
|
||||
// console.log('Analytics tracking calls must include the following properties: ' + JSON.stringify(REQUIRED_FIELDS));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function _doesNotHaveAllowedHitType (properties) {
|
||||
if (!includes(ALLOWED_HIT_TYPES, properties.hitType)) {
|
||||
// @TODO: Log with Winston?
|
||||
// console.log('Hit type of Analytics event must be one of the following: ' + JSON.stringify(ALLOWED_HIT_TYPES));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function _gatherUserStats (properties) {
|
||||
const user = store.state.user.data;
|
||||
const tasks = store.state.tasks.data;
|
||||
|
||||
properties.UUID = user._id;
|
||||
|
||||
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.tour.intro === -2;
|
||||
|
||||
properties['Number Of Tasks'] = {
|
||||
habits: tasks.habits.length,
|
||||
dailys: tasks.dailys.length,
|
||||
todos: tasks.todos.length,
|
||||
rewards: tasks.rewards.length,
|
||||
};
|
||||
|
||||
if (user.contributor.level) properties.contributorLevel = user.contributor.level;
|
||||
if (user.purchased.plan.planId) properties.subscription = user.purchased.plan.planId;
|
||||
}
|
||||
|
||||
export function setUser () {
|
||||
const user = store.state.user.data;
|
||||
window.amplitude.setUserId(user._id);
|
||||
window.ga('set', {userId: user._id});
|
||||
}
|
||||
|
||||
export function track (properties) {
|
||||
// Use nextTick to avoid blocking the UI
|
||||
Vue.nextTick(() => {
|
||||
if (_doesNotHaveRequiredFields(properties)) return false;
|
||||
if (_doesNotHaveAllowedHitType(properties)) return false;
|
||||
|
||||
window.amplitude.logEvent(properties.eventAction, properties);
|
||||
window.ga('send', properties);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateUser (properties) {
|
||||
// Use nextTick to avoid blocking the UI
|
||||
Vue.nextTick(() => {
|
||||
properties = properties || {};
|
||||
|
||||
_gatherUserStats(properties);
|
||||
|
||||
window.amplitude.setUserProperties(properties);
|
||||
window.ga('set', properties);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function setup () {
|
||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
|
||||
const AMPLITUDE_KEY = process.env.AMPLITUDE_KEY; // eslint-disable-line no-process-env
|
||||
const GA_ID = process.env.GA_ID; // eslint-disable-line no-process-env
|
||||
|
||||
// Setup queues until the real scripts are loaded
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// Amplitude
|
||||
var r = window.amplitude || {};
|
||||
r._q = [];
|
||||
function a(window) {r[window] = function() {r._q.push([window].concat(Array.prototype.slice.call(arguments, 0)));}}
|
||||
var i = ["init", "logEvent", "logRevenue", "setUserId", "setUserProperties", "setOptOut", "setVersionName", "setDomain", "setDeviceId", "setGlobalUserProperties"];
|
||||
for (var o = 0; o < i.length; o++) {a(i[o])}
|
||||
window.amplitude = r;
|
||||
amplitude.init(AMPLITUDE_KEY);
|
||||
|
||||
// Google Analytics (aka Universal Analytics)
|
||||
window['GoogleAnalyticsObject'] = 'ga';
|
||||
window['ga'] = window['ga'] || function() {
|
||||
(window['ga'].q = window['ga'].q || []).push(arguments)
|
||||
}, window['ga'].l = 1 * new Date();
|
||||
ga('create', GA_ID);
|
||||
/* eslint-enable */
|
||||
|
||||
// Load real scripts
|
||||
|
||||
if (!IS_PRODUCTION) return;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
// Amplitude
|
||||
const amplitudeScript = document.createElement('script');
|
||||
let firstScript = document.getElementsByTagName('script')[0];
|
||||
amplitudeScript.type = 'text/javascript';
|
||||
amplitudeScript.async = true;
|
||||
amplitudeScript.src = 'https://d24n15hnbwhuhn.cloudfront.net/libs/amplitude-2.2.0-min.gz.js';
|
||||
firstScript.parentNode.insertBefore(amplitudeScript, firstScript);
|
||||
|
||||
// Google Analytics
|
||||
const gaScript = document.createElement('script');
|
||||
firstScript = document.getElementsByTagName('script')[0];
|
||||
gaScript.async = 1;
|
||||
gaScript.src = '//www.google-analytics.com/analytics.js';
|
||||
firstScript.parentNode.insertBefore(gaScript, firstScript);
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ require('babel-polyfill');
|
||||
|
||||
import Vue from 'vue';
|
||||
import AppComponent from './app';
|
||||
import { setup as setupAnalytics } from 'client/libs/analytics';
|
||||
import router from './router';
|
||||
import getStore from './store';
|
||||
import StoreModule from './libs/store';
|
||||
@@ -26,6 +27,8 @@ Vue.config.productionTip = IS_PRODUCTION;
|
||||
Vue.use(i18n, {i18nData: window && window['habitica-i18n']});
|
||||
Vue.use(StoreModule);
|
||||
|
||||
setupAnalytics();
|
||||
|
||||
export default new Vue({
|
||||
el: '#app',
|
||||
router,
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import keys from 'lodash/keys';
|
||||
import pick from 'lodash/pick';
|
||||
import includes from 'lodash/includes';
|
||||
|
||||
let REQUIRED_FIELDS = ['hitType', 'eventCategory', 'eventAction'];
|
||||
let ALLOWED_HIT_TYPES = [
|
||||
'pageview',
|
||||
'screenview',
|
||||
'event',
|
||||
'transaction',
|
||||
'item',
|
||||
'social',
|
||||
'exception',
|
||||
'timing',
|
||||
];
|
||||
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
register (user) {
|
||||
// @TODO: What is was the timeout for?
|
||||
window.amplitude.setUserId(user._id);
|
||||
window.ga('set', {userId: user._id});
|
||||
},
|
||||
login (user) {
|
||||
window.amplitude.setUserId(user._id);
|
||||
window.ga('set', {userId: user._id});
|
||||
},
|
||||
track (properties) {
|
||||
if (this.doesNotHaveRequiredFields(properties)) return false;
|
||||
if (this.doesNotHaveAllowedHitType(properties)) return false;
|
||||
|
||||
window.amplitude.logEvent(properties.eventAction, properties);
|
||||
window.ga('send', properties);
|
||||
},
|
||||
updateUser (properties, user) {
|
||||
properties = properties || {};
|
||||
|
||||
this.gatherUserStats(user, properties);
|
||||
|
||||
window.amplitude.setUserProperties(properties);
|
||||
window.ga('set', properties);
|
||||
},
|
||||
gatherUserStats (user, properties) {
|
||||
if (user._id) properties.UUID = user._id;
|
||||
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;
|
||||
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;
|
||||
},
|
||||
doesNotHaveRequiredFields (properties) {
|
||||
if (!isEqual(keys(pick(properties, REQUIRED_FIELDS)), REQUIRED_FIELDS)) {
|
||||
// @TODO: Log with Winston?
|
||||
// console.log('Analytics tracking calls must include the following properties: ' + JSON.stringify(REQUIRED_FIELDS));
|
||||
return true;
|
||||
}
|
||||
},
|
||||
doesNotHaveAllowedHitType (properties) {
|
||||
if (!includes(ALLOWED_HIT_TYPES, properties.hitType)) {
|
||||
// @TODO: Log with Winston?
|
||||
// console.log('Hit type of Analytics event must be one of the following: ' + JSON.stringify(ALLOWED_HIT_TYPES));
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import times from 'lodash/times';
|
||||
import Intro from 'intro.js/';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
@@ -124,6 +125,7 @@ export default {
|
||||
},
|
||||
hoyo (user) {
|
||||
// @TODO: What is was the timeout for?
|
||||
// @TODO move to analytics
|
||||
window.amplitude.setUserId(user._id);
|
||||
window.ga('set', {userId: user._id});
|
||||
},
|
||||
@@ -143,6 +145,15 @@ export default {
|
||||
opts.steps = opts.steps.concat(this.chapters[chapter][p]);
|
||||
});
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'behavior',
|
||||
eventAction: 'tutorial',
|
||||
eventLabel: `${chapter}-web`,
|
||||
eventValue: page + 1,
|
||||
complete: true,
|
||||
});
|
||||
|
||||
// @TODO: Do we always need to initialize here?
|
||||
let intro = Intro.introJs();
|
||||
intro.setOptions({steps: opts.steps});
|
||||
@@ -173,9 +184,17 @@ export default {
|
||||
// // @TODO: Notification.showLoginIncentive(this.user, rewardData, Social.loadWidgets);
|
||||
// }
|
||||
|
||||
// Mark tour complete
|
||||
// Mark tour complete
|
||||
ups[`flags.tour.${chapter}`] = -2; // @TODO: Move magic numbers to enum
|
||||
// @TODO: Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'tutorial','eventLabel':k+'-web','eventValue':i+1,'complete':true})
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'behavior',
|
||||
eventAction: 'tutorial',
|
||||
eventLabel: `${chapter}-web`,
|
||||
eventValue: lastKnownStep,
|
||||
complete: true,
|
||||
});
|
||||
// }
|
||||
|
||||
this.$store.dispatch('user:set', ups);
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
let isGem = data && data.gift && data.gift.type === 'gems';
|
||||
let notEnoughGem = isGem && (!data.gift.gems.amount || data.gift.gems.amount === 0);
|
||||
if (notEnoughGem) {
|
||||
Notification.error(this.$t('badAmountOfGemsToPurchase'), true);
|
||||
this.error(this.$t('badAmountOfGemsToPurchase'), true);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import getStore from 'client/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
// import EmptyView from './components/emptyView';
|
||||
|
||||
@@ -14,7 +15,7 @@ const CommunityGuidelinesPage = () => import(/* webpackChunkName: "static" */'./
|
||||
const ContactPage = () => import(/* webpackChunkName: "static" */'./components/static/contact');
|
||||
const FAQPage = () => import(/* webpackChunkName: "static" */'./components/static/faq');
|
||||
const FeaturesPage = () => import(/* webpackChunkName: "static" */'./components/static/features');
|
||||
const FrontPage = () => import(/* webpackChunkName: "static" */'./components/static/front');
|
||||
const HomePage = () => import(/* webpackChunkName: "static" */'./components/static/home');
|
||||
const GroupPlansPage = () => import(/* webpackChunkName: "static" */'./components/static/groupPlans');
|
||||
const MaintenancePage = () => import(/* webpackChunkName: "static" */'./components/static/maintenance');
|
||||
const MaintenanceInfoPage = () => import(/* webpackChunkName: "static" */'./components/static/maintenanceInfo');
|
||||
@@ -99,7 +100,7 @@ const router = new VueRouter({
|
||||
},
|
||||
// requiresLogin is true by default, isStatic false
|
||||
routes: [
|
||||
{ name: 'home', path: '/home', component: FrontPage, meta: {requiresLogin: false} },
|
||||
{ name: 'home', path: '/home', component: HomePage, meta: {requiresLogin: false} },
|
||||
{ name: 'register', path: '/register', component: RegisterLogin, meta: {requiresLogin: false} },
|
||||
{ name: 'login', path: '/login', component: RegisterLogin, meta: {requiresLogin: false} },
|
||||
{ name: 'tasks', path: '/', component: UserTasks },
|
||||
@@ -287,6 +288,13 @@ router.beforeEach(function routerGuard (to, from, next) {
|
||||
return next({name: 'tasks'});
|
||||
}
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'pageview',
|
||||
eventCategory: 'navigation',
|
||||
eventAction: 'navigate',
|
||||
page: to.name || to.path,
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
@@ -21,16 +21,6 @@ export async function register (store, params) {
|
||||
},
|
||||
});
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
|
||||
// @TODO: I think we just need analytics here
|
||||
// Auth.runAuth(res.data._id, res.data.apiToken);
|
||||
// Analytics.register();
|
||||
// $scope.registrationInProgress = false;
|
||||
// Alert.authErrorAlert(data, status, headers, config)
|
||||
// Analytics.login();
|
||||
// Analytics.updateUser();
|
||||
|
||||
store.state.user.data = user;
|
||||
}
|
||||
|
||||
export async function login (store, params) {
|
||||
@@ -51,17 +41,6 @@ export async function login (store, params) {
|
||||
});
|
||||
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
|
||||
// @TODO: I think we just need analytics here
|
||||
// Auth.runAuth(res.data._id, res.data.apiToken);
|
||||
// Analytics.register();
|
||||
// $scope.registrationInProgress = false;
|
||||
// Alert.authErrorAlert(data, status, headers, config)
|
||||
// Analytics.login();
|
||||
// Analytics.updateUser();
|
||||
|
||||
// @TODO: Update the api to return the user?
|
||||
// store.state.user.data = user;
|
||||
}
|
||||
|
||||
export async function socialAuth (store, params) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
export async function getChat (store, payload) {
|
||||
let response = await axios.get(`/api/v3/groups/${payload.groupId}/chat`);
|
||||
@@ -7,12 +8,40 @@ export async function getChat (store, payload) {
|
||||
}
|
||||
|
||||
export async function postChat (store, payload) {
|
||||
let url = `/api/v3/groups/${payload.groupId}/chat`;
|
||||
const group = payload.group;
|
||||
|
||||
let url = `/api/v3/groups/${group._id}/chat`;
|
||||
|
||||
if (payload.previousMsg) {
|
||||
url += `?previousMsg=${payload.previousMsg}`;
|
||||
}
|
||||
|
||||
if (group.type === 'party') {
|
||||
Analytics.updateUser({
|
||||
partyID: group.id,
|
||||
partySize: group.memberCount,
|
||||
});
|
||||
}
|
||||
|
||||
if (group.privacy === 'public') {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'behavior',
|
||||
eventAction: 'group chat',
|
||||
groupType: group.type,
|
||||
privacy: group.privacy,
|
||||
groupName: group.name,
|
||||
});
|
||||
} else {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'behavior',
|
||||
eventAction: 'group chat',
|
||||
groupType: group.type,
|
||||
privacy: group.privacy,
|
||||
});
|
||||
}
|
||||
|
||||
let response = await axios.post(url, {
|
||||
message: payload.message,
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import axios from 'axios';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
// export async function initQuest (store) {
|
||||
// }
|
||||
|
||||
export async function sendAction (store, payload) {
|
||||
// Analytics.updateUser({
|
||||
// partyID: party._id,
|
||||
// partySize: party.memberCount
|
||||
// });
|
||||
Analytics.updateUser({
|
||||
partyID: store.state.party.data._id,
|
||||
partySize: store.state.party.data.memberCount,
|
||||
});
|
||||
|
||||
let response = await axios.post(`/api/v3/groups/${payload.groupId}/${payload.action}`);
|
||||
|
||||
// @TODO: Update user?
|
||||
|
||||
Reference in New Issue
Block a user