From 6fe88dba546c829c45c1b406dfe3746b8726f8f5 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sun, 2 Nov 2025 19:48:21 -0800 Subject: [PATCH] fix(telemetry): remove redundant run tracking; keep click analytics + single execution event (#6518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary 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 (they bypass commands) ## Problem PR #6499 added tracking at multiple layers: 1) Keybindings tracked via a dedicated method and then executed a command 2) Menu items tracked via a dedicated method and then executed a command 3) Commands also tracked execution Because these ultimately trigger the same command path, this produced duplicate (sometimes triplicate) events per user action and made it hard to attribute initiator precisely. ## Solution - Remove redundant tracking from keybindings (and previous legacy menu handler) - Commands now emit both: - `trackRunButton(...)` (click analytics, includes `trigger_source` when provided) - `trackWorkflowExecution()` (single execution start; includes the last `trigger_source`) - Legacy UI buttons (which call `app.queuePrompt(...)` directly) now also emit both events with `trigger_source = 'legacy_ui'` - Add `ExecutionTriggerSource` type and wire `trigger_source` through provider so `EXECUTION_START` matches the most recent click intent ### Telemetry behavior after this change - `RUN_BUTTON_CLICKED` (click analytics) - Emitted when a run is initiated via: - Button: `trigger_source = 'button'` - Keybinding: `trigger_source = 'keybinding'` - Legacy UI: `trigger_source = 'legacy_ui'` - `EXECUTION_START` (execution lifecycle) - Emitted once per run at start; includes `trigger_source` matched to the click intent above - Paired with `EXECUTION_SUCCESS` / `EXECUTION_ERROR` from execution handlers ## Benefits - ✅ Accurate counts by removing duplicated run events - ✅ Clear initiator attribution (button vs keybinding vs legacy UI) - ✅ Separation of “intent” (click) vs. “lifecycle” (execution) - ✅ Simpler implementation and maintenance ## Files Changed (high level) - `src/services/keybindingService.ts`: Route run commands with `trigger_source = 'keybinding'` - `src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue`: Send `trigger_source = 'button'` to commands - `src/scripts/ui.ts`: Legacy queue buttons emit `trackRunButton({ trigger_source: 'legacy_ui' })` and `trackWorkflowExecution()` - `src/composables/useCoreCommands.ts`: Commands emit `trackRunButton(...)` + `trackWorkflowExecution()`; accept telemetry metadata - `src/platform/telemetry/types.ts`: Add `ExecutionTriggerSource` and optional `trigger_source` in click + execution payloads - `src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts`: Carry `trigger_source` from click → execution and reset after use - `src/stores/commandStore.ts`: Allow commands to receive args (for telemetry metadata) - `src/extensions/core/groupNode.ts`: Adjust command function signatures to new execute signature ## Related - Reverts the multi‑event approach from #6499 - Keeps `trackWorkflowExecution()` as the canonical execution event while preserving click analytics and initiator attribution with `trackRunButton(...)` ┆Issue is synchronized with this Notion page by Unito --------- Co-authored-by: Christian Byrne Co-authored-by: Alexander Brown Co-authored-by: Benjamin Lu Co-authored-by: Claude --- .../ComfyRunButton/ComfyQueueButton.vue | 9 ++++--- src/composables/useCoreCommands.ts | 21 ++++++++++++--- src/extensions/core/groupNode.ts | 7 ++--- .../cloud/MixpanelTelemetryProvider.ts | 26 +++++++++++-------- src/platform/telemetry/types.ts | 17 ++++++++---- 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, 90 insertions(+), 55 deletions(-) diff --git a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue index 762ea7f2d6..8907ecf444 100644 --- a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue +++ b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue @@ -164,15 +164,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 c6f8182d8a..966fe40e45 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -22,6 +22,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' @@ -466,7 +467,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 @@ -485,7 +490,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 @@ -503,7 +512,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 @@ -526,6 +539,7 @@ 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', @@ -535,6 +549,7 @@ export function useCoreCommands(): ComfyCommand[] { }) return } + useTelemetry()?.trackWorkflowExecution() await app.queuePrompt(0, batchCount, executionIds) } }, diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index 42408fa0dd..5b4146c2d8 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -1694,21 +1694,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 2168c90d02..419ab6329d 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -17,6 +17,7 @@ import type { AuthMetadata, CreditTopupMetadata, ExecutionContext, + ExecutionTriggerSource, ExecutionErrorMetadata, ExecutionSuccessMetadata, HelpCenterClosedMetadata, @@ -65,6 +66,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { private mixpanel: OverridedMixpanel | null = null private eventQueue: QueuedEvent[] = [] private isInitialized = false + private lastTriggerSource: ExecutionTriggerSource | undefined constructor() { const token = window.__CONFIG__?.mixpanel_token @@ -196,7 +198,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { clearTopupUtil() } - trackRunButton(options?: { subscribe_to_run?: boolean }): void { + trackRunButton(options?: { + subscribe_to_run?: boolean + trigger_source?: ExecutionTriggerSource + }): void { const executionContext = this.getExecutionContext() const runButtonProperties: RunButtonProperties = { @@ -207,20 +212,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 @@ -323,7 +322,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { trackWorkflowExecution(): void { 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 27253bddd1..f16304e7c8 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -47,6 +47,7 @@ export interface RunButtonProperties { subgraph_count: number has_api_nodes: boolean api_node_names: string[] + trigger_source?: ExecutionTriggerSource } /** @@ -69,6 +70,7 @@ export interface ExecutionContext { total_node_count: number has_api_nodes: boolean api_node_names: string[] + trigger_source?: ExecutionTriggerSource } /** @@ -265,9 +267,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 @@ -336,8 +339,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', @@ -399,6 +400,12 @@ export const TelemetryEvents = { export type TelemetryEventName = (typeof TelemetryEvents)[keyof typeof TelemetryEvents] +export type ExecutionTriggerSource = + | 'button' + | 'keybinding' + | 'legacy_ui' + | 'unknown' + /** * Union type for all possible telemetry event properties */ diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts index 67d190f1ff..6a428c974e 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -476,7 +476,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) } @@ -583,7 +584,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 fff58fa573..d2f45e0053 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 d3da5d85f7..ade180d373 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 242e4641c5..0a24769a1d 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 bc2b920ba9..2c96faa057 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 }