mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 01:34:07 +00:00
refactor
This commit is contained in:
@@ -16,27 +16,40 @@
|
||||
*/
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
import { CloudAnalyticsProvider } from './providers/cloud/CloudAnalyticsProvider'
|
||||
import { MixpanelTelemetryProvider } from './providers/cloud/MixpanelTelemetryProvider'
|
||||
import type { TelemetryProvider } from './types'
|
||||
import { TelemetryService } from './services/TelemetryService'
|
||||
|
||||
// Singleton instance
|
||||
let _telemetryProvider: TelemetryProvider | null = null
|
||||
let _telemetryService: TelemetryService | null = null
|
||||
|
||||
/**
|
||||
* Telemetry factory - conditionally creates provider based on distribution
|
||||
* Telemetry service factory - conditionally creates service with providers based on distribution
|
||||
* Returns singleton instance.
|
||||
*
|
||||
* CRITICAL: This returns undefined in OSS builds. There is no telemetry provider
|
||||
* CRITICAL: This returns null in OSS builds. There is no telemetry service
|
||||
* for OSS builds and all tracking calls are no-ops.
|
||||
*/
|
||||
export function useTelemetry(): TelemetryProvider | null {
|
||||
if (_telemetryProvider === null) {
|
||||
export function useTelemetry(): TelemetryService | null {
|
||||
if (_telemetryService === null) {
|
||||
// Use distribution check for tree-shaking
|
||||
if (isCloud) {
|
||||
_telemetryProvider = new MixpanelTelemetryProvider()
|
||||
_telemetryService = new TelemetryService()
|
||||
|
||||
// Initialize and add both analytics providers
|
||||
const mixpanelProvider = new MixpanelTelemetryProvider()
|
||||
const cloudAnalyticsProvider = new CloudAnalyticsProvider()
|
||||
|
||||
void mixpanelProvider.initialize().then(() => {
|
||||
_telemetryService?.addProvider(mixpanelProvider)
|
||||
})
|
||||
|
||||
void cloudAnalyticsProvider.initialize().then(() => {
|
||||
_telemetryService?.addProvider(cloudAnalyticsProvider)
|
||||
})
|
||||
}
|
||||
// For OSS builds, _telemetryProvider stays null
|
||||
// For OSS builds, _telemetryService stays null
|
||||
}
|
||||
|
||||
return _telemetryProvider
|
||||
return _telemetryService
|
||||
}
|
||||
|
||||
49
src/platform/telemetry/interfaces/TelemetryHooks.ts
Normal file
49
src/platform/telemetry/interfaces/TelemetryHooks.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type {
|
||||
ExecutionContext,
|
||||
SurveyResponses,
|
||||
TemplateMetadata
|
||||
} from '../types'
|
||||
|
||||
/**
|
||||
* Context types provided by domain stores to telemetry
|
||||
*/
|
||||
interface UserContext {
|
||||
id: string
|
||||
email?: string
|
||||
tier?: 'free' | 'pro' | 'enterprise'
|
||||
}
|
||||
|
||||
interface WorkflowContext {
|
||||
filename?: string
|
||||
isTemplate: boolean
|
||||
nodeCount?: number
|
||||
hasCustomNodes?: boolean
|
||||
}
|
||||
|
||||
interface SubscriptionContext {
|
||||
isSubscribed: boolean
|
||||
plan?: string
|
||||
creditsRemaining?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Telemetry hooks interface for dependency inversion.
|
||||
* Domain stores register these hooks to provide context to telemetry
|
||||
* without creating circular dependencies.
|
||||
*/
|
||||
export interface TelemetryHooks {
|
||||
// Context providers
|
||||
getExecutionContext?(): ExecutionContext | null
|
||||
getCurrentUser?(): UserContext | null
|
||||
getActiveWorkflow?(): WorkflowContext | null
|
||||
|
||||
// Setting providers
|
||||
getSurveyData?(): SurveyResponses | null
|
||||
getSubscriptionStatus?(): SubscriptionContext | null
|
||||
|
||||
// Template metadata providers
|
||||
getTemplateMetadata?(filename: string): TemplateMetadata | null
|
||||
|
||||
// Feature flag providers
|
||||
getFeatureFlags?(): Record<string, boolean>
|
||||
}
|
||||
91
src/platform/telemetry/providers/TelemetryProviderBase.ts
Normal file
91
src/platform/telemetry/providers/TelemetryProviderBase.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
AuthMetadata,
|
||||
ExecutionContext,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
HelpCenterClosedMetadata,
|
||||
HelpCenterOpenedMetadata,
|
||||
HelpResourceClickedMetadata,
|
||||
NodeSearchMetadata,
|
||||
NodeSearchResultMetadata,
|
||||
PageVisibilityMetadata,
|
||||
RunButtonProperties,
|
||||
SettingChangedMetadata,
|
||||
SurveyResponses,
|
||||
TabCountMetadata,
|
||||
TelemetryProvider,
|
||||
TemplateFilterMetadata,
|
||||
TemplateLibraryClosedMetadata,
|
||||
TemplateLibraryMetadata,
|
||||
TemplateMetadata,
|
||||
UiButtonClickMetadata,
|
||||
WorkflowCreatedMetadata,
|
||||
WorkflowImportMetadata
|
||||
} from '../types'
|
||||
|
||||
/**
|
||||
* Abstract base class for telemetry providers with lifecycle management.
|
||||
* Concrete providers should extend this class for consistent behavior.
|
||||
*/
|
||||
export abstract class TelemetryProviderBase implements TelemetryProvider {
|
||||
protected isEnabled = true
|
||||
protected isInitialized = false
|
||||
|
||||
/**
|
||||
* Initialize the provider (e.g., load external libraries)
|
||||
*/
|
||||
abstract initialize(): Promise<void>
|
||||
|
||||
/**
|
||||
* Check if the provider is ready to track events
|
||||
*/
|
||||
isReady(): boolean {
|
||||
return this.isEnabled && this.isInitialized
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this.isEnabled = enabled
|
||||
}
|
||||
|
||||
abstract trackAuth(metadata: AuthMetadata): void
|
||||
abstract trackUserLoggedIn(): void
|
||||
abstract trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void
|
||||
abstract trackMonthlySubscriptionSucceeded(): void
|
||||
abstract trackAddApiCreditButtonClicked(): void
|
||||
abstract trackApiCreditTopupButtonPurchaseClicked(amount: number): void
|
||||
abstract trackApiCreditTopupSucceeded(): void
|
||||
abstract trackRunButton(properties: RunButtonProperties): void
|
||||
abstract startTopupTracking(): void
|
||||
abstract checkForCompletedTopup(events: any[] | undefined | null): boolean
|
||||
abstract clearTopupTracking(): void
|
||||
abstract trackSurvey(
|
||||
stage: 'opened' | 'submitted',
|
||||
responses?: SurveyResponses
|
||||
): void
|
||||
abstract trackEmailVerification(
|
||||
stage: 'opened' | 'requested' | 'completed'
|
||||
): void
|
||||
abstract trackTemplate(metadata: TemplateMetadata): void
|
||||
abstract trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void
|
||||
abstract trackTemplateLibraryClosed(
|
||||
metadata: TemplateLibraryClosedMetadata
|
||||
): void
|
||||
abstract trackWorkflowImported(metadata: WorkflowImportMetadata): void
|
||||
abstract trackWorkflowOpened(metadata: WorkflowImportMetadata): void
|
||||
abstract trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void
|
||||
abstract trackTabCount(metadata: TabCountMetadata): void
|
||||
abstract trackNodeSearch(metadata: NodeSearchMetadata): void
|
||||
abstract trackNodeSearchResultSelected(
|
||||
metadata: NodeSearchResultMetadata
|
||||
): void
|
||||
abstract trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void
|
||||
abstract trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void
|
||||
abstract trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void
|
||||
abstract trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void
|
||||
abstract trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void
|
||||
abstract trackWorkflowExecution(context?: ExecutionContext): void
|
||||
abstract trackExecutionError(metadata: ExecutionErrorMetadata): void
|
||||
abstract trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
|
||||
abstract trackSettingChanged(metadata: SettingChangedMetadata): void
|
||||
abstract trackUiButtonClicked(metadata: UiButtonClickMetadata): void
|
||||
}
|
||||
210
src/platform/telemetry/providers/cloud/CloudAnalyticsProvider.ts
Normal file
210
src/platform/telemetry/providers/cloud/CloudAnalyticsProvider.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { api } from '@/scripts/api'
|
||||
import type {
|
||||
AuthMetadata,
|
||||
CreditTopupMetadata,
|
||||
ExecutionContext,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
HelpCenterClosedMetadata,
|
||||
HelpCenterOpenedMetadata,
|
||||
HelpResourceClickedMetadata,
|
||||
NodeSearchMetadata,
|
||||
NodeSearchResultMetadata,
|
||||
PageVisibilityMetadata,
|
||||
RunButtonProperties,
|
||||
SettingChangedMetadata,
|
||||
SurveyResponses,
|
||||
TabCountMetadata,
|
||||
TelemetryEventName,
|
||||
TemplateFilterMetadata,
|
||||
TemplateLibraryClosedMetadata,
|
||||
TemplateLibraryMetadata,
|
||||
TemplateMetadata,
|
||||
UiButtonClickMetadata,
|
||||
WorkflowCreatedMetadata,
|
||||
WorkflowImportMetadata
|
||||
} from '../../types'
|
||||
import { TelemetryEvents } from '../../types'
|
||||
import { TelemetryProviderBase } from '../TelemetryProviderBase'
|
||||
|
||||
/**
|
||||
* Cloud Analytics Provider for server-side event tracking.
|
||||
* Posts events to the backend's cloud analytics service (ClickHouse) via api.postCloudAnalytics.
|
||||
* This complements client-side tracking (Mixpanel) with server-side data collection.
|
||||
*/
|
||||
export class CloudAnalyticsProvider extends TelemetryProviderBase {
|
||||
async initialize(): Promise<void> {
|
||||
this.isInitialized = true
|
||||
}
|
||||
|
||||
private async postEvent(
|
||||
eventName: TelemetryEventName,
|
||||
eventData?: any
|
||||
): Promise<void> {
|
||||
if (!this.isEnabled || !this.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.postCloudAnalytics(eventName, eventData || {})
|
||||
} catch (error) {
|
||||
console.error('Failed to post cloud analytics event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
trackAuth(metadata: AuthMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
|
||||
}
|
||||
|
||||
trackUserLoggedIn(): void {
|
||||
void this.postEvent(TelemetryEvents.USER_LOGGED_IN)
|
||||
}
|
||||
|
||||
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void {
|
||||
const eventName =
|
||||
event === 'modal_opened'
|
||||
? TelemetryEvents.SUBSCRIPTION_REQUIRED_MODAL_OPENED
|
||||
: TelemetryEvents.SUBSCRIBE_NOW_BUTTON_CLICKED
|
||||
void this.postEvent(eventName)
|
||||
}
|
||||
|
||||
trackMonthlySubscriptionSucceeded(): void {
|
||||
void this.postEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED)
|
||||
}
|
||||
|
||||
trackAddApiCreditButtonClicked(): void {
|
||||
void this.postEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
|
||||
}
|
||||
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
|
||||
const metadata: CreditTopupMetadata = { credit_amount: amount }
|
||||
void this.postEvent(
|
||||
TelemetryEvents.API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED,
|
||||
metadata
|
||||
)
|
||||
}
|
||||
|
||||
trackApiCreditTopupSucceeded(): void {
|
||||
void this.postEvent(TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED)
|
||||
}
|
||||
|
||||
trackRunButton(properties: RunButtonProperties): void {
|
||||
void this.postEvent(TelemetryEvents.RUN_BUTTON_CLICKED, properties)
|
||||
}
|
||||
|
||||
startTopupTracking(): void {
|
||||
// Not applicable for server-side tracking
|
||||
}
|
||||
|
||||
checkForCompletedTopup(_events: any[] | undefined | null): boolean {
|
||||
// Not applicable for server-side tracking
|
||||
return false
|
||||
}
|
||||
|
||||
clearTopupTracking(): void {
|
||||
// Not applicable for server-side tracking
|
||||
}
|
||||
|
||||
trackSurvey(
|
||||
stage: 'opened' | 'submitted',
|
||||
responses?: SurveyResponses
|
||||
): void {
|
||||
const eventName =
|
||||
stage === 'opened'
|
||||
? TelemetryEvents.USER_SURVEY_OPENED
|
||||
: TelemetryEvents.USER_SURVEY_SUBMITTED
|
||||
void this.postEvent(eventName, responses)
|
||||
}
|
||||
|
||||
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void {
|
||||
let eventName: TelemetryEventName
|
||||
switch (stage) {
|
||||
case 'opened':
|
||||
eventName = TelemetryEvents.USER_EMAIL_VERIFY_OPENED
|
||||
break
|
||||
case 'requested':
|
||||
eventName = TelemetryEvents.USER_EMAIL_VERIFY_REQUESTED
|
||||
break
|
||||
case 'completed':
|
||||
eventName = TelemetryEvents.USER_EMAIL_VERIFY_COMPLETED
|
||||
break
|
||||
}
|
||||
void this.postEvent(eventName)
|
||||
}
|
||||
|
||||
trackTemplate(metadata: TemplateMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.TEMPLATE_WORKFLOW_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.TEMPLATE_LIBRARY_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateLibraryClosed(metadata: TemplateLibraryClosedMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.TEMPLATE_LIBRARY_CLOSED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowImported(metadata: WorkflowImportMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.WORKFLOW_IMPORTED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowOpened(metadata: WorkflowImportMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.WORKFLOW_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.PAGE_VISIBILITY_CHANGED, metadata)
|
||||
}
|
||||
|
||||
trackTabCount(metadata: TabCountMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.TAB_COUNT_TRACKING, metadata)
|
||||
}
|
||||
|
||||
trackNodeSearch(metadata: NodeSearchMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.NODE_SEARCH, metadata)
|
||||
}
|
||||
|
||||
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.NODE_SEARCH_RESULT_SELECTED, metadata)
|
||||
}
|
||||
|
||||
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata)
|
||||
}
|
||||
|
||||
trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.HELP_CENTER_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.HELP_RESOURCE_CLICKED, metadata)
|
||||
}
|
||||
|
||||
trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.HELP_CENTER_CLOSED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.WORKFLOW_CREATED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowExecution(context?: ExecutionContext): void {
|
||||
void this.postEvent(TelemetryEvents.EXECUTION_START, context)
|
||||
}
|
||||
|
||||
trackExecutionError(metadata: ExecutionErrorMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.EXECUTION_ERROR, metadata)
|
||||
}
|
||||
|
||||
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.EXECUTION_SUCCESS, metadata)
|
||||
}
|
||||
|
||||
trackSettingChanged(metadata: SettingChangedMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.SETTING_CHANGED, metadata)
|
||||
}
|
||||
|
||||
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
|
||||
void this.postEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import type {
|
||||
TabCountMetadata,
|
||||
TelemetryEventName,
|
||||
TelemetryEventProperties,
|
||||
TelemetryProvider,
|
||||
TemplateFilterMetadata,
|
||||
TemplateLibraryClosedMetadata,
|
||||
TemplateLibraryMetadata,
|
||||
@@ -43,6 +42,7 @@ import type {
|
||||
} from '../../types'
|
||||
import { TelemetryEvents } from '../../types'
|
||||
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
|
||||
import { TelemetryProviderBase } from '../TelemetryProviderBase'
|
||||
|
||||
interface QueuedEvent {
|
||||
eventName: TelemetryEventName
|
||||
@@ -61,50 +61,42 @@ interface QueuedEvent {
|
||||
* 2. `grep -RinE --include='*.js' 'trackWorkflow|trackEvent|mixpanel' dist/` (should find nothing)
|
||||
* 3. Check dist/assets/*.js files contain no tracking code
|
||||
*/
|
||||
export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
private isEnabled = true
|
||||
export class MixpanelTelemetryProvider extends TelemetryProviderBase {
|
||||
private mixpanel: OverridedMixpanel | null = null
|
||||
private eventQueue: QueuedEvent[] = []
|
||||
private isInitialized = false
|
||||
private lastTriggerSource: ExecutionTriggerSource | undefined
|
||||
|
||||
constructor() {
|
||||
async initialize(): Promise<void> {
|
||||
const token = window.__CONFIG__?.mixpanel_token
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
// Dynamic import to avoid bundling mixpanel in OSS builds
|
||||
void import('mixpanel-browser')
|
||||
.then((mixpanelModule) => {
|
||||
this.mixpanel = mixpanelModule.default
|
||||
this.mixpanel.init(token, {
|
||||
debug: import.meta.env.DEV,
|
||||
track_pageview: true,
|
||||
api_host: 'https://mp.comfy.org',
|
||||
cross_subdomain_cookie: true,
|
||||
persistence: 'cookie',
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!token) {
|
||||
this.setEnabled(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const mixpanelModule = await import('mixpanel-browser')
|
||||
this.mixpanel = mixpanelModule.default
|
||||
|
||||
this.mixpanel.init(token, {
|
||||
debug: import.meta.env.DEV,
|
||||
track_pageview: true,
|
||||
api_host: 'https://mp.comfy.org',
|
||||
cross_subdomain_cookie: true,
|
||||
persistence: 'cookie',
|
||||
loaded: () => {
|
||||
this.isInitialized = true
|
||||
this.flushEventQueue()
|
||||
useCurrentUser().onUserResolved((user) => {
|
||||
if (this.mixpanel && user.id) {
|
||||
this.mixpanel.identify(user.id)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load Mixpanel:', error)
|
||||
this.isEnabled = false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Mixpanel:', error)
|
||||
this.isEnabled = false
|
||||
}
|
||||
} else {
|
||||
console.warn('Mixpanel token not provided in runtime config')
|
||||
this.isEnabled = false
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to load Mixpanel:', error)
|
||||
this.setEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
239
src/platform/telemetry/services/TelemetryService.ts
Normal file
239
src/platform/telemetry/services/TelemetryService.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import type {
|
||||
TelemetryProvider,
|
||||
AuthMetadata,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
HelpCenterClosedMetadata,
|
||||
HelpCenterOpenedMetadata,
|
||||
HelpResourceClickedMetadata,
|
||||
NodeSearchMetadata,
|
||||
NodeSearchResultMetadata,
|
||||
PageVisibilityMetadata,
|
||||
RunButtonProperties,
|
||||
SettingChangedMetadata,
|
||||
SurveyResponses,
|
||||
TabCountMetadata,
|
||||
TemplateFilterMetadata,
|
||||
TemplateLibraryClosedMetadata,
|
||||
TemplateLibraryMetadata,
|
||||
TemplateMetadata,
|
||||
UiButtonClickMetadata,
|
||||
WorkflowCreatedMetadata,
|
||||
WorkflowImportMetadata,
|
||||
ExecutionTriggerSource
|
||||
} from '../types'
|
||||
import type { TelemetryHooks } from '../interfaces/TelemetryHooks'
|
||||
|
||||
/**
|
||||
* Central telemetry service that coordinates multiple analytics providers.
|
||||
* Uses registered hooks to gather context data from application stores
|
||||
* without creating circular import dependencies.
|
||||
*/
|
||||
export class TelemetryService {
|
||||
private hooks: TelemetryHooks = {}
|
||||
private providers: TelemetryProvider[] = []
|
||||
|
||||
/**
|
||||
* Register hooks from domain stores to provide context
|
||||
*/
|
||||
registerHooks(hooks: Partial<TelemetryHooks>): void {
|
||||
this.hooks = { ...this.hooks, ...hooks }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add analytics provider to receive events
|
||||
*/
|
||||
addProvider(provider: TelemetryProvider): void {
|
||||
this.providers.push(provider)
|
||||
}
|
||||
|
||||
trackAuth(metadata: AuthMetadata): void {
|
||||
this.providers.forEach((provider) => provider.trackAuth(metadata))
|
||||
}
|
||||
|
||||
trackUserLoggedIn(): void {
|
||||
this.providers.forEach((provider) => provider.trackUserLoggedIn())
|
||||
}
|
||||
|
||||
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void {
|
||||
this.providers.forEach((provider) => provider.trackSubscription(event))
|
||||
}
|
||||
|
||||
trackMonthlySubscriptionSucceeded(): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackMonthlySubscriptionSucceeded()
|
||||
)
|
||||
}
|
||||
|
||||
trackAddApiCreditButtonClicked(): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackAddApiCreditButtonClicked()
|
||||
)
|
||||
}
|
||||
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackApiCreditTopupButtonPurchaseClicked(amount)
|
||||
)
|
||||
}
|
||||
|
||||
trackApiCreditTopupSucceeded(): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackApiCreditTopupSucceeded()
|
||||
)
|
||||
}
|
||||
|
||||
trackRunButton(options?: {
|
||||
subscribe_to_run?: boolean
|
||||
trigger_source?: ExecutionTriggerSource
|
||||
}): void {
|
||||
const context = this.hooks.getExecutionContext?.()
|
||||
if (!context) return // Don't track if no context available
|
||||
|
||||
const runButtonProperties: RunButtonProperties = {
|
||||
subscribe_to_run: options?.subscribe_to_run || false,
|
||||
workflow_type: context.is_template ? 'template' : 'custom',
|
||||
workflow_name: context.workflow_name ?? 'untitled',
|
||||
custom_node_count: context.custom_node_count,
|
||||
total_node_count: context.total_node_count,
|
||||
subgraph_count: context.subgraph_count,
|
||||
has_api_nodes: context.has_api_nodes,
|
||||
api_node_names: context.api_node_names,
|
||||
trigger_source: options?.trigger_source
|
||||
}
|
||||
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackRunButton(runButtonProperties)
|
||||
)
|
||||
}
|
||||
|
||||
startTopupTracking(): void {
|
||||
this.providers.forEach((provider) => provider.startTopupTracking())
|
||||
}
|
||||
|
||||
checkForCompletedTopup(events: any[] | undefined | null): boolean {
|
||||
return this.providers.some((provider) =>
|
||||
provider.checkForCompletedTopup(events)
|
||||
)
|
||||
}
|
||||
|
||||
clearTopupTracking(): void {
|
||||
this.providers.forEach((provider) => provider.clearTopupTracking())
|
||||
}
|
||||
|
||||
trackSurvey(
|
||||
stage: 'opened' | 'submitted',
|
||||
responses?: SurveyResponses
|
||||
): void {
|
||||
this.providers.forEach((provider) => provider.trackSurvey(stage, responses))
|
||||
}
|
||||
|
||||
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void {
|
||||
this.providers.forEach((provider) => provider.trackEmailVerification(stage))
|
||||
}
|
||||
|
||||
trackTemplate(metadata: TemplateMetadata): void {
|
||||
this.providers.forEach((provider) => provider.trackTemplate(metadata))
|
||||
}
|
||||
|
||||
trackTemplateLibraryOpened(metadata: TemplateLibraryMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackTemplateLibraryOpened(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackTemplateLibraryClosed(metadata: TemplateLibraryClosedMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackTemplateLibraryClosed(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackWorkflowImported(metadata: WorkflowImportMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackWorkflowImported(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackWorkflowOpened(metadata: WorkflowImportMetadata): void {
|
||||
this.providers.forEach((provider) => provider.trackWorkflowOpened(metadata))
|
||||
}
|
||||
|
||||
trackPageVisibilityChanged(metadata: PageVisibilityMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackPageVisibilityChanged(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackTabCount(metadata: TabCountMetadata): void {
|
||||
this.providers.forEach((provider) => provider.trackTabCount(metadata))
|
||||
}
|
||||
|
||||
trackNodeSearch(metadata: NodeSearchMetadata): void {
|
||||
this.providers.forEach((provider) => provider.trackNodeSearch(metadata))
|
||||
}
|
||||
|
||||
trackNodeSearchResultSelected(metadata: NodeSearchResultMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackNodeSearchResultSelected(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackTemplateFilterChanged(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackHelpCenterOpened(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackHelpResourceClicked(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackHelpCenterClosed(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackWorkflowCreated(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackWorkflowExecution(): void {
|
||||
const context = this.hooks.getExecutionContext?.()
|
||||
if (!context) return // Don't track if no context available
|
||||
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackWorkflowExecution(context)
|
||||
)
|
||||
}
|
||||
|
||||
trackExecutionError(metadata: ExecutionErrorMetadata): void {
|
||||
this.providers.forEach((provider) => provider.trackExecutionError(metadata))
|
||||
}
|
||||
|
||||
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackExecutionSuccess(metadata)
|
||||
)
|
||||
}
|
||||
|
||||
trackSettingChanged(metadata: SettingChangedMetadata): void {
|
||||
this.providers.forEach((provider) => provider.trackSettingChanged(metadata))
|
||||
}
|
||||
|
||||
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
|
||||
this.providers.forEach((provider) =>
|
||||
provider.trackUiButtonClicked(metadata)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -314,7 +314,7 @@ export interface TelemetryProvider {
|
||||
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void
|
||||
|
||||
// Workflow execution events
|
||||
trackWorkflowExecution(): void
|
||||
trackWorkflowExecution(context?: ExecutionContext): void
|
||||
trackExecutionError(metadata: ExecutionErrorMetadata): void
|
||||
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
|
||||
|
||||
|
||||
@@ -1251,6 +1251,28 @@ export class ComfyApi extends EventTarget {
|
||||
getServerFeatures(): Record<string, unknown> {
|
||||
return { ...this.serverFeatureFlags }
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts analytics event to cloud analytics service
|
||||
* @param eventName The name of the analytics event
|
||||
* @param eventData The event data (any JSON-serializable object)
|
||||
* @returns Promise resolving to the response
|
||||
*/
|
||||
async postCloudAnalytics(
|
||||
eventName: string,
|
||||
eventData: any
|
||||
): Promise<Response> {
|
||||
return this.fetchApi(this.internalURL('/cloud_analytics'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event_name: eventName,
|
||||
event_data: eventData
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ComfyApi()
|
||||
|
||||
Reference in New Issue
Block a user