feat: decouple auth and telemetry with event hooks

- Create authEventBus.ts with typed event hooks

- Create authTracking.ts to subscribe telemetry to auth events

- Remove direct telemetry imports from firebaseAuthStore

- Remove useCurrentUser import from MixpanelTelemetryProvider

- Breaks ~600 circular dependency cycles

Amp-Thread-ID: https://ampcode.com/threads/T-019bfe73-6a29-7638-8160-8de515af8707
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-27 00:10:07 -08:00
parent 7ad43c689c
commit 21ba6a6823
6 changed files with 87 additions and 11 deletions

View File

@@ -0,0 +1,26 @@
import { authEventHook, userResolvedHook } from '@/stores/authEventBus'
import type { TelemetryProvider } from './types'
export function initAuthTracking(
getTelemetry: () => TelemetryProvider | null
): void {
authEventHook.on((event) => {
const telemetry = getTelemetry()
if (!telemetry) return
if (event.type === 'login' || event.type === 'register') {
telemetry.trackAuth({
method: event.method,
is_new_user: event.is_new_user
})
}
})
userResolvedHook.on((event) => {
const telemetry = getTelemetry()
if (!telemetry) return
telemetry.identify?.(event.userId)
})
}

View File

@@ -16,11 +16,13 @@
*/
import { isCloud } from '@/platform/distribution/types'
import { initAuthTracking } from './authTracking'
import { MixpanelTelemetryProvider } from './providers/cloud/MixpanelTelemetryProvider'
import type { TelemetryProvider } from './types'
// Singleton instance
let _telemetryProvider: TelemetryProvider | null = null
let _authTrackingInitialized = false
/**
* Telemetry factory - conditionally creates provider based on distribution
@@ -34,6 +36,11 @@ export function useTelemetry(): TelemetryProvider | null {
// Use distribution check for tree-shaking
if (isCloud) {
_telemetryProvider = new MixpanelTelemetryProvider()
if (!_authTrackingInitialized) {
initAuthTracking(() => _telemetryProvider)
_authTrackingInitialized = true
}
}
// For OSS builds, _telemetryProvider stays null
}

View File

@@ -1,7 +1,6 @@
import type { OverridedMixpanel } from 'mixpanel-browser'
import { watch } from 'vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import {
checkForCompletedTopup as checkTopupUtil,
clearTopupTracking as clearTopupUtil,
@@ -120,11 +119,6 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
loaded: () => {
this.isInitialized = true
this.flushEventQueue() // flush events that were queued while initializing
useCurrentUser().onUserResolved((user) => {
if (this.mixpanel && user.id) {
this.mixpanel.identify(user.id)
}
})
}
})
})
@@ -205,6 +199,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
)
}
identify(userId: string): void {
if (this.mixpanel) {
this.mixpanel.identify(userId)
}
}
trackSignupOpened(): void {
this.trackEvent(TelemetryEvents.USER_SIGN_UP_OPENED)
}

View File

@@ -272,6 +272,9 @@ export interface WorkflowCreatedMetadata {
* Core telemetry provider interface
*/
export interface TelemetryProvider {
// User identification (called by auth event hooks)
identify?(userId: string): void
// Authentication flow events
trackSignupOpened(): void
trackAuth(metadata: AuthMetadata): void

View File

@@ -0,0 +1,16 @@
import { createEventHook } from '@vueuse/core'
import type { AuthMetadata } from '@/platform/telemetry/types'
export interface AuthEvent extends AuthMetadata {
type: 'login' | 'register' | 'logout' | 'password_reset'
}
export interface UserResolvedEvent {
userId: string
email?: string | null
displayName?: string | null
}
export const authEventHook = createEventHook<AuthEvent>()
export const userResolvedHook = createEventHook<UserResolvedEvent>()

View File

@@ -25,8 +25,8 @@ 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 { useDialogService } from '@/services/dialogService'
import { authEventHook, userResolvedHook } from '@/stores/authEventBus'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import type { AuthHeader } from '@/types/authTypes'
import type { operations } from '@/types/comfyRegistryTypes'
@@ -323,10 +323,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
)
if (isCloud) {
useTelemetry()?.trackAuth({
authEventHook.trigger({
type: 'login',
method: 'email',
is_new_user: false
})
userResolvedHook.trigger({
userId: result.user.uid,
email: result.user.email,
displayName: result.user.displayName
})
}
return result
@@ -343,10 +349,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
)
if (isCloud) {
useTelemetry()?.trackAuth({
authEventHook.trigger({
type: 'register',
method: 'email',
is_new_user: true
})
userResolvedHook.trigger({
userId: result.user.uid,
email: result.user.email,
displayName: result.user.displayName
})
}
return result
@@ -361,10 +373,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
if (isCloud) {
const additionalUserInfo = getAdditionalUserInfo(result)
const isNewUser = additionalUserInfo?.isNewUser ?? false
useTelemetry()?.trackAuth({
authEventHook.trigger({
type: isNewUser ? 'register' : 'login',
method: 'google',
is_new_user: isNewUser
})
userResolvedHook.trigger({
userId: result.user.uid,
email: result.user.email,
displayName: result.user.displayName
})
}
return result
@@ -379,10 +397,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
if (isCloud) {
const additionalUserInfo = getAdditionalUserInfo(result)
const isNewUser = additionalUserInfo?.isNewUser ?? false
useTelemetry()?.trackAuth({
authEventHook.trigger({
type: isNewUser ? 'register' : 'login',
method: 'github',
is_new_user: isNewUser
})
userResolvedHook.trigger({
userId: result.user.uid,
email: result.user.email,
displayName: result.user.displayName
})
}
return result