Files
open-design/docs/telemetry/responsiveness.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

14 KiB
Raw Permalink Blame History

Responsiveness telemetry (OPEND-3720)

This is an implementation candidate based on aa88f4387, not ticket acceptance, a deployment, or proof of an improvement. The scope is observation, not a fix for the underlying rendering stalls. The common envelope and original stability metrics remain defined in stability-metrics.md.

Integration status

The four new event schemas are registered in client-metrics.ts. The existing transport builds the common envelope with buildClientTelemetryParams and admits each new payload through parseClientMetricEvent; unknown fields are stripped and invalid payloads are dropped. Existing safety events keep their historical shape. The added metrics use metric_schema_version=1; historical event_schema_version semantics remain unchanged.

稳定性埋点沿用安全通道,不受匿名指标开关控制(telemetry.metrics)。 Long tasks, input latency, scroll frames, freeze events, recovery and chat-health reports all use the bounded safety buffer. Observers start when installed; there is no consent fetch, grant epoch or send-time withdrawal fence. Existing key/environment gates, visibility filters, throttles and budgets still apply. Local checks establish an implementation candidate, not QA acceptance.

Product analytics and content collection retain their existing consent rules. No DOM value, key text, selector, path, URL, prompt, attribution container name or raw PID is reported. Fixed DOM markers are read for classification only. The shared envelope supplies installation/session/platform/version metadata, using null for unavailable values. device_id is an installation, not an account. The early buffer is bounded and delivery remains best effort without ingestion acknowledgment. Missing PostHog credentials leave the transport idle.

Long tasks and qualified exposure

client_long_task retains its duration/start-time meaning for every observed foreground task with duration_ms >= 5000, including exactly 5000. It is emitted immediately, without a session cap. Occurrence time is the task start, not the subsequent flush time. No per-small-task network request or attribution scan remains. Tasks in the 100–4999ms range are counted in constant space.

client_responsiveness_window, measurement=long_task, reports every 30 seconds, including clean windows with zero counts. Its fields are:

  • long_task_count: tasks >=100ms, including severe tasks;
  • severe_task_count: tasks >=5000ms;
  • max_duration_ms: maximum observed duration, or null when no task occurred;
  • window_ms: measured eligible foreground exposure;
  • measurable, telemetry_capability, unavailable_reason: denominator gates.

Eligibility requires a successfully installed Long Tasks observer and an uninterrupted foreground interval. A hidden interval, hide/reveal between ticks, wall/monotonic disagreement >=1000ms, or delayed tick >35 seconds makes the window unmeasurable. Its counts and duration are null, not fake zeros. Unsupported API and an observer that throws are reported as unsupported. Entries predating observer installation/reveal are excluded; observation is deliberately unbuffered.

measurement=event_timing has its own support/foreground heartbeat. Support for one API never implies support for the other. Qualified exposure is renewed each window; initial installation alone is not an active-session denominator. Session identity comes from the common transport envelope; null session IDs cannot be used to manufacture session counts.

A session-day can be counted only with a measurable heartbeat for the relevant API. Daily aggregation must use occurrence time and the chosen reporting time zone, splitting the [occurred_at-window_ms, occurred_at] exposure interval at midnight when necessary. For the severe-stall KPI, intersect the numerator with the same eligible session/account-day cohort and deduplicate account-day, not event count. A severe event and its recovery summary must never both contribute to that numerator.

Recovery and what a renderer cannot prove

A single bounded local checkpoint stores an opaque recovery ID, timestamp, maximum observed severe duration and severe-task count. There is at most one ordinary storage write per 30-second foreground window, plus a write for each severe task before attempting its network report. No task list grows over time. Storage denial/corruption cannot interrupt the application. Metrics opt-out does not discard the checkpoint. Backgrounding and normal page exit clear unknown markers; an already observed severe checkpoint survives them for best-effort recovery.

On the next collector start, client_responsiveness_recovery reports:

  • observed_severe_task: callback ran and persisted a >=5s task before a possible failed send; original timestamp, maximum duration and count are preserved;
  • unclean_exit_unknown: only an unfinished foreground marker survived; duration is null and this is not proof of a >=5s stall.

Recovery is best effort and diagnostic. The current transport has no receipt acknowledgment, so an observed-severe summary may duplicate a successfully sent live event. Do not add recovery counts to live counts. The one origin-local checkpoint cannot distinguish overlapping windows/tabs, browser termination, crash, power loss, or OS sleep without a final callback. It can also be evicted or cleared. A process killed before its observer callback cannot persist its unknown duration. Exact fatal-hang detection and durable per-event delivery need an independent native watchdog/acknowledged outbox; they remain unimplemented. Do not use unknown-exit reports as the severe-stall KPI numerator.

Input attempts, qualified samples and latency

client_input_responsiveness reports one aggregate per active input area per 30-second window. The bounded area vocabulary is composer, search, settings and other_input. It covers chat inputs, semantic search controls, the actual project-search input marker and editable controls in .modal-settings. Password fields are excluded. Unmarked editable controls remain other_input; iframe contents and inputs inside closed shadow roots are not inspected.

  • input_attempt_count counts trusted DOM keydown attempts while the API is supported and foreground. It includes fast attempts; synthetic events do not count. These are keyboard interactions, not characters or submitted messages.
  • qualified_sample_count counts Event Timing keydown records >=16ms. Keyup, beforeinput and pointer records cannot duplicate a keyboard sample. Duplicate or pre-observation timing timestamps are ignored.
  • over_500ms_count counts qualified samples with duration strictly >500ms.
  • max_input_to_presentation_ms is Event Timing duration (input timestamp through the browser's next presentation), with its native 8ms quantization.
  • max_queue_delay_ms is separately processingStart-startTime; absent timing metadata stays null. It is not the same as total input-to-presentation time.

The supported sample rate is sum(over_500ms_count) / sum(qualified_sample_count) for the same area/eligible exposure. It is explicitly the rate among API-qualified samples, not all keystrokes. A zero denominator means not calculable. Counts are independent of window maxima and continue for the full session after the legacy report cap. An attempt with no exposed Event Timing record does not become a manufactured 0ms sample.

The DOM attempt count is a separate coverage counter: the API censors records below 16ms, targets can disappear, IME/mobile input may not produce keydowns, and API delivery can cross a window boundary. Consequently dividing a window's slow samples by its raw attempts does not establish an all-input latency rate. The complete all-input/IME rate remains not calculable with this candidate.

The historical client_chat_interaction_latency event keeps its >=200ms, chat-only 30-second maximum and 20-reports-per-page-session cap. Its interaction_count remains slow DOM Event Timing entries in that window, including pointer interactions. It is never a denominator for the new input rate. Strict 500ms boundary tests use reported duration; real QA should also use 600ms to avoid confusing API quantization with measurement error.

Scroll frames versus scroll freeze

client_chat_scroll_frames reports sampled chat-scroll cadence independently of client_chat_scroll_frozen. Trusted wheel/scroll activity in the chat log arms rAF for up to one second after activity, with a hard budget of 120 callbacks per 30-second window. Idle pages schedule no frame loop. It retains only counters and a maximum. Fields include frame_count, interval_count, over_50ms_count, max_frame_interval_ms, and sampling_truncated.

interval_count counts actual within-burst adjacent frame intervals, not first frames of bursts. It is the only suitable denominator for a sampled >50ms frame rate. Truncated windows describe only their sampled prefix; never extrapolate them to all scrolling. Visibility changes cancel/discard sampling, >=5s suspended frame gaps are discarded as ambiguous sleep/blockage, and the exposure clock also rejects sleep. Unsupported or unmeasurable values stay null.

The existing frozen event continues describing compositor/scroll-ceiling failure and its existing structural evidence. A frame gap alone does not synthesize a freeze, nor does successful scrolling prove the absence of a main-thread stall. Daily frozen-user / DAU needs an existing, verified account-day DAU source and matching eligible cohort. Neither freeze-event counts nor installation counts are DAU. Without that identity/DAU join the requested user ratio is not calculable.

Resource budget and local measurement

Budgets for new collection: <1% renderer CPU (300ms per 30-second window), <256KiB retained collector heap, <=120 sampled frames/window, constant-size state (four input areas, one checkpoint <=512 bytes), and <=8KiB of ordinary aggregate payloads/window for all four active areas plus heartbeats and one frame summary. Legacy chat maxima add at most 20 events/session. Severe tasks are uncapped by design and add one immediate report and one checkpoint write each. Transport framing/TLS and existing forensic census work are separate from the payload/collector budget. The grant check adds one <=3s config request.

A reproducible local headless Chromium 148 fixture bundled the real source collectors, injected 10,000 small-task callbacks and 10,000 input callbacks, and used 26 trusted keyboard attempts. OS renderer process CPU from CDP SystemInfo.getProcessInfo was 12.621ms; renderer task duration was 10.029ms. GC-to-GC retained JS heap increased 123,192 bytes. Eight local ingest requests used 6,519 JSON bytes for four active input areas, two heartbeats, the legacy maximum and one scroll-frame summary. No production endpoint or actual Open Design runtime was used.

These are fixture results, not an all-platform result. The measurement includes collector callback work and local serialization/delivery; it does not exercise actual PerformanceObserver entry delivery, reaching the frame budget, the existing freeze forensic census, production network behavior, or Electron on Windows/macOS. This 2026-10-19 refresh uses the registered schemas and final serializer. The script and raw JSON (3720-integration-budget.json) are retained beside the task logs outside the repository. CPU is the measured callback burst, not a 30-second physical-device CPU utilization reading; memory is one GC-to-GC delta, not a long-session leak test.

Verification and QA handoff

Focused tests cover severe thresholds, aggregation, clean/unsupported/background/ sleep exposure, recovery, input thresholds and counters, password exclusion, scroll frame budgets, schema privacy and occurrence time. Safety events are checked after metrics opt-out, including early buffer draining; non-stability analytics retains a regression for suppression. Exact commands, results and any baseline failures are recorded in the external task conclusion/logs. No full web or daemon suite is required locally.

QA must then:

  1. On macOS and Windows, install the candidate version and verify envelope/version/identity and all four aggregate event names in a local sink. Repeat startup, new-project entry and preview while typing/scrolling.
  2. Use a local test harness to block the foreground main thread for 5000–6000ms. Verify the immediate long-task duration and window count; repeat a burst of smaller tasks and confirm only the aggregate grows. Never run this in prod.
  3. In the chat composer, project search and settings, type fast and then inject a 600ms handler. Confirm attempt/sample/slow counts are separate from maxima, queue time differs from total presentation time, and counts continue after twenty legacy windows. Test IME/mobile separately and record coverage gaps.
  4. Scroll a long chat normally and with a controlled 80–200ms render stall. Verify the independent frame event, interval_count, truncation and no invented frozen event. Exercise a real compositor freeze separately.
  5. Hide/reveal, suspend/resume and disable each API in the harness. Verify null unmeasurable windows, no qualified denominator during those intervals, and no stale entry attribution after reveal.
  6. Revoke metrics before transport initialization and during activity; verify continued collection and buffered safety sends. Test unreadable consent and failed storage without breaking the app; confirm product analytics remains blocked and private fields remain absent.
  7. Kill the renderer after a severe callback checkpoint, restart and verify its diagnostic recovery summary. Kill it before the callback and verify only an unknown/null duration, not a synthetic 5s event. Test multiple app windows.
  8. Profile CPU/heap/network on physical macOS/Windows machines, including the schema-valid transport and existing freeze census. Attach exact build IDs, traces, consent state, API support and test steps.

Owners and dates for the native watchdog, acknowledged/deduplicated replay, complete IME/cross-frame input coverage, user identity/DAU aggregation and remaining platform profiling are unassigned pending integration-owner scheduling. Do not mark these residuals complete. Real ingestion, QA/product sign-off and at least seven days of post-release observation are still required; a 10/22 release cannot have seven-day evidence before 10/29.