mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-08 16:17:58 +00:00
Compare commits
2 Commits
main
...
bl/fix-pr-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ece2807f22 | ||
|
|
5aa3ee9934 |
@@ -8,6 +8,10 @@ import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workfl
|
||||
type ModifiedWorkflow = Pick<ComfyWorkflow, 'path' | 'isModified'>
|
||||
|
||||
const mockAuthStore = vi.hoisted(() => ({
|
||||
login: vi.fn().mockResolvedValue(undefined),
|
||||
loginWithGoogle: vi.fn().mockResolvedValue(undefined),
|
||||
loginWithGithub: vi.fn().mockResolvedValue(undefined),
|
||||
register: vi.fn().mockResolvedValue(undefined),
|
||||
logout: vi.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
|
||||
@@ -28,10 +32,12 @@ const mockDialogService = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
const mockToastErrorHandler = vi.hoisted(() => vi.fn())
|
||||
const mockTrackAuthFailed = vi.hoisted(() => vi.fn())
|
||||
|
||||
const knownAuthErrorCodes = new Set([
|
||||
'auth/invalid-credential',
|
||||
'auth/email-already-in-use'
|
||||
'auth/email-already-in-use',
|
||||
'auth/user-not-found'
|
||||
])
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
@@ -48,7 +54,9 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => undefined)
|
||||
useTelemetry: vi.fn(() => ({
|
||||
trackAuthFailed: mockTrackAuthFailed
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
@@ -81,9 +89,19 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
|
||||
vi.mock('@/composables/useErrorHandling', () => ({
|
||||
useErrorHandling: () => ({
|
||||
wrapWithErrorHandlingAsync: <TArgs extends unknown[], TReturn>(
|
||||
action: (...args: TArgs) => Promise<TReturn> | TReturn
|
||||
) => action,
|
||||
wrapWithErrorHandlingAsync:
|
||||
<TArgs extends unknown[], TReturn>(
|
||||
action: (...args: TArgs) => Promise<TReturn> | TReturn,
|
||||
errorHandler?: (error: unknown) => void
|
||||
) =>
|
||||
async (...args: TArgs) => {
|
||||
try {
|
||||
return await action(...args)
|
||||
} catch (error) {
|
||||
;(errorHandler ?? mockToastErrorHandler)(error)
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
toastErrorHandler: mockToastErrorHandler
|
||||
})
|
||||
}))
|
||||
@@ -156,10 +174,13 @@ describe('useAuthActions.logout', () => {
|
||||
)
|
||||
const { logout } = useAuthActions()
|
||||
|
||||
await expect(logout()).rejects.toThrow('auth.signOut.saveFailed:a.json')
|
||||
await logout()
|
||||
|
||||
expect(mockWorkflowService.saveWorkflow).toHaveBeenCalledTimes(1)
|
||||
expect(mockAuthStore.logout).not.toHaveBeenCalled()
|
||||
expect(mockToastErrorHandler).toHaveBeenCalledExactlyOnceWith(
|
||||
new Error('auth.signOut.saveFailed:a.json')
|
||||
)
|
||||
})
|
||||
|
||||
it('saves every modified workflow before signing out when user picks Save (true)', async () => {
|
||||
@@ -206,6 +227,85 @@ describe('useAuthActions.logout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('useAuthActions auth flow error telemetry', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowStore.modifiedWorkflows = []
|
||||
})
|
||||
|
||||
it('tracks email sign-in Firebase failures and still shows the error toast', async () => {
|
||||
const error = new FirebaseError('auth/user-not-found', 'msg')
|
||||
mockAuthStore.login.mockRejectedValueOnce(error)
|
||||
const { signInWithEmail } = useAuthActions()
|
||||
|
||||
await expect(
|
||||
signInWithEmail('user@example.com', 'password')
|
||||
).resolves.toBeUndefined()
|
||||
|
||||
expect(mockTrackAuthFailed).toHaveBeenCalledExactlyOnceWith({
|
||||
error_code: 'auth/user-not-found',
|
||||
auth_action: 'email_sign_in'
|
||||
})
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'auth.errors.auth/user-not-found'
|
||||
})
|
||||
})
|
||||
|
||||
it('tracks unknown errors for email sign-up failures', async () => {
|
||||
const error = new Error('network failed')
|
||||
mockAuthStore.register.mockRejectedValueOnce(error)
|
||||
const { signUpWithEmail } = useAuthActions()
|
||||
|
||||
await expect(
|
||||
signUpWithEmail('user@example.com', 'password')
|
||||
).resolves.toBeUndefined()
|
||||
|
||||
expect(mockTrackAuthFailed).toHaveBeenCalledExactlyOnceWith({
|
||||
error_code: 'unknown',
|
||||
auth_action: 'email_sign_up'
|
||||
})
|
||||
})
|
||||
|
||||
it('tracks Google sign-up failures separately from sign-in failures', async () => {
|
||||
const error = new FirebaseError('auth/popup-closed-by-user', 'msg')
|
||||
mockAuthStore.loginWithGoogle.mockRejectedValueOnce(error)
|
||||
const { signInWithGoogle } = useAuthActions()
|
||||
|
||||
await expect(signInWithGoogle({ isNewUser: true })).resolves.toBeUndefined()
|
||||
|
||||
expect(mockTrackAuthFailed).toHaveBeenCalledExactlyOnceWith({
|
||||
error_code: 'auth/popup-closed-by-user',
|
||||
auth_action: 'google_sign_up'
|
||||
})
|
||||
})
|
||||
|
||||
it('tracks GitHub sign-up failures separately from sign-in failures', async () => {
|
||||
const error = new FirebaseError('auth/popup-closed-by-user', 'msg')
|
||||
mockAuthStore.loginWithGithub.mockRejectedValueOnce(error)
|
||||
const { signInWithGithub } = useAuthActions()
|
||||
|
||||
await expect(signInWithGithub({ isNewUser: true })).resolves.toBeUndefined()
|
||||
|
||||
expect(mockTrackAuthFailed).toHaveBeenCalledExactlyOnceWith({
|
||||
error_code: 'auth/popup-closed-by-user',
|
||||
auth_action: 'github_sign_up'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not track auth failures for logout failures', async () => {
|
||||
const error = new FirebaseError('auth/network-request-failed', 'msg')
|
||||
mockAuthStore.logout.mockRejectedValueOnce(error)
|
||||
const { logout } = useAuthActions()
|
||||
|
||||
await logout()
|
||||
|
||||
expect(mockTrackAuthFailed).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useAuthActions.reportError', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling'
|
||||
import { st, t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { AuthFlowAction } from '@/platform/telemetry/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -28,6 +29,15 @@ export const useAuthActions = () => {
|
||||
|
||||
const accessError = ref(false)
|
||||
|
||||
const reportAuthFlowError =
|
||||
(authAction: AuthFlowAction) => (error: unknown) => {
|
||||
useTelemetry()?.trackAuthFailed({
|
||||
error_code: error instanceof FirebaseError ? error.code : 'unknown',
|
||||
auth_action: authAction
|
||||
})
|
||||
reportError(error)
|
||||
}
|
||||
|
||||
const reportError = (error: unknown) => {
|
||||
// Ref: https://firebase.google.com/docs/auth/admin/errors
|
||||
if (
|
||||
@@ -127,7 +137,7 @@ export const useAuthActions = () => {
|
||||
life: 5000
|
||||
})
|
||||
},
|
||||
reportError
|
||||
reportAuthFlowError('password_reset')
|
||||
)
|
||||
|
||||
const purchaseCredits = wrapWithErrorHandlingAsync(async (amount: number) => {
|
||||
@@ -177,32 +187,34 @@ export const useAuthActions = () => {
|
||||
return result
|
||||
}, reportError)
|
||||
|
||||
const signInWithGoogle = wrapWithErrorHandlingAsync(
|
||||
async (options?: { isNewUser?: boolean }) => {
|
||||
return await authStore.loginWithGoogle(options)
|
||||
},
|
||||
reportError
|
||||
)
|
||||
const signInWithGoogle = async (options?: { isNewUser?: boolean }) =>
|
||||
await wrapWithErrorHandlingAsync(
|
||||
async () => await authStore.loginWithGoogle(options),
|
||||
reportAuthFlowError(
|
||||
options?.isNewUser ? 'google_sign_up' : 'google_sign_in'
|
||||
)
|
||||
)()
|
||||
|
||||
const signInWithGithub = wrapWithErrorHandlingAsync(
|
||||
async (options?: { isNewUser?: boolean }) => {
|
||||
return await authStore.loginWithGithub(options)
|
||||
},
|
||||
reportError
|
||||
)
|
||||
const signInWithGithub = async (options?: { isNewUser?: boolean }) =>
|
||||
await wrapWithErrorHandlingAsync(
|
||||
async () => await authStore.loginWithGithub(options),
|
||||
reportAuthFlowError(
|
||||
options?.isNewUser ? 'github_sign_up' : 'github_sign_in'
|
||||
)
|
||||
)()
|
||||
|
||||
const signInWithEmail = wrapWithErrorHandlingAsync(
|
||||
async (email: string, password: string) => {
|
||||
return await authStore.login(email, password)
|
||||
},
|
||||
reportError
|
||||
reportAuthFlowError('email_sign_in')
|
||||
)
|
||||
|
||||
const signUpWithEmail = wrapWithErrorHandlingAsync(
|
||||
async (email: string, password: string, turnstileToken?: string) => {
|
||||
return await authStore.register(email, password, turnstileToken)
|
||||
},
|
||||
reportError
|
||||
reportAuthFlowError('email_sign_up')
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,23 @@ describe('TelemetryRegistry', () => {
|
||||
expect(a.trackBeginCheckout).toHaveBeenCalledExactlyOnceWith(metadata)
|
||||
})
|
||||
|
||||
it('dispatches trackAuthFailed to every registered provider', () => {
|
||||
const a: TelemetryProvider = { trackAuthFailed: vi.fn() }
|
||||
const b: TelemetryProvider = { trackAuthFailed: vi.fn() }
|
||||
const registry = new TelemetryRegistry()
|
||||
registry.registerProvider(a)
|
||||
registry.registerProvider(b)
|
||||
|
||||
const payload = {
|
||||
error_code: 'auth/user-not-found',
|
||||
auth_action: 'email_sign_in' as const
|
||||
}
|
||||
registry.trackAuthFailed(payload)
|
||||
|
||||
expect(a.trackAuthFailed).toHaveBeenCalledExactlyOnceWith(payload)
|
||||
expect(b.trackAuthFailed).toHaveBeenCalledExactlyOnceWith(payload)
|
||||
})
|
||||
|
||||
it('dispatches trackAddApiCreditButtonClicked with its source', () => {
|
||||
const provider: TelemetryProvider = {
|
||||
trackAddApiCreditButtonClicked: vi.fn()
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AuditLog } from '@/services/customerEventsService'
|
||||
|
||||
import type {
|
||||
AddCreditsClickMetadata,
|
||||
AuthErrorMetadata,
|
||||
AuthMetadata,
|
||||
BeginCheckoutMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
@@ -75,6 +76,10 @@ export class TelemetryRegistry implements TelemetryDispatcher {
|
||||
this.dispatch((provider) => provider.trackAuth?.(metadata))
|
||||
}
|
||||
|
||||
trackAuthFailed(metadata: AuthErrorMetadata): void {
|
||||
this.dispatch((provider) => provider.trackAuthFailed?.(metadata))
|
||||
}
|
||||
|
||||
trackUserLoggedIn(): void {
|
||||
this.dispatch((provider) => provider.trackUserLoggedIn?.())
|
||||
}
|
||||
|
||||
@@ -361,6 +361,24 @@ describe('PostHogTelemetryProvider', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('captures auth failure events with metadata', async () => {
|
||||
const provider = createProvider()
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
provider.trackAuthFailed({
|
||||
error_code: 'auth/user-not-found',
|
||||
auth_action: 'email_sign_in'
|
||||
})
|
||||
|
||||
expect(hoisted.mockCapture).toHaveBeenCalledWith(
|
||||
TelemetryEvents.USER_AUTH_FAILED,
|
||||
{
|
||||
error_code: 'auth/user-not-found',
|
||||
auth_action: 'email_sign_in'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it.for([
|
||||
['flow_opened', TelemetryEvents.SUBSCRIPTION_CANCEL_FLOW_OPENED, {}],
|
||||
['confirmed', TelemetryEvents.SUBSCRIPTION_CANCEL_CONFIRMED, {}],
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { RemoteConfig } from '@/platform/remoteConfig/types'
|
||||
|
||||
import type {
|
||||
AddCreditsClickMetadata,
|
||||
AuthErrorMetadata,
|
||||
AuthMetadata,
|
||||
BeginCheckoutMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
@@ -353,6 +354,10 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
|
||||
}
|
||||
|
||||
trackAuthFailed(metadata: AuthErrorMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.USER_AUTH_FAILED, metadata)
|
||||
}
|
||||
|
||||
trackUserLoggedIn(): void {
|
||||
this.trackEvent(TelemetryEvents.USER_LOGGED_IN)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,23 @@ export interface AuthMetadata {
|
||||
utm_campaign?: string
|
||||
}
|
||||
|
||||
export type AuthFlowAction =
|
||||
| 'email_sign_in'
|
||||
| 'email_sign_up'
|
||||
| 'google_sign_in'
|
||||
| 'google_sign_up'
|
||||
| 'github_sign_in'
|
||||
| 'github_sign_up'
|
||||
| 'password_reset'
|
||||
|
||||
/**
|
||||
* Metadata for failed authentication attempts
|
||||
*/
|
||||
export interface AuthErrorMetadata {
|
||||
error_code: string
|
||||
auth_action: AuthFlowAction
|
||||
}
|
||||
|
||||
/**
|
||||
* Survey response data for user profiling
|
||||
* Maps 1-to-1 with actual survey fields
|
||||
@@ -525,6 +542,7 @@ export interface TelemetryProvider {
|
||||
// Authentication flow events
|
||||
trackSignupOpened?(): void
|
||||
trackAuth?(metadata: AuthMetadata): void
|
||||
trackAuthFailed?(metadata: AuthErrorMetadata): void
|
||||
trackUserLoggedIn?(): void
|
||||
|
||||
// Subscription flow events
|
||||
@@ -637,6 +655,7 @@ export const TelemetryEvents = {
|
||||
// Authentication Flow
|
||||
USER_SIGN_UP_OPENED: 'app:user_sign_up_opened',
|
||||
USER_AUTH_COMPLETED: 'app:user_auth_completed',
|
||||
USER_AUTH_FAILED: 'app:user_auth_failed',
|
||||
USER_LOGGED_IN: 'app:user_logged_in',
|
||||
|
||||
// Subscription Flow
|
||||
@@ -743,6 +762,7 @@ export type ExecutionTriggerSource =
|
||||
*/
|
||||
export type TelemetryEventProperties =
|
||||
| AuthMetadata
|
||||
| AuthErrorMetadata
|
||||
| SurveyResponses
|
||||
| TemplateMetadata
|
||||
| ExecutionContext
|
||||
|
||||
Reference in New Issue
Block a user