Files
ComfyUI_frontend/src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.ts
Robin Huang 6c2680f0ba feat: Add PostHog telemetry provider (#9409)
Add PostHog as a telemetry provider for cloud builds so custom events
can be correlated with session recordings. Follows the same pattern as
MixpanelTelemetryProvider with dynamic import, event queuing, and
disabled events from remote config. Tree-shaken away in OSS builds.

The posthog-js package uses Apache-2.0 (verified from its LICENSE file)
but declares it as "SEE LICENSE IN LICENSE" in package.json, which
  the license checker can't parse.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9409-feat-Add-PostHog-telemetry-provider-31a6d73d3650818b8e86c772c6551099)
by [Unito](https://www.unito.io)
2026-03-05 16:19:35 -08:00

247 lines
6.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TelemetryEvents } from '../../types'
const hoisted = vi.hoisted(() => {
const mockCapture = vi.fn()
const mockInit = vi.fn()
const mockIdentify = vi.fn()
const mockPeopleSet = vi.fn()
const mockOnUserResolved = vi.fn()
return {
mockCapture,
mockInit,
mockIdentify,
mockPeopleSet,
mockOnUserResolved,
mockPosthog: {
default: {
init: mockInit,
capture: mockCapture,
identify: mockIdentify,
people: { set: mockPeopleSet }
}
}
}
})
vi.mock('vue', async () => {
const actual = await vi.importActual('vue')
return {
...actual,
watch: vi.fn()
}
})
vi.mock('@/composables/auth/useCurrentUser', () => ({
useCurrentUser: () => ({
onUserResolved: hoisted.mockOnUserResolved
})
}))
vi.mock('@/platform/remoteConfig/remoteConfig', () => ({
remoteConfig: { value: null }
}))
vi.mock('posthog-js', () => hoisted.mockPosthog)
import { PostHogTelemetryProvider } from './PostHogTelemetryProvider'
function createProvider(
config: Partial<typeof window.__CONFIG__> = {}
): PostHogTelemetryProvider {
const original = window.__CONFIG__
window.__CONFIG__ = { ...original, ...config }
const provider = new PostHogTelemetryProvider()
window.__CONFIG__ = original
return provider
}
describe('PostHogTelemetryProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
window.__CONFIG__ = {
posthog_project_token: 'phc_test_token'
} as typeof window.__CONFIG__
})
describe('initialization', () => {
it('disables itself when posthog_project_token is not provided', async () => {
const provider = createProvider({ posthog_project_token: undefined })
await vi.dynamicImportSettled()
provider.trackSignupOpened()
expect(hoisted.mockCapture).not.toHaveBeenCalled()
})
it('calls posthog.init with the token and default api_host', async () => {
createProvider()
await vi.dynamicImportSettled()
expect(hoisted.mockInit).toHaveBeenCalledWith('phc_test_token', {
api_host: 'https://ph.comfy.org',
autocapture: false,
capture_pageview: false,
capture_pageleave: false,
persistence: 'localStorage+cookie'
})
})
it('uses custom api_host from config when provided', async () => {
window.__CONFIG__ = {
posthog_project_token: 'phc_test_token',
posthog_api_host: 'https://custom.host.com'
} as typeof window.__CONFIG__
new PostHogTelemetryProvider()
await vi.dynamicImportSettled()
expect(hoisted.mockInit).toHaveBeenCalledWith(
'phc_test_token',
expect.objectContaining({ api_host: 'https://custom.host.com' })
)
})
it('registers onUserResolved callback after init', async () => {
createProvider()
await vi.dynamicImportSettled()
expect(hoisted.mockOnUserResolved).toHaveBeenCalledOnce()
})
it('identifies user when onUserResolved fires', async () => {
createProvider()
await vi.dynamicImportSettled()
const callback = hoisted.mockOnUserResolved.mock.calls[0][0]
callback({ id: 'user-123' })
expect(hoisted.mockIdentify).toHaveBeenCalledWith('user-123')
})
})
describe('event tracking', () => {
it('captures events after initialization', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackSignupOpened()
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.USER_SIGN_UP_OPENED,
{}
)
})
it('captures events with metadata', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackAuth({ method: 'google' })
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.USER_AUTH_COMPLETED,
{ method: 'google' }
)
})
it('queues events before initialization and flushes after', async () => {
const provider = createProvider()
provider.trackUserLoggedIn()
expect(hoisted.mockCapture).not.toHaveBeenCalled()
await vi.dynamicImportSettled()
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.USER_LOGGED_IN,
{}
)
})
})
describe('disabled events', () => {
it('does not capture default disabled events', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackWorkflowOpened({
missing_node_count: 0,
missing_node_types: []
})
expect(hoisted.mockCapture).not.toHaveBeenCalled()
})
it('captures events not in the disabled list', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackMonthlySubscriptionSucceeded()
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED,
{}
)
})
})
describe('survey tracking', () => {
it('sets user properties on survey submission', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
const responses = { familiarity: 'beginner', industry: 'tech' }
provider.trackSurvey('submitted', responses)
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.USER_SURVEY_SUBMITTED,
expect.objectContaining({ familiarity: 'beginner' })
)
expect(hoisted.mockPeopleSet).toHaveBeenCalled()
})
it('does not set user properties on survey opened', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackSurvey('opened')
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.USER_SURVEY_OPENED,
{}
)
expect(hoisted.mockPeopleSet).not.toHaveBeenCalled()
})
})
describe('page view', () => {
it('captures page view with page_name property', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackPageView('workflow_editor')
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.PAGE_VIEW,
{ page_name: 'workflow_editor' }
)
})
it('forwards additional metadata', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackPageView('workflow_editor', {
path: '/workflows/123'
})
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.PAGE_VIEW,
{ page_name: 'workflow_editor', path: '/workflows/123' }
)
})
})
})