mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
add telemetry provider for cloud distribution (#6154)
## Summary This code is entirely excluded from open-source, local, and desktop builds. During minification and dead-code elimination, the Mixpanel library is fully tree-shaken -- meaning no telemetry code is ever included or downloaded in those builds. Even the inline callsites are removed during the build (because `isCloud` becomes false and the entire block becomes dead code and is removed). The code not only has no effect, is not even distributed in the first place. We’ve gone to great lengths to ensure this behavior. Verification proof: https://github.com/user-attachments/assets/b66c35f7-e233-447f-93da-4d70c433908d Telemetry is *enabled only in the ComfyUI Cloud environment*. Its goal is to help us understand and improve onboarding and new-user adoption. ComfyUI aims to be accessible to everyone, but we know the learning curve can be steep. Anonymous usage insights will help us identify where users struggle and guide us toward making the experience more intuitive and welcoming. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6154-add-telemetry-provider-for-cloud-distribution-2926d73d3650813cb9ccfb3a2733848b) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import type { OverridedMixpanel } from 'mixpanel-browser'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
|
||||
import type {
|
||||
AuthMetadata,
|
||||
ExecutionContext,
|
||||
RunButtonProperties,
|
||||
SurveyResponses,
|
||||
TelemetryEventName,
|
||||
TelemetryEventProperties,
|
||||
TelemetryProvider,
|
||||
TemplateMetadata
|
||||
} from '../../types'
|
||||
import { TelemetryEvents } from '../../types'
|
||||
|
||||
interface QueuedEvent {
|
||||
eventName: TelemetryEventName
|
||||
properties?: TelemetryEventProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixpanel Telemetry Provider - Cloud Build Implementation
|
||||
*
|
||||
* CRITICAL: OSS Build Safety
|
||||
* This provider integrates with Mixpanel for cloud telemetry tracking.
|
||||
* Entire file is tree-shaken away in OSS builds (DISTRIBUTION unset).
|
||||
*
|
||||
* To verify OSS builds exclude this code:
|
||||
* 1. `DISTRIBUTION= pnpm build` (OSS build)
|
||||
* 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
|
||||
private mixpanel: OverridedMixpanel | null = null
|
||||
private eventQueue: QueuedEvent[] = []
|
||||
private isInitialized = false
|
||||
|
||||
constructor() {
|
||||
const token = __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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.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')
|
||||
this.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private flushEventQueue(): void {
|
||||
if (!this.isInitialized || !this.mixpanel) {
|
||||
return
|
||||
}
|
||||
|
||||
while (this.eventQueue.length > 0) {
|
||||
const event = this.eventQueue.shift()!
|
||||
try {
|
||||
this.mixpanel.track(event.eventName, event.properties || {})
|
||||
} catch (error) {
|
||||
console.error('Failed to track queued event:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private trackEvent(
|
||||
eventName: TelemetryEventName,
|
||||
properties?: TelemetryEventProperties
|
||||
): void {
|
||||
if (!this.isEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const event: QueuedEvent = { eventName, properties }
|
||||
|
||||
if (this.isInitialized && this.mixpanel) {
|
||||
// Mixpanel is ready, track immediately
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
trackAuth(metadata: AuthMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
|
||||
}
|
||||
|
||||
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void {
|
||||
const eventName =
|
||||
event === 'modal_opened'
|
||||
? TelemetryEvents.SUBSCRIPTION_REQUIRED_MODAL_OPENED
|
||||
: TelemetryEvents.SUBSCRIBE_NOW_BUTTON_CLICKED
|
||||
|
||||
this.trackEvent(eventName)
|
||||
}
|
||||
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): 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'
|
||||
}
|
||||
|
||||
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties)
|
||||
}
|
||||
|
||||
trackSurvey(
|
||||
stage: 'opened' | 'submitted',
|
||||
responses?: SurveyResponses
|
||||
): void {
|
||||
const eventName =
|
||||
stage === 'opened'
|
||||
? TelemetryEvents.USER_SURVEY_OPENED
|
||||
: TelemetryEvents.USER_SURVEY_SUBMITTED
|
||||
|
||||
this.trackEvent(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
|
||||
}
|
||||
|
||||
this.trackEvent(eventName)
|
||||
}
|
||||
|
||||
trackTemplate(metadata: TemplateMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.TEMPLATE_WORKFLOW_OPENED, metadata)
|
||||
}
|
||||
|
||||
trackWorkflowExecution(): void {
|
||||
const context = this.getExecutionContext()
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_EXECUTION_STARTED, context)
|
||||
}
|
||||
|
||||
getExecutionContext(): ExecutionContext {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const templatesStore = useWorkflowTemplatesStore()
|
||||
const activeWorkflow = workflowStore.activeWorkflow
|
||||
|
||||
if (activeWorkflow?.filename) {
|
||||
const isTemplate = templatesStore.knownTemplateNames.has(
|
||||
activeWorkflow.filename
|
||||
)
|
||||
|
||||
if (isTemplate) {
|
||||
const template = templatesStore.getTemplateByName(
|
||||
activeWorkflow.filename
|
||||
)
|
||||
return {
|
||||
is_template: true,
|
||||
workflow_name: activeWorkflow.filename,
|
||||
template_source: template?.sourceModule,
|
||||
template_category: template?.category,
|
||||
template_tags: template?.tags,
|
||||
template_models: template?.models,
|
||||
template_use_case: template?.useCase,
|
||||
template_license: template?.license
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: activeWorkflow.filename
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user