feat(telemetry): instrument the install -> boot -> canvas funnel - #1132
Conversation
Adds the missing desktop-side funnel steps so we can see where users drop between download and first render, instead of only knowing a launch started. - comfy.desktop.install.phase — per-phase install boundaries (start / end / error) for the standalone installer. PostHog carries the funnel timing; the error rows ride the Datadog mirror so a monitor can page when a phase hard-fails for a population after a release. - comfy.desktop.comfyui.boot_phase — launch-progress phase timings, buffered in memory per installation and flushed ONLY when the boot fails or times out (healthy-boot timing is already covered by instance_started, and boot_started alone is ~258k/14d — emitting per-phase on every boot would multiply that for no gain). The phases explain WHERE a failed boot stalled. - comfy.desktop.comfyui.boot_failed — port-wait timeout / early process exit / renderer load failure / render-process-gone. Datadog-mirrored; paired with the flushed boot_phase breakdown. - comfy.desktop.comfyui.canvas_rendered — first dom-ready of a LOCAL install's main frame (the bottom of the install->canvas funnel), with server_ready_to_canvas_ms. Deduped per launch; the failed-load leg is recorded separately. - comfy.desktop.first_use.abandoned — the first-use takeover unmounting without any completion path firing (the chooser-drop signal; pairs with first_use.completed to give onboarding its denominator). - comfy.desktop.cloud.entry_blocked — the cloud capacity gate on every gated entry (picker / first_use), with raw flag + tier + decision, so we can see how many cloud entries the kill-switch shed vs. warned vs. let through. boot_phase / install.phase buffers are bounded and terminally cleared.
📝 WalkthroughWalkthroughThe PR instruments multiple telemetry funnels across the desktop app: boot-phase timings buffered and only emitted on failure, canvas-rendered dedup tracking with success/failure signals, install-phase boundary callbacks wired through the installer and standalone install flow, session retry metrics propagated from main to renderer via IPC, and cloud capacity entry gating enriched with a source context for attribution. A first-use abandonment event is also added. ChangesMain-process boot, install, and canvas telemetry
Cloud capacity entry source tagging and first-use abandonment
Sequence DiagramssequenceDiagram
participant launch.ts
participant launchProgress.ts
participant bootPhaseBuffer
participant telemetry
participant _addSession
launch.ts->>bootPhaseBuffer: startBootPhases(installationId, variant)
launch.ts->>launchProgress.ts: createLaunchProgressTracker({ onPhaseEnter })
loop each boot phase entered
launchProgress.ts->>launch.ts: onPhaseEnter(phase)
launch.ts->>bootPhaseBuffer: recordBootPhase(installationId, phase)
end
alt terminal boot failure
launch.ts->>bootPhaseBuffer: flushBootPhasesOnFailure(installationId)
bootPhaseBuffer->>telemetry: emit boot_phase ×N
launch.ts->>telemetry: emit boot_failed(failedPhase, portRetries, rebootRetries)
else boot succeeded
launch.ts->>bootPhaseBuffer: clearBootPhases(installationId)
launch.ts->>_addSession: _addSession(id, info, bootTimeMs, { portRetries, rebootRetries })
end
sequenceDiagram
participant attach.ts
participant canvasEntry.ts
participant ipc/shared.ts
participant telemetry
attach.ts->>canvasEntry.ts: noteCanvasRendered(installationId)
canvasEntry.ts->>ipc/shared.ts: getSessionStartedAt(installationId)
ipc/shared.ts-->>canvasEntry.ts: startedAt (ms epoch)
canvasEntry.ts->>telemetry: emit canvas_rendered(server_ready_to_canvas_ms, load_failed=false)
Note over canvasEntry.ts: dedup guard set — subsequent success calls skipped
attach.ts->>canvasEntry.ts: noteCanvasRendered(id, { loadFailed: true })
canvasEntry.ts->>telemetry: emit canvas_rendered(load_failed=true)
Note over canvasEntry.ts: loadFailed bypasses and does not consume dedup slot
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
…pacity gate - bootPhaseBuffer: failure-only flush, first-write-wins per phase, bounded lifecycle (start resets, success clears, flush emits one boot_phase per buffered phase + returns last phase + clears). - canvasEntry: first-render dedup per installation, failed-load leg bypasses the dedup, server_ready_to_canvas_ms from the session anchor. - useCloudCapacity.confirmEntry: cloud.entry_blocked decisions (no_op / declined / proceeded), normal flag emits nothing, paid-user relaxation past a disabled kill-switch.
# Conflicts: # src/main/index.ts
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/lib/ipc/sessionActions/launch.ts (1)
132-147:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPer-attempt boot-phase buffering is reset, but phase-entry state is not, so retry failures can emit incomplete or wrong phase telemetry.
Because
launchTrackeris reused andonPhaseEnteronly fires on first entry per phase index, later retries can flush sparse/emptyboot_phasedata and a misleadingfailed_phase—telemetry should ride shotgun, not grab the wheel.Also applies to: 717-721, 797-803
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/lib/ipc/sessionActions/launch.ts` around lines 132 - 147, The `launchTracker` is reused across retry attempts but its phase-entry tracking state is not reset, causing `onPhaseEnter` callbacks to not fire on subsequent retries and resulting in incomplete or incorrect boot phase telemetry. In the `armLaunchTracker` function, remove the early return that reuses the existing `launchTracker` so that a fresh tracker is created for each launch attempt, ensuring `onPhaseEnter` fires properly for all retry attempts. Apply the same fix pattern to the other retry locations mentioned at lines 717-721 and 797-803 to ensure consistent boot phase telemetry across all retry scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/lib/canvasEntry.ts`:
- Line 34: The calculation of serverReadyToCanvasMs can produce negative values
when the system clock moves backward, which corrupts telemetry data. After
calculating the time difference in the line where serverReadyToCanvasMs is
assigned, apply a Math.max operation to clamp the value to a minimum of 0,
ensuring that any negative values are converted to 0 before the metric is
emitted.
In `@src/main/lib/installer.ts`:
- Around line 51-59: The onPhase callbacks in the withInstallPhase function can
throw exceptions that break the installation process, but they are meant to be
side-channel-only and should never affect the actual install flow. Wrap each of
the three onPhase invocations (the one at the start, the one on success with
durationMs, and the one on error with durationMs and error) in their own
try/catch blocks. In each catch block, silently handle the error without
rethrowing it so that callback failures never abort the install or mask the
actual install errors.
In `@src/main/sources/standalone/install.ts`:
- Around line 42-61: The emitInstallPhase function lacks error handling around
telemetry operations, which can cause the entire installation process to fail if
telemetry classification or emission fails. Wrap the telemetry logic (the
mainTelemetry.bucketError call and mainTelemetry.emit call) in a try/catch block
to ensure telemetry failures are caught and handled gracefully without
interrupting the install execution. The catch block should handle the error
silently or with minimal logging to prevent side-channel metrics from derailing
the installation process.
In `@src/renderer/src/composables/useCloudCapacity.test.ts`:
- Around line 44-45: The type annotation for the `composable` variable on line
44 is incorrectly resolving to the composable function itself rather than its
return value. Change the type of `composable` from `Awaited<ReturnType<typeof
importComposable>>` to properly represent the return type of calling the
useCloudCapacity function (the composable's actual return value, not the
function reference). This ensures the declared type matches what is actually
assigned on line 61 when composable is set to `ReturnType<typeof
useCloudCapacity>`.
---
Outside diff comments:
In `@src/main/lib/ipc/sessionActions/launch.ts`:
- Around line 132-147: The `launchTracker` is reused across retry attempts but
its phase-entry tracking state is not reset, causing `onPhaseEnter` callbacks to
not fire on subsequent retries and resulting in incomplete or incorrect boot
phase telemetry. In the `armLaunchTracker` function, remove the early return
that reuses the existing `launchTracker` so that a fresh tracker is created for
each launch attempt, ensuring `onPhaseEnter` fires properly for all retry
attempts. Apply the same fix pattern to the other retry locations mentioned at
lines 717-721 and 797-803 to ensure consistent boot phase telemetry across all
retry scenarios.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b68e4114-a061-48d7-9e10-d4fb1c104894
📒 Files selected for processing (19)
src/main/host/attach.tssrc/main/index.tssrc/main/lib/bootPhaseBuffer.test.tssrc/main/lib/bootPhaseBuffer.tssrc/main/lib/canvasEntry.test.tssrc/main/lib/canvasEntry.tssrc/main/lib/installer.tssrc/main/lib/ipc/sessionActions/launch.tssrc/main/lib/ipc/shared.tssrc/main/lib/launchProgress.tssrc/main/sources/standalone/install.tssrc/renderer/src/comfyTitlePopup/InstancePickerView.vuesrc/renderer/src/composables/useCloudCapacity.test.tssrc/renderer/src/composables/useCloudCapacity.tssrc/renderer/src/lib/rendererBootstrap.tssrc/renderer/src/views/ChooserView.vuesrc/renderer/src/views/FirstUseTakeover.vuesrc/shared/datadogMirroredEvents.tssrc/types/ipc.ts
Follow-ups to the merged install -> boot -> canvas instrumentation (#1132); all side-channel-hardening, no behavior change to the funnel events: - canvasEntry: clamp server_ready_to_canvas_ms to >= 0 so a backward clock step can't emit a negative, funnel-polluting duration. - installer.withInstallPhase: isolate every onPhase tap in try/catch so a throwing telemetry callback can never abort or mask an install. - install.emitInstallPhase: guard classification/emission so side-channel metrics fail quietly (this is also the installer's onPhase tap). - useCloudCapacity.test: fix the loadComposable return type — it resolved to the composable function, not its call result.
Summary
Adds the missing desktop-side funnel steps between download and first render, so we can see where users drop instead of only knowing that a launch started. Companion to the cloud-side funnel instrumentation.
Events added
comfy.desktop.install.phasecomfy.desktop.comfyui.boot_phasecomfy.desktop.comfyui.boot_failedcomfy.desktop.comfyui.canvas_renderedcomfy.desktop.first_use.abandonedcomfy.desktop.cloud.entry_blockedReview focus
boot_phaseis failure-only by design (bootPhaseBuffer.ts).boot_startedis ~258k/14d in prod; emitting a per-phase event on every healthy boot would multiply that for no analytical gain (healthy-boot timing is already ininstance_started.boot_time_ms). So phase timings are buffered in memory perinstallation_idand flushed only when the boot fails/times out, as the breakdown that explains where it stalled. Buffers are bounded (one per id, one entry per phase) and terminally cleared (success clears, failure flushes-then-clears, re-attempt resets).datadogMirroredEvents.ts): onlyinstall.phase(filterstatus:error) andboot_failedride the mirror;boot_phaseis PostHog-only funnel data.canvas_renderedis local-only — the cloud entry has its owncloud.enteredpath; this fires on the localdid-frame-finish-load, deduped per launch, with a separate failed-load leg.cloud.entry_blockedreports the raw capacity flag + tier + decision (no_op= hard-blocked,declined= backed out of the degraded warning,proceeded= entered through it), so a paid user relaxed past adisabledkill-switch reads asstatus: disabled, tier: paid, decision: proceeded. Skipped on anormalflag.Follow-up (not in this PR)
port_retriesintoinstance_started(P1 refinement) is deferred.Companion PRs
billing:subscription_succeeded/billing:subscription_renewed, sampledqueue_state_changed).