mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-17 05:01:02 +00:00
Compare commits
2 Commits
codex/clou
...
telemetry/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2afed8dd | ||
|
|
860afe2c14 |
@@ -17,26 +17,32 @@
|
|||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
|
|
||||||
import { MixpanelTelemetryProvider } from './providers/cloud/MixpanelTelemetryProvider'
|
import { MixpanelTelemetryProvider } from './providers/cloud/MixpanelTelemetryProvider'
|
||||||
import type { TelemetryProvider } from './types'
|
import { TelemetryService } from './services/TelemetryService'
|
||||||
|
|
||||||
// Singleton instance
|
// 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.
|
* 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.
|
* for OSS builds and all tracking calls are no-ops.
|
||||||
*/
|
*/
|
||||||
export function useTelemetry(): TelemetryProvider | null {
|
export function useTelemetry(): TelemetryService | null {
|
||||||
if (_telemetryProvider === null) {
|
if (_telemetryService === null) {
|
||||||
// Use distribution check for tree-shaking
|
// Use distribution check for tree-shaking
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
_telemetryProvider = new MixpanelTelemetryProvider()
|
_telemetryService = new TelemetryService()
|
||||||
|
|
||||||
|
// Initialize and add Mixpanel provider
|
||||||
|
const mixpanelProvider = new MixpanelTelemetryProvider()
|
||||||
|
void mixpanelProvider.initialize().then(() => {
|
||||||
|
_telemetryService?.addProvider(mixpanelProvider)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// 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
|
||||||
|
}
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
import type { OverridedMixpanel } from 'mixpanel-browser'
|
import type { OverridedMixpanel } from 'mixpanel-browser'
|
||||||
|
|
||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
|
||||||
import {
|
import {
|
||||||
checkForCompletedTopup as checkTopupUtil,
|
checkForCompletedTopup as checkTopupUtil,
|
||||||
clearTopupTracking as clearTopupUtil,
|
clearTopupTracking as clearTopupUtil,
|
||||||
startTopupTracking as startTopupUtil
|
startTopupTracking as startTopupUtil
|
||||||
} from '@/platform/telemetry/topupTracker'
|
} from '@/platform/telemetry/topupTracker'
|
||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
|
||||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
|
||||||
import { app } from '@/scripts/app'
|
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
|
||||||
import { NodeSourceType } from '@/types/nodeSource'
|
|
||||||
import { reduceAllNodes } from '@/utils/graphTraversalUtil'
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AuthMetadata,
|
AuthMetadata,
|
||||||
@@ -32,7 +25,6 @@ import type {
|
|||||||
TabCountMetadata,
|
TabCountMetadata,
|
||||||
TelemetryEventName,
|
TelemetryEventName,
|
||||||
TelemetryEventProperties,
|
TelemetryEventProperties,
|
||||||
TelemetryProvider,
|
|
||||||
TemplateFilterMetadata,
|
TemplateFilterMetadata,
|
||||||
TemplateLibraryClosedMetadata,
|
TemplateLibraryClosedMetadata,
|
||||||
TemplateLibraryMetadata,
|
TemplateLibraryMetadata,
|
||||||
@@ -43,11 +35,7 @@ import type {
|
|||||||
} from '../../types'
|
} from '../../types'
|
||||||
import { TelemetryEvents } from '../../types'
|
import { TelemetryEvents } from '../../types'
|
||||||
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
|
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
|
||||||
|
import { TelemetryProviderBase } from '../TelemetryProviderBase'
|
||||||
interface QueuedEvent {
|
|
||||||
eventName: TelemetryEventName
|
|
||||||
properties?: TelemetryEventProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mixpanel Telemetry Provider - Cloud Build Implementation
|
* Mixpanel Telemetry Provider - Cloud Build Implementation
|
||||||
@@ -61,65 +49,38 @@ interface QueuedEvent {
|
|||||||
* 2. `grep -RinE --include='*.js' 'trackWorkflow|trackEvent|mixpanel' dist/` (should find nothing)
|
* 2. `grep -RinE --include='*.js' 'trackWorkflow|trackEvent|mixpanel' dist/` (should find nothing)
|
||||||
* 3. Check dist/assets/*.js files contain no tracking code
|
* 3. Check dist/assets/*.js files contain no tracking code
|
||||||
*/
|
*/
|
||||||
export class MixpanelTelemetryProvider implements TelemetryProvider {
|
export class MixpanelTelemetryProvider extends TelemetryProviderBase {
|
||||||
private isEnabled = true
|
|
||||||
private mixpanel: OverridedMixpanel | null = null
|
private mixpanel: OverridedMixpanel | null = null
|
||||||
private eventQueue: QueuedEvent[] = []
|
|
||||||
private isInitialized = false
|
|
||||||
private lastTriggerSource: ExecutionTriggerSource | undefined
|
private lastTriggerSource: ExecutionTriggerSource | undefined
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const token = window.__CONFIG__?.mixpanel_token
|
super()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private flushEventQueue(): void {
|
async initialize(): Promise<void> {
|
||||||
if (!this.isInitialized || !this.mixpanel) {
|
const token = window.__CONFIG__?.mixpanel_token
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
this.setEnabled(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
while (this.eventQueue.length > 0) {
|
try {
|
||||||
const event = this.eventQueue.shift()!
|
const mixpanelModule = await import('mixpanel-browser')
|
||||||
try {
|
this.mixpanel = mixpanelModule.default
|
||||||
this.mixpanel.track(event.eventName, event.properties || {})
|
|
||||||
} catch (error) {
|
this.mixpanel.init(token, {
|
||||||
console.error('Failed to track queued event:', error)
|
debug: import.meta.env.DEV,
|
||||||
}
|
track_pageview: true,
|
||||||
|
api_host: 'https://mp.comfy.org',
|
||||||
|
cross_subdomain_cookie: true,
|
||||||
|
persistence: 'cookie'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isInitialized = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load Mixpanel:', error)
|
||||||
|
this.setEnabled(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,22 +88,14 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
|||||||
eventName: TelemetryEventName,
|
eventName: TelemetryEventName,
|
||||||
properties?: TelemetryEventProperties
|
properties?: TelemetryEventProperties
|
||||||
): void {
|
): void {
|
||||||
if (!this.isEnabled) {
|
if (!this.isEnabled || !this.isInitialized || !this.mixpanel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const event: QueuedEvent = { eventName, properties }
|
try {
|
||||||
|
this.mixpanel.track(eventName, properties || {})
|
||||||
if (this.isInitialized && this.mixpanel) {
|
} catch (error) {
|
||||||
// Mixpanel is ready, track immediately
|
console.error('Failed to track event:', error)
|
||||||
try {
|
|
||||||
this.mixpanel.track(eventName, properties || {})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to track event:', error)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mixpanel not ready yet, queue the event
|
|
||||||
this.eventQueue.push(event)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +138,6 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
|||||||
this.trackEvent(TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED)
|
this.trackEvent(TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credit top-up tracking methods (composition with utility functions)
|
|
||||||
startTopupTracking(): void {
|
startTopupTracking(): void {
|
||||||
startTopupUtil()
|
startTopupUtil()
|
||||||
}
|
}
|
||||||
@@ -198,26 +150,9 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
|||||||
clearTopupUtil()
|
clearTopupUtil()
|
||||||
}
|
}
|
||||||
|
|
||||||
trackRunButton(options?: {
|
trackRunButton(properties: RunButtonProperties): void {
|
||||||
subscribe_to_run?: boolean
|
this.lastTriggerSource = properties.trigger_source
|
||||||
trigger_source?: ExecutionTriggerSource
|
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, properties)
|
||||||
}): void {
|
|
||||||
const executionContext = this.getExecutionContext()
|
|
||||||
|
|
||||||
const runButtonProperties: RunButtonProperties = {
|
|
||||||
subscribe_to_run: options?.subscribe_to_run || false,
|
|
||||||
workflow_type: executionContext.is_template ? 'template' : 'custom',
|
|
||||||
workflow_name: executionContext.workflow_name ?? 'untitled',
|
|
||||||
custom_node_count: executionContext.custom_node_count,
|
|
||||||
total_node_count: executionContext.total_node_count,
|
|
||||||
subgraph_count: executionContext.subgraph_count,
|
|
||||||
has_api_nodes: executionContext.has_api_nodes,
|
|
||||||
api_node_names: executionContext.api_node_names,
|
|
||||||
trigger_source: options?.trigger_source
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastTriggerSource = options?.trigger_source
|
|
||||||
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trackSurvey(
|
trackSurvey(
|
||||||
@@ -229,14 +164,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
|||||||
? TelemetryEvents.USER_SURVEY_OPENED
|
? TelemetryEvents.USER_SURVEY_OPENED
|
||||||
: TelemetryEvents.USER_SURVEY_SUBMITTED
|
: TelemetryEvents.USER_SURVEY_SUBMITTED
|
||||||
|
|
||||||
// Apply normalization to survey responses
|
|
||||||
const normalizedResponses = responses
|
const normalizedResponses = responses
|
||||||
? normalizeSurveyResponses(responses)
|
? normalizeSurveyResponses(responses)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
this.trackEvent(eventName, normalizedResponses)
|
this.trackEvent(eventName, normalizedResponses)
|
||||||
|
|
||||||
// If this is a survey submission, also set user properties with normalized data
|
|
||||||
if (stage === 'submitted' && normalizedResponses && this.mixpanel) {
|
if (stage === 'submitted' && normalizedResponses && this.mixpanel) {
|
||||||
try {
|
try {
|
||||||
this.mixpanel.people.set(normalizedResponses)
|
this.mixpanel.people.set(normalizedResponses)
|
||||||
@@ -320,9 +253,8 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
|||||||
this.trackEvent(TelemetryEvents.WORKFLOW_CREATED, metadata)
|
this.trackEvent(TelemetryEvents.WORKFLOW_CREATED, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
trackWorkflowExecution(): void {
|
trackWorkflowExecution(context: ExecutionContext): void {
|
||||||
const context = this.getExecutionContext()
|
const eventContext = {
|
||||||
const eventContext: ExecutionContext = {
|
|
||||||
...context,
|
...context,
|
||||||
trigger_source: this.lastTriggerSource ?? 'unknown'
|
trigger_source: this.lastTriggerSource ?? 'unknown'
|
||||||
}
|
}
|
||||||
@@ -345,98 +277,4 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
|||||||
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
|
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
|
||||||
this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
|
this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
getExecutionContext(): ExecutionContext {
|
|
||||||
const workflowStore = useWorkflowStore()
|
|
||||||
const templatesStore = useWorkflowTemplatesStore()
|
|
||||||
const nodeDefStore = useNodeDefStore()
|
|
||||||
const activeWorkflow = workflowStore.activeWorkflow
|
|
||||||
|
|
||||||
// Calculate node metrics in a single traversal
|
|
||||||
type NodeMetrics = {
|
|
||||||
custom_node_count: number
|
|
||||||
api_node_count: number
|
|
||||||
subgraph_count: number
|
|
||||||
total_node_count: number
|
|
||||||
has_api_nodes: boolean
|
|
||||||
api_node_names: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeCounts = reduceAllNodes<NodeMetrics>(
|
|
||||||
app.graph,
|
|
||||||
(metrics, node) => {
|
|
||||||
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
|
|
||||||
const isCustomNode =
|
|
||||||
nodeDef?.nodeSource?.type === NodeSourceType.CustomNodes
|
|
||||||
const isApiNode = nodeDef?.api_node === true
|
|
||||||
const isSubgraph = node.isSubgraphNode?.() === true
|
|
||||||
|
|
||||||
if (isApiNode) {
|
|
||||||
metrics.has_api_nodes = true
|
|
||||||
const canonicalName = nodeDef?.name
|
|
||||||
if (
|
|
||||||
canonicalName &&
|
|
||||||
!metrics.api_node_names.includes(canonicalName)
|
|
||||||
) {
|
|
||||||
metrics.api_node_names.push(canonicalName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics.custom_node_count += isCustomNode ? 1 : 0
|
|
||||||
metrics.api_node_count += isApiNode ? 1 : 0
|
|
||||||
metrics.subgraph_count += isSubgraph ? 1 : 0
|
|
||||||
metrics.total_node_count += 1
|
|
||||||
|
|
||||||
return metrics
|
|
||||||
},
|
|
||||||
{
|
|
||||||
custom_node_count: 0,
|
|
||||||
api_node_count: 0,
|
|
||||||
subgraph_count: 0,
|
|
||||||
total_node_count: 0,
|
|
||||||
has_api_nodes: false,
|
|
||||||
api_node_names: []
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (activeWorkflow?.filename) {
|
|
||||||
const isTemplate = templatesStore.knownTemplateNames.has(
|
|
||||||
activeWorkflow.filename
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isTemplate) {
|
|
||||||
const template = templatesStore.getTemplateByName(
|
|
||||||
activeWorkflow.filename
|
|
||||||
)
|
|
||||||
|
|
||||||
const englishMetadata = templatesStore.getEnglishMetadata(
|
|
||||||
activeWorkflow.filename
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
is_template: true,
|
|
||||||
workflow_name: activeWorkflow.filename,
|
|
||||||
template_source: template?.sourceModule,
|
|
||||||
template_category: englishMetadata?.category ?? template?.category,
|
|
||||||
template_tags: englishMetadata?.tags ?? template?.tags,
|
|
||||||
template_models: englishMetadata?.models ?? template?.models,
|
|
||||||
template_use_case: englishMetadata?.useCase ?? template?.useCase,
|
|
||||||
template_license: englishMetadata?.license ?? template?.license,
|
|
||||||
...nodeCounts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
is_template: false,
|
|
||||||
workflow_name: activeWorkflow.filename,
|
|
||||||
...nodeCounts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
is_template: false,
|
|
||||||
workflow_name: undefined,
|
|
||||||
...nodeCounts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -267,10 +267,7 @@ export interface TelemetryProvider {
|
|||||||
trackAddApiCreditButtonClicked(): void
|
trackAddApiCreditButtonClicked(): void
|
||||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void
|
trackApiCreditTopupButtonPurchaseClicked(amount: number): void
|
||||||
trackApiCreditTopupSucceeded(): void
|
trackApiCreditTopupSucceeded(): void
|
||||||
trackRunButton(options?: {
|
trackRunButton(properties: RunButtonProperties): void
|
||||||
subscribe_to_run?: boolean
|
|
||||||
trigger_source?: ExecutionTriggerSource
|
|
||||||
}): void
|
|
||||||
|
|
||||||
// Credit top-up tracking (composition with internal utilities)
|
// Credit top-up tracking (composition with internal utilities)
|
||||||
startTopupTracking(): void
|
startTopupTracking(): void
|
||||||
@@ -314,7 +311,7 @@ export interface TelemetryProvider {
|
|||||||
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void
|
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void
|
||||||
|
|
||||||
// Workflow execution events
|
// Workflow execution events
|
||||||
trackWorkflowExecution(): void
|
trackWorkflowExecution(context?: ExecutionContext): void
|
||||||
trackExecutionError(metadata: ExecutionErrorMetadata): void
|
trackExecutionError(metadata: ExecutionErrorMetadata): void
|
||||||
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
|
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,22 @@ import type {
|
|||||||
LGraphNode,
|
LGraphNode,
|
||||||
Subgraph
|
Subgraph
|
||||||
} from '@/lib/litegraph/src/litegraph'
|
} from '@/lib/litegraph/src/litegraph'
|
||||||
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
import type {
|
import type {
|
||||||
ComfyWorkflowJSON,
|
ComfyWorkflowJSON,
|
||||||
NodeId
|
NodeId
|
||||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
|
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||||
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app as comfyApp } from '@/scripts/app'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||||
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
import { defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { UserFile } from '@/stores/userFileStore'
|
import { UserFile } from '@/stores/userFileStore'
|
||||||
|
import { NodeSourceType } from '@/types/nodeSource'
|
||||||
|
import { reduceAllNodes } from '@/utils/graphTraversalUtil'
|
||||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||||
import {
|
import {
|
||||||
createNodeExecutionId,
|
createNodeExecutionId,
|
||||||
@@ -708,6 +713,111 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
|||||||
return createNodeExecutionId([...path, localNodeId])
|
return createNodeExecutionId([...path, localNodeId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register telemetry hooks
|
||||||
|
const telemetryService = useTelemetry()
|
||||||
|
telemetryService?.registerHooks({
|
||||||
|
getActiveWorkflow: () =>
|
||||||
|
activeWorkflow.value
|
||||||
|
? {
|
||||||
|
filename: activeWorkflow.value.filename,
|
||||||
|
isTemplate: false, // This will be enhanced when we add template detection
|
||||||
|
nodeCount: activeWorkflow.value.activeState?.nodes?.length
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
|
||||||
|
getExecutionContext: () => {
|
||||||
|
const templatesStore = useWorkflowTemplatesStore()
|
||||||
|
const nodeDefStore = useNodeDefStore()
|
||||||
|
|
||||||
|
// Calculate node metrics in a single traversal
|
||||||
|
type NodeMetrics = {
|
||||||
|
custom_node_count: number
|
||||||
|
api_node_count: number
|
||||||
|
subgraph_count: number
|
||||||
|
total_node_count: number
|
||||||
|
has_api_nodes: boolean
|
||||||
|
api_node_names: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeCounts = reduceAllNodes<NodeMetrics>(
|
||||||
|
comfyApp.graph,
|
||||||
|
(metrics, node) => {
|
||||||
|
const nodeDef = nodeDefStore.nodeDefsByName[node.type]
|
||||||
|
const isCustomNode =
|
||||||
|
nodeDef?.nodeSource?.type === NodeSourceType.CustomNodes
|
||||||
|
const isApiNode = nodeDef?.api_node === true
|
||||||
|
const isSubgraph = node.isSubgraphNode?.() === true
|
||||||
|
|
||||||
|
if (isApiNode) {
|
||||||
|
metrics.has_api_nodes = true
|
||||||
|
const canonicalName = nodeDef?.name
|
||||||
|
if (
|
||||||
|
canonicalName &&
|
||||||
|
!metrics.api_node_names.includes(canonicalName)
|
||||||
|
) {
|
||||||
|
metrics.api_node_names.push(canonicalName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.custom_node_count += isCustomNode ? 1 : 0
|
||||||
|
metrics.api_node_count += isApiNode ? 1 : 0
|
||||||
|
metrics.subgraph_count += isSubgraph ? 1 : 0
|
||||||
|
metrics.total_node_count += 1
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
},
|
||||||
|
{
|
||||||
|
custom_node_count: 0,
|
||||||
|
api_node_count: 0,
|
||||||
|
subgraph_count: 0,
|
||||||
|
total_node_count: 0,
|
||||||
|
has_api_nodes: false,
|
||||||
|
api_node_names: []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (activeWorkflow.value?.filename) {
|
||||||
|
const isTemplate = templatesStore.knownTemplateNames.has(
|
||||||
|
activeWorkflow.value.filename
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isTemplate) {
|
||||||
|
const template = templatesStore.getTemplateByName(
|
||||||
|
activeWorkflow.value.filename
|
||||||
|
)
|
||||||
|
|
||||||
|
const englishMetadata = templatesStore.getEnglishMetadata(
|
||||||
|
activeWorkflow.value.filename
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_template: true,
|
||||||
|
workflow_name: activeWorkflow.value.filename,
|
||||||
|
template_source: template?.sourceModule,
|
||||||
|
template_category: englishMetadata?.category ?? template?.category,
|
||||||
|
template_tags: englishMetadata?.tags ?? template?.tags,
|
||||||
|
template_models: englishMetadata?.models ?? template?.models,
|
||||||
|
template_use_case: englishMetadata?.useCase ?? template?.useCase,
|
||||||
|
template_license: englishMetadata?.license ?? template?.license,
|
||||||
|
...nodeCounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_template: false,
|
||||||
|
workflow_name: activeWorkflow.value.filename,
|
||||||
|
...nodeCounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_template: false,
|
||||||
|
workflow_name: undefined,
|
||||||
|
...nodeCounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeWorkflow,
|
activeWorkflow,
|
||||||
attachWorkflow,
|
attachWorkflow,
|
||||||
|
|||||||
Reference in New Issue
Block a user