diff --git a/packages/frontend-2/lib/core/clients/mp.ts b/packages/frontend-2/lib/core/clients/mp.ts index e78b6357ba..cc461079e8 100644 --- a/packages/frontend-2/lib/core/clients/mp.ts +++ b/packages/frontend-2/lib/core/clients/mp.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { type Nullable, resolveMixpanelServerId } from '@speckle/shared' -import mixpanel from 'mixpanel-browser' +import { isString, mapKeys } from 'lodash-es' import { useOnAuthStateChange } from '~/lib/auth/composables/auth' import { HOST_APP, @@ -16,6 +16,29 @@ function getMixpanelServerId(): string { return resolveMixpanelServerId(window.location.hostname) } +function useMixpanelUtmCollection() { + const route = useRoute() + return () => { + const campaignKeywords = [ + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_content', + 'utm_term' + ] + + const result: Record = {} + for (const campaignKeyword of campaignKeywords) { + const value = route.query[campaignKeyword] + if (value && isString(value)) { + result[campaignKeyword] = value + } + } + + return result + } +} + /** * Composable that builds the user (re-)identification function. Needs to be invoked on app * init and when the active user changes (e.g. after signing out/in) @@ -59,8 +82,12 @@ export const useClientsideMixpanelClientBuilder = () => { } = useRuntimeConfig() const { reidentify } = useMixpanelUserIdentification() const onAuthStateChange = useOnAuthStateChange() + const logger = useLogger() + const collectUtmTags = useMixpanelUtmCollection() return async (): Promise> => { + // Dynamic import to be able to suppress loading errors that happen because of adblock + const mixpanel = (await import('mixpanel-browser')).default if (!mixpanel || !mixpanelTokenId.length || !mixpanelApiHost.length) { return null } @@ -70,12 +97,24 @@ export const useClientsideMixpanelClientBuilder = () => { api_host: mixpanelApiHost, debug: !!import.meta.dev && logCsrEmitProps }) + const utmParams = collectUtmTags() // Reidentify on auth change await onAuthStateChange(() => reidentify(mixpanel), { immediate: true }) + // Track UTM (only on initial visit) + if (Object.values(utmParams).length) { + const firstTouch = mapKeys(utmParams, (_val, key) => `${key} [first touch]`) + const lastTouch = mapKeys(utmParams, (_val, key) => `${key} [last touch]`) + + mixpanel.people.set(lastTouch) + mixpanel.people.set_once(firstTouch) + mixpanel.register(lastTouch) + } + // Track app visit mixpanel.track(`Visit ${HOST_APP_DISPLAY_NAME}`) + logger.info('MP client initialized') return mixpanel } diff --git a/packages/frontend-2/plugins/007-mpClient.client.ts b/packages/frontend-2/plugins/007-mpClient.client.ts index b87039c8a4..b79fd5cfaa 100644 --- a/packages/frontend-2/plugins/007-mpClient.client.ts +++ b/packages/frontend-2/plugins/007-mpClient.client.ts @@ -1,7 +1,6 @@ import { LogicError } from '@speckle/ui-components' -import { provideApolloClient } from '@vue/apollo-composable' -import { useApolloClientFromNuxt } from '~/lib/common/composables/graphql' import { fakeMixpanelClient, type MixpanelClient } from '~/lib/common/helpers/mp' +import { useClientsideMixpanelClientBuilder } from '~/lib/core/clients/mp' /** * mixpanel-browser only supports being ran on the client-side (hence the name)! So it's only going to be accessible @@ -10,17 +9,12 @@ import { fakeMixpanelClient, type MixpanelClient } from '~/lib/common/helpers/mp export default defineNuxtPlugin(async () => { const logger = useLogger() - const apollo = useApolloClientFromNuxt() + const build = useClientsideMixpanelClientBuilder() let mixpanel: MixpanelClient | undefined = undefined try { // Dynamic import to allow suppressing loading errors that happen because of adblock - const builder = (await import('~/lib/core/clients/mp')) - .useClientsideMixpanelClientBuilder - - // Not sure why, but apollo client is inaccessible so we have to explicitly provide it - const build = provideApolloClient(apollo)(() => builder()) mixpanel = (await build()) || undefined } catch (e) { logger.warn(e, 'Failed to load mixpanel in CSR') diff --git a/packages/frontend-2/plugins/008-mp.client.ts b/packages/frontend-2/plugins/008-mp.client.ts index e28263d633..50d256bece 100644 --- a/packages/frontend-2/plugins/008-mp.client.ts +++ b/packages/frontend-2/plugins/008-mp.client.ts @@ -1,18 +1,23 @@ import { useMixpanel } from '~/lib/core/composables/mp' import type { RouteLocationNormalized } from 'vue-router' +import type { Optional } from '@speckle/shared' export default defineNuxtPlugin(() => { const mp = useMixpanel() const router = useRouter() const route = useRoute() + let previousPath: Optional = undefined const track = (to: RouteLocationNormalized) => { - const pathDefinition = getRouteDefinition(to) const path = to.path + if (path === previousPath) return + + const pathDefinition = getRouteDefinition(to) mp.track('Route Visited', { path, pathDefinition }) + previousPath = path } // Track init page view