mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 16:24:06 +00:00
track cloud-specific onboarding events and add performance optimizations for hosted cloud app (#6158)
## Summary - Complete telemetry implementation with circular dependency fix - Add build performance optimizations from main branch ### Telemetry Features - ✅ Final telemetry events: signup opened, survey flow, email verification - ✅ Onboarding mode to prevent circular dependencies during app initialization - ✅ Lazy composable loading with dynamic imports for workflow tracking - ✅ Survey responses as both event properties and persistent user properties - ✅ User identification method for onboarding flow - ✅ Deferred user property setting until user is authenticated ### Performance Optimizations - ✅ Tree-shaking enabled to remove unused code - ✅ Manual chunk splitting for vendor libraries (primevue, vue, tiptap, chart.js, etc.) - ✅ Enhanced esbuild minification with console removal in production builds - ✅ GENERATE_SOURCEMAP environment variable control - ✅ Maintained ImportMap disabled for cloud performance ## Test plan - [x] Telemetry events track correctly in Mixpanel - [x] No circular dependency errors on app startup - [x] Survey responses appear as both event properties and user properties - [x] Build optimizations reduce bundle size and improve loading performance - [x] All lint/format/typecheck passes ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6158-track-cloud-specific-onboarding-events-and-add-performance-optimizations-for-hosted-cloud-2926d73d365081a7b533dde249d5f734) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,5 @@
|
||||
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,
|
||||
@@ -39,6 +35,16 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
private eventQueue: QueuedEvent[] = []
|
||||
private isInitialized = false
|
||||
|
||||
// Onboarding mode - starts true, set to false when app is fully ready
|
||||
private isOnboardingMode = true
|
||||
|
||||
// Lazy-loaded composables - only imported once when app is ready
|
||||
private _workflowStore: any = null
|
||||
private _templatesStore: any = null
|
||||
private _currentUser: any = null
|
||||
private _settingStore: any = null
|
||||
private _composablesReady = false
|
||||
|
||||
constructor() {
|
||||
const token = __MIXPANEL_TOKEN__
|
||||
|
||||
@@ -54,14 +60,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
api_host: 'https://mp.comfy.org',
|
||||
cross_subdomain_cookie: true,
|
||||
persistence: 'cookie',
|
||||
save_referrer: true,
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -94,6 +96,154 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify the current user for telemetry tracking.
|
||||
* Can be called during onboarding without circular dependencies.
|
||||
*/
|
||||
identifyUser(userId: string): void {
|
||||
if (!this.mixpanel) return
|
||||
|
||||
try {
|
||||
this.mixpanel.identify(userId)
|
||||
|
||||
// If we have pending survey responses, set them now that user is identified
|
||||
if (this.pendingSurveyResponses) {
|
||||
this.setSurveyUserProperties(this.pendingSurveyResponses)
|
||||
this.pendingSurveyResponses = null
|
||||
}
|
||||
|
||||
// Load existing survey data if available (only when app is ready)
|
||||
if (!this.isOnboardingMode) {
|
||||
this.initializeExistingSurveyData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to identify user:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that the main app is fully initialized and advanced telemetry features can be used.
|
||||
* Call this after the app bootstrap is complete.
|
||||
*/
|
||||
markAppReady(): void {
|
||||
this.isOnboardingMode = false
|
||||
// Trigger composable initialization now that it's safe
|
||||
void this.initializeComposables()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy initialization of Vue composables to avoid circular dependencies during module loading.
|
||||
* Only imports and initializes composables once when app is ready.
|
||||
*/
|
||||
private async initializeComposables(): Promise<boolean> {
|
||||
if (this._composablesReady || this.isOnboardingMode) {
|
||||
return this._composablesReady
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic imports to avoid circular dependencies during module loading
|
||||
const [
|
||||
{ useWorkflowStore },
|
||||
{ useWorkflowTemplatesStore },
|
||||
{ useCurrentUser },
|
||||
{ useSettingStore }
|
||||
] = await Promise.all([
|
||||
import('@/platform/workflow/management/stores/workflowStore'),
|
||||
import(
|
||||
'@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
),
|
||||
import('@/composables/auth/useCurrentUser'),
|
||||
import('@/platform/settings/settingStore')
|
||||
])
|
||||
|
||||
// Initialize composables once
|
||||
this._workflowStore = useWorkflowStore()
|
||||
this._templatesStore = useWorkflowTemplatesStore()
|
||||
this._currentUser = useCurrentUser()
|
||||
this._settingStore = useSettingStore()
|
||||
|
||||
this._composablesReady = true
|
||||
|
||||
// Now that composables are ready, set up user tracking
|
||||
if (this.mixpanel) {
|
||||
this._currentUser.onUserResolved((user: any) => {
|
||||
if (this.mixpanel && user.id) {
|
||||
this.mixpanel.identify(user.id)
|
||||
this.initializeExistingSurveyData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize composables:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private initializeExistingSurveyData(): void {
|
||||
if (!this.mixpanel) return
|
||||
|
||||
try {
|
||||
// If composables are ready, use cached store
|
||||
if (this._settingStore) {
|
||||
const surveyData = this._settingStore.get('onboarding_survey')
|
||||
|
||||
if (surveyData && typeof surveyData === 'object') {
|
||||
const survey = surveyData as any
|
||||
this.mixpanel.people.set({
|
||||
survey_industry: survey.industry,
|
||||
survey_team_size: survey.team_size,
|
||||
survey_use_case: survey.useCase,
|
||||
survey_familiarity: survey.familiarity,
|
||||
survey_intended_use:
|
||||
survey.useCase === 'personal'
|
||||
? 'personal'
|
||||
: survey.useCase === 'client'
|
||||
? 'client'
|
||||
: 'inhouse'
|
||||
})
|
||||
}
|
||||
}
|
||||
// If in onboarding mode, try dynamic import (safe since user is identified)
|
||||
else if (this.isOnboardingMode) {
|
||||
import('@/platform/settings/settingStore')
|
||||
.then(({ useSettingStore }) => {
|
||||
try {
|
||||
const settingStore = useSettingStore()
|
||||
const surveyData = settingStore.get('onboarding_survey')
|
||||
|
||||
if (surveyData && typeof surveyData === 'object') {
|
||||
const survey = surveyData as any
|
||||
this.mixpanel?.people.set({
|
||||
survey_industry: survey.industry,
|
||||
survey_team_size: survey.team_size,
|
||||
survey_use_case: survey.useCase,
|
||||
survey_familiarity: survey.familiarity,
|
||||
survey_intended_use:
|
||||
survey.useCase === 'personal'
|
||||
? 'personal'
|
||||
: survey.useCase === 'client'
|
||||
? 'client'
|
||||
: 'inhouse'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to load existing survey data during onboarding:',
|
||||
error
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to import settings store:', error)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize existing survey data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private trackEvent(
|
||||
eventName: TelemetryEventName,
|
||||
properties?: TelemetryEventProperties
|
||||
@@ -117,6 +267,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
trackSignupOpened(): void {
|
||||
this.trackEvent(TelemetryEvents.USER_SIGN_UP_OPENED)
|
||||
}
|
||||
|
||||
trackAuth(metadata: AuthMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.USER_AUTH_COMPLETED, metadata)
|
||||
}
|
||||
@@ -131,6 +285,16 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): void {
|
||||
if (this.isOnboardingMode) {
|
||||
// During onboarding, track basic run button click without workflow context
|
||||
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, {
|
||||
subscribe_to_run: options?.subscribe_to_run || false,
|
||||
workflow_type: 'custom',
|
||||
workflow_name: 'untitled'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const executionContext = this.getExecutionContext()
|
||||
|
||||
const runButtonProperties: RunButtonProperties = {
|
||||
@@ -151,7 +315,48 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
? TelemetryEvents.USER_SURVEY_OPENED
|
||||
: TelemetryEvents.USER_SURVEY_SUBMITTED
|
||||
|
||||
this.trackEvent(eventName, responses)
|
||||
// Include survey responses as event properties for submitted events
|
||||
const eventProperties =
|
||||
stage === 'submitted' && responses
|
||||
? {
|
||||
industry: responses.industry,
|
||||
team_size: responses.team_size,
|
||||
use_case: responses.use_case,
|
||||
familiarity: responses.familiarity,
|
||||
intended_use: responses.intended_use
|
||||
}
|
||||
: undefined
|
||||
|
||||
this.trackEvent(eventName, eventProperties)
|
||||
|
||||
// Also set survey responses as persistent user properties
|
||||
if (stage === 'submitted' && responses && this.mixpanel) {
|
||||
// During onboarding, we need to defer user property setting until user is identified
|
||||
if (this.isOnboardingMode) {
|
||||
// Store responses to be set once user is identified
|
||||
this.pendingSurveyResponses = responses
|
||||
} else {
|
||||
this.setSurveyUserProperties(responses)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private pendingSurveyResponses: SurveyResponses | null = null
|
||||
|
||||
private setSurveyUserProperties(responses: SurveyResponses): void {
|
||||
if (!this.mixpanel) return
|
||||
|
||||
try {
|
||||
this.mixpanel.people.set({
|
||||
survey_industry: responses.industry,
|
||||
survey_team_size: responses.team_size,
|
||||
survey_use_case: responses.use_case,
|
||||
survey_familiarity: responses.familiarity,
|
||||
survey_intended_use: responses.intended_use
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to set survey user properties:', error)
|
||||
}
|
||||
}
|
||||
|
||||
trackEmailVerification(stage: 'opened' | 'requested' | 'completed'): void {
|
||||
@@ -177,45 +382,76 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
}
|
||||
|
||||
trackWorkflowExecution(): void {
|
||||
if (this.isOnboardingMode) {
|
||||
// During onboarding, track basic execution without workflow context
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_EXECUTION_STARTED, {
|
||||
is_template: false,
|
||||
workflow_name: undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const context = this.getExecutionContext()
|
||||
this.trackEvent(TelemetryEvents.WORKFLOW_EXECUTION_STARTED, context)
|
||||
}
|
||||
|
||||
getExecutionContext(): ExecutionContext {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const templatesStore = useWorkflowTemplatesStore()
|
||||
const activeWorkflow = workflowStore.activeWorkflow
|
||||
// Try to initialize composables if not ready and not in onboarding mode
|
||||
if (!this._composablesReady && !this.isOnboardingMode) {
|
||||
void this.initializeComposables()
|
||||
}
|
||||
|
||||
if (activeWorkflow?.filename) {
|
||||
const isTemplate = templatesStore.knownTemplateNames.has(
|
||||
activeWorkflow.filename
|
||||
)
|
||||
if (
|
||||
!this._composablesReady ||
|
||||
!this._workflowStore ||
|
||||
!this._templatesStore
|
||||
) {
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (isTemplate) {
|
||||
const template = templatesStore.getTemplateByName(
|
||||
try {
|
||||
const activeWorkflow = this._workflowStore.activeWorkflow
|
||||
|
||||
if (activeWorkflow?.filename) {
|
||||
const isTemplate = this._templatesStore.knownTemplateNames.has(
|
||||
activeWorkflow.filename
|
||||
)
|
||||
|
||||
if (isTemplate) {
|
||||
const template = this._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: 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
|
||||
is_template: false,
|
||||
workflow_name: activeWorkflow.filename
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: activeWorkflow.filename
|
||||
workflow_name: undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get execution context:', error)
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: undefined
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
is_template: false,
|
||||
workflow_name: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface TemplateMetadata {
|
||||
*/
|
||||
export interface TelemetryProvider {
|
||||
// Authentication flow events
|
||||
trackSignupOpened(): void
|
||||
trackAuth(metadata: AuthMetadata): void
|
||||
|
||||
// Subscription flow events
|
||||
@@ -94,6 +95,10 @@ export interface TelemetryProvider {
|
||||
|
||||
// Workflow execution events
|
||||
trackWorkflowExecution(): void
|
||||
|
||||
// App lifecycle management
|
||||
markAppReady?(): void
|
||||
identifyUser?(userId: string): void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,6 +106,7 @@ export interface TelemetryProvider {
|
||||
*/
|
||||
export const TelemetryEvents = {
|
||||
// Authentication Flow
|
||||
USER_SIGN_UP_OPENED: 'user_sign_up_opened',
|
||||
USER_AUTH_COMPLETED: 'user_auth_completed',
|
||||
|
||||
// Subscription Flow
|
||||
|
||||
Reference in New Issue
Block a user