Files
open-design/docs/telemetry/stability-metrics.md
5b6fbb55ea [OPEND-3712] feat(telemetry): stability metrics for launch, project entry, and memory (#8805)
* feat(analytics): define client metrics schema and unify telemetry metadata

* feat(desktop): measure process startup phases through business readiness

Add launch_attempt, business_ready, and launch_result timing with monotonic
phase offsets, per-attempt correlation, shared app session identity, explicit
process/activation measurement boundaries, and idempotent terminal results.
Preserve packaged sidecar phases across the desktop handoff and report bounded
startup failures without blocking successful startup on telemetry.

Window mount/reveal alone does not declare business Ready. Preload/web owners
must wire od:startup:context and od:startup:business-ready with matching
launch_attempt_id/app_session_id plus required_data_ready/actions_ready=true.

Align with the shared event registry/schema and envelope fields (device_id,
event_id, app_session_id, client_type, capture_source, env, channel,
app_version, os_version, arch, occurred_at, reported_at, capability), plus
startup_type, launch_attempt_id, started_at, duration_ms, outcome, reason,
failure_stage, stage_offsets_ms, stage_durations_ms, measurement_start,
start_time_source, and os_launch_time_available. The existing daemon safety
bridge overwrites some envelope fields; shared-envelope alignment stays with
that owner.

Coverage limits: no OS icon-click timestamp or separate launcher-process span;
no new packaged launch report without real installation identity; failures
before packaged runtime context is armed and forced process exits can be
unobserved. Cold means a fresh desktop process; warm means hidden-window
restore; reopen means desktop restored from a headless owner. Measurement
acceptance and preload ownership need maintainer confirmation; native
QA/publication remain.

Validation (local Node 24 / pnpm 10.33.2):
- Red specs captured before implementation.
- desktop focused tests: 7 files, 47 passed.
- packaged focused tests: 6 files, 87 passed.
- pnpm --filter @open-design/desktop typecheck: passed.
- pnpm --filter @open-design/packaged typecheck: passed.
- pnpm guard: passed.
- pnpm typecheck: passed after building missing local workspace artifacts.
- git diff --cached --check: passed.

Local commit only. No web/memory-sampling changes or external operations.

* feat(web): measure project navigation intent to actionable ready

* feat(observability): add bounded periodic process memory sampling

* feat(telemetry): integrate launch readiness and bounded process memory metrics

Unify metric envelopes, classify process launches with a persisted heuristic, wire preload and renderer readiness, cancel abandoned navigation, and bind consent-aware memory sampling to the Electron lifecycle. Document the metric definitions and stabilize validation fixtures that inherited host configuration or raced signal-handler readiness.

* docs(telemetry): translate stability drafts to English

Rewrite the historical project-entry draft in English and neutralize the
stability-metrics intro so odcrew-facing docs stay English-only.

* fix(telemetry): distinguish Linux Node installs from packaged runtimes

* fix(telemetry): wait for committed conversation navigation before readiness

* fix(telemetry): exclude deferred headless restores from launch timing

* fix(telemetry): require current Home project and agent reads to succeed

* fix(telemetry): fail closed on damaged memory sampling consent

* docs(telemetry): note catalog read failures cannot block Home readiness

* feat(telemetry): prepare update stage and desktop restart observations

* feat(telemetry): prepare responsiveness collectors pending transport integration

* fix(desktop): cancel consent lookup and anchor memory sampling to ready

* fix(telemetry): classify and correlate crash and exit reports

* feat(telemetry): measure consented visible preview attempts

* feat(telemetry): wire page switch intent and readiness timing

* fix(telemetry): validate responsiveness metrics and fence revoked buffers

* fix(telemetry): require successful scoped Home resources before readiness

* fix(telemetry): clear chat health collection on consent withdrawal

* docs(telemetry): record integrated responsiveness validation and budgets

* fix(telemetry): admit update lifecycle stage metrics

* test(contracts): restore merged client metric schema cases

* fix(telemetry): route native stability metrics through safety capture

Remove metrics-consent checks from startup, memory, exit and update-stage
reporting while preserving product analytics consent and privacy projection.

* docs(telemetry): align stability metrics with safety-channel policy

Document consent-independent stability reporting and unchanged product/content
consent, privacy projection, budgets and remaining native QA.

* fix(telemetry): observe runtime daemon exits and correlate memory samples

Capture parent-observed daemon death through the safety channel, preserve stronger evidence across restart, and join exits to real packaged memory sampling with opaque process IDs and nullable V8 heap limits.

* fix(telemetry): send web stability metrics on the safety channel

Drop the metrics-consent gate from web responsiveness, preview and chat
health collectors to match the native safety-channel routing, and remove
test hooks for the deleted consent API.

* test(web): record an open intent before asserting a trusted first paint

* test(web): align component tests with commit-driven navigation and visible-preview gating

ProjectView switches conversations in the router onCommit callback, preview
diagnostics require a visible frame, and catalog/page-switch reads changed
their call shape; update mocks and expectations accordingly.

* fix(telemetry): require successful catalogs before reporting page navigation ready

A failed design-system/skill/template read cleared loading without marking the
catalog ready, so page switches still reported ready. Mirror the startup gate.

* fix(desktop): prune expired stage-only update check flows

Every scheduled check opens a metric flow directory. Remove stage-only flows
past the replay TTL during the existing startup scan; installer flows stay.

* fix(telemetry): isolate reporting from navigation and catalog fallback

* fix(telemetry): preserve business behavior with passive observation

* fix(telemetry): classify daemon channel by service packaged flag

On Linux CI the Node binary lives under /opt/, so execPath heuristics
marked unpackaged runs as packaged and reported channel=stable. Without
an explicit or version-inferred channel, derive it from the service's
packagedRuntime flag instead. Also restore the desktop SHOW-hook boundary
assertion to match activeDesktop.show().

* fix(telemetry): keep killed daemons and fresh-install launch attempts observable

OPEND-4221: the sidecar supervisor exited 0 whenever its target died from a
signal, so an external SIGKILL of the daemon reached packaged as
code=0/signal=null and was reported as clean-exit. The supervisor now exits 0
only for stops it forwarded itself and encodes any other terminating signal as
128 + N; observeDaemonExit decodes that back to reason=killed with the signal.

OPEND-4220: packaged flushed client_launch_attempt directly at daemon-ready and
dropped it when installation.json did not exist yet, which is always the case
on a fresh install. Launch events now stay buffered until desktop main arms the
daemon-backed reporter, and the daemon retains launch metrics (bounded, in
order) until the installation identity exists, replaying them before later
events. Explicit opt-out still delivers immediately on the safety channel.

* fix(telemetry): exclude url_load previews from the preview denominator

trackVisiblePreview probes the frame with od:preview-metric-probe and, after
15s without od:preview-metric-result, finishes the attempt as error /
probe_unverified. URL-loaded artifacts never receive the srcdoc observability
bridge, so they cannot answer; every visible, successful URL preview was
therefore counted as an error. Frames that cannot answer the probe are now
excluded from the denominator, matching how the PR scopes Live Artifact HTML.

* fix(telemetry): move renderer safety identity to the installation id on fresh install

OPEND-4220 retest: on a fresh install the renderer safety channel bootstraps
before the installation id is minted, falls back to the anonymous id, and kept
it for the whole session, so renderer stability events carried a different
device_id than native launch/memory events. setIdentity now also patches the
safety context, and a late bootstrap keeps the installation id.

---------

Co-authored-by: heggria <bshengtao@gmail.com>
2026-10-22 05:55:52 +00:00

12 KiB

Stability telemetry metrics

Authoritative definition for the integrated stability telemetry on this branch (based on 9b5dc619d). This definition supersedes the earlier local launch and project-entry drafts wherever they differ. These are internal product diagnostics; no new user-facing capability or CLI command is introduced.

Launch

A launch is a new desktop main process, including the packaged process before sidecar startup. Start is Node's main-process performance.timeOrigin (monotonic offset zero), never Electron ready, module evaluation, window creation, or an inferred OS icon click. Installed launcher work before this process is outside the span. Hidden-window activation/focus does not start another process launch. Headless-only/delegating processes do not synthesize desktop success. Restoring a window in the same deferred headless process, including retries after a failed restore, does not create a launch attempt or any launch timing.

client_launch_attempt, client_business_ready, and client_launch_result share launch_attempt_id and app_session_id. Terminal results are idempotent. The three duration fields are milliseconds from the same process origin:

Field End point
launch_to_window_shown Actual main-window reveal (onRevealed), including fallback reveal
launch_to_first_visible First committed content surface, checked on a foreground animation frame
launch_to_interactive Usable inputs/actions and required data ready, on a foreground frame; headline

The renderer obtains attempt/session IDs through the optional host startup bridge. Preload forwards only context/readiness messages; main verifies the owning main WebContents, top frame and app origin. Pet/splash/foreign frames cannot declare Ready. Merely mounting a loading placeholder or showing a window does not declare business readiness. Home requires a successful project-list read for the current account/workspace/view, with no failure recorded for that scope, successful agent discovery and config, and current-scope resource catalogs; merely clearing loading flags is insufficient. Skills, design templates, design systems and prompt templates must each finish a successful catalog read; a successful empty catalog is usable, while a failed request cannot declare Home Ready. Workspace-scoped catalogs must succeed under the current identity. Non-startup callers retain their existing empty-list fallback on request failure. A project waits for current detail, workspace scope, conversation/message authority, initialized messages and enabled send/new-conversation actions. The layout effect runs after the corresponding React controls commit. A hidden window waits for foreground reveal. Later route changes cancel pending effects.

Main retains sidecar/runtime phase offsets. The foreground renderer signal marks first_visible; interactive_ready also requires required_data_ready=true and actions_ready=true. duration_ms on business Ready and ready result is the headline duration. A launch failure/cancellation does not manufacture success or missing phase durations. Window reveal alone can still have a cancelled result.

Launch type heuristic

launch_type is cold or warm; it describes an operational heuristic rather than verified filesystem/OS cache state. A small stability-launch.json in Electron userData holds {version,lastRunAt}. Cold means any of:

  • marker absent, unreadable, malformed, or timestamp invalid/future;
  • active payload app version differs (first run after install/upgrade, also downgrade; not the carrier version);
  • now - lastRunAt > os.uptime() * 1000 (first run since OS boot);
  • now - lastRunAt > 30 minutes (exactly 30 minutes remains warm).

Otherwise it is warm. Classification is independent of the measurement origin: a warm process launch still starts at main-process origin. The compatibility startup_type field follows the same classification for process launches. A crash fallback is written at launch; normal will-quit or process exit updates lastRunAt to the end. A hard kill cannot write an end timestamp, so a long-running process killed shortly before restart can be classified cold. Clock adjustments and missing metadata conservatively bias toward cold. The marker has no account, project, prompt, or credential data; write failure cannot block the app. Classification is not a precise OS cache measurement.

Project entry

The click/open intent starts a navigation attempt before async project resolution. client_navigation_result carries navigation_id, started_at, navigation_type=project_enter, source/target type, and result:

  • started: intent receipt;
  • first_visible with readiness_point=first_visible: first committed project content on a foreground frame; this is not terminal;
  • ready with readiness_point=interactive_ready: current project data, message authority, editor/inputs and actions usable; headline metric;
  • cancelled / failed: explicit terminal reason, excluded from success latency.

Both visible and interactive durations use the same intent origin. First-visible is deduplicated; terminal results are deduplicated and stale completions ignored. Readiness failure, switching away, supersession, scope change, page exit, router guard rejection and same-route no-op cannot emit success. Router onCancel is per navigation, so an older guard cannot cancel a newer attempt. Conversation-switch attempts use the same schema with a distinct navigation type. Initial direct routes, create/import/duplicate and external deep links do not invent a click intent; their launch can still have desktop readiness signals. Read-only or otherwise disabled actions do not satisfy actionable Ready.

Conversation-switch surfaces become eligible for first-visible and Ready only after that navigation commits. A pending guard cannot declare either point; a rejected guard cancels the correlated navigation attempt.

Memory

The Electron sampler starts after app.whenReady(). Its first collection is 30 seconds after ready, then every 60 seconds. app.getAppMetrics() provides main (Browser), owned renderer (Tab), GPU and utility process RSS. The main process additionally uses process.memoryUsage() for JS heap/external memory and as an RSS fallback. Renderer/GPU/utility heap/external are null, not fabricated zeros. Electron workingSetSize is KiB and is converted to bytes. A missing native process category is omitted, not zero-filled.

Each process emits client_process_memory_sample: session/version/platform/OS/ architecture, role, opaque process-instance ID, uptime, byte fields, sample time and interval. PID/creation-time hashes distinguish PID reuse without transmitting raw PIDs. Main has one process UUID. No paths/content are collected. App session IDs are shared with launch; desktop renderer metric bootstrap obtains the native session ID through the host bridge (browser session identity remains separate).

稳定性埋点沿用安全通道,不受匿名指标开关控制(telemetry.metrics)。 Memory collection, ingestion and safety capture continue when metrics is false or the consent config is unavailable; no consent lookup is performed. before-quit, will-quit, and explicit desktop shutdown stop the sampler idempotently, including before its first sample. No new sample or remaining batch request is sent after stop; in-flight HTTP is aborted, although an already-received remote event cannot be recalled.

Budget: less than 1% CPU, i.e. less than 600 ms sampler CPU per 60-second period. Collection uses one app metrics call, one main memory read and at most 64 native metrics. No heap snapshots, renderer evaluation, subprocesses, retained history, retry queue or overlapping batches. Slow delivery skips ticks; requests time out in 5 seconds. Fake-clock tests cover cadence, long operation, safety capture after opt-out, PID reuse, absent processes, slow/failing sinks and stop. A local macOS arm64 Electron probe (isolated userData, activation prohibited, show:false, 32 ticks, main/renderer/GPU/utility present) measured 0.682 ms of main CPU per sample, equivalent to 0.00114% at the production cadence. This measures collection and fixture delivery, not production HTTP transport or every OS/Electron build; Windows/Linux and production transport profiling remain. The reproducible local probe and output are retained in the task validation logs.

The cherry-picked daemon-health memory extension remains opt-in and unarmed by default; it is not one of the three Electron process categories above.

Product analytics retains its consent checks, including fail-closed config reads. The optional daemon-health sampler remains unarmed by default. Safety capture still requires the existing PostHog key/environment gates; missing credentials do not trigger network traffic. Content consent and privacy projection are unchanged.

Envelope and verification

Common fields/schema are owned by packages/contracts/analytics: event_schema_version=4, metric_schema_version=1, event/install/app-session identity, client/source, env/channel/app version, platform/product OS version, architecture, occurrence/report times and capability. A Linux Node installation under /opt or /usr/share alone is not evidence of a packaged runtime; packaged resources or an injected app version establish that classification. Unsupported values stay null. Producers normalize draft names to this schema; undeclared properties are stripped. The packaged HTTPS serializer preserves this validated metric envelope instead of applying the legacy crash schema 2 / packaged_main envelope. Its serialized-payload regression failed before that transport fix and passed after. Tests check actual produced launch/navigation payload admission, not only independently manufactured schema fixtures.

Integration includes local commits 36f88b286, 5b9c47556, 4bbe85fce and df1920f95 via cherry-pick, followed by wiring and regression fixes. The optional cc9f214cf Top5 draft is omitted: it depends on unimplemented client_metric_cohort eligibility and a historical candidate query model. No production Top5 or success rate is claimed from this branch.

Run package tests and typechecks for contracts, host, desktop, packaged, web and daemon, plus repo guard/typecheck. Results and exact commands are recorded in the local delivery copy stability-telemetry.md requested by the integration owner. Tests use unit/HTTP fixtures and hidden (show:false) Electron capture. No interactive browser/runtime launch, deployment, remote query, push, PR or merge is part of this work. Native installation QA, live ingestion verification, real hardware CPU profiling, Top5/cohort analytics and product acceptance remain outside local test evidence.

The full daemon validation exposed two existing cancellation-fixture races: process/running markers could appear before the fake CLI installed its SIGTERM handler. The fixtures now signal handler readiness before tests cancel them (connection-test.test.ts and od-next-automatic-simple-server.test.ts). Both failed before the fixture fix and passed afterward; assertions and production cancellation behavior retain their existing contract. Claude auth tests require unsetting inherited ANTHROPIC_BASE_URL for their standard-login scenarios; this changes only the test command environment, not local credentials/config.

Two Codex preflight suites also inherited macOS managed preferences and therefore exercised the managed-config fail-open branch instead of their declared ordinary CLI fixtures. They now fixture the absence of host MDM while retaining explicit system/managed-config cases. Their 12 tests pass; no actual host preference is changed by the fixtures.