From 65bc3b1b48a8dc873313a7fba00d2e7d671c4eea Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 24 Jan 2026 19:26:31 -0800 Subject: [PATCH 01/17] feat: add cloud gtm injection --- vite.config.mts | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/vite.config.mts b/vite.config.mts index 9769ec300ae..d868e894a82 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -37,6 +37,15 @@ const VITE_OG_DESC = 'Bring your creative ideas to life with Comfy Cloud. Build and run your workflows to generate stunning images and videos instantly using powerful GPUs — all from your browser, no installation required.' const VITE_OG_IMAGE = `${VITE_OG_URL}/assets/images/og-image.png` const VITE_OG_KEYWORDS = 'ComfyUI, Comfy Cloud, ComfyUI online' +const GTM_CONTAINER_ID = 'GTM-NP9JM6K7' +const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': +new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], +j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= +'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); +})(window,document,'script','dataLayer','${GTM_CONTAINER_ID}');` +const GTM_NO_SCRIPT = + `' // Auto-detect cloud mode from DEV_SERVER_COMFYUI_URL const DEV_SERVER_COMFYUI_ENV_URL = process.env.DEV_SERVER_COMFYUI_URL @@ -416,7 +425,33 @@ export default defineConfig({ } }) ] - : []) + : []), + // Google Tag Manager (cloud distribution only) + { + name: 'inject-gtm', + transformIndexHtml: { + order: 'post', + handler(html) { + if (DISTRIBUTION !== 'cloud') return html + + return { + html, + tags: [ + { + tag: 'script', + children: GTM_SCRIPT, + injectTo: 'head-prepend' + }, + { + tag: 'noscript', + children: GTM_NO_SCRIPT, + injectTo: 'body-prepend' + } + ] + } + } + } + } ], build: { From 43f6251f7a764eaa0f470cf20e194d475bfc47ab Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 24 Jan 2026 19:34:54 -0800 Subject: [PATCH 02/17] feat: track page views in gtm --- global.d.ts | 1 + src/router.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/global.d.ts b/global.d.ts index 7f7dd832f89..ec455707f45 100644 --- a/global.d.ts +++ b/global.d.ts @@ -30,6 +30,7 @@ interface Window { badge?: string } } + dataLayer?: Array> } interface Navigator { diff --git a/src/router.ts b/src/router.ts index b489d225733..88f651899dd 100644 --- a/src/router.ts +++ b/src/router.ts @@ -36,6 +36,18 @@ function getBasePath(): string { const basePath = getBasePath() +function pushPageView(to: RouteLocationNormalized): void { + if (!isCloud) return + + const dataLayer = window.dataLayer ?? (window.dataLayer = []) + dataLayer.push({ + event: 'page_view', + page_location: window.location.href, + page_title: document.title, + page_path: to.fullPath + }) +} + const router = createRouter({ history: isFileProtocol ? createWebHashHistory() @@ -93,6 +105,10 @@ installPreservedQueryTracker(router, [ } ]) +router.afterEach((to) => { + pushPageView(to) +}) + if (isCloud) { const { flags } = useFeatureFlags() const PUBLIC_ROUTE_NAMES = new Set([ From ef1c55b1f6f8851c83db51449f00f37b64e1eebb Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 24 Jan 2026 19:37:02 -0800 Subject: [PATCH 03/17] chore: simplify gtm noscript string --- vite.config.mts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vite.config.mts b/vite.config.mts index d868e894a82..1eb51dff9d2 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -43,9 +43,7 @@ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','${GTM_CONTAINER_ID}');` -const GTM_NO_SCRIPT = - `' +const GTM_NO_SCRIPT = `` // Auto-detect cloud mode from DEV_SERVER_COMFYUI_URL const DEV_SERVER_COMFYUI_ENV_URL = process.env.DEV_SERVER_COMFYUI_URL From c43d4aa5f7d601fe739fa718bcc18224d4172bce Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 24 Jan 2026 19:45:52 -0800 Subject: [PATCH 04/17] fix: align gtm page view payload --- src/router.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/router.ts b/src/router.ts index 88f651899dd..ad5c93cf3a2 100644 --- a/src/router.ts +++ b/src/router.ts @@ -36,15 +36,14 @@ function getBasePath(): string { const basePath = getBasePath() -function pushPageView(to: RouteLocationNormalized): void { +function pushPageView(): void { if (!isCloud) return const dataLayer = window.dataLayer ?? (window.dataLayer = []) dataLayer.push({ event: 'page_view', page_location: window.location.href, - page_title: document.title, - page_path: to.fullPath + page_title: document.title }) } @@ -105,8 +104,8 @@ installPreservedQueryTracker(router, [ } ]) -router.afterEach((to) => { - pushPageView(to) +router.afterEach(() => { + pushPageView() }) if (isCloud) { From c1cb37c6afad46f13a42c0896a47d9a3492ac355 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 24 Jan 2026 19:47:59 -0800 Subject: [PATCH 05/17] feat: track gtm signup event --- src/stores/firebaseAuthStore.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index 6073a43040f..c6fcb2f6d71 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -81,6 +81,19 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` + const pushDataLayerEvent = (event: Record) => { + if (!isCloud || typeof window === 'undefined') return + const dataLayer = window.dataLayer ?? (window.dataLayer = []) + dataLayer.push(event) + } + + const trackSignUp = (method: 'email' | 'google' | 'github') => { + pushDataLayerEvent({ + event: 'sign_up', + method + }) + } + // Providers const googleProvider = new GoogleAuthProvider() googleProvider.addScope('email') @@ -347,6 +360,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { method: 'email', is_new_user: true }) + trackSignUp('email') } return result @@ -365,6 +379,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { method: 'google', is_new_user: isNewUser }) + if (isNewUser) { + trackSignUp('google') + } } return result @@ -383,6 +400,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { method: 'github', is_new_user: isNewUser }) + if (isNewUser) { + trackSignUp('github') + } } return result From 35d81c662e40470df19a5ec14471abde19a6ac25 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 24 Jan 2026 19:57:49 -0800 Subject: [PATCH 06/17] feat: hash gtm signup user id --- src/stores/firebaseAuthStore.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index c6fcb2f6d71..cdd62d78e51 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -87,10 +87,22 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { dataLayer.push(event) } - const trackSignUp = (method: 'email' | 'google' | 'github') => { + const hashSha256 = async (value: string): Promise => { + if (typeof crypto === 'undefined' || !crypto.subtle) 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('') + } + + const trackSignUp = async (method: 'email' | 'google' | 'github') => { + const userId = currentUser.value?.uid + const hashedUserId = userId ? await hashSha256(userId) : undefined pushDataLayerEvent({ event: 'sign_up', - method + method, + ...(hashedUserId ? { user_id: hashedUserId } : {}) }) } @@ -360,7 +372,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { method: 'email', is_new_user: true }) - trackSignUp('email') + await trackSignUp('email') } return result @@ -380,7 +392,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { is_new_user: isNewUser }) if (isNewUser) { - trackSignUp('google') + await trackSignUp('google') } } @@ -401,7 +413,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { is_new_user: isNewUser }) if (isNewUser) { - trackSignUp('github') + await trackSignUp('github') } } From 3d17edd21e1d8b001ef0c10f21a5ae684e7b30bd Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 26 Jan 2026 11:17:51 -0800 Subject: [PATCH 07/17] fix: guard gtm injection in dev --- vite.config.mts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/vite.config.mts b/vite.config.mts index 1eb51dff9d2..b005b3f7f5c 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -37,14 +37,6 @@ const VITE_OG_DESC = 'Bring your creative ideas to life with Comfy Cloud. Build and run your workflows to generate stunning images and videos instantly using powerful GPUs — all from your browser, no installation required.' const VITE_OG_IMAGE = `${VITE_OG_URL}/assets/images/og-image.png` const VITE_OG_KEYWORDS = 'ComfyUI, Comfy Cloud, ComfyUI online' -const GTM_CONTAINER_ID = 'GTM-NP9JM6K7' -const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': -new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], -j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= -'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); -})(window,document,'script','dataLayer','${GTM_CONTAINER_ID}');` -const GTM_NO_SCRIPT = `` - // Auto-detect cloud mode from DEV_SERVER_COMFYUI_URL const DEV_SERVER_COMFYUI_ENV_URL = process.env.DEV_SERVER_COMFYUI_URL const IS_CLOUD_URL = DEV_SERVER_COMFYUI_ENV_URL?.includes('.comfy.org') @@ -58,6 +50,16 @@ const DISTRIBUTION: 'desktop' | 'localhost' | 'cloud' = ? 'cloud' : 'localhost' +const ENABLE_GTM = + process.env.ENABLE_GTM === 'true' || (!IS_DEV && DISTRIBUTION === 'cloud') +const GTM_CONTAINER_ID = 'GTM-NP9JM6K7' +const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': +new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], +j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= +'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); +})(window,document,'script','dataLayer','${GTM_CONTAINER_ID}');` +const GTM_NO_SCRIPT = `` + // Nightly builds are from main branch; RC/stable builds are from core/* branches // Can be overridden via IS_NIGHTLY env var for testing const IS_NIGHTLY = @@ -430,7 +432,7 @@ export default defineConfig({ transformIndexHtml: { order: 'post', handler(html) { - if (DISTRIBUTION !== 'cloud') return html + if (!ENABLE_GTM) return html return { html, From a0e840c04fa6edf49609db2d39f3eea49c867748 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 26 Jan 2026 11:24:32 -0800 Subject: [PATCH 08/17] nit --- vite.config.mts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.mts b/vite.config.mts index b005b3f7f5c..f1584ab9ff5 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -37,6 +37,7 @@ const VITE_OG_DESC = 'Bring your creative ideas to life with Comfy Cloud. Build and run your workflows to generate stunning images and videos instantly using powerful GPUs — all from your browser, no installation required.' const VITE_OG_IMAGE = `${VITE_OG_URL}/assets/images/og-image.png` const VITE_OG_KEYWORDS = 'ComfyUI, Comfy Cloud, ComfyUI online' + // Auto-detect cloud mode from DEV_SERVER_COMFYUI_URL const DEV_SERVER_COMFYUI_ENV_URL = process.env.DEV_SERVER_COMFYUI_URL const IS_CLOUD_URL = DEV_SERVER_COMFYUI_ENV_URL?.includes('.comfy.org') From b0b1f6f025ef8c2d059335466f6f7fe8249022f7 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 11:31:02 -0800 Subject: [PATCH 09/17] feat: gate gtm with build flag --- global.d.ts | 1 + scripts/vite-define-shim.ts | 3 ++ .../composables/useSubscription.test.ts | 45 +++++++++++++++++++ src/router.ts | 2 +- src/stores/firebaseAuthStore.ts | 2 +- vite.config.mts | 1 + vitest.setup.ts | 1 + 7 files changed, 53 insertions(+), 2 deletions(-) diff --git a/global.d.ts b/global.d.ts index ec455707f45..0cb65758cb1 100644 --- a/global.d.ts +++ b/global.d.ts @@ -4,6 +4,7 @@ declare const __SENTRY_DSN__: string declare const __ALGOLIA_APP_ID__: string declare const __ALGOLIA_API_KEY__: string declare const __USE_PROD_CONFIG__: boolean +declare const __GTM_ENABLED__: boolean interface Window { __CONFIG__: { diff --git a/scripts/vite-define-shim.ts b/scripts/vite-define-shim.ts index 970d09aef03..4912e084d46 100644 --- a/scripts/vite-define-shim.ts +++ b/scripts/vite-define-shim.ts @@ -11,6 +11,7 @@ declare global { const __ALGOLIA_APP_ID__: string const __ALGOLIA_API_KEY__: string const __USE_PROD_CONFIG__: boolean + const __GTM_ENABLED__: boolean const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud' const __IS_NIGHTLY__: boolean } @@ -22,6 +23,7 @@ type GlobalWithDefines = typeof globalThis & { __ALGOLIA_APP_ID__: string __ALGOLIA_API_KEY__: string __USE_PROD_CONFIG__: boolean + __GTM_ENABLED__: boolean __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud' __IS_NIGHTLY__: boolean window?: Record @@ -37,6 +39,7 @@ globalWithDefines.__SENTRY_DSN__ = '' globalWithDefines.__ALGOLIA_APP_ID__ = '' globalWithDefines.__ALGOLIA_API_KEY__ = '' globalWithDefines.__USE_PROD_CONFIG__ = false +globalWithDefines.__GTM_ENABLED__ = false globalWithDefines.__DISTRIBUTION__ = 'localhost' globalWithDefines.__IS_NIGHTLY__ = false diff --git a/src/platform/cloud/subscription/composables/useSubscription.test.ts b/src/platform/cloud/subscription/composables/useSubscription.test.ts index ffa370a903d..47a6b84022c 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.test.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.test.ts @@ -206,6 +206,51 @@ describe('useSubscription', () => { ) }) + it('pushes purchase event after a pending subscription completes', async () => { + const originalGtmEnabled = __GTM_ENABLED__ + try { + vi.stubGlobal('__GTM_ENABLED__', true) + window.dataLayer = [] + localStorage.setItem( + 'pending_subscription_purchase', + JSON.stringify({ + tierKey: 'creator', + billingCycle: 'monthly', + timestamp: Date.now() + }) + ) + + vi.mocked(global.fetch).mockResolvedValue({ + ok: true, + json: async () => ({ + is_active: true, + subscription_id: 'sub_123', + subscription_tier: 'CREATOR', + subscription_duration: 'MONTHLY' + }) + } as Response) + + mockIsLoggedIn.value = true + const { fetchStatus } = useSubscription() + + await fetchStatus() + + expect(window.dataLayer).toHaveLength(1) + expect(window.dataLayer?.[0]).toMatchObject({ + event: 'purchase', + transaction_id: 'sub_123', + currency: 'USD', + item_id: 'monthly_creator', + item_variant: 'monthly', + item_category: 'subscription', + quantity: 1 + }) + expect(localStorage.getItem('pending_subscription_purchase')).toBeNull() + } finally { + vi.stubGlobal('__GTM_ENABLED__', originalGtmEnabled) + } + }) + it('should handle fetch errors gracefully', async () => { vi.mocked(global.fetch).mockResolvedValue({ ok: false, diff --git a/src/router.ts b/src/router.ts index ad5c93cf3a2..6b405fccffb 100644 --- a/src/router.ts +++ b/src/router.ts @@ -37,7 +37,7 @@ function getBasePath(): string { const basePath = getBasePath() function pushPageView(): void { - if (!isCloud) return + if (!__GTM_ENABLED__ || typeof window === 'undefined') return const dataLayer = window.dataLayer ?? (window.dataLayer = []) dataLayer.push({ diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index cdd62d78e51..e793c5e3d54 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -82,7 +82,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` const pushDataLayerEvent = (event: Record) => { - if (!isCloud || typeof window === 'undefined') return + if (!__GTM_ENABLED__ || typeof window === 'undefined') return const dataLayer = window.dataLayer ?? (window.dataLayer = []) dataLayer.push(event) } diff --git a/vite.config.mts b/vite.config.mts index f1584ab9ff5..270caf10be9 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -546,6 +546,7 @@ export default defineConfig({ __ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''), __ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''), __USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true', + __GTM_ENABLED__: JSON.stringify(ENABLE_GTM), __DISTRIBUTION__: JSON.stringify(DISTRIBUTION), __IS_NIGHTLY__: JSON.stringify(IS_NIGHTLY) }, diff --git a/vitest.setup.ts b/vitest.setup.ts index 81c39723a5c..f174dcf0696 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -45,6 +45,7 @@ globalThis.__SENTRY_DSN__ = '' globalThis.__ALGOLIA_APP_ID__ = '' globalThis.__ALGOLIA_API_KEY__ = '' globalThis.__USE_PROD_CONFIG__ = false +globalThis.__GTM_ENABLED__ = false globalThis.__DISTRIBUTION__ = 'localhost' globalThis.__IS_NIGHTLY__ = false From 8d1ff4e043bcf6ff6416c36f3ba6c00254fb47f8 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 11:36:45 -0800 Subject: [PATCH 10/17] feat: add gtm purchase tracking --- .../composables/useSubscription.ts | 53 +++++++++++++- .../utils/subscriptionCheckoutUtil.ts | 4 ++ .../utils/subscriptionPurchaseTracker.ts | 69 +++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index f5948ee6964..227f54001e4 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -1,11 +1,11 @@ import { computed, ref, watch } from 'vue' +import { useI18n } from 'vue-i18n' import { createSharedComposable } from '@vueuse/core' import { useCurrentUser } from '@/composables/auth/useCurrentUser' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' 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 { @@ -13,7 +13,14 @@ import { 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' @@ -29,6 +36,7 @@ function useSubscriptionInternal() { const subscriptionStatus = ref(null) const telemetry = useTelemetry() const isInitialized = ref(false) + const { t } = useI18n() const isSubscribedOrIsNotCloud = computed(() => { if (!isCloud || !window.__CONFIG__?.subscription_required) return true @@ -94,6 +102,46 @@ function useSubscriptionInternal() { }) const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` + const isGtmEnabled = __GTM_ENABLED__ + + const pushDataLayerEvent = (event: Record) => { + if (!isGtmEnabled || typeof window === 'undefined') return + const dataLayer = window.dataLayer ?? (window.dataLayer = []) + dataLayer.push(event) + } + + const trackSubscriptionPurchase = ( + status: CloudSubscriptionStatusResponse | null + ) => { + 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', + item_id: `${billingCycle}_${tierKey}`, + item_name: planName, + item_category: 'subscription', + item_variant: billingCycle, + price: value, + quantity: 1 + }) + + clearPendingSubscriptionPurchase() + } const fetchStatus = wrapWithErrorHandlingAsync( fetchSubscriptionStatus, @@ -194,6 +242,7 @@ function useSubscriptionInternal() { const statusData = await response.json() subscriptionStatus.value = statusData + trackSubscriptionPurchase(statusData) return statusData } diff --git a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts index 746a7d61667..1de59820c8d 100644 --- a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts +++ b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts @@ -6,6 +6,7 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing' +import { startSubscriptionPurchaseTracking } from '@/platform/cloud/subscription/utils/subscriptionPurchaseTracker' import type { BillingCycle } from './subscriptionTierRank' type CheckoutTier = TierKey | `${TierKey}-yearly` @@ -78,6 +79,9 @@ export async function performSubscriptionCheckout( const data = await response.json() if (data.checkout_url) { + if (__GTM_ENABLED__) { + startSubscriptionPurchaseTracking(tierKey, currentBillingCycle) + } if (openInNewTab) { window.open(data.checkout_url, '_blank') } else { diff --git a/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts b/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts new file mode 100644 index 00000000000..058897b3384 --- /dev/null +++ b/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts @@ -0,0 +1,69 @@ +import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing' +import type { BillingCycle } from './subscriptionTierRank' + +type PendingSubscriptionPurchase = { + tierKey: TierKey + billingCycle: BillingCycle + timestamp: number +} + +const STORAGE_KEY = 'pending_subscription_purchase' +const MAX_AGE_MS = 24 * 60 * 60 * 1000 // 24 hours +const VALID_TIERS: TierKey[] = ['standard', 'creator', 'pro', 'founder'] +const VALID_CYCLES: BillingCycle[] = ['monthly', 'yearly'] + +export function startSubscriptionPurchaseTracking( + tierKey: TierKey, + billingCycle: BillingCycle +): void { + if (typeof window === 'undefined') return + try { + const payload: PendingSubscriptionPurchase = { + tierKey, + billingCycle, + timestamp: Date.now() + } + localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)) + } catch { + // Ignore storage errors (e.g. private browsing mode) + } +} + +export function getPendingSubscriptionPurchase(): PendingSubscriptionPurchase | null { + if (typeof window === 'undefined') return null + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return null + + try { + const parsed = JSON.parse(raw) as PendingSubscriptionPurchase + if (!parsed || typeof parsed !== 'object') { + localStorage.removeItem(STORAGE_KEY) + return null + } + + const { tierKey, billingCycle, timestamp } = parsed + if ( + !VALID_TIERS.includes(tierKey) || + !VALID_CYCLES.includes(billingCycle) || + typeof timestamp !== 'number' + ) { + localStorage.removeItem(STORAGE_KEY) + return null + } + + if (Date.now() - timestamp > MAX_AGE_MS) { + localStorage.removeItem(STORAGE_KEY) + return null + } + + return parsed + } catch { + localStorage.removeItem(STORAGE_KEY) + return null + } +} + +export function clearPendingSubscriptionPurchase(): void { + if (typeof window === 'undefined') return + localStorage.removeItem(STORAGE_KEY) +} From 51e7291d58f4a1f95271b75ff0375f579dbd890c Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 11:38:33 -0800 Subject: [PATCH 11/17] fix: avoid useI18n in shared composable --- src/platform/cloud/subscription/composables/useSubscription.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index 227f54001e4..a12167d30ae 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -1,11 +1,11 @@ import { computed, ref, watch } from 'vue' -import { useI18n } from 'vue-i18n' import { createSharedComposable } from '@vueuse/core' import { useCurrentUser } from '@/composables/auth/useCurrentUser' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' 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 { @@ -36,7 +36,6 @@ function useSubscriptionInternal() { const subscriptionStatus = ref(null) const telemetry = useTelemetry() const isInitialized = ref(false) - const { t } = useI18n() const isSubscribedOrIsNotCloud = computed(() => { if (!isCloud || !window.__CONFIG__?.subscription_required) return true From 02a4d3ec03147d7ab2686be814eb9793fdff5a63 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 11:52:33 -0800 Subject: [PATCH 12/17] fix: read gtm flag at push time --- .../cloud/subscription/composables/useSubscription.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index a12167d30ae..9f78d3cc690 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -101,10 +101,8 @@ function useSubscriptionInternal() { }) const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` - const isGtmEnabled = __GTM_ENABLED__ - const pushDataLayerEvent = (event: Record) => { - if (!isGtmEnabled || typeof window === 'undefined') return + if (!__GTM_ENABLED__ || typeof window === 'undefined') return const dataLayer = window.dataLayer ?? (window.dataLayer = []) dataLayer.push(event) } From cea03a5597877263419d03eee5b0cb820393eec5 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 12:06:40 -0800 Subject: [PATCH 13/17] feat: load gtm at runtime for cloud --- global.d.ts | 1 - scripts/vite-define-shim.ts | 3 - src/main.ts | 3 + .../composables/useSubscription.test.ts | 72 +++++++++---------- .../composables/useSubscription.ts | 6 +- .../utils/subscriptionCheckoutUtil.ts | 4 +- src/platform/telemetry/gtm.ts | 43 +++++++++++ src/router.ts | 6 +- src/stores/firebaseAuthStore.ts | 8 +-- vite.config.mts | 39 +--------- vitest.setup.ts | 1 - 11 files changed, 87 insertions(+), 99 deletions(-) create mode 100644 src/platform/telemetry/gtm.ts diff --git a/global.d.ts b/global.d.ts index 0cb65758cb1..ec455707f45 100644 --- a/global.d.ts +++ b/global.d.ts @@ -4,7 +4,6 @@ declare const __SENTRY_DSN__: string declare const __ALGOLIA_APP_ID__: string declare const __ALGOLIA_API_KEY__: string declare const __USE_PROD_CONFIG__: boolean -declare const __GTM_ENABLED__: boolean interface Window { __CONFIG__: { diff --git a/scripts/vite-define-shim.ts b/scripts/vite-define-shim.ts index 4912e084d46..970d09aef03 100644 --- a/scripts/vite-define-shim.ts +++ b/scripts/vite-define-shim.ts @@ -11,7 +11,6 @@ declare global { const __ALGOLIA_APP_ID__: string const __ALGOLIA_API_KEY__: string const __USE_PROD_CONFIG__: boolean - const __GTM_ENABLED__: boolean const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud' const __IS_NIGHTLY__: boolean } @@ -23,7 +22,6 @@ type GlobalWithDefines = typeof globalThis & { __ALGOLIA_APP_ID__: string __ALGOLIA_API_KEY__: string __USE_PROD_CONFIG__: boolean - __GTM_ENABLED__: boolean __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud' __IS_NIGHTLY__: boolean window?: Record @@ -39,7 +37,6 @@ globalWithDefines.__SENTRY_DSN__ = '' globalWithDefines.__ALGOLIA_APP_ID__ = '' globalWithDefines.__ALGOLIA_API_KEY__ = '' globalWithDefines.__USE_PROD_CONFIG__ = false -globalWithDefines.__GTM_ENABLED__ = false globalWithDefines.__DISTRIBUTION__ = 'localhost' globalWithDefines.__IS_NIGHTLY__ = false diff --git a/src/main.ts b/src/main.ts index 4d395dbb675..e0fa4f99422 100644 --- a/src/main.ts +++ b/src/main.ts @@ -30,6 +30,9 @@ if (isCloud) { const { refreshRemoteConfig } = await import('@/platform/remoteConfig/refreshRemoteConfig') await refreshRemoteConfig({ useAuth: false }) + + const { initGtm } = await import('@/platform/telemetry/gtm') + initGtm() } const ComfyUIPreset = definePreset(Aura, { diff --git a/src/platform/cloud/subscription/composables/useSubscription.test.ts b/src/platform/cloud/subscription/composables/useSubscription.test.ts index 47a6b84022c..40115d38d1a 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.test.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.test.ts @@ -207,48 +207,42 @@ describe('useSubscription', () => { }) it('pushes purchase event after a pending subscription completes', async () => { - const originalGtmEnabled = __GTM_ENABLED__ - try { - vi.stubGlobal('__GTM_ENABLED__', true) - window.dataLayer = [] - localStorage.setItem( - 'pending_subscription_purchase', - JSON.stringify({ - tierKey: 'creator', - billingCycle: 'monthly', - timestamp: Date.now() - }) - ) - - vi.mocked(global.fetch).mockResolvedValue({ - ok: true, - json: async () => ({ - is_active: true, - subscription_id: 'sub_123', - subscription_tier: 'CREATOR', - subscription_duration: 'MONTHLY' - }) - } as Response) + window.dataLayer = [] + localStorage.setItem( + 'pending_subscription_purchase', + JSON.stringify({ + tierKey: 'creator', + billingCycle: 'monthly', + timestamp: Date.now() + }) + ) + + vi.mocked(global.fetch).mockResolvedValue({ + ok: true, + json: async () => ({ + is_active: true, + subscription_id: 'sub_123', + subscription_tier: 'CREATOR', + subscription_duration: 'MONTHLY' + }) + } as Response) - mockIsLoggedIn.value = true - const { fetchStatus } = useSubscription() + mockIsLoggedIn.value = true + const { fetchStatus } = useSubscription() - await fetchStatus() + await fetchStatus() - expect(window.dataLayer).toHaveLength(1) - expect(window.dataLayer?.[0]).toMatchObject({ - event: 'purchase', - transaction_id: 'sub_123', - currency: 'USD', - item_id: 'monthly_creator', - item_variant: 'monthly', - item_category: 'subscription', - quantity: 1 - }) - expect(localStorage.getItem('pending_subscription_purchase')).toBeNull() - } finally { - vi.stubGlobal('__GTM_ENABLED__', originalGtmEnabled) - } + expect(window.dataLayer).toHaveLength(1) + expect(window.dataLayer?.[0]).toMatchObject({ + event: 'purchase', + transaction_id: 'sub_123', + currency: 'USD', + item_id: 'monthly_creator', + item_variant: 'monthly', + item_category: 'subscription', + quantity: 1 + }) + expect(localStorage.getItem('pending_subscription_purchase')).toBeNull() }) it('should handle fetch errors gracefully', async () => { diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index 9f78d3cc690..4412db77392 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -8,6 +8,7 @@ import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi' import { t } from '@/i18n' import { isCloud } from '@/platform/distribution/types' import { useTelemetry } from '@/platform/telemetry' +import { pushDataLayerEvent } from '@/platform/telemetry/gtm' import { FirebaseAuthStoreError, useFirebaseAuthStore @@ -101,11 +102,6 @@ function useSubscriptionInternal() { }) const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` - const pushDataLayerEvent = (event: Record) => { - if (!__GTM_ENABLED__ || typeof window === 'undefined') return - const dataLayer = window.dataLayer ?? (window.dataLayer = []) - dataLayer.push(event) - } const trackSubscriptionPurchase = ( status: CloudSubscriptionStatusResponse | null diff --git a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts index 1de59820c8d..8be996ebc93 100644 --- a/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts +++ b/src/platform/cloud/subscription/utils/subscriptionCheckoutUtil.ts @@ -79,9 +79,7 @@ export async function performSubscriptionCheckout( const data = await response.json() if (data.checkout_url) { - if (__GTM_ENABLED__) { - startSubscriptionPurchaseTracking(tierKey, currentBillingCycle) - } + startSubscriptionPurchaseTracking(tierKey, currentBillingCycle) if (openInNewTab) { window.open(data.checkout_url, '_blank') } else { diff --git a/src/platform/telemetry/gtm.ts b/src/platform/telemetry/gtm.ts new file mode 100644 index 00000000000..d5c738ae4ac --- /dev/null +++ b/src/platform/telemetry/gtm.ts @@ -0,0 +1,43 @@ +import { isCloud } from '@/platform/distribution/types' + +const GTM_CONTAINER_ID = 'GTM-NP9JM6K7' + +let isInitialized = false +let initPromise: Promise | null = null + +export function initGtm(): void { + if (!isCloud || typeof window === 'undefined') return + if (typeof document === 'undefined') return + if (isInitialized) return + + if (!initPromise) { + initPromise = new Promise((resolve) => { + const dataLayer = window.dataLayer ?? (window.dataLayer = []) + dataLayer.push({ + 'gtm.start': Date.now(), + event: 'gtm.js' + }) + + const script = document.createElement('script') + script.async = true + script.src = `https://www.googletagmanager.com/gtm.js?id=${GTM_CONTAINER_ID}` + + const finalize = () => { + isInitialized = true + resolve() + } + + script.addEventListener('load', finalize, { once: true }) + script.addEventListener('error', finalize, { once: true }) + document.head?.appendChild(script) + }) + } + + void initPromise +} + +export function pushDataLayerEvent(event: Record): void { + if (!isCloud || typeof window === 'undefined') return + const dataLayer = window.dataLayer ?? (window.dataLayer = []) + dataLayer.push(event) +} diff --git a/src/router.ts b/src/router.ts index 6b405fccffb..365e7859e26 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/gtm' import { useDialogService } from '@/services/dialogService' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { useUserStore } from '@/stores/userStore' @@ -37,10 +38,9 @@ function getBasePath(): string { const basePath = getBasePath() function pushPageView(): void { - if (!__GTM_ENABLED__ || typeof window === 'undefined') return + if (!isCloud || typeof window === 'undefined') return - const dataLayer = window.dataLayer ?? (window.dataLayer = []) - dataLayer.push({ + pushDataLayerEvent({ event: 'page_view', page_location: window.location.href, page_title: document.title diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index e793c5e3d54..6c9f9d5db7e 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -26,6 +26,7 @@ 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 } from '@/platform/telemetry/gtm' import { useDialogService } from '@/services/dialogService' import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore' import type { AuthHeader } from '@/types/authTypes' @@ -81,12 +82,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` - const pushDataLayerEvent = (event: Record) => { - if (!__GTM_ENABLED__ || typeof window === 'undefined') return - const dataLayer = window.dataLayer ?? (window.dataLayer = []) - dataLayer.push(event) - } - const hashSha256 = async (value: string): Promise => { if (typeof crypto === 'undefined' || !crypto.subtle) return const data = new TextEncoder().encode(value) @@ -97,6 +92,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { } const trackSignUp = async (method: 'email' | 'google' | 'github') => { + if (!isCloud) return const userId = currentUser.value?.uid const hashedUserId = userId ? await hashSha256(userId) : undefined pushDataLayerEvent({ diff --git a/vite.config.mts b/vite.config.mts index 270caf10be9..9769ec300ae 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -51,16 +51,6 @@ const DISTRIBUTION: 'desktop' | 'localhost' | 'cloud' = ? 'cloud' : 'localhost' -const ENABLE_GTM = - process.env.ENABLE_GTM === 'true' || (!IS_DEV && DISTRIBUTION === 'cloud') -const GTM_CONTAINER_ID = 'GTM-NP9JM6K7' -const GTM_SCRIPT = `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': -new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], -j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= -'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); -})(window,document,'script','dataLayer','${GTM_CONTAINER_ID}');` -const GTM_NO_SCRIPT = `` - // Nightly builds are from main branch; RC/stable builds are from core/* branches // Can be overridden via IS_NIGHTLY env var for testing const IS_NIGHTLY = @@ -426,33 +416,7 @@ export default defineConfig({ } }) ] - : []), - // Google Tag Manager (cloud distribution only) - { - name: 'inject-gtm', - transformIndexHtml: { - order: 'post', - handler(html) { - if (!ENABLE_GTM) return html - - return { - html, - tags: [ - { - tag: 'script', - children: GTM_SCRIPT, - injectTo: 'head-prepend' - }, - { - tag: 'noscript', - children: GTM_NO_SCRIPT, - injectTo: 'body-prepend' - } - ] - } - } - } - } + : []) ], build: { @@ -546,7 +510,6 @@ export default defineConfig({ __ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''), __ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || ''), __USE_PROD_CONFIG__: process.env.USE_PROD_CONFIG === 'true', - __GTM_ENABLED__: JSON.stringify(ENABLE_GTM), __DISTRIBUTION__: JSON.stringify(DISTRIBUTION), __IS_NIGHTLY__: JSON.stringify(IS_NIGHTLY) }, diff --git a/vitest.setup.ts b/vitest.setup.ts index f174dcf0696..81c39723a5c 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -45,7 +45,6 @@ globalThis.__SENTRY_DSN__ = '' globalThis.__ALGOLIA_APP_ID__ = '' globalThis.__ALGOLIA_API_KEY__ = '' globalThis.__USE_PROD_CONFIG__ = false -globalThis.__GTM_ENABLED__ = false globalThis.__DISTRIBUTION__ = 'localhost' globalThis.__IS_NIGHTLY__ = false From 7bdd527d672a43a2fb5bfae92b38772dc6d78242 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 12:10:38 -0800 Subject: [PATCH 14/17] fix: use function declarations in subscription helper --- .../cloud/subscription/composables/useSubscription.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index 4412db77392..fbb93ac0c0f 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -101,11 +101,13 @@ function useSubscriptionInternal() { : baseName }) - const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` + function buildApiUrl(path: string): string { + return `${getComfyApiBaseUrl()}${path}` + } - const trackSubscriptionPurchase = ( + function trackSubscriptionPurchase( status: CloudSubscriptionStatusResponse | null - ) => { + ): void { if (!status?.is_active || !status.subscription_id) return const pendingPurchase = getPendingSubscriptionPurchase() From cba42e283947e34f330b161e1a914b917ae574ee Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 12:12:30 -0800 Subject: [PATCH 15/17] fix: guard subscription purchase storage --- .../utils/subscriptionPurchaseTracker.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts b/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts index 058897b3384..41bbb348427 100644 --- a/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts +++ b/src/platform/cloud/subscription/utils/subscriptionPurchaseTracker.ts @@ -12,6 +12,14 @@ const MAX_AGE_MS = 24 * 60 * 60 * 1000 // 24 hours const VALID_TIERS: TierKey[] = ['standard', 'creator', 'pro', 'founder'] const VALID_CYCLES: BillingCycle[] = ['monthly', 'yearly'] +const safeRemove = (): void => { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + // Ignore storage errors (e.g. private browsing mode) + } +} + export function startSubscriptionPurchaseTracking( tierKey: TierKey, billingCycle: BillingCycle @@ -31,13 +39,14 @@ export function startSubscriptionPurchaseTracking( export function getPendingSubscriptionPurchase(): PendingSubscriptionPurchase | null { if (typeof window === 'undefined') return null - const raw = localStorage.getItem(STORAGE_KEY) - if (!raw) return null try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return null + const parsed = JSON.parse(raw) as PendingSubscriptionPurchase if (!parsed || typeof parsed !== 'object') { - localStorage.removeItem(STORAGE_KEY) + safeRemove() return null } @@ -47,23 +56,23 @@ export function getPendingSubscriptionPurchase(): PendingSubscriptionPurchase | !VALID_CYCLES.includes(billingCycle) || typeof timestamp !== 'number' ) { - localStorage.removeItem(STORAGE_KEY) + safeRemove() return null } if (Date.now() - timestamp > MAX_AGE_MS) { - localStorage.removeItem(STORAGE_KEY) + safeRemove() return null } return parsed } catch { - localStorage.removeItem(STORAGE_KEY) + safeRemove() return null } } export function clearPendingSubscriptionPurchase(): void { if (typeof window === 'undefined') return - localStorage.removeItem(STORAGE_KEY) + safeRemove() } From 523f1c4933298599a83ad2c628ade76fbb8f516c Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 12:19:08 -0800 Subject: [PATCH 16/17] fix: make gtm sign-up tracking best-effort --- src/stores/firebaseAuthStore.ts | 38 +++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index 6c9f9d5db7e..c1c81037d78 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -26,7 +26,7 @@ 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 } from '@/platform/telemetry/gtm' +import { pushDataLayerEvent as pushDataLayerEventBase } from '@/platform/telemetry/gtm' import { useDialogService } from '@/services/dialogService' import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore' import type { AuthHeader } from '@/types/authTypes' @@ -82,8 +82,19 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const buildApiUrl = (path: string) => `${getComfyApiBaseUrl()}${path}` - const hashSha256 = async (value: string): Promise => { + 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)) @@ -91,15 +102,20 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { .join('') } - const trackSignUp = async (method: 'email' | 'google' | 'github') => { - if (!isCloud) return - const userId = currentUser.value?.uid - const hashedUserId = userId ? await hashSha256(userId) : undefined - pushDataLayerEvent({ - event: 'sign_up', - method, - ...(hashedUserId ? { user_id: hashedUserId } : {}) - }) + 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 From 62968f329d70abb7abc740e5f00203bb5878c9a8 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 27 Jan 2026 12:24:21 -0800 Subject: [PATCH 17/17] fix: guard subscription purchase tracking --- .../cloud/subscription/composables/useSubscription.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/platform/cloud/subscription/composables/useSubscription.ts b/src/platform/cloud/subscription/composables/useSubscription.ts index fbb93ac0c0f..b5ef7f63b64 100644 --- a/src/platform/cloud/subscription/composables/useSubscription.ts +++ b/src/platform/cloud/subscription/composables/useSubscription.ts @@ -237,7 +237,12 @@ function useSubscriptionInternal() { const statusData = await response.json() subscriptionStatus.value = statusData - trackSubscriptionPurchase(statusData) + + try { + await trackSubscriptionPurchase(statusData) + } catch (error) { + console.error('Failed to track subscription purchase', error) + } return statusData }