[Backport to rh-test] fix(telemetry): remove redundant run tracking; keep click analytics + single execution event (#6552)

## 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: 6fe88dba54

## 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 <c.byrne@comfy.org>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Christian Byrne <chrbyrne96@gmail.com>
This commit is contained in:
Christian Byrne
2025-11-02 20:49:02 -08:00
committed by GitHub
parent 044b675138
commit 56412a4076
10 changed files with 128 additions and 57 deletions

View File

@@ -100,7 +100,7 @@ import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore() const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore()) const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore()) const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n() const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => { const queueModeMenuItemLookup = computed(() => {
@@ -158,9 +158,18 @@ const queuePrompt = async (e: Event) => {
? 'Comfy.QueuePromptFront' ? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt' : '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'
}
})
} }
</script> </script>

View File

@@ -21,6 +21,7 @@ import { useSubscription } from '@/platform/cloud/subscription/composables/useSu
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { SUPPORT_URL } from '@/platform/support/config' import { SUPPORT_URL } from '@/platform/support/config'
import { useTelemetry } from '@/platform/telemetry' import { useTelemetry } from '@/platform/telemetry'
import type { ExecutionTriggerSource } from '@/platform/telemetry/types'
import { useToastStore } from '@/platform/updates/common/toastStore' import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService' import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -465,7 +466,11 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Queue Prompt', label: 'Queue Prompt',
versionAdded: '1.3.7', versionAdded: '1.3.7',
category: 'essentials' as const, category: 'essentials' as const,
function: async () => { function: async (metadata?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
if (!isActiveSubscription.value) { if (!isActiveSubscription.value) {
showSubscriptionDialog() showSubscriptionDialog()
return return
@@ -484,7 +489,11 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Queue Prompt (Front)', label: 'Queue Prompt (Front)',
versionAdded: '1.3.7', versionAdded: '1.3.7',
category: 'essentials' as const, category: 'essentials' as const,
function: async () => { function: async (metadata?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
if (!isActiveSubscription.value) { if (!isActiveSubscription.value) {
showSubscriptionDialog() showSubscriptionDialog()
return return
@@ -502,7 +511,11 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-play', icon: 'pi pi-play',
label: 'Queue Selected Output Nodes', label: 'Queue Selected Output Nodes',
versionAdded: '1.19.6', versionAdded: '1.19.6',
function: async () => { function: async (metadata?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
if (!isActiveSubscription.value) { if (!isActiveSubscription.value) {
showSubscriptionDialog() showSubscriptionDialog()
return return
@@ -525,6 +538,17 @@ export function useCoreCommands(): ComfyCommand[] {
// Get execution IDs for all selected output nodes and their descendants // Get execution IDs for all selected output nodes and their descendants
const executionIds = const executionIds =
getExecutionIdsForSelectedNodes(selectedOutputNodes) 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) await app.queuePrompt(0, batchCount, executionIds)
} }
}, },

View File

@@ -1742,21 +1742,22 @@ const ext: ComfyExtension = {
label: 'Convert selected nodes to group node', label: 'Convert selected nodes to group node',
icon: 'pi pi-sitemap', icon: 'pi pi-sitemap',
versionAdded: '1.3.17', versionAdded: '1.3.17',
function: convertSelectedNodesToGroupNode function: () => convertSelectedNodesToGroupNode()
}, },
{ {
id: 'Comfy.GroupNode.UngroupSelectedGroupNodes', id: 'Comfy.GroupNode.UngroupSelectedGroupNodes',
label: 'Ungroup selected group nodes', label: 'Ungroup selected group nodes',
icon: 'pi pi-sitemap', icon: 'pi pi-sitemap',
versionAdded: '1.3.17', versionAdded: '1.3.17',
function: ungroupSelectedGroupNodes function: () => ungroupSelectedGroupNodes()
}, },
{ {
id: 'Comfy.GroupNode.ManageGroupNodes', id: 'Comfy.GroupNode.ManageGroupNodes',
label: 'Manage group nodes', label: 'Manage group nodes',
icon: 'pi pi-cog', icon: 'pi pi-cog',
versionAdded: '1.3.17', versionAdded: '1.3.17',
function: manageGroupNodes function: (...args: unknown[]) =>
manageGroupNodes(args[0] as string | undefined)
} }
], ],
keybindings: [ keybindings: [

View File

@@ -16,6 +16,7 @@ import type {
ExecutionContext, ExecutionContext,
ExecutionErrorMetadata, ExecutionErrorMetadata,
ExecutionSuccessMetadata, ExecutionSuccessMetadata,
ExecutionTriggerSource,
HelpCenterClosedMetadata, HelpCenterClosedMetadata,
HelpCenterOpenedMetadata, HelpCenterOpenedMetadata,
HelpResourceClickedMetadata, HelpResourceClickedMetadata,
@@ -32,6 +33,7 @@ import type {
TemplateLibraryClosedMetadata, TemplateLibraryClosedMetadata,
TemplateLibraryMetadata, TemplateLibraryMetadata,
TemplateMetadata, TemplateMetadata,
UiButtonClickMetadata,
WorkflowCreatedMetadata, WorkflowCreatedMetadata,
WorkflowImportMetadata WorkflowImportMetadata
} from '../../types' } from '../../types'
@@ -59,6 +61,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
private mixpanel: OverridedMixpanel | null = null private mixpanel: OverridedMixpanel | null = null
private eventQueue: QueuedEvent[] = [] private eventQueue: QueuedEvent[] = []
private isInitialized = false private isInitialized = false
private lastTriggerSource: ExecutionTriggerSource | undefined
// Onboarding mode - starts true, set to false when app is fully ready // Onboarding mode - starts true, set to false when app is fully ready
private isOnboardingMode = true private isOnboardingMode = true
@@ -354,7 +357,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
clearTopupUtil() clearTopupUtil()
} }
trackRunButton(options?: { subscribe_to_run?: boolean }): void { trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void {
if (this.isOnboardingMode) { if (this.isOnboardingMode) {
// During onboarding, track basic run button click without workflow context // During onboarding, track basic run button click without workflow context
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, { this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, {
@@ -365,7 +371,8 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
total_node_count: 0, total_node_count: 0,
subgraph_count: 0, subgraph_count: 0,
has_api_nodes: false, has_api_nodes: false,
api_node_names: [] api_node_names: [],
trigger_source: options?.trigger_source
}) })
return return
} }
@@ -380,20 +387,14 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
total_node_count: executionContext.total_node_count, total_node_count: executionContext.total_node_count,
subgraph_count: executionContext.subgraph_count, subgraph_count: executionContext.subgraph_count,
has_api_nodes: executionContext.has_api_nodes, 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) this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties)
} }
trackRunTriggeredViaKeybinding(): void {
this.trackEvent(TelemetryEvents.RUN_TRIGGERED_KEYBINDING)
}
trackRunTriggeredViaMenu(): void {
this.trackEvent(TelemetryEvents.RUN_TRIGGERED_MENU)
}
trackSurvey( trackSurvey(
stage: 'opened' | 'submitted', stage: 'opened' | 'submitted',
responses?: SurveyResponses responses?: SurveyResponses
@@ -501,6 +502,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.WORKFLOW_CREATED, metadata) this.trackEvent(TelemetryEvents.WORKFLOW_CREATED, metadata)
} }
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
}
trackWorkflowExecution(): void { trackWorkflowExecution(): void {
if (this.isOnboardingMode) { if (this.isOnboardingMode) {
// During onboarding, track basic execution without workflow context // During onboarding, track basic execution without workflow context
@@ -518,7 +523,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
} }
const context = this.getExecutionContext() 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 { trackExecutionError(metadata: ExecutionErrorMetadata): void {

View File

@@ -48,6 +48,7 @@ export interface RunButtonProperties {
subgraph_count: number subgraph_count: number
has_api_nodes: boolean has_api_nodes: boolean
api_node_names: string[] api_node_names: string[]
trigger_source?: ExecutionTriggerSource
} }
/** /**
@@ -70,6 +71,7 @@ export interface ExecutionContext {
total_node_count: number total_node_count: number
has_api_nodes: boolean has_api_nodes: boolean
api_node_names: string[] api_node_names: string[]
trigger_source?: ExecutionTriggerSource
} }
/** /**
@@ -193,6 +195,14 @@ export interface TemplateFilterMetadata {
total_count: number 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 * Help center opened metadata
*/ */
@@ -250,9 +260,10 @@ export interface TelemetryProvider {
trackAddApiCreditButtonClicked(): void trackAddApiCreditButtonClicked(): void
trackApiCreditTopupButtonPurchaseClicked(amount: number): void trackApiCreditTopupButtonPurchaseClicked(amount: number): void
trackApiCreditTopupSucceeded(): void trackApiCreditTopupSucceeded(): void
trackRunButton(options?: { subscribe_to_run?: boolean }): void trackRunButton(options?: {
trackRunTriggeredViaKeybinding(): void subscribe_to_run?: boolean
trackRunTriggeredViaMenu(): void trigger_source?: ExecutionTriggerSource
}): void
// Credit top-up tracking (composition with internal utilities) // Credit top-up tracking (composition with internal utilities)
startTopupTracking(): void startTopupTracking(): void
@@ -284,6 +295,9 @@ export interface TelemetryProvider {
// Template filter tracking events // Template filter tracking events
trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void trackTemplateFilterChanged(metadata: TemplateFilterMetadata): void
// Generic UI button click events
trackUiButtonClicked(metadata: UiButtonClickMetadata): void
// Help center events // Help center events
trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void trackHelpCenterOpened(metadata: HelpCenterOpenedMetadata): void
trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void trackHelpResourceClicked(metadata: HelpResourceClickedMetadata): void
@@ -321,8 +335,6 @@ export const TelemetryEvents = {
// Subscription Flow // Subscription Flow
RUN_BUTTON_CLICKED: 'app:run_button_click', 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', SUBSCRIPTION_REQUIRED_MODAL_OPENED: 'app:subscription_required_modal_opened',
SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked', SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked',
MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded', MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded',
@@ -368,12 +380,21 @@ export const TelemetryEvents = {
// Execution Lifecycle // Execution Lifecycle
EXECUTION_START: 'execution_start', EXECUTION_START: 'execution_start',
EXECUTION_ERROR: 'execution_error', 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 } as const
export type TelemetryEventName = export type TelemetryEventName =
(typeof TelemetryEvents)[keyof typeof TelemetryEvents] (typeof TelemetryEvents)[keyof typeof TelemetryEvents]
export type ExecutionTriggerSource =
| 'button'
| 'keybinding'
| 'legacy_ui'
| 'unknown'
/** /**
* Union type for all possible telemetry event properties * Union type for all possible telemetry event properties
*/ */
@@ -394,6 +415,7 @@ export type TelemetryEventProperties =
| NodeSearchMetadata | NodeSearchMetadata
| NodeSearchResultMetadata | NodeSearchResultMetadata
| TemplateFilterMetadata | TemplateFilterMetadata
| UiButtonClickMetadata
| HelpCenterOpenedMetadata | HelpCenterOpenedMetadata
| HelpResourceClickedMetadata | HelpResourceClickedMetadata
| HelpCenterClosedMetadata | HelpCenterClosedMetadata

View File

@@ -480,7 +480,8 @@ export class ComfyUI {
textContent: 'Queue Prompt', textContent: 'Queue Prompt',
onclick: () => { onclick: () => {
if (isCloud) { if (isCloud) {
useTelemetry()?.trackRunTriggeredViaMenu() useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' })
useTelemetry()?.trackWorkflowExecution()
} }
app.queuePrompt(0, this.batchCount) app.queuePrompt(0, this.batchCount)
} }
@@ -587,7 +588,8 @@ export class ComfyUI {
textContent: 'Queue Front', textContent: 'Queue Front',
onclick: () => { onclick: () => {
if (isCloud) { if (isCloud) {
useTelemetry()?.trackRunTriggeredViaMenu() useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' })
useTelemetry()?.trackWorkflowExecution()
} }
app.queuePrompt(-1, this.batchCount) app.queuePrompt(-1, this.batchCount)
} }

View File

@@ -1,7 +1,5 @@
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings' import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore' import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore' import { useDialogStore } from '@/stores/dialogStore'
@@ -66,15 +64,20 @@ export const useKeybindingService = () => {
// Prevent default browser behavior first, then execute the command // Prevent default browser behavior first, then execute the command
event.preventDefault() event.preventDefault()
if ( const runCommandIds = new Set([
isCloud && 'Comfy.QueuePrompt',
(keybinding.commandId === 'Comfy.QueuePrompt' || 'Comfy.QueuePromptFront',
keybinding.commandId === 'Comfy.QueuePromptFront' || 'Comfy.QueueSelectedOutputNodes'
keybinding.commandId === 'Comfy.QueueSelectedOutputNodes') ])
) { if (runCommandIds.has(keybinding.commandId)) {
useTelemetry()?.trackRunTriggeredViaKeybinding() await commandStore.execute(keybinding.commandId, {
metadata: {
trigger_source: 'keybinding'
}
})
} else {
await commandStore.execute(keybinding.commandId)
} }
await commandStore.execute(keybinding.commandId)
return return
} }

View File

@@ -9,7 +9,7 @@ import type { KeybindingImpl } from './keybindingStore'
export interface ComfyCommand { export interface ComfyCommand {
id: string id: string
function: () => void | Promise<void> function: (metadata?: Record<string, unknown>) => void | Promise<void>
label?: string | (() => string) label?: string | (() => string)
icon?: string | (() => string) icon?: string | (() => string)
@@ -24,7 +24,7 @@ export interface ComfyCommand {
export class ComfyCommandImpl implements ComfyCommand { export class ComfyCommandImpl implements ComfyCommand {
id: string id: string
function: () => void | Promise<void> function: (metadata?: Record<string, unknown>) => void | Promise<void>
_label?: string | (() => string) _label?: string | (() => string)
_icon?: string | (() => string) _icon?: string | (() => string)
_tooltip?: string | (() => string) _tooltip?: string | (() => string)
@@ -96,11 +96,17 @@ export const useCommandStore = defineStore('command', () => {
const { wrapWithErrorHandlingAsync } = useErrorHandling() const { wrapWithErrorHandlingAsync } = useErrorHandling()
const execute = async ( const execute = async (
commandId: string, commandId: string,
errorHandler?: (error: any) => void options?: {
errorHandler?: (error: unknown) => void
metadata?: Record<string, unknown>
}
) => { ) => {
const command = getCommand(commandId) const command = getCommand(commandId)
if (command) { if (command) {
await wrapWithErrorHandlingAsync(command.function, errorHandler)() await wrapWithErrorHandlingAsync(
() => command.function(options?.metadata),
options?.errorHandler
)()
} else { } else {
throw new Error(`Command ${commandId} not found`) throw new Error(`Command ${commandId} not found`)
} }

View File

@@ -3,8 +3,6 @@ import type { MenuItem } from 'primevue/menuitem'
import { ref } from 'vue' import { ref } from 'vue'
import { CORE_MENU_COMMANDS } from '@/constants/coreMenuCommands' 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 type { ComfyExtension } from '@/types/comfy'
import { useCommandStore } from './commandStore' import { useCommandStore } from './commandStore'
@@ -64,17 +62,7 @@ export const useMenuItemStore = defineStore('menuItem', () => {
.map( .map(
(command) => (command) =>
({ ({
command: () => { command: () => commandStore.execute(command.id),
if (
isCloud &&
(command.id === 'Comfy.QueuePrompt' ||
command.id === 'Comfy.QueuePromptFront' ||
command.id === 'Comfy.QueueSelectedOutputNodes')
) {
useTelemetry()?.trackRunTriggeredViaMenu()
}
return commandStore.execute(command.id)
},
label: command.menubarLabel, label: command.menubarLabel,
icon: command.icon, icon: command.icon,
tooltip: command.tooltip, tooltip: command.tooltip,

View File

@@ -114,5 +114,11 @@ export interface ExtensionManager {
export interface CommandManager { export interface CommandManager {
commands: ComfyCommand[] commands: ComfyCommand[]
execute(command: string, errorHandler?: (error: any) => void): void execute(
command: string,
options?: {
errorHandler?: (error: unknown) => void
metadata?: Record<string, unknown>
}
): void
} }