feat: add TelemetryRegistry for multi-provider dispatch

- Add TelemetryRegistry that dispatches to multiple providers
- Add GtmTelemetryProvider for GTM/GA4 integration
- Convert TelemetryProvider methods to optional (providers implement what they need)
- Add TelemetryDispatcher type (Required<TelemetryProvider>) for call sites
- Dynamically import providers to ensure tree-shaking in OSS builds
- Track page_view, sign_up, login, and purchase events via GTM
- Remove gtm.ts in favor of GtmTelemetryProvider

Amp-Thread-ID: https://ampcode.com/threads/T-019c07ca-7748-77c8-b15b-d79d42779f8f
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Subagent 5
2026-01-28 20:17:19 -08:00
parent a3ccd92366
commit c0ee82014d
14 changed files with 431 additions and 176 deletions

1
global.d.ts vendored
View File

@@ -8,6 +8,7 @@ declare const __USE_PROD_CONFIG__: boolean
interface Window {
__CONFIG__: {
mixpanel_token?: string
gtm_id?: string
require_whitelist?: boolean
subscription_required?: boolean
max_upload_size?: number

View File

@@ -32,8 +32,8 @@ if (isCloud) {
await import('@/platform/remoteConfig/refreshRemoteConfig')
await refreshRemoteConfig({ useAuth: false })
const { initGtm } = await import('@/platform/telemetry')
initGtm()
const { initTelemetry } = await import('@/platform/telemetry')
await initTelemetry()
}
const ComfyUIPreset = definePreset(Aura, {

View File

@@ -9,7 +9,6 @@ const {
mockAccessBillingPortal,
mockShowSubscriptionRequiredDialog,
mockGetAuthHeader,
mockPushDataLayerEvent,
mockTelemetry,
mockUserId
} = vi.hoisted(() => ({
@@ -20,7 +19,6 @@ const {
mockGetAuthHeader: vi.fn(() =>
Promise.resolve({ Authorization: 'Bearer test-token' })
),
mockPushDataLayerEvent: vi.fn(),
mockTelemetry: {
trackSubscription: vi.fn(),
trackMonthlySubscriptionCancelled: vi.fn()
@@ -50,7 +48,6 @@ vi.mock('@/composables/auth/useCurrentUser', () => ({
}))
vi.mock('@/platform/telemetry', () => ({
pushDataLayerEvent: mockPushDataLayerEvent,
useTelemetry: vi.fn(() => mockTelemetry)
}))
@@ -114,12 +111,8 @@ describe('useSubscription', () => {
mockIsLoggedIn.value = false
mockTelemetry.trackSubscription.mockReset()
mockTelemetry.trackMonthlySubscriptionCancelled.mockReset()
mockPushDataLayerEvent.mockReset()
mockUserId.value = 'user-123'
mockPushDataLayerEvent.mockImplementation((event) => {
const dataLayer = window.dataLayer ?? (window.dataLayer = [])
dataLayer.push(event)
})
window.dataLayer = []
window.__CONFIG__ = {
subscription_required: true
} as typeof window.__CONFIG__

View File

@@ -7,7 +7,7 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { pushDataLayerEvent, useTelemetry } from '@/platform/telemetry'
import { useTelemetry } from '@/platform/telemetry'
import {
FirebaseAuthStoreError,
useFirebaseAuthStore
@@ -122,22 +122,25 @@ function useSubscriptionInternal() {
: 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
}
]
})
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || []
window.dataLayer.push({
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()
}

View File

@@ -4,12 +4,12 @@ import type { EffectScope } from 'vue'
import type { CloudSubscriptionStatusResponse } from '@/platform/cloud/subscription/composables/useSubscription'
import { useSubscriptionCancellationWatcher } from '@/platform/cloud/subscription/composables/useSubscriptionCancellationWatcher'
import type { TelemetryProvider } from '@/platform/telemetry/types'
import type { TelemetryDispatcher } from '@/platform/telemetry/types'
describe('useSubscriptionCancellationWatcher', () => {
const trackMonthlySubscriptionCancelled = vi.fn()
const telemetryMock: Pick<
TelemetryProvider,
TelemetryDispatcher,
'trackMonthlySubscriptionCancelled'
> = {
trackMonthlySubscriptionCancelled

View File

@@ -2,7 +2,7 @@ import { onScopeDispose, ref } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { defaultWindow, useEventListener, useTimeoutFn } from '@vueuse/core'
import type { TelemetryProvider } from '@/platform/telemetry/types'
import type { TelemetryDispatcher } from '@/platform/telemetry/types'
import type { CloudSubscriptionStatusResponse } from './useSubscription'
@@ -14,7 +14,10 @@ type CancellationWatcherOptions = {
fetchStatus: () => Promise<CloudSubscriptionStatusResponse | null | void>
isActiveSubscription: ComputedRef<boolean>
subscriptionStatus: Ref<CloudSubscriptionStatusResponse | null>
telemetry: Pick<TelemetryProvider, 'trackMonthlySubscriptionCancelled'> | null
telemetry: Pick<
TelemetryDispatcher,
'trackMonthlySubscriptionCancelled'
> | null
shouldWatchCancellation: () => boolean
}

View File

@@ -27,6 +27,7 @@ type FirebaseRuntimeConfig = {
*/
export type RemoteConfig = {
mixpanel_token?: string
gtm_id?: string
subscription_required?: boolean
server_health_alert?: ServerHealthAlert
max_upload_size?: number

View File

@@ -0,0 +1,198 @@
import type { AuditLog } from '@/services/customerEventsService'
import type {
AuthMetadata,
EnterLinearMetadata,
ExecutionErrorMetadata,
ExecutionSuccessMetadata,
ExecutionTriggerSource,
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
NodeSearchMetadata,
NodeSearchResultMetadata,
PageViewMetadata,
PageVisibilityMetadata,
SettingChangedMetadata,
SurveyResponses,
TabCountMetadata,
TelemetryDispatcher,
TelemetryProvider,
TemplateFilterMetadata,
TemplateLibraryClosedMetadata,
TemplateLibraryMetadata,
TemplateMetadata,
UiButtonClickMetadata,
WorkflowCreatedMetadata,
WorkflowImportMetadata
} from './types'
/**
* Registry that holds multiple telemetry providers and dispatches
* all tracking calls to each registered provider.
*
* Implements TelemetryDispatcher (all methods required) while dispatching
* to TelemetryProvider instances using optional chaining since providers
* only implement the methods they care about.
*/
export class TelemetryRegistry implements TelemetryDispatcher {
private providers: TelemetryProvider[] = []
registerProvider(provider: TelemetryProvider): void {
this.providers.push(provider)
}
trackSignupOpened(): void {
this.providers.forEach((p) => p.trackSignupOpened?.())
}
trackAuth(metadata: AuthMetadata): void {
this.providers.forEach((p) => p.trackAuth?.(metadata))
}
trackUserLoggedIn(): void {
this.providers.forEach((p) => p.trackUserLoggedIn?.())
}
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void {
this.providers.forEach((p) => p.trackSubscription?.(event))
}
trackMonthlySubscriptionSucceeded(): void {
this.providers.forEach((p) => p.trackMonthlySubscriptionSucceeded?.())
}
trackMonthlySubscriptionCancelled(): void {
this.providers.forEach((p) => p.trackMonthlySubscriptionCancelled?.())
}
trackAddApiCreditButtonClicked(): void {
this.providers.forEach((p) => p.trackAddApiCreditButtonClicked?.())
}
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
this.providers.forEach((p) =>
p.trackApiCreditTopupButtonPurchaseClicked?.(amount)
)
}
trackApiCreditTopupSucceeded(): void {
this.providers.forEach((p) => p.trackApiCreditTopupSucceeded?.())
}
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void {
this.providers.forEach((p) => p.trackRunButton?.(options))
}
startTopupTracking(): void {
this.providers.forEach((p) => p.startTopupTracking?.())
}
checkForCompletedTopup(events: AuditLog[] | undefined | null): boolean {
return this.providers.some(
(p) => p.checkForCompletedTopup?.(events) ?? false
)
}
clearTopupTracking(): void {
this.providers.forEach((p) => p.clearTopupTracking?.())
}
trackSurvey(
stage: 'opened' | 'submitted',
responses?: SurveyResponses
): void {
this.providers.forEach((p) => p.trackSurvey?.(stage, responses))
}
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void {
this.providers.forEach((p) => p.trackEmailVerification?.(stage))
}
trackTemplate(metadata: TemplateMetadata): void {
this.providers.forEach((p) => p.trackTemplate?.(metadata))
}
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void {
this.providers.forEach((p) => p.trackTemplateLibraryOpened?.(metadata))
}
trackTemplateLibraryClosed(metadata: TemplateLibraryClosedMetadata): void {
this.providers.forEach((p) => p.trackTemplateLibraryClosed?.(metadata))
}
trackWorkflowImported(metadata: WorkflowImportMetadata): void {
this.providers.forEach((p) => p.trackWorkflowImported?.(metadata))
}
trackWorkflowOpened(metadata: WorkflowImportMetadata): void {
this.providers.forEach((p) => p.trackWorkflowOpened?.(metadata))
}
trackEnterLinear(metadata: EnterLinearMetadata): void {
this.providers.forEach((p) => p.trackEnterLinear?.(metadata))
}
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
this.providers.forEach((p) => p.trackPageVisibilityChanged?.(metadata))
}
trackTabCount(metadata: TabCountMetadata): void {
this.providers.forEach((p) => p.trackTabCount?.(metadata))
}
trackNodeSearch(metadata: NodeSearchMetadata): void {
this.providers.forEach((p) => p.trackNodeSearch?.(metadata))
}
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void {
this.providers.forEach((p) => p.trackNodeSearchResultSelected?.(metadata))
}
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
this.providers.forEach((p) => p.trackTemplateFilterChanged?.(metadata))
}
trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void {
this.providers.forEach((p) => p.trackHelpCenterOpened?.(metadata))
}
trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void {
this.providers.forEach((p) => p.trackHelpResourceClicked?.(metadata))
}
trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void {
this.providers.forEach((p) => p.trackHelpCenterClosed?.(metadata))
}
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void {
this.providers.forEach((p) => p.trackWorkflowCreated?.(metadata))
}
trackWorkflowExecution(): void {
this.providers.forEach((p) => p.trackWorkflowExecution?.())
}
trackExecutionError(metadata: ExecutionErrorMetadata): void {
this.providers.forEach((p) => p.trackExecutionError?.(metadata))
}
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void {
this.providers.forEach((p) => p.trackExecutionSuccess?.(metadata))
}
trackSettingChanged(metadata: SettingChangedMetadata): void {
this.providers.forEach((p) => p.trackSettingChanged?.(metadata))
}
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
this.providers.forEach((p) => p.trackUiButtonClicked?.(metadata))
}
trackPageView(pageName: string, properties?: PageViewMetadata): void {
this.providers.forEach((p) => p.trackPageView?.(pageName, properties))
}
}

View File

@@ -1,43 +0,0 @@
import { isCloud } from '@/platform/distribution/types'
const GTM_CONTAINER_ID = 'GTM-NP9JM6K7'
let isInitialized = false
let initPromise: Promise<void> | 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<string, unknown>): void {
if (!isCloud || typeof window === 'undefined') return
const dataLayer = window.dataLayer ?? (window.dataLayer = [])
dataLayer.push(event)
}

View File

@@ -2,71 +2,57 @@
* Telemetry Provider - OSS Build Safety
*
* CRITICAL: OSS Build Safety
* This module is conditionally compiled based on distribution. When building
* the open source version (DISTRIBUTION unset), this entire module and its dependencies
* are excluded through via tree-shaking.
* This module uses dynamic imports to ensure all cloud telemetry code
* is tree-shaken from OSS builds. No top-level imports of provider code.
*
* To verify OSS builds exclude this code:
* 1. `DISTRIBUTION= pnpm build` (OSS build)
* 2. `grep -RinE --include='*.js' 'trackWorkflow|trackEvent|mixpanel' dist/` (should find nothing)
* 3. Check dist/assets/*.js files contain no tracking code
*
* This approach maintains complete separation between cloud and OSS builds
* while ensuring the open source version contains no telemetry dependencies.
* 2. `grep -RinE --include='*.js' 'mixpanel|googletagmanager|dataLayer' dist/`
* 3. Should find nothing
*/
import { MixpanelTelemetryProvider } from './providers/cloud/MixpanelTelemetryProvider'
import type {
initGtm as gtmInit,
pushDataLayerEvent as gtmPushDataLayerEvent
} from './gtm'
import type { TelemetryProvider } from './types'
import type { TelemetryDispatcher } from './types'
type GtmModule = {
initGtm: typeof gtmInit
pushDataLayerEvent: typeof gtmPushDataLayerEvent
}
// Singleton instance
let _telemetryProvider: TelemetryProvider | null = null
let gtmModulePromise: Promise<GtmModule> | null = null
const IS_CLOUD_BUILD = __DISTRIBUTION__ === 'cloud'
function loadGtmModule(): Promise<GtmModule> {
if (!gtmModulePromise) {
gtmModulePromise = import('./gtm')
}
return gtmModulePromise
let _telemetryRegistry: TelemetryDispatcher | null = null
let _initPromise: Promise<void> | null = null
/**
* Initialize telemetry providers for cloud builds.
* Must be called early in app startup (e.g., main.ts).
* Safe to call multiple times - only initializes once.
*/
export async function initTelemetry(): Promise<void> {
if (!IS_CLOUD_BUILD) return
if (_initPromise) return _initPromise
_initPromise = (async () => {
const [
{ TelemetryRegistry },
{ MixpanelTelemetryProvider },
{ GtmTelemetryProvider }
] = await Promise.all([
import('./TelemetryRegistry'),
import('./providers/cloud/MixpanelTelemetryProvider'),
import('./providers/cloud/GtmTelemetryProvider')
])
const registry = new TelemetryRegistry()
registry.registerProvider(new MixpanelTelemetryProvider())
registry.registerProvider(new GtmTelemetryProvider())
_telemetryRegistry = registry
})()
return _initPromise
}
/**
* Telemetry factory - conditionally creates provider based on distribution
* Returns singleton instance.
* Get the telemetry dispatcher for tracking events.
* Returns null in OSS builds - all tracking calls become no-ops.
*
* CRITICAL: This returns undefined in OSS builds. There is no telemetry provider
* for OSS builds and all tracking calls are no-ops.
* Usage: useTelemetry()?.trackAuth({ method: 'google' })
*/
export function useTelemetry(): TelemetryProvider | null {
if (_telemetryProvider === null) {
// Use distribution check for tree-shaking
if (IS_CLOUD_BUILD) {
_telemetryProvider = new MixpanelTelemetryProvider()
}
// For OSS builds, _telemetryProvider stays 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<string, unknown>): void {
if (!IS_CLOUD_BUILD || typeof window === 'undefined') return
void loadGtmModule().then(({ pushDataLayerEvent }) => {
pushDataLayerEvent(event)
})
export function useTelemetry(): TelemetryDispatcher | null {
return _telemetryRegistry
}

View File

@@ -0,0 +1,93 @@
import type {
AuthMetadata,
PageViewMetadata,
TelemetryProvider
} from '../../types'
declare global {
interface Window {
dataLayer?: Record<string, unknown>[]
}
}
/**
* Google Tag Manager telemetry provider.
* Pushes events to the GTM dataLayer for GA4 and marketing integrations.
*
* Only implements events relevant to GTM/GA4 tracking.
* Other methods are no-ops (not implemented since interface is optional).
*/
export class GtmTelemetryProvider implements TelemetryProvider {
private dataLayer: Record<string, unknown>[] = []
private initialized = false
constructor() {
this.initialize()
}
private initialize(): void {
if (typeof window === 'undefined') return
const gtmId = window.__CONFIG__?.gtm_id
if (!gtmId) {
if (import.meta.env.MODE === 'development') {
console.warn('[GTM] No GTM ID configured, skipping initialization')
}
return
}
window.dataLayer = window.dataLayer || []
this.dataLayer = window.dataLayer
this.dataLayer.push({
'gtm.start': new Date().getTime(),
event: 'gtm.js'
})
const script = document.createElement('script')
script.async = true
script.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`
document.head.insertBefore(script, document.head.firstChild)
this.initialized = true
}
private pushEvent(event: string, properties?: Record<string, unknown>): void {
if (!this.initialized) return
this.dataLayer.push({ event, ...properties })
}
trackPageView(pageName: string, properties?: PageViewMetadata): void {
this.pushEvent('page_view', {
page_title: pageName,
page_location: properties?.path,
page_referrer: properties?.referrer
})
}
trackAuth(metadata: AuthMetadata): void {
if (metadata.is_new_user) {
this.pushEvent('sign_up', {
method: metadata.method
})
} else {
this.pushEvent('login', {
method: metadata.method
})
}
}
trackMonthlySubscriptionSucceeded(): void {
this.pushEvent('purchase', {
currency: 'USD',
items: [{ item_name: 'Monthly Subscription' }]
})
}
trackApiCreditTopupSucceeded(): void {
this.pushEvent('purchase', {
currency: 'USD',
items: [{ item_name: 'API Credits' }]
})
}
}

View File

@@ -269,80 +269,101 @@ export interface WorkflowCreatedMetadata {
}
/**
* Core telemetry provider interface
* Page view metadata for route tracking
*/
export interface PageViewMetadata {
path?: string
referrer?: string
title?: string
[key: string]: unknown
}
/**
* Telemetry provider interface for individual providers.
* All methods are optional - providers only implement what they need.
*/
export interface TelemetryProvider {
// Authentication flow events
trackSignupOpened(): void
trackAuth(metadata: AuthMetadata): void
trackUserLoggedIn(): void
trackSignupOpened?(): void
trackAuth?(metadata: AuthMetadata): void
trackUserLoggedIn?(): void
// Subscription flow events
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void
trackMonthlySubscriptionSucceeded(): void
trackMonthlySubscriptionCancelled(): void
trackAddApiCreditButtonClicked(): void
trackApiCreditTopupButtonPurchaseClicked(amount: number): void
trackApiCreditTopupSucceeded(): void
trackRunButton(options?: {
trackSubscription?(event: 'modal_opened' | 'subscribe_clicked'): void
trackMonthlySubscriptionSucceeded?(): void
trackMonthlySubscriptionCancelled?(): void
trackAddApiCreditButtonClicked?(): void
trackApiCreditTopupButtonPurchaseClicked?(amount: number): void
trackApiCreditTopupSucceeded?(): void
trackRunButton?(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void
// Credit top-up tracking (composition with internal utilities)
startTopupTracking(): void
checkForCompletedTopup(events: AuditLog[] | undefined | null): boolean
clearTopupTracking(): void
startTopupTracking?(): void
checkForCompletedTopup?(events: AuditLog[] | undefined | null): boolean
clearTopupTracking?(): void
// Survey flow events
trackSurvey(stage: 'opened' | 'submitted', responses?: SurveyResponses): void
trackSurvey?(stage: 'opened' | 'submitted', responses?: SurveyResponses): void
// Email verification events
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void
trackEmailVerification?(stage: 'opened' | 'requested' | 'completed'): void
// Template workflow events
trackTemplate(metadata: TemplateMetadata): void
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void
trackTemplateLibraryClosed(metadata: TemplateLibraryClosedMetadata): void
trackTemplate?(metadata: TemplateMetadata): void
trackTemplateLibraryOpened?(metadata: TemplateLibraryMetadata): void
trackTemplateLibraryClosed?(metadata: TemplateLibraryClosedMetadata): void
// Workflow management events
trackWorkflowImported(metadata: WorkflowImportMetadata): void
trackWorkflowOpened(metadata: WorkflowImportMetadata): void
trackEnterLinear(metadata: EnterLinearMetadata): void
trackWorkflowImported?(metadata: WorkflowImportMetadata): void
trackWorkflowOpened?(metadata: WorkflowImportMetadata): void
trackEnterLinear?(metadata: EnterLinearMetadata): void
// Page visibility events
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void
trackPageVisibilityChanged?(metadata: PageVisibilityMetadata): void
// Tab tracking events
trackTabCount(metadata: TabCountMetadata): void
trackTabCount?(metadata: TabCountMetadata): void
// Node search analytics events
trackNodeSearch(metadata: NodeSearchMetadata): void
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void
trackNodeSearch?(metadata: NodeSearchMetadata): void
trackNodeSearchResultSelected?(metadata: NodeSearchResultMetadata): void
// Template filter tracking events
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void
trackTemplateFilterChanged?(metadata: TemplateFilterMetadata): void
// Help center events
trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void
trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void
trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void
trackHelpCenterOpened?(metadata: HelpCenterOpenedMetadata): void
trackHelpResourceClicked?(metadata: HelpResourceClickedMetadata): void
trackHelpCenterClosed?(metadata: HelpCenterClosedMetadata): void
// Workflow creation events
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void
trackWorkflowCreated?(metadata: WorkflowCreatedMetadata): void
// Workflow execution events
trackWorkflowExecution(): void
trackExecutionError(metadata: ExecutionErrorMetadata): void
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
trackWorkflowExecution?(): void
trackExecutionError?(metadata: ExecutionErrorMetadata): void
trackExecutionSuccess?(metadata: ExecutionSuccessMetadata): void
// Settings events
trackSettingChanged(metadata: SettingChangedMetadata): void
trackSettingChanged?(metadata: SettingChangedMetadata): void
// Generic UI button click events
trackUiButtonClicked(metadata: UiButtonClickMetadata): void
trackUiButtonClicked?(metadata: UiButtonClickMetadata): void
// Page view tracking
trackPageView?(pageName: string, properties?: PageViewMetadata): void
}
/**
* Telemetry dispatcher interface returned by useTelemetry().
* All methods are required - the registry implements all methods and dispatches
* to registered providers using optional chaining.
*/
export type TelemetryDispatcher = Required<TelemetryProvider>
/**
* Telemetry event constants
*
@@ -415,7 +436,10 @@ export const TelemetryEvents = {
EXECUTION_ERROR: 'execution_error',
EXECUTION_SUCCESS: 'execution_success',
// Generic UI Button Click
UI_BUTTON_CLICKED: 'app:ui_button_clicked'
UI_BUTTON_CLICKED: 'app:ui_button_clicked',
// Page View
PAGE_VIEW: 'app:page_view'
} as const
export type TelemetryEventName =

View File

@@ -9,7 +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 { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useUserStore } from '@/stores/userStore'
@@ -40,10 +40,8 @@ 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
useTelemetry()?.trackPageView(document.title, {
path: window.location.href
})
}

View File

@@ -25,10 +25,7 @@ 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 {
pushDataLayerEvent as pushDataLayerEventBase,
useTelemetry
} from '@/platform/telemetry'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import type { AuthHeader } from '@/types/authTypes'
@@ -88,7 +85,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
if (!isCloud || typeof window === 'undefined') return
try {
pushDataLayerEventBase(event)
window.dataLayer = window.dataLayer || []
window.dataLayer.push(event)
} catch (error) {
console.warn('Failed to push data layer event', error)
}