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:
Christian Byrne
2025-10-19 23:16:56 -07:00
committed by GitHub
parent b708ebf540
commit 0d4d68fec9
8 changed files with 387 additions and 44 deletions

View File

@@ -2104,7 +2104,7 @@
"desc": "Zero setup required. Works on any device.",
"explain": "Generate multiple outputs at once. Share workflows with ease.",
"learnAboutButton": "Learn about Cloud",
"wantToRun": "Want to run comfyUI locally instead?",
"wantToRun": "Want to run ComfyUI locally instead?",
"download": "Download ComfyUI"
},
"checkingStatus": "Checking your account status...",
@@ -2121,7 +2121,7 @@
"cloudStart_desc": "Zero setup required. Works on any device.",
"cloudStart_explain": "Generate multiple outputs at once. Share workflows with ease.",
"cloudStart_learnAboutButton": "Learn about Cloud",
"cloudStart_wantToRun": "Want to run comfyUI locally instead?",
"cloudStart_wantToRun": "Want to run ComfyUI locally instead?",
"cloudStart_download": "Download ComfyUI",
"cloudStart_invited": "YOU'RE INVITED",
"cloudStart_invited_signin": "Sign in to continue onto Cloud.",

View File

@@ -100,6 +100,8 @@ import { useRoute, useRouter } from 'vue-router'
import SignUpForm from '@/components/dialog/content/signin/SignUpForm.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import type { SignUpData } from '@/schemas/signInSchema'
import { translateAuthError } from '@/utils/authErrorTranslation'
import { isInChina } from '@/utils/networkUtil'
@@ -155,6 +157,11 @@ const signUpWithEmail = async (values: SignUpData) => {
}
onMounted(async () => {
// Track signup screen opened
if (isCloud) {
useTelemetry()?.trackSignupOpened()
}
userIsInChina.value = await isInChina()
})
</script>

View File

@@ -222,6 +222,8 @@ import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { getSurveyCompletedStatus, submitSurvey } from '@/api/auth'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
const { t } = useI18n()
const router = useRouter()
@@ -233,6 +235,11 @@ onMounted(async () => {
if (surveyCompleted) {
// User already completed survey, redirect to waitlist
await router.replace({ name: 'cloud-waitlist' })
} else {
// Track survey opened event
if (isCloud) {
useTelemetry()?.trackSurvey('opened')
}
}
} catch (error) {
console.error('Failed to check survey status:', error)
@@ -342,7 +349,25 @@ const onSubmitSurvey = async () => {
: surveyData.value.industry,
making: surveyData.value.making
}
await submitSurvey(payload)
// Track survey submitted event with responses
if (isCloud) {
useTelemetry()?.trackSurvey('submitted', {
industry: payload.industry,
team_size: undefined, // Not collected in this survey
use_case: payload.useCase,
familiarity: payload.familiarity,
intended_use:
payload.useCase === 'personal'
? 'personal'
: payload.useCase === 'client'
? 'client'
: 'inhouse'
})
}
await router.push({ name: 'cloud-user-check' })
} finally {
isSubmitting.value = false

View File

@@ -40,6 +40,8 @@ import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { useFirebaseAuth } from 'vuefire'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -106,6 +108,12 @@ const goBack = async () => {
async function onSend() {
try {
await authStore.verifyEmail()
// Track email verification requested
if (isCloud) {
useTelemetry()?.trackEmailVerification('requested')
}
useToastStore().add({
severity: 'success',
summary: t('cloudVerifyEmail_toast_success', {
@@ -121,6 +129,11 @@ async function onSend() {
}
onMounted(async () => {
// Track email verification screen opened
if (isCloud) {
useTelemetry()?.trackEmailVerification('opened')
}
// If the user is already verified (email link already clicked),
// continue to the next step automatically.
if (authStore.isEmailVerified) {
@@ -135,6 +148,10 @@ onMounted(async () => {
if (auth.currentUser && !redirectInProgress.value) {
await auth.currentUser.reload()
if (auth.currentUser?.emailVerified) {
// Track email verification completed
if (isCloud) {
useTelemetry()?.trackEmailVerification('completed')
}
void redirectToNextStep()
}
}

View File

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

View File

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

View File

@@ -520,7 +520,9 @@ const zSettings = z.object({
'main.sub.setting.name': z.any(),
'single.setting': z.any(),
'LiteGraph.Node.DefaultPadding': z.boolean(),
'LiteGraph.Pointer.TrackpadGestures': z.boolean()
'LiteGraph.Pointer.TrackpadGestures': z.boolean(),
/** Onboarding survey data */
onboarding_survey: z.record(z.unknown()).optional()
})
export type EmbeddingsResponse = z.infer<typeof zEmbeddingsResponse>

View File

@@ -19,14 +19,15 @@ const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true'
// vite dev server will listen on all addresses, including LAN and public addresses
const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true'
const DISABLE_TEMPLATES_PROXY = process.env.DISABLE_TEMPLATES_PROXY === 'true'
const DISABLE_VUE_PLUGINS = false // Always enable Vue DevTools for development
const DISABLE_VUE_PLUGINS = process.env.DISABLE_VUE_PLUGINS === 'true'
const GENERATE_SOURCEMAP = process.env.GENERATE_SOURCEMAP !== 'false'
// CLOUD PERFORMANCE: ImportMap entries for Vue/PrimeVue temporarily disabled (see generateImportMapPlugin below)
// This reduces 600+ HTTP requests to ~8 bundled files for better cloud deployment performance
// Hardcoded to staging cloud for testing frontend changes against cloud backend
const DEV_SERVER_COMFYUI_URL =
process.env.DEV_SERVER_COMFYUI_URL || 'https://stagingcloud.comfy.org'
process.env.DEV_SERVER_COMFYUI_URL || 'https://testcloud.comfy.org'
// To use local backend, change to: 'http://127.0.0.1:8188'
// Optional: Add API key to .env as STAGING_API_KEY if needed for authentication
@@ -235,19 +236,68 @@ export default defineConfig({
build: {
minify: SHOULD_MINIFY ? 'esbuild' : false,
target: 'es2022',
sourcemap: true,
sourcemap: GENERATE_SOURCEMAP,
rollupOptions: {
// Disabling tree-shaking
// Prevent vite remove unused exports
treeshake: false
treeshake: true,
output: {
manualChunks: (id) => {
if (!id.includes('node_modules')) {
return undefined
}
if (id.includes('primevue') || id.includes('@primeuix')) {
return 'vendor-primevue'
}
if (id.includes('@tiptap')) {
return 'vendor-tiptap'
}
if (id.includes('chart.js')) {
return 'vendor-chart'
}
if (id.includes('three') || id.includes('@xterm')) {
return 'vendor-visualization'
}
if (id.includes('/vue') || id.includes('pinia')) {
return 'vendor-vue'
}
return 'vendor-other'
}
}
}
},
esbuild: {
minifyIdentifiers: false,
minifyIdentifiers: SHOULD_MINIFY,
keepNames: true,
minifySyntax: SHOULD_MINIFY,
minifyWhitespace: SHOULD_MINIFY
minifyWhitespace: SHOULD_MINIFY,
pure: SHOULD_MINIFY
? [
'console.log',
'console.debug',
'console.info',
'console.trace',
'console.dir',
'console.dirxml',
'console.group',
'console.groupCollapsed',
'console.groupEnd',
'console.table',
'console.time',
'console.timeEnd',
'console.timeLog',
'console.count',
'console.countReset',
'console.profile',
'console.profileEnd',
'console.clear'
]
: []
},
test: {