feat(telemetry): add tracking for sidebar, run menu, dialogs, subgraphs, settings and credits (#6511)

Summary
- Add comprehensive telemetry for key UI interactions using existing
telemetry hooks (cloud-enabled, no-op in OSS):

Sidebar and top-level
- Node library button: `node_library`
- Model library button: `model_library`
- Workflows button: `workflows`
- Assets/Media button: `assets`
- Templates button: `templates`
- Keyboard Shortcuts: `keyboard_shortcuts`
- Console: `console`
- Help Center: `help_center`
- Settings (from Comfy logo menu): `settings_menu`

Floating canvas menu
- Minimap toggle: `minimap_toggle`
- Hide links toggle: `hide_links`

Run button and queue
- Run button handle (drag): `run_button_handle`
- Run mode selection: `run_instant`, `run_on_change`
- Queue multiple: `queue_multiple` fires on each run when batch count >
1 (moved from batch-count-change to run-time, per guidance)

Error dialogs
- Close (X/mask/ESC): `error_dialog_close` via dialog onClose
- Show report: `error_show_report`
- Help fix this: `error_help_fix_this`
- Find issues: `error_find_issues`

Nodes / Subgraphs
- Selection toolbox “Node info”: `node_info`
- Enter subgraph (node header enter): `open_subgraph`
- Subgraph breadcrumb navigation: `subgraph_breadcrumb_item` and
`subgraph_breadcrumb_root`

Settings / Credits / Search
- Settings menu button (under Comfy logo): `settings_menu`
- Purchase credits (Settings > Credits panel): tracked via existing
`trackAddApiCreditButtonClicked`
- Purchase credits (Avatar popover Top Up): tracked via existing
`trackAddApiCreditButtonClicked`
- Debounced search telemetry already present for node search and
template filters; left as-is

Notes and answers
- Error dialog onClose: only fires when the dialog actually closes (X,
mask, ESC, or programmatic close). “Show report” and “Help fix this” do
not close the dialog; they each emit their own events.
- Telemetry is behind the cloud provider; calls are optional
(`useTelemetry()?.…`). OSS builds send nothing.

Open questions / follow-ups
- Primary Run button click: today cloud-only `trackRunButton` exists; we
can also emit a UI-level `run` click (`UI_BUTTON_CLICKED` style)
alongside it if desired. Confirm preference and I can add it.
- Subgraph usage richness: if we want structured analytics (e.g.,
action, depth, subgraph id, node count), I can add a dedicated provider
method and include richer metadata at enter/breadcrumb.
- Optional parity: track the Comfy menu’s “Browse Templates” item in
addition to the sidebar Templates button.

Quality
- Ran `pnpm lint:fix` and `pnpm typecheck`; both pass locally.

Implementation details
- All handlers refactored to named functions where needed; used `void`
for intentionally unawaited async calls per lint rules.
- Event names kept consistent in `button_id` strings; happy to align to
a different naming scheme if you prefer.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6511-feat-telemetry-add-tracking-for-sidebar-run-menu-dialogs-subgraphs-settings-and-cre-29e6d73d365081a1b8b4fdfbbf40e18b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Benjamin Lu
2025-11-01 23:09:41 -07:00
committed by GitHub
parent 31e405abc8
commit 7bf8e5af38
19 changed files with 210 additions and 11 deletions

View File

@@ -48,6 +48,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { cn } from '@/utils/tailwindUtil'
import ComfyRunButton from './ComfyRunButton'
@@ -132,6 +133,15 @@ watch(visible, async (newVisible) => {
}
})
/**
* Track run button handle drag start using mousedown on the drag handle.
*/
useEventListener(dragHandleRef, 'mousedown', () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'actionbar_run_handle_drag_start'
})
})
const lastDragState = ref({
x: x.value,
y: y.value,

View File

@@ -100,7 +100,7 @@ import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => {
@@ -118,6 +118,9 @@ const queueModeMenuItemLookup = computed(() => {
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_on_change_selected'
})
queueMode.value = 'change'
}
}
@@ -128,6 +131,9 @@ const queueModeMenuItemLookup = computed(() => {
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_instant_selected'
})
queueMode.value = 'instant'
}
}
@@ -160,6 +166,12 @@ const queuePrompt = async (e: Event) => {
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
if (batchCount.value > 1) {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_run_multiple_batches_submitted'
})
}
await commandStore.execute(commandId)
}
</script>

View File

@@ -40,6 +40,7 @@ import { computed, onUpdated, ref, watch } from 'vue'
import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -73,6 +74,9 @@ const items = computed(() => {
const items = navigationStore.navigationStack.map<MenuItem>((subgraph) => ({
label: subgraph.name,
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'breadcrumb_subgraph_item_selected'
})
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')
@@ -97,6 +101,9 @@ const home = computed(() => ({
key: 'root',
isBlueprint: isBlueprint.value,
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'breadcrumb_subgraph_root_selected'
})
const canvas = useCanvasStore().getCanvas()
if (!canvas.graph) throw new TypeError('Canvas has no graph')

View File

@@ -87,7 +87,13 @@ const repoOwner = 'comfyanonymous'
const repoName = 'ComfyUI'
const reportContent = ref('')
const reportOpen = ref(false)
/**
* Open the error report content and track telemetry.
*/
const showReport = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_show_report_clicked'
})
reportOpen.value = true
}
const toast = useToast()
@@ -99,6 +105,9 @@ const title = computed<string>(
() => error.nodeType ?? error.exceptionType ?? t('errorDialog.defaultTitle')
)
/**
* Open contact support flow from error dialog and track telemetry.
*/
const showContactSupport = async () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',

View File

@@ -11,6 +11,8 @@
import Button from 'primevue/button'
import { computed } from 'vue'
import { useTelemetry } from '@/platform/telemetry'
const props = defineProps<{
errorMessage: string
repoOwner: string
@@ -19,7 +21,13 @@ const props = defineProps<{
const queryString = computed(() => props.errorMessage + ' is:issue')
/**
* Open GitHub issues search and track telemetry.
*/
const openGitHubIssues = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_find_existing_issues_clicked'
})
const query = encodeURIComponent(queryString.value)
const url = `https://github.com/${props.repoOwner}/${props.repoName}/issues?q=${query}`
window.open(url, '_blank')

View File

@@ -164,6 +164,8 @@ watch(
)
const handlePurchaseCreditsClick = () => {
// Track purchase credits entry from Settings > Credits panel
useTelemetry()?.trackAddApiCreditButtonClicked()
dialogService.showTopUpCreditsDialog()
}

View File

@@ -61,7 +61,7 @@
data-testid="toggle-minimap-button"
:style="stringifiedMinimapStyles.buttonStyles"
:class="minimapButtonClass"
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
@click="onMinimapToggleClick"
>
<template #icon>
<i class="icon-[lucide--map] h-4 w-4" />
@@ -82,7 +82,7 @@
:aria-label="linkVisibilityAriaLabel"
data-testid="toggle-link-visibility-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
@click="onLinkVisibilityToggleClick"
>
<template #icon>
<i class="icon-[lucide--route-off] h-4 w-4" />
@@ -101,6 +101,7 @@ import { useI18n } from 'vue-i18n'
import { useZoomControls } from '@/composables/useZoomControls'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
@@ -218,6 +219,26 @@ onMounted(() => {
canvasStore.initScaleSync()
})
/**
* Track minimap toggle button click and execute the command.
*/
const onMinimapToggleClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'graph_menu_minimap_toggle_clicked'
})
void commandStore.execute('Comfy.Canvas.ToggleMinimap')
}
/**
* Track hide/show links button click and execute the command.
*/
const onLinkVisibilityToggleClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'graph_menu_hide_links_toggle_clicked'
})
void commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')
}
onBeforeUnmount(() => {
canvasStore.cleanupScaleSync()
})

View File

@@ -7,7 +7,7 @@
data-testid="info-button"
text
severity="secondary"
@click="toggleHelp"
@click="onInfoClick"
>
<i class="icon-[lucide--info] h-4 w-4" />
</Button>
@@ -17,6 +17,17 @@
import Button from 'primevue/button'
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { useTelemetry } from '@/platform/telemetry'
const { showNodeHelp: toggleHelp } = useSelectionState()
/**
* Track node info button click and toggle node help.
*/
const onInfoClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'selection_toolbox_node_info_opened'
})
toggleHelp()
}
</script>

View File

@@ -4,7 +4,7 @@
:class="{
'comfy-menu-button-active': menuRef?.visible
}"
@click="menuRef?.toggle($event)"
@click="onLogoMenuClick($event)"
>
<ComfyLogoTransparent
alt="ComfyUI Logo"
@@ -78,6 +78,7 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
import ComfyLogoTransparent from '@/components/icons/ComfyLogoTransparent.vue'
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import { useTelemetry } from '@/platform/telemetry'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
@@ -104,6 +105,15 @@ const menuRef = ref<
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
>(null)
const telemetry = useTelemetry()
function onLogoMenuClick(event: MouseEvent) {
telemetry?.trackUiButtonClicked({
button_id: 'sidebar_comfy_menu_opened'
})
menuRef.value?.toggle(event)
}
const translateMenuItem = (item: MenuItem): MenuItem => {
const label = typeof item.label === 'function' ? item.label() : item.label
const translatedLabel = label
@@ -167,7 +177,12 @@ const extraMenuItems = computed(() => [
key: 'settings',
label: t('g.settings'),
icon: 'mdi mdi-cog-outline',
command: () => showSettings()
command: () => {
telemetry?.trackUiButtonClicked({
button_id: 'sidebar_settings_menu_opened'
})
showSettings()
}
},
{
key: 'manage-extensions',

View File

@@ -57,6 +57,7 @@ import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
@@ -97,10 +98,40 @@ const isConnected = computed(
const tabs = computed(() => workspaceStore.getSidebarTabs())
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
const onTabClick = async (item: SidebarTabExtension) =>
/**
* Handle sidebar tab icon click.
* - Emits UI button telemetry for known tabs
* - Delegates to the corresponding toggle command
*/
const onTabClick = async (item: SidebarTabExtension) => {
const telemetry = useTelemetry()
const isNodeLibraryTab = item.id === 'node-library'
const isModelLibraryTab = item.id === 'model-library'
const isWorkflowsTab = item.id === 'workflows'
const isAssetsTab = item.id === 'assets'
if (isNodeLibraryTab)
telemetry?.trackUiButtonClicked({
button_id: 'sidebar_tab_node_library_selected'
})
else if (isModelLibraryTab)
telemetry?.trackUiButtonClicked({
button_id: 'sidebar_tab_model_library_selected'
})
else if (isWorkflowsTab)
telemetry?.trackUiButtonClicked({
button_id: 'sidebar_tab_workflows_selected'
})
else if (isAssetsTab)
telemetry?.trackUiButtonClicked({
button_id: 'sidebar_tab_assets_media_selected'
})
await commandStore.commands
.find((cmd) => cmd.id === `Workspace.ToggleSidebarTab.${item.id}`)
?.function?.()
}
const keybindingStore = useKeybindingStore()
const getTabTooltipSuffix = (tab: SidebarTabExtension) => {

View File

@@ -3,7 +3,7 @@
:label="$t('sideToolbar.labels.console')"
:tooltip="$t('menu.toggleBottomPanel')"
:selected="bottomPanelStore.activePanel == 'terminal'"
@click="bottomPanelStore.toggleBottomPanel"
@click="toggleConsole"
>
<template #icon>
<i-ph:terminal-bold />
@@ -12,9 +12,20 @@
</template>
<script setup lang="ts">
import { useTelemetry } from '@/platform/telemetry'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import SidebarIcon from './SidebarIcon.vue'
const bottomPanelStore = useBottomPanelStore()
/**
* Toggle console bottom panel and track UI button click.
*/
const toggleConsole = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'sidebar_bottom_panel_console_toggled'
})
bottomPanelStore.toggleBottomPanel()
}
</script>

View File

@@ -65,6 +65,7 @@ import { computed, onMounted, toRefs } from 'vue'
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import ReleaseNotificationToast from '@/platform/updates/components/ReleaseNotificationToast.vue'
import WhatsNewPopup from '@/platform/updates/components/WhatsNewPopup.vue'
@@ -104,7 +105,13 @@ const sidebarLocation = computed(() =>
settingStore.get('Comfy.Sidebar.Location')
)
/**
* Toggle Help Center and track UI button click.
*/
const toggleHelpCenter = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'sidebar_help_center_toggled'
})
helpCenterStore.toggle()
}

View File

@@ -15,6 +15,7 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
@@ -34,7 +35,13 @@ const tooltipText = computed(
() => `${t('shortcuts.keyboardShortcuts')} (${formatKeySequence(command)})`
)
/**
* Toggle keyboard shortcuts panel and track UI button click.
*/
const toggleShortcutsPanel = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'sidebar_shortcuts_panel_toggled'
})
bottomPanelStore.togglePanel('shortcuts')
}
</script>

View File

@@ -14,6 +14,7 @@ import { computed } from 'vue'
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import SidebarIcon from './SidebarIcon.vue'
@@ -23,7 +24,13 @@ const isSmall = computed(
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
)
/**
* Open templates dialog from sidebar and track UI button click.
*/
const openTemplates = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'sidebar_templates_dialog_opened'
})
useWorkflowTemplateSelectorDialog().show('sidebar')
}
</script>

View File

@@ -104,6 +104,7 @@ import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthAction
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
const emit = defineEmits<{
@@ -136,6 +137,8 @@ const handleOpenPlanAndCreditsSettings = () => {
}
const handleTopUp = () => {
// Track purchase credits entry from avatar popover
useTelemetry()?.trackAddApiCreditButtonClicked()
dialogService.showTopUpCreditsDialog()
emit('close')
}

View File

@@ -36,6 +36,7 @@ import type {
TemplateLibraryMetadata,
TemplateLibraryClosedMetadata,
TemplateMetadata,
UiButtonClickMetadata,
WorkflowCreatedMetadata,
WorkflowImportMetadata
} from '../../types'
@@ -332,6 +333,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.EXECUTION_SUCCESS, metadata)
}
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
}
getExecutionContext(): ExecutionContext {
const workflowStore = useWorkflowStore()
const templatesStore = useWorkflowTemplatesStore()

View File

@@ -192,6 +192,14 @@ export interface TemplateFilterMetadata {
total_count: number
}
/**
* UI button click tracking metadata
*/
export interface UiButtonClickMetadata {
/** Canonical identifier for the button (e.g., "comfy_logo") */
button_id: string
}
/**
* Help center opened metadata
*/
@@ -297,6 +305,9 @@ export interface TelemetryProvider {
trackWorkflowExecution(): void
trackExecutionError(metadata: ExecutionErrorMetadata): void
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
// Generic UI button click events
trackUiButtonClicked(metadata: UiButtonClickMetadata): void
}
/**
@@ -365,7 +376,9 @@ export const TelemetryEvents = {
// Execution Lifecycle
EXECUTION_START: 'execution_start',
EXECUTION_ERROR: 'execution_error',
EXECUTION_SUCCESS: 'execution_success'
EXECUTION_SUCCESS: 'execution_success',
// Generic UI Button Click
UI_BUTTON_CLICKED: 'app:ui_button_clicked'
} as const
export type TelemetryEventName =
@@ -391,6 +404,7 @@ export type TelemetryEventProperties =
| NodeSearchMetadata
| NodeSearchResultMetadata
| TemplateFilterMetadata
| UiButtonClickMetadata
| HelpCenterOpenedMetadata
| HelpResourceClickedMetadata
| HelpCenterClosedMetadata

View File

@@ -142,6 +142,7 @@ import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
@@ -415,6 +416,9 @@ const handleHeaderTitleUpdate = (newTitle: string) => {
}
const handleEnterSubgraph = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'graph_node_open_subgraph_clicked'
})
const graph = app.graph?.rootGraph || app.graph
if (!graph) {
console.warn('LGraphNode: No graph available for subgraph navigation')

View File

@@ -13,6 +13,7 @@ import UpdatePasswordContent from '@/components/dialog/content/UpdatePasswordCon
import ComfyOrgHeader from '@/components/dialog/header/ComfyOrgHeader.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import { t } from '@/i18n'
import { useTelemetry } from '@/platform/telemetry'
import { isCloud } from '@/platform/distribution/types'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
@@ -108,7 +109,14 @@ export const useDialogService = () => {
dialogStore.showDialog({
key: 'global-execution-error',
component: ErrorDialogContent,
props
props,
dialogComponentProps: {
onClose: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_closed'
})
}
}
})
}
@@ -190,7 +198,14 @@ export const useDialogService = () => {
dialogStore.showDialog({
key: 'global-error',
component: ErrorDialogContent,
props
props,
dialogComponentProps: {
onClose: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_closed'
})
}
}
})
}