Files
ComfyUI_frontend/src/platform/telemetry/providers/cloud/ImpactTelemetryProvider.ts
Christian Byrne 62979e3818 refactor: rename firebaseAuthStore to authStore with shared test fixtures (#10483)
## Summary

Rename `useFirebaseAuthStore` → `useAuthStore` and
`FirebaseAuthStoreError` → `AuthStoreError`. Introduce shared mock
factory (`authStoreMock.ts`) to replace 16 independent bespoke mocks.

## Changes

- **What**: Mechanical rename of store, composable, class, and store ID
(`firebaseAuth` → `auth`). Created
`src/stores/__tests__/authStoreMock.ts` — a shared mock factory with
reactive controls, used by all consuming test files. Migrated all 16
test files from ad-hoc mocks to the shared factory.
- **Files**: 62 files changed (rename propagation + new test infra)

## Review Focus

- Mock factory API design in `authStoreMock.ts` — covers all store
properties with reactive `controls` for per-test customization
- Self-test in `authStoreMock.test.ts` validates computed reactivity

Fixes #8219

## Stack

This is PR 1/5 in a stacked refactoring series:
1. **→ This PR**: Rename + shared test fixtures
2. #10484: Extract auth-routing from workspaceApi
3. #10485: Auth token priority tests
4. #10486: Decompose MembersPanelContent
5. #10487: Consolidate SubscriptionTier type

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-27 00:31:11 -07:00

174 lines
4.5 KiB
TypeScript

import { captureCheckoutAttributionFromSearch } from '@/platform/telemetry/utils/checkoutAttribution'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useAuthStore } from '@/stores/authStore'
import type { PageViewMetadata, TelemetryProvider } from '../../types'
const IMPACT_SCRIPT_URL =
'https://utt.impactcdn.com/A6951770-3747-434a-9ac7-4e582e67d91f1.js'
const IMPACT_QUEUE_NAME = 'ire'
const EMPTY_CUSTOMER_VALUE = ''
/**
* Impact telemetry provider.
* Initializes the Impact queue globals and loads the runtime script.
*/
export class ImpactTelemetryProvider implements TelemetryProvider {
private initialized = false
private stores: {
apiKeyAuthStore: ReturnType<typeof useApiKeyAuthStore>
authStore: ReturnType<typeof useAuthStore>
} | null = null
constructor() {
this.initialize()
}
trackPageView(_pageName: string, properties?: PageViewMetadata): void {
const search = this.extractSearchFromPath(properties?.path)
if (search) {
captureCheckoutAttributionFromSearch(search)
} else if (typeof window !== 'undefined') {
captureCheckoutAttributionFromSearch(window.location.search)
}
void this.identifyCurrentUser()
}
private initialize(): void {
if (typeof window === 'undefined' || this.initialized) return
window.ire_o = IMPACT_QUEUE_NAME
if (!window.ire) {
const queueFn: NonNullable<Window['ire']> = (...args: unknown[]) => {
;(queueFn.a ??= []).push(args)
}
window.ire = queueFn
}
const existingScript = document.querySelector(
`script[src="${IMPACT_SCRIPT_URL}"]`
)
if (existingScript) {
this.initialized = true
return
}
const script = document.createElement('script')
script.async = true
script.src = IMPACT_SCRIPT_URL
document.head.insertBefore(script, document.head.firstChild)
this.initialized = true
}
private extractSearchFromPath(path?: string): string {
if (!path) return ''
if (typeof window !== 'undefined') {
try {
const url = new URL(path, window.location.origin)
return url.search
} catch {
// Fall through to manual parsing.
}
}
const queryIndex = path.indexOf('?')
return queryIndex >= 0 ? path.slice(queryIndex) : ''
}
private async identifyCurrentUser(): Promise<void> {
if (typeof window === 'undefined') return
const { customerId, customerEmail } = this.resolveCustomerIdentity()
const normalizedEmail = customerEmail.trim().toLowerCase()
// Impact's Identify spec requires customerEmail to be sent as a SHA1 hash.
const hashedEmail = normalizedEmail
? await this.hashSha1(normalizedEmail)
: EMPTY_CUSTOMER_VALUE
window.ire?.('identify', {
customerId,
customerEmail: hashedEmail
})
}
private resolveCustomerIdentity(): {
customerId: string
customerEmail: string
} {
const stores = this.resolveAuthStores()
if (!stores) {
return {
customerId: EMPTY_CUSTOMER_VALUE,
customerEmail: EMPTY_CUSTOMER_VALUE
}
}
if (stores.authStore.currentUser) {
return {
customerId: stores.authStore.currentUser.uid ?? EMPTY_CUSTOMER_VALUE,
customerEmail:
stores.authStore.currentUser.email ?? EMPTY_CUSTOMER_VALUE
}
}
if (stores.apiKeyAuthStore.isAuthenticated) {
return {
customerId:
stores.apiKeyAuthStore.currentUser?.id ?? EMPTY_CUSTOMER_VALUE,
customerEmail:
stores.apiKeyAuthStore.currentUser?.email ?? EMPTY_CUSTOMER_VALUE
}
}
return {
customerId: EMPTY_CUSTOMER_VALUE,
customerEmail: EMPTY_CUSTOMER_VALUE
}
}
private resolveAuthStores(): {
apiKeyAuthStore: ReturnType<typeof useApiKeyAuthStore>
authStore: ReturnType<typeof useAuthStore>
} | null {
if (this.stores) {
return this.stores
}
try {
const stores = {
apiKeyAuthStore: useApiKeyAuthStore(),
authStore: useAuthStore()
}
this.stores = stores
return stores
} catch {
return null
}
}
private async hashSha1(value: string): Promise<string> {
try {
if (!globalThis.crypto?.subtle || typeof TextEncoder === 'undefined') {
return EMPTY_CUSTOMER_VALUE
}
const digestBuffer = await crypto.subtle.digest(
'SHA-1',
new TextEncoder().encode(value)
)
return Array.from(new Uint8Array(digestBuffer))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')
} catch {
return EMPTY_CUSTOMER_VALUE
}
}
}