diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 3bed81b38..536813f3b 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -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.", diff --git a/src/platform/onboarding/cloud/CloudSignupView.vue b/src/platform/onboarding/cloud/CloudSignupView.vue index fb49dfcc4..a18e53b11 100644 --- a/src/platform/onboarding/cloud/CloudSignupView.vue +++ b/src/platform/onboarding/cloud/CloudSignupView.vue @@ -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() }) diff --git a/src/platform/onboarding/cloud/CloudSurveyView.vue b/src/platform/onboarding/cloud/CloudSurveyView.vue index 19122b6cd..7b85f121d 100644 --- a/src/platform/onboarding/cloud/CloudSurveyView.vue +++ b/src/platform/onboarding/cloud/CloudSurveyView.vue @@ -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 diff --git a/src/platform/onboarding/cloud/CloudVerifyEmailView.vue b/src/platform/onboarding/cloud/CloudVerifyEmailView.vue index d2a137c46..b6854f05e 100644 --- a/src/platform/onboarding/cloud/CloudVerifyEmailView.vue +++ b/src/platform/onboarding/cloud/CloudVerifyEmailView.vue @@ -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() } } diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index 05866cfd0..4fd0dcbe0 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -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 { + 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 } } } diff --git a/src/platform/telemetry/types.ts b/src/platform/telemetry/types.ts index c749ca569..35f8318f4 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -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 diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 019e4a9b3..39eea3ee0 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -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 diff --git a/vite.config.mts b/vite.config.mts index 72d34a42b..33821b41c 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -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: {