From ede4aa24f69ec02d3b7ae8af588b6f0bbbc888ec Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 19:59:33 -0800 Subject: [PATCH] fix: route gtm through telemetry entrypoint --- src/main.ts | 3 + .../composables/useSubscription.ts | 57 ++++++++++++++++++- src/platform/telemetry/index.ts | 32 ++++++++++- src/router.ts | 15 +++++ src/stores/firebaseAuthStore.ts | 48 +++++++++++++++- 5 files changed, 148 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index ca80b87f8..966b380d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,6 +31,9 @@ if (isCloud) { const { refreshRemoteConfig } = await import('@/platform/remoteConfig/refreshRemoteConfig') await refreshRemoteConfig({ useAuth: false }) + + const { initGtm } = await import('@/platform/telemetry') + initGtm() } const ComfyUIPreset = definePreset(Aura, { diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index 7d8e8a4f8..e3ab00781 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -7,13 +7,20 @@ import { useErrorHandling } from '@/composables/useErrorHandling' import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi' import { t } from '@/i18n' import { isCloud } from '@/platform/distribution/types' -import { useTelemetry } from '@/platform/telemetry' +import { pushDataLayerEvent, useTelemetry } from '@/platform/telemetry' import { FirebaseAuthStoreError, useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { useDialogService } from '@/services/dialogService' -import { TIER_TO_KEY } from '@/platform/cloud/subscription/constants/tierPricing' +import { + getTierPrice, + TIER_TO_KEY +} from '@/platform/cloud/subscription/constants/tierPricing' +import { + clearPendingSubscriptionPurchase, + getPendingSubscriptionPurchase +} from '@/platform/cloud/subscription/utils/subscriptionPurchaseTracker' import type { operations } from '@/types/comfyRegistryTypes' import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher' @@ -93,7 +100,45 @@ function useSubscriptionInternal() { : baseName }) - const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` + function buildApiUrl(path: string): string { + return `${getComfyApiBaseUrl()}${path}` + } + + function trackSubscriptionPurchase( + status: CloudSubscriptionStatusResponse | null + ): void { + if (!status?.is_active || !status.subscription_id) return + + const pendingPurchase = getPendingSubscriptionPurchase() + if (!pendingPurchase) return + + const { tierKey, billingCycle } = pendingPurchase + const isYearly = billingCycle === 'yearly' + const baseName = t(`subscription.tiers.${tierKey}.name`) + const planName = isYearly + ? t('subscription.tierNameYearly', { name: baseName }) + : baseName + const unitPrice = getTierPrice(tierKey, isYearly) + const value = isYearly && tierKey !== 'founder' ? unitPrice * 12 : unitPrice + pushDataLayerEvent({ + event: 'purchase', + transaction_id: status.subscription_id, + value, + currency: 'USD', + items: [ + { + item_id: `${billingCycle}_${tierKey}`, + item_name: planName, + item_category: 'subscription', + item_variant: billingCycle, + price: value, + quantity: 1 + } + ] + }) + + clearPendingSubscriptionPurchase() + } const fetchStatus = wrapWithErrorHandlingAsync( fetchSubscriptionStatus, @@ -194,6 +239,12 @@ function useSubscriptionInternal() { const statusData = await response.json() subscriptionStatus.value = statusData + + try { + await trackSubscriptionPurchase(statusData) + } catch (error) { + console.error('Failed to track subscription purchase', error) + } return statusData } diff --git a/src/platform/telemetry/index.ts b/src/platform/telemetry/index.ts index 83d7f2c9f..74bb7157d 100644 --- a/src/platform/telemetry/index.ts +++ b/src/platform/telemetry/index.ts @@ -14,13 +14,25 @@ * This approach maintains complete separation between cloud and OSS builds * while ensuring the open source version contains no telemetry dependencies. */ -import { isCloud } from '@/platform/distribution/types' - import { MixpanelTelemetryProvider } from './providers/cloud/MixpanelTelemetryProvider' import type { TelemetryProvider } from './types' +type GtmModule = { + initGtm: () => void + pushDataLayerEvent: (event: Record) => void +} + // Singleton instance let _telemetryProvider: TelemetryProvider | null = null +let gtmModulePromise: Promise | null = null +const IS_CLOUD_BUILD = __DISTRIBUTION__ === 'cloud' + +function loadGtmModule(): Promise { + if (!gtmModulePromise) { + gtmModulePromise = import('./gtm') + } + return gtmModulePromise +} /** * Telemetry factory - conditionally creates provider based on distribution @@ -32,7 +44,7 @@ let _telemetryProvider: TelemetryProvider | null = null export function useTelemetry(): TelemetryProvider | null { if (_telemetryProvider === null) { // Use distribution check for tree-shaking - if (isCloud) { + if (IS_CLOUD_BUILD) { _telemetryProvider = new MixpanelTelemetryProvider() } // For OSS builds, _telemetryProvider stays null @@ -40,3 +52,17 @@ export function useTelemetry(): TelemetryProvider | null { return _telemetryProvider } + +export function initGtm(): void { + if (!IS_CLOUD_BUILD || typeof window === 'undefined') return + void loadGtmModule().then(({ initGtm }) => { + initGtm() + }) +} + +export function pushDataLayerEvent(event: Record): void { + if (!IS_CLOUD_BUILD || typeof window === 'undefined') return + void loadGtmModule().then(({ pushDataLayerEvent }) => { + pushDataLayerEvent(event) + }) +} diff --git a/src/router.ts b/src/router.ts index b489d2257..adc6848af 100644 --- a/src/router.ts +++ b/src/router.ts @@ -9,6 +9,7 @@ import type { RouteLocationNormalized } from 'vue-router' import { useFeatureFlags } from '@/composables/useFeatureFlags' import { isCloud } from '@/platform/distribution/types' +import { pushDataLayerEvent } from '@/platform/telemetry' import { useDialogService } from '@/services/dialogService' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { useUserStore } from '@/stores/userStore' @@ -36,6 +37,16 @@ function getBasePath(): string { const basePath = getBasePath() +function pushPageView(): void { + if (!isCloud || typeof window === 'undefined') return + + pushDataLayerEvent({ + event: 'page_view', + page_location: window.location.href, + page_title: document.title + }) +} + const router = createRouter({ history: isFileProtocol ? createWebHashHistory() @@ -93,6 +104,10 @@ installPreservedQueryTracker(router, [ } ]) +router.afterEach(() => { + pushPageView() +}) + if (isCloud) { const { flags } = useFeatureFlags() const PUBLIC_ROUTE_NAMES = new Set([ diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index 41bae52a2..965ba3a8a 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -25,7 +25,10 @@ import { getComfyApiBaseUrl } from '@/config/comfyApi' import { t } from '@/i18n' import { WORKSPACE_STORAGE_KEYS } from '@/platform/auth/workspace/workspaceConstants' import { isCloud } from '@/platform/distribution/types' -import { useTelemetry } from '@/platform/telemetry' +import { + pushDataLayerEvent as pushDataLayerEventBase, + useTelemetry +} from '@/platform/telemetry' import { useDialogService } from '@/services/dialogService' import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore' import type { AuthHeader } from '@/types/authTypes' @@ -81,6 +84,42 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` + function pushDataLayerEvent(event: Record): void { + if (!isCloud || typeof window === 'undefined') return + + try { + pushDataLayerEventBase(event) + } catch (error) { + console.warn('Failed to push data layer event', error) + } + } + + async function hashSha256(value: string): Promise { + if (typeof crypto === 'undefined' || !crypto.subtle) return + if (typeof TextEncoder === 'undefined') return + const data = new TextEncoder().encode(value) + const hash = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') + } + + async function trackSignUp(method: 'email' | 'google' | 'github') { + if (!isCloud || typeof window === 'undefined') return + + try { + const userId = currentUser.value?.uid + const hashedUserId = userId ? await hashSha256(userId) : undefined + pushDataLayerEvent({ + event: 'sign_up', + method, + ...(hashedUserId ? { user_id: hashedUserId } : {}) + }) + } catch (error) { + console.warn('Failed to track sign up', error) + } + } + // Providers const googleProvider = new GoogleAuthProvider() googleProvider.addScope('email') @@ -372,6 +411,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { method: 'email', is_new_user: true }) + await trackSignUp('email') } return result @@ -390,6 +430,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { method: 'google', is_new_user: isNewUser }) + if (isNewUser) { + await trackSignUp('google') + } } return result @@ -408,6 +451,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { method: 'github', is_new_user: isNewUser }) + if (isNewUser) { + await trackSignUp('github') + } } return result