feat(telemetry): help center and workflow creation (#6505)

## Summary

For Cloud distribution:
1. Track help center usage
2. Track workflow creation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6505-feat-telemetry-help-center-and-workflow-creation-29e6d73d36508185af8ccbf19d5af9e7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Christian Byrne <chrbyrne96@gmail.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Arjan Singh
2025-11-01 12:21:50 -07:00
committed by GitHub
parent dad2d803df
commit de535269ee
9 changed files with 190 additions and 18 deletions

View File

@@ -158,9 +158,7 @@ const queuePrompt = async (e: Event) => {
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
if (isCloud) {
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
}
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
await commandStore.execute(commandId)
}

View File

@@ -61,6 +61,7 @@ import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useTelemetry } from '@/platform/telemetry'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
@@ -92,12 +93,18 @@ const showReport = () => {
const toast = useToast()
const { t } = useI18n()
const systemStatsStore = useSystemStatsStore()
const telemetry = useTelemetry()
const title = computed<string>(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
const showContactSupport = async () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'error_dialog'
})
await useCommandStore().execute('Comfy.ContactSupport')
}

View File

@@ -124,6 +124,7 @@ import UserCredit from '@/components/common/UserCredit.vue'
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -140,6 +141,7 @@ const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const { isActiveSubscription } = useSubscription()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
@@ -170,6 +172,11 @@ const handleCreditsHistoryClick = async () => {
}
const handleMessageSupport = async () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'credits_panel'
})
await commandStore.execute('Comfy.ContactSupport')
}

View File

@@ -138,13 +138,14 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { computed, nextTick, onMounted, ref } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import type { CSSProperties, Component } from 'vue'
import { useI18n } from 'vue-i18n'
import PuzzleIcon from '@/components/icons/PuzzleIcon.vue'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { useCommandStore } from '@/stores/commandStore'
@@ -196,6 +197,7 @@ const { t, locale } = useI18n()
const releaseStore = useReleaseStore()
const commandStore = useCommandStore()
const settingStore = useSettingStore()
const telemetry = useTelemetry()
// Emits
const emit = defineEmits<{
@@ -207,6 +209,7 @@ const isSubmenuVisible = ref(false)
const submenuRef = ref<HTMLElement | null>(null)
const submenuStyle = ref<CSSProperties>({})
let hoverTimeout: number | null = null
const openedAt = ref<number>(Date.now())
// Computed
const hasReleases = computed(() => releaseStore.releases.length > 0)
@@ -226,6 +229,7 @@ const moreItems = computed<MenuItem[]>(() => {
label: t('helpCenter.desktopUserGuide'),
visible: isElectron(),
action: () => {
trackResourceClick('docs', true)
const docsUrl =
electronAPI().getPlatform() === 'darwin'
? EXTERNAL_LINKS.DESKTOP_GUIDE_MACOS
@@ -281,6 +285,7 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-book',
label: t('helpCenter.docs'),
action: () => {
trackResourceClick('docs', true)
openExternalLink(EXTERNAL_LINKS.DOCS)
emit('close')
}
@@ -291,6 +296,7 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-discord',
label: 'Discord',
action: () => {
trackResourceClick('discord', true)
openExternalLink(EXTERNAL_LINKS.DISCORD)
emit('close')
}
@@ -301,6 +307,7 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-github',
label: t('helpCenter.github'),
action: () => {
trackResourceClick('github', true)
openExternalLink(EXTERNAL_LINKS.GITHUB)
emit('close')
}
@@ -311,6 +318,7 @@ const menuItems = computed<MenuItem[]>(() => {
icon: 'pi pi-question-circle',
label: t('helpCenter.helpFeedback'),
action: () => {
trackResourceClick('help_feedback', false)
void commandStore.execute('Comfy.ContactSupport')
emit('close')
}
@@ -326,6 +334,7 @@ const menuItems = computed<MenuItem[]>(() => {
label: t('helpCenter.managerExtension'),
showRedDot: shouldShowManagerRedDot.value,
action: async () => {
trackResourceClick('manager', false)
await useManagerState().openManager({
initialTab: ManagerTab.All,
showToastOnLegacyError: false
@@ -349,6 +358,23 @@ const menuItems = computed<MenuItem[]>(() => {
})
// Utility Functions
const trackResourceClick = (
resourceType:
| 'docs'
| 'discord'
| 'github'
| 'help_feedback'
| 'manager'
| 'release_notes',
isExternal: boolean
): void => {
telemetry?.trackHelpResourceClicked({
resource_type: resourceType,
is_external: isExternal,
source: 'help_center'
})
}
const openExternalLink = (url: string): void => {
window.open(url, '_blank', 'noopener,noreferrer')
}
@@ -504,6 +530,7 @@ const onReinstall = (): void => {
}
const onReleaseClick = (release: ReleaseNote): void => {
trackResourceClick('release_notes', true)
void releaseStore.handleShowChangelog(release.version)
const versionAnchor = formatVersionAnchor(release.version)
const changelogUrl = `${getChangelogUrl()}#${versionAnchor}`
@@ -512,6 +539,7 @@ const onReleaseClick = (release: ReleaseNote): void => {
}
const onUpdate = (_: ReleaseNote): void => {
trackResourceClick('docs', true)
openExternalLink(EXTERNAL_LINKS.UPDATE_GUIDE)
emit('close')
}
@@ -526,10 +554,16 @@ const getChangelogUrl = (): string => {
// Lifecycle
onMounted(async () => {
telemetry?.trackHelpCenterOpened({ source: 'sidebar' })
if (!hasReleases.value) {
await releaseStore.fetchReleases()
}
})
onBeforeUnmount(() => {
const timeSpentSeconds = Math.round((Date.now() - openedAt.value) / 1000)
telemetry?.trackHelpCenterClosed({ time_spent_seconds: timeSpentSeconds })
})
</script>
<style scoped>

View File

@@ -19,7 +19,6 @@ import type { Point } from '@/lib/litegraph/src/litegraph'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { SUPPORT_URL } from '@/platform/support/config'
import { useTelemetry } from '@/platform/telemetry'
@@ -74,6 +73,7 @@ export function useCoreCommands(): ComfyCommand[] {
const toastStore = useToastStore()
const canvasStore = useCanvasStore()
const executionStore = useExecutionStore()
const telemetry = useTelemetry()
const bottomPanelStore = useBottomPanelStore()
@@ -102,7 +102,14 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'New Blank Workflow',
menubarLabel: 'New',
category: 'essentials' as const,
function: () => workflowService.loadBlankWorkflow()
function: async () => {
const previousWorkflowHadNodes = app.graph._nodes.length > 0
await workflowService.loadBlankWorkflow()
telemetry?.trackWorkflowCreated({
workflow_type: 'blank',
previous_workflow_had_nodes: previousWorkflowHadNodes
})
}
},
{
id: 'Comfy.OpenWorkflow',
@@ -118,7 +125,14 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.LoadDefaultWorkflow',
icon: 'pi pi-code',
label: 'Load Default Workflow',
function: () => workflowService.loadDefaultWorkflow()
function: async () => {
const previousWorkflowHadNodes = app.graph._nodes.length > 0
await workflowService.loadDefaultWorkflow()
telemetry?.trackWorkflowCreated({
workflow_type: 'default',
previous_workflow_had_nodes: previousWorkflowHadNodes
})
}
},
{
id: 'Comfy.SaveWorkflow',
@@ -460,9 +474,7 @@ export function useCoreCommands(): ComfyCommand[] {
const batchCount = useQueueSettingsStore().batchCount
if (isCloud) {
useTelemetry()?.trackWorkflowExecution()
}
useTelemetry()?.trackWorkflowExecution()
await app.queuePrompt(0, batchCount)
}
@@ -481,9 +493,7 @@ export function useCoreCommands(): ComfyCommand[] {
const batchCount = useQueueSettingsStore().batchCount
if (isCloud) {
useTelemetry()?.trackWorkflowExecution()
}
useTelemetry()?.trackWorkflowExecution()
await app.queuePrompt(-1, batchCount)
}
@@ -721,6 +731,11 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'ComfyUI Issues',
versionAdded: '1.5.5',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'github',
is_external: true,
source: 'menu'
})
window.open(
'https://github.com/comfyanonymous/ComfyUI/issues',
'_blank'
@@ -734,6 +749,11 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'ComfyUI Docs',
versionAdded: '1.5.5',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'docs',
is_external: true,
source: 'menu'
})
window.open('https://docs.comfy.org/', '_blank')
}
},
@@ -744,6 +764,11 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'Comfy-Org Discord',
versionAdded: '1.5.5',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'discord',
is_external: true,
source: 'menu'
})
window.open('https://www.comfy.org/discord', '_blank')
}
},
@@ -801,6 +826,11 @@ export function useCoreCommands(): ComfyCommand[] {
menubarLabel: 'ComfyUI Forum',
versionAdded: '1.8.2',
function: () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'menu'
})
window.open('https://forum.comfy.org/', '_blank')
}
},

View File

@@ -3,6 +3,8 @@ import { useI18n } from 'vue-i18n'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
@@ -14,6 +16,7 @@ export function useSubscriptionActions() {
const dialogService = useDialogService()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const { fetchStatus, formattedRenewalDate } = useSubscription()
const isLoadingSupport = ref(false)
@@ -35,6 +38,13 @@ export function useSubscriptionActions() {
const handleMessageSupport = async () => {
try {
isLoadingSupport.value = true
if (isCloud) {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'subscription'
})
}
await commandStore.execute('Comfy.ContactSupport')
} catch (error) {
console.error('[useSubscriptionActions] Error contacting support:', error)

View File

@@ -15,6 +15,9 @@ import type {
ExecutionContext,
ExecutionErrorMetadata,
ExecutionSuccessMetadata,
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
NodeSearchMetadata,
NodeSearchResultMetadata,
PageVisibilityMetadata,
@@ -28,6 +31,7 @@ import type {
TemplateLibraryMetadata,
TemplateLibraryClosedMetadata,
TemplateMetadata,
WorkflowCreatedMetadata,
WorkflowImportMetadata
} from '../../types'
import { TelemetryEvents } from '../../types'
@@ -277,6 +281,22 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.TEMPLATE_FILTER_CHANGED, metadata)
}
trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void {
this.trackEvent(TelemetryEvents.HELP_CENTER_OPENED, metadata)
}
trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void {
this.trackEvent(TelemetryEvents.HELP_RESOURCE_CLICKED, metadata)
}
trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void {
this.trackEvent(TelemetryEvents.HELP_CENTER_CLOSED, metadata)
}
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void {
this.trackEvent(TelemetryEvents.WORKFLOW_CREATED, metadata)
}
trackWorkflowExecution(): void {
const context = this.getExecutionContext()
this.trackEvent(TelemetryEvents.EXECUTION_START, context)

View File

@@ -192,6 +192,48 @@ export interface TemplateFilterMetadata {
total_count: number
}
/**
* Help center opened metadata
*/
export interface HelpCenterOpenedMetadata {
source: 'menu' | 'topbar' | 'sidebar'
}
/**
* Help resource clicked metadata
*/
export interface HelpResourceClickedMetadata {
resource_type:
| 'docs'
| 'discord'
| 'github'
| 'help_feedback'
| 'manager'
| 'release_notes'
is_external: boolean
source:
| 'menu'
| 'help_center'
| 'error_dialog'
| 'credits_panel'
| 'subscription'
}
/**
* Help center closed metadata
*/
export interface HelpCenterClosedMetadata {
time_spent_seconds: number
}
/**
* Workflow created metadata
*/
export interface WorkflowCreatedMetadata {
workflow_type: 'blank' | 'default'
previous_workflow_had_nodes: boolean
}
/**
* Core telemetry provider interface
*/
@@ -237,6 +279,14 @@ export interface TelemetryProvider {
// Template filter tracking events
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void
// Help center events
trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void
trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void
trackHelpCenterClosed(metadata: HelpCenterClosedMetadata): void
// Workflow creation events
trackWorkflowCreated(metadata: WorkflowCreatedMetadata): void
// Workflow execution events
trackWorkflowExecution(): void
trackExecutionError(metadata: ExecutionErrorMetadata): void
@@ -297,6 +347,14 @@ export const TelemetryEvents = {
// Template Filter Analytics
TEMPLATE_FILTER_CHANGED: 'app:template_filter_changed',
// Help Center Analytics
HELP_CENTER_OPENED: 'app:help_center_opened',
HELP_RESOURCE_CLICKED: 'app:help_resource_clicked',
HELP_CENTER_CLOSED: 'app:help_center_closed',
// Workflow Creation
WORKFLOW_CREATED: 'app:workflow_created',
// Execution Lifecycle
EXECUTION_START: 'execution_start',
EXECUTION_ERROR: 'execution_error',
@@ -326,3 +384,7 @@ export type TelemetryEventProperties =
| NodeSearchMetadata
| NodeSearchResultMetadata
| TemplateFilterMetadata
| HelpCenterOpenedMetadata
| HelpResourceClickedMetadata
| HelpCenterClosedMetadata
| WorkflowCreatedMetadata

View File

@@ -12,11 +12,15 @@ const mockT = vi.fn((key: string) => {
return key
})
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: mockT
})
}))
vi.mock('vue-i18n', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-i18n')>()
return {
...actual,
useI18n: () => ({
t: mockT
})
}
})
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
useFirebaseAuthActions: () => ({