mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 06:47:33 +00:00
## Summary Fixes all usages of `SurveyResponses` interface to match the updated structure. ## Problem After PR #6314 updated the `SurveyResponses` interface, several files still used the old property names causing TypeScript errors: - `team_size` (removed) - `use_case` (should be `useCase`) - `intended_use` (removed) ## Changes Updated all survey response usages: **CloudSurveyView.vue:** - Updated `trackSurvey` call to use new field names - Removed obsolete `team_size` and `intended_use` fields - Added `making` field for content type tracking **MixpanelTelemetryProvider.ts (4 locations):** - User properties from cached store - User properties from dynamic import - Event properties in `trackSurvey` - `setSurveyUserProperties` method ## Testing - [x] Type checking passes - [x] Survey data now maps 1-to-1 with actual survey fields ## Related Follow-up to PR #6314 which updated the interface definition. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6325-bugfix-update-survey-response-usage-to-match-new-interface-2996d73d36508128bb62deb545b76c7b) by [Unito](https://www.unito.io)
459 lines
14 KiB
TypeScript
459 lines
14 KiB
TypeScript
import type { OverridedMixpanel } from 'mixpanel-browser'
|
|
|
|
import type {
|
|
AuthMetadata,
|
|
ExecutionContext,
|
|
ExecutionErrorMetadata,
|
|
ExecutionSuccessMetadata,
|
|
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
|
|
|
|
// 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 = window.__CONFIG__?.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',
|
|
save_referrer: true,
|
|
loaded: () => {
|
|
this.isInitialized = true
|
|
this.flushEventQueue() // flush events that were queued while initializing
|
|
}
|
|
})
|
|
})
|
|
.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 in runtime config')
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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_use_case: survey.useCase,
|
|
survey_familiarity: survey.familiarity,
|
|
survey_making: survey.making
|
|
})
|
|
}
|
|
}
|
|
// 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_use_case: survey.useCase,
|
|
survey_familiarity: survey.familiarity,
|
|
survey_making: survey.making
|
|
})
|
|
}
|
|
} 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
|
|
): 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)
|
|
}
|
|
}
|
|
|
|
trackSignupOpened(): void {
|
|
this.trackEvent(TelemetryEvents.USER_SIGN_UP_OPENED)
|
|
}
|
|
|
|
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 {
|
|
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 = {
|
|
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
|
|
|
|
// Include survey responses as event properties for submitted events
|
|
const eventProperties =
|
|
stage === 'submitted' && responses
|
|
? {
|
|
industry: responses.industry,
|
|
useCase: responses.useCase,
|
|
familiarity: responses.familiarity,
|
|
making: responses.making
|
|
}
|
|
: 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_use_case: responses.useCase,
|
|
survey_familiarity: responses.familiarity,
|
|
survey_making: responses.making
|
|
})
|
|
} catch (error) {
|
|
console.error('Failed to set survey user properties:', error)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if (this.isOnboardingMode) {
|
|
// During onboarding, track basic execution without workflow context
|
|
this.trackEvent(TelemetryEvents.EXECUTION_START, {
|
|
is_template: false,
|
|
workflow_name: undefined
|
|
})
|
|
return
|
|
}
|
|
|
|
const context = this.getExecutionContext()
|
|
this.trackEvent(TelemetryEvents.EXECUTION_START, context)
|
|
}
|
|
|
|
trackExecutionError(metadata: ExecutionErrorMetadata): void {
|
|
this.trackEvent(TelemetryEvents.EXECUTION_ERROR, metadata)
|
|
}
|
|
|
|
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void {
|
|
this.trackEvent(TelemetryEvents.EXECUTION_SUCCESS, metadata)
|
|
}
|
|
|
|
getExecutionContext(): ExecutionContext {
|
|
// Try to initialize composables if not ready and not in onboarding mode
|
|
if (!this._composablesReady && !this.isOnboardingMode) {
|
|
void this.initializeComposables()
|
|
}
|
|
|
|
if (
|
|
!this._composablesReady ||
|
|
!this._workflowStore ||
|
|
!this._templatesStore
|
|
) {
|
|
return {
|
|
is_template: false,
|
|
workflow_name: undefined
|
|
}
|
|
}
|
|
|
|
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
|
|
)
|
|
|
|
const englishMetadata = this._templatesStore.getEnglishMetadata(
|
|
activeWorkflow.filename
|
|
)
|
|
|
|
return {
|
|
is_template: true,
|
|
workflow_name: activeWorkflow.filename,
|
|
template_source: template?.sourceModule,
|
|
template_category: englishMetadata?.category ?? template?.category,
|
|
template_tags: englishMetadata?.tags ?? template?.tags,
|
|
template_models: englishMetadata?.models ?? template?.models,
|
|
template_use_case: englishMetadata?.useCase ?? template?.useCase,
|
|
template_license: englishMetadata?.license ?? template?.license
|
|
}
|
|
}
|
|
|
|
return {
|
|
is_template: false,
|
|
workflow_name: activeWorkflow.filename
|
|
}
|
|
}
|
|
|
|
return {
|
|
is_template: false,
|
|
workflow_name: undefined
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to get execution context:', error)
|
|
return {
|
|
is_template: false,
|
|
workflow_name: undefined
|
|
}
|
|
}
|
|
}
|
|
}
|