[backport rh-test] add telemetry provider for cloud distribution (#6155)

## Summary
This PR manually backports the telemetry provider implementation to the
rh-test branch after the automated backport failed due to merge
conflicts.


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6155-Manual-backport-add-telemetry-provider-for-cloud-distribution-2926d73d3650812e94e2fe0bd5e9bc59)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-10-19 20:04:57 -07:00
committed by GitHub
parent b210e63f3c
commit b708ebf540
20 changed files with 751 additions and 96 deletions

View File

@@ -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
}
}
}