Files
lefarcen e899fb93fe feat(packaged): main-process telemetry for pre-daemon startup failures (#4696)
* feat(packaged): main-process telemetry for pre-daemon startup failures

The daemon hosts the PostHog client, so when daemon/web sidecars die before
reporting status (issue #4638: better-sqlite3 vanished from the bundle ->
ERR_MODULE_NOT_FOUND), nothing emits telemetry and the whole pre-daemon crash
class is invisible on every dashboard. Add a main-process direct capture on
the fatal-exit path: classify daemon-vs-web by log-path segment, parse the real
error code + missing module from the sidecar log tail, scrub home dirs, and
POST one packaged_runtime_failed event over plain fetch (no posthog-node SDK,
no new dependency).

Zero happy-path side-effects by construction: all logic runs only inside
main().catch, fetch is wrapped in Promise.race with a hard timeout, errors are
swallowed, and it is a no-op without a PostHog key.

Crash telemetry follows the existing captureSafety policy (stability data is
retained even for opted-out users); Settings -> Privacy copy follow-up tracked
in the PR. Slice B of the crash-governance spec (PR #4694).

* fix(packaged): Windows web-path classify + main-process installationId (review)

Address PerishCode review on #4696:
- classifyStartupFailure normalizes backslashes before the /web/ segment check
  so Windows web-sidecar failures aren't misreported as daemon-start.
- resolveStartupDistinctId now takes an explicit installationRoot (threaded from
  paths.installationRoot); OD_INSTALLATION_DIR is only set in the daemon child
  env, so the main process previously always fell back to the synthetic id.
Adds a Windows web-path fixture and an installationId-resolution test (18 total).

* fix(packaged): raise crash-telemetry flush timeout 1.5s -> 5s (e2e finding)

Real-machine e2e (local packaged build -> delete better-sqlite3 to reproduce
issue #4638 -> launch -> query PostHog) showed the 1.5s cap silently DROPPED
the event: a cold DNS+TLS handshake to us.i.posthog.com from a fresh main
process exceeds 1.5s, so Promise.race timed out and process.exit(1) killed the
in-flight POST. Confirmed with a standalone repro (1.5s never landed, 6s did).
5s lets the single request complete on a cold connection; an offline machine
still exits fast because fetch rejects on DNS failure (send resolves early),
so only a black-hole network pays the full bound — acceptable on a crash exit.

* feat(packaged): stamp shared safety-event envelope on crash telemetry (review)

Address PerishCode review on #4696: the direct PostHog emitter only sent the
startup props + $os, omitting the envelope captureSafety always stamps. Mirror
it so env/schema-filtered dashboards bucket packaged_runtime_failed beside the
rest of the stream: event_schema_version, env, device_id, client_type
(packaged_main), capture_source (packaged/startup), ui_version, event_id and
$insert_id (dedup key). EVENT_SCHEMA_VERSION + the env resolver are replicated
(packaged doesn't depend on contracts) with lockstep comments. Adds a typed
PackagedRuntimeFailedProps + AnalyticsEventPayload entry in contracts, and a
test asserting the envelope wire fields (19 tests).

* fix(contracts): restore packaged_runtime_failed in AnalyticsEventName (review)

The name union member was clobbered when the envelope change synced events.ts;
only the AnalyticsEventPayload entry survived. PerishCode caught the regression
— restore the canonical AnalyticsEventName member so the event name and its
typed payload stay in lockstep.
2026-06-24 04:10:31 +00:00
..