mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 09:45:13 +00:00
feat(telemetry): track API credit top-up success via audit events (#6500)
Summary - Add TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED and provider method trackApiCreditTopupSucceeded - Introduce topupTrackerStore to persist pending top-ups per user (localStorage) and reconcile against recent audit logs - Hook purchase flow to start tracking before opening Stripe checkout - Reconcile after fetching audit events (UsageLogsTable) and after fetchBalance, then emit telemetry, refresh balance, and clear pending - Minor refactor in customerEventsService to return awaited result Implementation details - Matching strategy: - Event type: credit_added - Time window: createdAt between top-up start time and +24h - Amount: if known, e.params.amount must equal expected cents - Cross-tab/user changes: synchronize via storage event and userId watcher Limitations / Follow-up - Reconciliation fetches only page 1 (limit 10) of events; in high-volume cases, a recent credit_added could fall outside the first page - The window and pagination issue will be "resolved by a followup PR to core and cloud" Files touched - src/stores/topupTrackerStore.ts (new) - src/components/dialog/content/setting/UsageLogsTable.vue - src/composables/auth/useFirebaseAuthActions.ts - src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts - src/platform/telemetry/types.ts - src/services/customerEventsService.ts ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6500-feat-telemetry-track-API-credit-top-up-success-via-audit-events-29e6d73d365081169941efae70cf71fe) by [Unito](https://www.unito.io) --------- Co-authored-by: Christian Byrne <chrbyrne96@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
207
tests-ui/tests/platform/telemetry/topupTracker.test.ts
Normal file
207
tests-ui/tests/platform/telemetry/topupTracker.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
|
||||
// Mock localStorage
|
||||
const mockLocalStorage = vi.hoisted(() => ({
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn()
|
||||
}))
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockLocalStorage,
|
||||
writable: true
|
||||
})
|
||||
|
||||
// Mock telemetry
|
||||
const mockTelemetry = vi.hoisted(() => ({
|
||||
trackApiCreditTopupSucceeded: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => mockTelemetry)
|
||||
}))
|
||||
|
||||
describe('topupTracker', () => {
|
||||
let topupTracker: typeof import('@/platform/telemetry/topupTracker')
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
// Dynamically import to ensure fresh module state
|
||||
topupTracker = await import('@/platform/telemetry/topupTracker')
|
||||
})
|
||||
|
||||
describe('startTopupTracking', () => {
|
||||
it('should save current timestamp to localStorage', () => {
|
||||
const beforeTimestamp = Date.now()
|
||||
|
||||
topupTracker.startTopupTracking()
|
||||
|
||||
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
||||
'pending_topup_timestamp',
|
||||
expect.any(String)
|
||||
)
|
||||
|
||||
const savedTimestamp = parseInt(
|
||||
mockLocalStorage.setItem.mock.calls[0][1],
|
||||
10
|
||||
)
|
||||
expect(savedTimestamp).toBeGreaterThanOrEqual(beforeTimestamp)
|
||||
expect(savedTimestamp).toBeLessThanOrEqual(Date.now())
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkForCompletedTopup', () => {
|
||||
it('should return false if no pending topup exists', () => {
|
||||
mockLocalStorage.getItem.mockReturnValue(null)
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup([])
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return false if events array is empty', () => {
|
||||
mockLocalStorage.getItem.mockReturnValue(Date.now().toString())
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup([])
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return false if events array is null', () => {
|
||||
mockLocalStorage.getItem.mockReturnValue(Date.now().toString())
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup(null)
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should auto-cleanup if timestamp is older than 24 hours', () => {
|
||||
const oldTimestamp = Date.now() - 25 * 60 * 60 * 1000 // 25 hours ago
|
||||
mockLocalStorage.getItem.mockReturnValue(oldTimestamp.toString())
|
||||
|
||||
const events: AuditLog[] = [
|
||||
{
|
||||
event_id: 'test-1',
|
||||
event_type: 'credit_added',
|
||||
createdAt: new Date().toISOString(),
|
||||
params: { amount: 500 }
|
||||
}
|
||||
]
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup(events)
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'pending_topup_timestamp'
|
||||
)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should detect completed topup and fire telemetry', () => {
|
||||
const startTimestamp = Date.now() - 5 * 60 * 1000 // 5 minutes ago
|
||||
mockLocalStorage.getItem.mockReturnValue(startTimestamp.toString())
|
||||
|
||||
const events: AuditLog[] = [
|
||||
{
|
||||
event_id: 'test-1',
|
||||
event_type: 'api_usage_completed',
|
||||
createdAt: new Date(startTimestamp - 1000).toISOString(),
|
||||
params: {}
|
||||
},
|
||||
{
|
||||
event_id: 'test-2',
|
||||
event_type: 'credit_added',
|
||||
createdAt: new Date(startTimestamp + 1000).toISOString(),
|
||||
params: { amount: 500 }
|
||||
}
|
||||
]
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup(events)
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).toHaveBeenCalledOnce()
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'pending_topup_timestamp'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not detect topup if credit_added event is before tracking started', () => {
|
||||
const startTimestamp = Date.now()
|
||||
mockLocalStorage.getItem.mockReturnValue(startTimestamp.toString())
|
||||
|
||||
const events: AuditLog[] = [
|
||||
{
|
||||
event_id: 'test-1',
|
||||
event_type: 'credit_added',
|
||||
createdAt: new Date(startTimestamp - 1000).toISOString(), // Before tracking
|
||||
params: { amount: 500 }
|
||||
}
|
||||
]
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup(events)
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled()
|
||||
expect(mockLocalStorage.removeItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should ignore events without createdAt timestamp', () => {
|
||||
const startTimestamp = Date.now()
|
||||
mockLocalStorage.getItem.mockReturnValue(startTimestamp.toString())
|
||||
|
||||
const events: AuditLog[] = [
|
||||
{
|
||||
event_id: 'test-1',
|
||||
event_type: 'credit_added',
|
||||
createdAt: undefined,
|
||||
params: { amount: 500 }
|
||||
}
|
||||
]
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup(events)
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should only match credit_added events, not other event types', () => {
|
||||
const startTimestamp = Date.now()
|
||||
mockLocalStorage.getItem.mockReturnValue(startTimestamp.toString())
|
||||
|
||||
const events: AuditLog[] = [
|
||||
{
|
||||
event_id: 'test-1',
|
||||
event_type: 'api_usage_completed',
|
||||
createdAt: new Date(startTimestamp + 1000).toISOString(),
|
||||
params: {}
|
||||
},
|
||||
{
|
||||
event_id: 'test-2',
|
||||
event_type: 'account_created',
|
||||
createdAt: new Date(startTimestamp + 2000).toISOString(),
|
||||
params: {}
|
||||
}
|
||||
]
|
||||
|
||||
const result = topupTracker.checkForCompletedTopup(events)
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearTopupTracking', () => {
|
||||
it('should remove pending topup from localStorage', () => {
|
||||
topupTracker.clearTopupTracking()
|
||||
|
||||
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(
|
||||
'pending_topup_timestamp'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user