From 3bf4af8d8bf65733269c1d3f0857d964139fa442 Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Thu, 4 Sep 2025 13:43:18 -0500 Subject: [PATCH] Set up analytics scripts on demand post user load (#15501) * fix(analytics): can't get consented user during main,js load * fix(race): don't let gtag load twice also refactor to avoid unnecessary _getConsentedUser() calls * fix(lint): need user ID for gtag config * fix(analytics): adjust script loads and refs * fix(vue): try moving plugin to most relevant file * fix(amplitude): correct event fn * fix(analytics): direct load gtag from uri * fix(ga): use ga-gtag for loading google * fix(lint): import order * refactor(analytics): remove superfluous setUser fn * fix(amplitude): return to Javascript SDK syntax * refactor(misc): remove unneeded asyncs * refactor(analytics): slim down if checks --- website/client/package-lock.json | 12 ++++---- website/client/package.json | 2 +- website/client/src/libs/analytics.js | 40 ++++++++++++-------------- website/client/src/main.js | 6 ---- website/client/src/pages/user-main.vue | 3 +- 5 files changed, 26 insertions(+), 37 deletions(-) diff --git a/website/client/package-lock.json b/website/client/package-lock.json index df62c25ec8..7d929c5b46 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -23,6 +23,7 @@ "eslint-config-habitrpg": "6.2.0", "eslint-plugin-mocha": "5.3.0", "eslint-plugin-vue": "7.20.0", + "ga-gtag": "^1.2.0", "habitica-markdown": "^3.0.0", "hellojs": "^1.20.0", "intro.js": "^7.2.0", @@ -42,7 +43,6 @@ "vue": "^2.7.10", "vue-fragment": "^1.6.0", "vue-mugen-scroll": "^0.2.6", - "vue-plugin-load-script": "^1.3.6", "vue-router": "^3.6.5", "vuedraggable": "^2.24.3", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0" @@ -5161,6 +5161,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ga-gtag": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ga-gtag/-/ga-gtag-1.2.0.tgz", + "integrity": "sha512-j9gxutMdpGMdwaX1SzOG31Ddm+IGFjeNf+N3Z5g+BBpS8FSXOALlrM+ORIGc/QKszGJEDlw+6PfIsJZICsqsGQ==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8818,11 +8823,6 @@ "node": ">=4" } }, - "node_modules/vue-plugin-load-script": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/vue-plugin-load-script/-/vue-plugin-load-script-1.3.6.tgz", - "integrity": "sha512-O+mpw32dXY3tMBBNKm7/qIByJV1QbHJ+0CXI4GeXx4NHU/Rg+bv7bvzugkEWnYcB/43WvR8ZD2K9KQIeVng1bA==" - }, "node_modules/vue-router": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", diff --git a/website/client/package.json b/website/client/package.json index 44fb393d61..c1c7c5d4c2 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -27,6 +27,7 @@ "eslint-config-habitrpg": "6.2.0", "eslint-plugin-mocha": "5.3.0", "eslint-plugin-vue": "7.20.0", + "ga-gtag": "^1.2.0", "habitica-markdown": "^3.0.0", "hellojs": "^1.20.0", "intro.js": "^7.2.0", @@ -46,7 +47,6 @@ "vue": "^2.7.10", "vue-fragment": "^1.6.0", "vue-mugen-scroll": "^0.2.6", - "vue-plugin-load-script": "^1.3.6", "vue-router": "^3.6.5", "vuedraggable": "^2.24.3", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0" diff --git a/website/client/src/libs/analytics.js b/website/client/src/libs/analytics.js index 62996b2327..6520a449ae 100644 --- a/website/client/src/libs/analytics.js +++ b/website/client/src/libs/analytics.js @@ -3,6 +3,7 @@ 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'; @@ -10,9 +11,11 @@ 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; @@ -63,15 +66,22 @@ function _gatherUserStats (properties) { if (user.purchased.plan.planId) properties.subscription = user.purchased.plan.planId; } -export function setUser () { - const user = _getConsentedUser(); - if (!user) return; - amplitude.getInstance().setUserId(user._id); +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 = {}) { const user = _getConsentedUser(); if (!user) return; + safeSetup(user._id); // Use nextTick to avoid blocking the UI Vue.nextTick(() => { if (_doesNotHaveRequiredFields(properties)) return; @@ -80,9 +90,7 @@ export function track (properties, options = {}) { // Track events on the server by default if (trackOnClient === true) { amplitude.getInstance().logEvent(properties.eventAction, properties); - if (window.gtag) { - window.gtag('event', properties.eventAction, properties); - } + gtag('event', properties.eventAction, properties); } else { const store = getStore(); store.dispatch('analytics:trackEvent', properties); @@ -93,26 +101,14 @@ export function track (properties, options = {}) { export function updateUser (properties = {}) { const user = _getConsentedUser(); if (!user) return; + safeSetup(user._id); // Use nextTick to avoid blocking the UI Vue.nextTick(() => { _gatherUserStats(properties); - if (window.gtag) { - window.gtag('set', 'user_properties', properties); - } + gtag('set', 'user_properties', properties); forEach(properties, (value, key) => { const identify = new amplitude.Identify().set(key, value); amplitude.getInstance().identify(identify); }); }); } - -export async function setup () { - const user = _getConsentedUser(); - if (!user) return; - await Vue.loadScript(`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`); - window.gtag('config', GA_ID, { - debug_mode: DEBUG_ENABLED || !IS_PRODUCTION, - user_id: user._id, - }); - amplitude.getInstance().init(AMPLITUDE_KEY); -} diff --git a/website/client/src/main.js b/website/client/src/main.js index 0a5cb9271d..e1096cc061 100644 --- a/website/client/src/main.js +++ b/website/client/src/main.js @@ -12,11 +12,7 @@ import { CollapsePlugin, } from 'bootstrap-vue'; import Fragment from 'vue-fragment'; -import LoadScript from 'vue-plugin-load-script'; import AppComponent from './app'; -import { - setup as setupAnalytics, -} from '@/libs/analytics'; import { setUpLogging } from '@/libs/logging'; import router from './router/index'; import getStore from './store'; @@ -49,10 +45,8 @@ Vue.use(TooltipPlugin); Vue.use(NavbarPlugin); Vue.use(CollapsePlugin); Vue.use(Fragment.Plugin); -Vue.use(LoadScript); setUpLogging(); -setupAnalytics(); const store = getStore(); if (import.meta.env.TIME_TRAVEL_ENABLED === 'true') { diff --git a/website/client/src/pages/user-main.vue b/website/client/src/pages/user-main.vue index 1a3b254260..3e23050dfe 100644 --- a/website/client/src/pages/user-main.vue +++ b/website/client/src/pages/user-main.vue @@ -263,8 +263,6 @@ export default { this.$store.dispatch('tasks:fetchUserTasks'), ]).then(() => { this.$store.state.isUserLoaded = true; - Analytics.setUser(); - Analytics.updateUser(); const analyticsConsent = localStorage.getItem('analyticsConsent'); if (analyticsConsent !== null && analyticsConsent !== this.user.preferences.analyticsConsent @@ -281,6 +279,7 @@ export default { return null; } } + Analytics.updateUser(); return axios.get( '/api/v4/i18n/browser-script', {