Skip to content

Commit 941d80a

Browse files
christian-byrneChristian ByrneDrJKLbenceruleanluclaude
authored andcommitted
fix(telemetry): remove redundant run tracking; keep click analytics + single execution event (#6518)
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) 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. - 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 - `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 - ✅ 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 - `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 - 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 <[email protected]> Co-authored-by: Alexander Brown <[email protected]> Co-authored-by: Benjamin Lu <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 044b675 commit 941d80a

File tree

10 files changed

+107
-56
lines changed

10 files changed

+107
-56
lines changed

src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ import BatchCountEdit from '../BatchCountEdit.vue'
100100
101101
const workspaceStore = useWorkspaceStore()
102102
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
103-
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
103+
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
104104
105105
const { t } = useI18n()
106106
const queueModeMenuItemLookup = computed(() => {
@@ -158,9 +158,18 @@ const queuePrompt = async (e: Event) => {
158158
? 'Comfy.QueuePromptFront'
159159
: 'Comfy.QueuePrompt'
160160
161-
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
161+
if (batchCount.value > 1) {
162+
useTelemetry()?.trackUiButtonClicked({
163+
button_id: 'queue_run_multiple_batches_submitted'
164+
})
165+
}
162166
163-
await commandStore.execute(commandId)
167+
await commandStore.execute(commandId, {
168+
metadata: {
169+
subscribe_to_run: false,
170+
trigger_source: 'button'
171+
}
172+
})
164173
}
165174
</script>
166175

src/composables/useCoreCommands.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { useSubscription } from '@/platform/cloud/subscription/composables/useSu
2121
import { useSettingStore } from '@/platform/settings/settingStore'
2222
import { SUPPORT_URL } from '@/platform/support/config'
2323
import { useTelemetry } from '@/platform/telemetry'
24+
import type { ExecutionTriggerSource } from '@/platform/telemetry/types'
2425
import { useToastStore } from '@/platform/updates/common/toastStore'
2526
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
2627
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -465,7 +466,11 @@ export function useCoreCommands(): ComfyCommand[] {
465466
label: 'Queue Prompt',
466467
versionAdded: '1.3.7',
467468
category: 'essentials' as const,
468-
function: async () => {
469+
function: async (metadata?: {
470+
subscribe_to_run?: boolean
471+
trigger_source?: ExecutionTriggerSource
472+
}) => {
473+
useTelemetry()?.trackRunButton(metadata)
469474
if (!isActiveSubscription.value) {
470475
showSubscriptionDialog()
471476
return
@@ -484,7 +489,11 @@ export function useCoreCommands(): ComfyCommand[] {
484489
label: 'Queue Prompt (Front)',
485490
versionAdded: '1.3.7',
486491
category: 'essentials' as const,
487-
function: async () => {
492+
function: async (metadata?: {
493+
subscribe_to_run?: boolean
494+
trigger_source?: ExecutionTriggerSource
495+
}) => {
496+
useTelemetry()?.trackRunButton(metadata)
488497
if (!isActiveSubscription.value) {
489498
showSubscriptionDialog()
490499
return
@@ -502,7 +511,11 @@ export function useCoreCommands(): ComfyCommand[] {
502511
icon: 'pi pi-play',
503512
label: 'Queue Selected Output Nodes',
504513
versionAdded: '1.19.6',
505-
function: async () => {
514+
function: async (metadata?: {
515+
subscribe_to_run?: boolean
516+
trigger_source?: ExecutionTriggerSource
517+
}) => {
518+
useTelemetry()?.trackRunButton(metadata)
506519
if (!isActiveSubscription.value) {
507520
showSubscriptionDialog()
508521
return
@@ -525,6 +538,17 @@ export function useCoreCommands(): ComfyCommand[] {
525538
// Get execution IDs for all selected output nodes and their descendants
526539
const executionIds =
527540
getExecutionIdsForSelectedNodes(selectedOutputNodes)
541+
542+
if (executionIds.length === 0) {
543+
toastStore.add({
544+
severity: 'error',
545+
summary: t('toastMessages.failedToQueue'),
546+
detail: t('toastMessages.failedExecutionPathResolution'),
547+
life: 3000
548+
})
549+
return
550+
}
551+
useTelemetry()?.trackWorkflowExecution()
528552
await app.queuePrompt(0, batchCount, executionIds)
529553
}
530554
},

src/extensions/core/groupNode.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,21 +1742,22 @@ const ext: ComfyExtension = {
17421742
label: 'Convert selected nodes to group node',
17431743
icon: 'pi pi-sitemap',
17441744
versionAdded: '1.3.17',
1745-
function: convertSelectedNodesToGroupNode
1745+
function: () => convertSelectedNodesToGroupNode()
17461746
},
17471747
{
17481748
id: 'Comfy.GroupNode.UngroupSelectedGroupNodes',
17491749
label: 'Ungroup selected group nodes',
17501750
icon: 'pi pi-sitemap',
17511751
versionAdded: '1.3.17',
1752-
function: ungroupSelectedGroupNodes
1752+
function: () => ungroupSelectedGroupNodes()
17531753
},
17541754
{
17551755
id: 'Comfy.GroupNode.ManageGroupNodes',
17561756
label: 'Manage group nodes',
17571757
icon: 'pi pi-cog',
17581758
versionAdded: '1.3.17',
1759-
function: manageGroupNodes
1759+
function: (...args: unknown[]) =>
1760+
manageGroupNodes(args[0] as string | undefined)
17601761
}
17611762
],
17621763
keybindings: [

src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
ExecutionContext,
1717
ExecutionErrorMetadata,
1818
ExecutionSuccessMetadata,
19+
ExecutionTriggerSource,
1920
HelpCenterClosedMetadata,
2021
HelpCenterOpenedMetadata,
2122
HelpResourceClickedMetadata,
@@ -59,6 +60,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
5960
private mixpanel: OverridedMixpanel | null = null
6061
private eventQueue: QueuedEvent[] = []
6162
private isInitialized = false
63+
private lastTriggerSource: ExecutionTriggerSource | undefined
6264

6365
// Onboarding mode - starts true, set to false when app is fully ready
6466
private isOnboardingMode = true
@@ -354,7 +356,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
354356
clearTopupUtil()
355357
}
356358

357-
trackRunButton(options?: { subscribe_to_run?: boolean }): void {
359+
trackRunButton(options?: {
360+
subscribe_to_run?: boolean
361+
trigger_source?: ExecutionTriggerSource
362+
}): void {
358363
if (this.isOnboardingMode) {
359364
// During onboarding, track basic run button click without workflow context
360365
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, {
@@ -365,7 +370,8 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
365370
total_node_count: 0,
366371
subgraph_count: 0,
367372
has_api_nodes: false,
368-
api_node_names: []
373+
api_node_names: [],
374+
trigger_source: options?.trigger_source
369375
})
370376
return
371377
}
@@ -380,20 +386,14 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
380386
total_node_count: executionContext.total_node_count,
381387
subgraph_count: executionContext.subgraph_count,
382388
has_api_nodes: executionContext.has_api_nodes,
383-
api_node_names: executionContext.api_node_names
389+
api_node_names: executionContext.api_node_names,
390+
trigger_source: options?.trigger_source
384391
}
385392

393+
this.lastTriggerSource = options?.trigger_source
386394
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties)
387395
}
388396

389-
trackRunTriggeredViaKeybinding(): void {
390-
this.trackEvent(TelemetryEvents.RUN_TRIGGERED_KEYBINDING)
391-
}
392-
393-
trackRunTriggeredViaMenu(): void {
394-
this.trackEvent(TelemetryEvents.RUN_TRIGGERED_MENU)
395-
}
396-
397397
trackSurvey(
398398
stage: 'opened' | 'submitted',
399399
responses?: SurveyResponses
@@ -518,7 +518,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
518518
}
519519

520520
const context = this.getExecutionContext()
521-
this.trackEvent(TelemetryEvents.EXECUTION_START, context)
521+
const eventContext: ExecutionContext = {
522+
...context,
523+
trigger_source: this.lastTriggerSource ?? 'unknown'
524+
}
525+
this.trackEvent(TelemetryEvents.EXECUTION_START, eventContext)
526+
this.lastTriggerSource = undefined
522527
}
523528

524529
trackExecutionError(metadata: ExecutionErrorMetadata): void {

src/platform/telemetry/types.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface RunButtonProperties {
4848
subgraph_count: number
4949
has_api_nodes: boolean
5050
api_node_names: string[]
51+
trigger_source?: ExecutionTriggerSource
5152
}
5253

5354
/**
@@ -70,6 +71,7 @@ export interface ExecutionContext {
7071
total_node_count: number
7172
has_api_nodes: boolean
7273
api_node_names: string[]
74+
trigger_source?: ExecutionTriggerSource
7375
}
7476

7577
/**
@@ -250,9 +252,10 @@ export interface TelemetryProvider {
250252
trackAddApiCreditButtonClicked(): void
251253
trackApiCreditTopupButtonPurchaseClicked(amount: number): void
252254
trackApiCreditTopupSucceeded(): void
253-
trackRunButton(options?: { subscribe_to_run?: boolean }): void
254-
trackRunTriggeredViaKeybinding(): void
255-
trackRunTriggeredViaMenu(): void
255+
trackRunButton(options?: {
256+
subscribe_to_run?: boolean
257+
trigger_source?: ExecutionTriggerSource
258+
}): void
256259

257260
// Credit top-up tracking (composition with internal utilities)
258261
startTopupTracking(): void
@@ -321,8 +324,6 @@ export const TelemetryEvents = {
321324

322325
// Subscription Flow
323326
RUN_BUTTON_CLICKED: 'app:run_button_click',
324-
RUN_TRIGGERED_KEYBINDING: 'app:run_triggered_keybinding',
325-
RUN_TRIGGERED_MENU: 'app:run_triggered_menu',
326327
SUBSCRIPTION_REQUIRED_MODAL_OPENED: 'app:subscription_required_modal_opened',
327328
SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked',
328329
MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded',
@@ -374,6 +375,12 @@ export const TelemetryEvents = {
374375
export type TelemetryEventName =
375376
(typeof TelemetryEvents)[keyof typeof TelemetryEvents]
376377

378+
export type ExecutionTriggerSource =
379+
| 'button'
380+
| 'keybinding'
381+
| 'legacy_ui'
382+
| 'unknown'
383+
377384
/**
378385
* Union type for all possible telemetry event properties
379386
*/

src/scripts/ui.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,8 @@ export class ComfyUI {
480480
textContent: 'Queue Prompt',
481481
onclick: () => {
482482
if (isCloud) {
483-
useTelemetry()?.trackRunTriggeredViaMenu()
483+
useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' })
484+
useTelemetry()?.trackWorkflowExecution()
484485
}
485486
app.queuePrompt(0, this.batchCount)
486487
}
@@ -587,7 +588,8 @@ export class ComfyUI {
587588
textContent: 'Queue Front',
588589
onclick: () => {
589590
if (isCloud) {
590-
useTelemetry()?.trackRunTriggeredViaMenu()
591+
useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' })
592+
useTelemetry()?.trackWorkflowExecution()
591593
}
592594
app.queuePrompt(-1, this.batchCount)
593595
}

src/services/keybindingService.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
2-
import { isCloud } from '@/platform/distribution/types'
32
import { useSettingStore } from '@/platform/settings/settingStore'
4-
import { useTelemetry } from '@/platform/telemetry'
53
import { app } from '@/scripts/app'
64
import { useCommandStore } from '@/stores/commandStore'
75
import { useDialogStore } from '@/stores/dialogStore'
@@ -66,15 +64,20 @@ export const useKeybindingService = () => {
6664

6765
// Prevent default browser behavior first, then execute the command
6866
event.preventDefault()
69-
if (
70-
isCloud &&
71-
(keybinding.commandId === 'Comfy.QueuePrompt' ||
72-
keybinding.commandId === 'Comfy.QueuePromptFront' ||
73-
keybinding.commandId === 'Comfy.QueueSelectedOutputNodes')
74-
) {
75-
useTelemetry()?.trackRunTriggeredViaKeybinding()
67+
const runCommandIds = new Set([
68+
'Comfy.QueuePrompt',
69+
'Comfy.QueuePromptFront',
70+
'Comfy.QueueSelectedOutputNodes'
71+
])
72+
if (runCommandIds.has(keybinding.commandId)) {
73+
await commandStore.execute(keybinding.commandId, {
74+
metadata: {
75+
trigger_source: 'keybinding'
76+
}
77+
})
78+
} else {
79+
await commandStore.execute(keybinding.commandId)
7680
}
77-
await commandStore.execute(keybinding.commandId)
7881
return
7982
}
8083

src/stores/commandStore.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { KeybindingImpl } from './keybindingStore'
99

1010
export interface ComfyCommand {
1111
id: string
12-
function: () => void | Promise<void>
12+
function: (metadata?: Record<string, unknown>) => void | Promise<void>
1313

1414
label?: string | (() => string)
1515
icon?: string | (() => string)
@@ -24,7 +24,7 @@ export interface ComfyCommand {
2424

2525
export class ComfyCommandImpl implements ComfyCommand {
2626
id: string
27-
function: () => void | Promise<void>
27+
function: (metadata?: Record<string, unknown>) => void | Promise<void>
2828
_label?: string | (() => string)
2929
_icon?: string | (() => string)
3030
_tooltip?: string | (() => string)
@@ -96,11 +96,17 @@ export const useCommandStore = defineStore('command', () => {
9696
const { wrapWithErrorHandlingAsync } = useErrorHandling()
9797
const execute = async (
9898
commandId: string,
99-
errorHandler?: (error: any) => void
99+
options?: {
100+
errorHandler?: (error: unknown) => void
101+
metadata?: Record<string, unknown>
102+
}
100103
) => {
101104
const command = getCommand(commandId)
102105
if (command) {
103-
await wrapWithErrorHandlingAsync(command.function, errorHandler)()
106+
await wrapWithErrorHandlingAsync(
107+
() => command.function(options?.metadata),
108+
options?.errorHandler
109+
)()
104110
} else {
105111
throw new Error(`Command ${commandId} not found`)
106112
}

src/stores/menuItemStore.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import type { MenuItem } from 'primevue/menuitem'
33
import { ref } from 'vue'
44

55
import { CORE_MENU_COMMANDS } from '@/constants/coreMenuCommands'
6-
import { isCloud } from '@/platform/distribution/types'
7-
import { useTelemetry } from '@/platform/telemetry'
86
import type { ComfyExtension } from '@/types/comfy'
97

108
import { useCommandStore } from './commandStore'
@@ -64,17 +62,7 @@ export const useMenuItemStore = defineStore('menuItem', () => {
6462
.map(
6563
(command) =>
6664
({
67-
command: () => {
68-
if (
69-
isCloud &&
70-
(command.id === 'Comfy.QueuePrompt' ||
71-
command.id === 'Comfy.QueuePromptFront' ||
72-
command.id === 'Comfy.QueueSelectedOutputNodes')
73-
) {
74-
useTelemetry()?.trackRunTriggeredViaMenu()
75-
}
76-
return commandStore.execute(command.id)
77-
},
65+
command: () => commandStore.execute(command.id),
7866
label: command.menubarLabel,
7967
icon: command.icon,
8068
tooltip: command.tooltip,

src/types/extensionTypes.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,11 @@ export interface ExtensionManager {
114114

115115
export interface CommandManager {
116116
commands: ComfyCommand[]
117-
execute(command: string, errorHandler?: (error: any) => void): void
117+
execute(
118+
command: string,
119+
options?: {
120+
errorHandler?: (error: unknown) => void
121+
metadata?: Record<string, unknown>
122+
}
123+
): void
118124
}

0 commit comments

Comments
 (0)