From 56412a4076fdcd95eb66e1836c14dc3e6d4162e2 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sun, 2 Nov 2025 20:49:02 -0800 Subject: [PATCH] [Backport to rh-test] fix(telemetry): remove redundant run tracking; keep click analytics + single execution event (#6552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Manual backport of #6518 to the `rh-test` branch. Deduplicates workflow run telemetry and keeps a single source of truth for execution while retaining click analytics and attributing initiator source. - Keep execution tracking in one place via `trackWorkflowExecution()` - Keep click analytics via `trackRunButton(...)` - Attribute initiator with `trigger_source` = 'button' | 'keybinding' | 'legacy_ui' - Remove pre-tracking from keybindings to avoid double/triple counting - Update legacy UI buttons to emit both click + execution events ## Backport Notes This backport required manual conflict resolution in: - `src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue` - Added batchCount tracking and trigger_source metadata - `src/composables/useCoreCommands.ts` - Added error handling and execution tracking - `src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts` - Updated trackRunButton signature with trigger_source support Additionally added: - `trackUiButtonClicked` method to TelemetryProvider interface - `UiButtonClickMetadata` type definition - `UI_BUTTON_CLICKED` event constant All conflicts resolved intelligently to maintain the intent of the original PR while adapting to the rh-test branch codebase. ## Original PR - Original PR: #6518 - Original commit: 6fe88dba546c829c45c1b406dfe3746b8726f8f5 ## Testing - ✅ Typecheck passed - ✅ Pre-commit hooks passed (lint, format) - ✅ All conflicts resolved ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6552-Backport-to-rh-test-fix-telemetry-remove-redundant-run-tracking-keep-click-analytics-2a06d73d365081f78e4ad46a16be69f1) by [Unito](https://www.unito.io) --------- Co-authored-by: Christian Byrne Co-authored-by: Alexander Brown Co-authored-by: Benjamin Lu Co-authored-by: Claude Co-authored-by: Christian Byrne --- .../ComfyRunButton/ComfyQueueButton.vue | 15 ++++++-- src/composables/useCoreCommands.ts | 30 ++++++++++++++-- src/extensions/core/groupNode.ts | 7 ++-- .../cloud/MixpanelTelemetryProvider.ts | 34 ++++++++++++------- src/platform/telemetry/types.ts | 34 +++++++++++++++---- src/scripts/ui.ts | 6 ++-- src/services/keybindingService.ts | 23 +++++++------ src/stores/commandStore.ts | 14 +++++--- src/stores/menuItemStore.ts | 14 +------- src/types/extensionTypes.ts | 8 ++++- 10 files changed, 128 insertions(+), 57 deletions(-) diff --git a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue index cba25fc5a..388a61a80 100644 --- a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue +++ b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue @@ -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(() => { @@ -158,9 +158,18 @@ const queuePrompt = async (e: Event) => { ? 'Comfy.QueuePromptFront' : 'Comfy.QueuePrompt' - useTelemetry()?.trackRunButton({ subscribe_to_run: false }) + if (batchCount.value > 1) { + useTelemetry()?.trackUiButtonClicked({ + button_id: 'queue_run_multiple_batches_submitted' + }) + } - await commandStore.execute(commandId) + await commandStore.execute(commandId, { + metadata: { + subscribe_to_run: false, + trigger_source: 'button' + } + }) } diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 2d6510a27..7f22c580f 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -21,6 +21,7 @@ import { useSubscription } from '@/platform/cloud/subscription/composables/useSu import { useSettingStore } from '@/platform/settings/settingStore' import { SUPPORT_URL } from '@/platform/support/config' import { useTelemetry } from '@/platform/telemetry' +import type { ExecutionTriggerSource } from '@/platform/telemetry/types' import { useToastStore } from '@/platform/updates/common/toastStore' import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' @@ -465,7 +466,11 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Queue Prompt', versionAdded: '1.3.7', category: 'essentials' as const, - function: async () => { + function: async (metadata?: { + subscribe_to_run?: boolean + trigger_source?: ExecutionTriggerSource + }) => { + useTelemetry()?.trackRunButton(metadata) if (!isActiveSubscription.value) { showSubscriptionDialog() return @@ -484,7 +489,11 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Queue Prompt (Front)', versionAdded: '1.3.7', category: 'essentials' as const, - function: async () => { + function: async (metadata?: { + subscribe_to_run?: boolean + trigger_source?: ExecutionTriggerSource + }) => { + useTelemetry()?.trackRunButton(metadata) if (!isActiveSubscription.value) { showSubscriptionDialog() return @@ -502,7 +511,11 @@ export function useCoreCommands(): ComfyCommand[] { icon: 'pi pi-play', label: 'Queue Selected Output Nodes', versionAdded: '1.19.6', - function: async () => { + function: async (metadata?: { + subscribe_to_run?: boolean + trigger_source?: ExecutionTriggerSource + }) => { + useTelemetry()?.trackRunButton(metadata) if (!isActiveSubscription.value) { showSubscriptionDialog() return @@ -525,6 +538,17 @@ export function useCoreCommands(): ComfyCommand[] { // Get execution IDs for all selected output nodes and their descendants const executionIds = getExecutionIdsForSelectedNodes(selectedOutputNodes) + + if (executionIds.length === 0) { + toastStore.add({ + severity: 'error', + summary: t('toastMessages.failedToQueue'), + detail: t('toastMessages.failedExecutionPathResolution'), + life: 3000 + }) + return + } + useTelemetry()?.trackWorkflowExecution() await app.queuePrompt(0, batchCount, executionIds) } }, diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index 2be2980eb..829ba0f28 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -1742,21 +1742,22 @@ const ext: ComfyExtension = { label: 'Convert selected nodes to group node', icon: 'pi pi-sitemap', versionAdded: '1.3.17', - function: convertSelectedNodesToGroupNode + function: () => convertSelectedNodesToGroupNode() }, { id: 'Comfy.GroupNode.UngroupSelectedGroupNodes', label: 'Ungroup selected group nodes', icon: 'pi pi-sitemap', versionAdded: '1.3.17', - function: ungroupSelectedGroupNodes + function: () => ungroupSelectedGroupNodes() }, { id: 'Comfy.GroupNode.ManageGroupNodes', label: 'Manage group nodes', icon: 'pi pi-cog', versionAdded: '1.3.17', - function: manageGroupNodes + function: (...args: unknown[]) => + manageGroupNodes(args[0] as string | undefined) } ], keybindings: [ diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index f7f9f80c8..e025e48ac 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -16,6 +16,7 @@ import type { ExecutionContext, ExecutionErrorMetadata, ExecutionSuccessMetadata, + ExecutionTriggerSource, HelpCenterClosedMetadata, HelpCenterOpenedMetadata, HelpResourceClickedMetadata, @@ -32,6 +33,7 @@ import type { TemplateLibraryClosedMetadata, TemplateLibraryMetadata, TemplateMetadata, + UiButtonClickMetadata, WorkflowCreatedMetadata, WorkflowImportMetadata } from '../../types' @@ -59,6 +61,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { private mixpanel: OverridedMixpanel | null = null private eventQueue: QueuedEvent[] = [] private isInitialized = false + private lastTriggerSource: ExecutionTriggerSource | undefined // Onboarding mode - starts true, set to false when app is fully ready private isOnboardingMode = true @@ -354,7 +357,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { clearTopupUtil() } - trackRunButton(options?: { subscribe_to_run?: boolean }): void { + trackRunButton(options?: { + subscribe_to_run?: boolean + trigger_source?: ExecutionTriggerSource + }): void { if (this.isOnboardingMode) { // During onboarding, track basic run button click without workflow context this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, { @@ -365,7 +371,8 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { total_node_count: 0, subgraph_count: 0, has_api_nodes: false, - api_node_names: [] + api_node_names: [], + trigger_source: options?.trigger_source }) return } @@ -380,20 +387,14 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { total_node_count: executionContext.total_node_count, subgraph_count: executionContext.subgraph_count, has_api_nodes: executionContext.has_api_nodes, - api_node_names: executionContext.api_node_names + api_node_names: executionContext.api_node_names, + trigger_source: options?.trigger_source } + this.lastTriggerSource = options?.trigger_source this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties) } - trackRunTriggeredViaKeybinding(): void { - this.trackEvent(TelemetryEvents.RUN_TRIGGERED_KEYBINDING) - } - - trackRunTriggeredViaMenu(): void { - this.trackEvent(TelemetryEvents.RUN_TRIGGERED_MENU) - } - trackSurvey( stage: 'opened' | 'submitted', responses?: SurveyResponses @@ -501,6 +502,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { this.trackEvent(TelemetryEvents.WORKFLOW_CREATED, metadata) } + trackUiButtonClicked(metadata: UiButtonClickMetadata): void { + this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata) + } + trackWorkflowExecution(): void { if (this.isOnboardingMode) { // During onboarding, track basic execution without workflow context @@ -518,7 +523,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { } const context = this.getExecutionContext() - this.trackEvent(TelemetryEvents.EXECUTION_START, context) + const eventContext: ExecutionContext = { + ...context, + trigger_source: this.lastTriggerSource ?? 'unknown' + } + this.trackEvent(TelemetryEvents.EXECUTION_START, eventContext) + this.lastTriggerSource = undefined } trackExecutionError(metadata: ExecutionErrorMetadata): void { diff --git a/src/platform/telemetry/types.ts b/src/platform/telemetry/types.ts index 1d563a251..ba54a0973 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -48,6 +48,7 @@ export interface RunButtonProperties { subgraph_count: number has_api_nodes: boolean api_node_names: string[] + trigger_source?: ExecutionTriggerSource } /** @@ -70,6 +71,7 @@ export interface ExecutionContext { total_node_count: number has_api_nodes: boolean api_node_names: string[] + trigger_source?: ExecutionTriggerSource } /** @@ -193,6 +195,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 */ @@ -250,9 +260,10 @@ export interface TelemetryProvider { trackAddApiCreditButtonClicked(): void trackApiCreditTopupButtonPurchaseClicked(amount: number): void trackApiCreditTopupSucceeded(): void - trackRunButton(options?: { subscribe_to_run?: boolean }): void - trackRunTriggeredViaKeybinding(): void - trackRunTriggeredViaMenu(): void + trackRunButton(options?: { + subscribe_to_run?: boolean + trigger_source?: ExecutionTriggerSource + }): void // Credit top-up tracking (composition with internal utilities) startTopupTracking(): void @@ -284,6 +295,9 @@ export interface TelemetryProvider { // Template filter tracking events trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void + // Generic UI button click events + trackUiButtonClicked(metadata: UiButtonClickMetadata): void + // Help center events trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void @@ -321,8 +335,6 @@ export const TelemetryEvents = { // Subscription Flow RUN_BUTTON_CLICKED: 'app:run_button_click', - RUN_TRIGGERED_KEYBINDING: 'app:run_triggered_keybinding', - RUN_TRIGGERED_MENU: 'app:run_triggered_menu', SUBSCRIPTION_REQUIRED_MODAL_OPENED: 'app:subscription_required_modal_opened', SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked', MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded', @@ -368,12 +380,21 @@ 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 = (typeof TelemetryEvents)[keyof typeof TelemetryEvents] +export type ExecutionTriggerSource = + | 'button' + | 'keybinding' + | 'legacy_ui' + | 'unknown' + /** * Union type for all possible telemetry event properties */ @@ -394,6 +415,7 @@ export type TelemetryEventProperties = | NodeSearchMetadata | NodeSearchResultMetadata | TemplateFilterMetadata + | UiButtonClickMetadata | HelpCenterOpenedMetadata | HelpResourceClickedMetadata | HelpCenterClosedMetadata diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts index 869370609..15747d876 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -480,7 +480,8 @@ export class ComfyUI { textContent: 'Queue Prompt', onclick: () => { if (isCloud) { - useTelemetry()?.trackRunTriggeredViaMenu() + useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' }) + useTelemetry()?.trackWorkflowExecution() } app.queuePrompt(0, this.batchCount) } @@ -587,7 +588,8 @@ export class ComfyUI { textContent: 'Queue Front', onclick: () => { if (isCloud) { - useTelemetry()?.trackRunTriggeredViaMenu() + useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' }) + useTelemetry()?.trackWorkflowExecution() } app.queuePrompt(-1, this.batchCount) } diff --git a/src/services/keybindingService.ts b/src/services/keybindingService.ts index fff58fa57..d2f45e005 100644 --- a/src/services/keybindingService.ts +++ b/src/services/keybindingService.ts @@ -1,7 +1,5 @@ import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings' -import { isCloud } from '@/platform/distribution/types' import { useSettingStore } from '@/platform/settings/settingStore' -import { useTelemetry } from '@/platform/telemetry' import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' import { useDialogStore } from '@/stores/dialogStore' @@ -66,15 +64,20 @@ export const useKeybindingService = () => { // Prevent default browser behavior first, then execute the command event.preventDefault() - if ( - isCloud && - (keybinding.commandId === 'Comfy.QueuePrompt' || - keybinding.commandId === 'Comfy.QueuePromptFront' || - keybinding.commandId === 'Comfy.QueueSelectedOutputNodes') - ) { - useTelemetry()?.trackRunTriggeredViaKeybinding() + const runCommandIds = new Set([ + 'Comfy.QueuePrompt', + 'Comfy.QueuePromptFront', + 'Comfy.QueueSelectedOutputNodes' + ]) + if (runCommandIds.has(keybinding.commandId)) { + await commandStore.execute(keybinding.commandId, { + metadata: { + trigger_source: 'keybinding' + } + }) + } else { + await commandStore.execute(keybinding.commandId) } - await commandStore.execute(keybinding.commandId) return } diff --git a/src/stores/commandStore.ts b/src/stores/commandStore.ts index d3da5d85f..ade180d37 100644 --- a/src/stores/commandStore.ts +++ b/src/stores/commandStore.ts @@ -9,7 +9,7 @@ import type { KeybindingImpl } from './keybindingStore' export interface ComfyCommand { id: string - function: () => void | Promise + function: (metadata?: Record) => void | Promise label?: string | (() => string) icon?: string | (() => string) @@ -24,7 +24,7 @@ export interface ComfyCommand { export class ComfyCommandImpl implements ComfyCommand { id: string - function: () => void | Promise + function: (metadata?: Record) => void | Promise _label?: string | (() => string) _icon?: string | (() => string) _tooltip?: string | (() => string) @@ -96,11 +96,17 @@ export const useCommandStore = defineStore('command', () => { const { wrapWithErrorHandlingAsync } = useErrorHandling() const execute = async ( commandId: string, - errorHandler?: (error: any) => void + options?: { + errorHandler?: (error: unknown) => void + metadata?: Record + } ) => { const command = getCommand(commandId) if (command) { - await wrapWithErrorHandlingAsync(command.function, errorHandler)() + await wrapWithErrorHandlingAsync( + () => command.function(options?.metadata), + options?.errorHandler + )() } else { throw new Error(`Command ${commandId} not found`) } diff --git a/src/stores/menuItemStore.ts b/src/stores/menuItemStore.ts index 242e4641c..0a24769a1 100644 --- a/src/stores/menuItemStore.ts +++ b/src/stores/menuItemStore.ts @@ -3,8 +3,6 @@ import type { MenuItem } from 'primevue/menuitem' import { ref } from 'vue' import { CORE_MENU_COMMANDS } from '@/constants/coreMenuCommands' -import { isCloud } from '@/platform/distribution/types' -import { useTelemetry } from '@/platform/telemetry' import type { ComfyExtension } from '@/types/comfy' import { useCommandStore } from './commandStore' @@ -64,17 +62,7 @@ export const useMenuItemStore = defineStore('menuItem', () => { .map( (command) => ({ - command: () => { - if ( - isCloud && - (command.id === 'Comfy.QueuePrompt' || - command.id === 'Comfy.QueuePromptFront' || - command.id === 'Comfy.QueueSelectedOutputNodes') - ) { - useTelemetry()?.trackRunTriggeredViaMenu() - } - return commandStore.execute(command.id) - }, + command: () => commandStore.execute(command.id), label: command.menubarLabel, icon: command.icon, tooltip: command.tooltip, diff --git a/src/types/extensionTypes.ts b/src/types/extensionTypes.ts index bc2b920ba..2c96faa05 100644 --- a/src/types/extensionTypes.ts +++ b/src/types/extensionTypes.ts @@ -114,5 +114,11 @@ export interface ExtensionManager { export interface CommandManager { commands: ComfyCommand[] - execute(command: string, errorHandler?: (error: any) => void): void + execute( + command: string, + options?: { + errorHandler?: (error: unknown) => void + metadata?: Record + } + ): void }