mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(packaged): label main-process startup telemetry production, not development (#5681)
Every `packaged_runtime_failed` event was tagged `env: development` in production — 1,241 development vs 2 production over 3 days — so real packaged startup crashes were filtered out of the env=production dashboards and the daily stability report couldn't tell real users from dev machines. Cause: the daemon child is always spawned with NODE_ENV=production (spawnSidecarChild), so daemon/web events correctly report `production`. But the packaged MAIN process — which emits packaged_runtime_failed — has no NODE_ENV of its own, and resolveTelemetryEnv defaulted an unset NODE_ENV to `development`. apps/packaged only ever runs as a packaged build, so that default was wrong. Fix: resolveTelemetryEnv now treats anything that isn't an explicit development marker as `production`, matching the daemon it spawns. Explicit overrides (OD_TELEMETRY_ENV / OPEN_DESIGN_ENV / POSTHOG_ENV / LANGFUSE_ENVIRONMENT) still win. Red-spec tests: env resolves to production by default (unset NODE_ENV) and honors an explicit override.
This commit is contained in:
@@ -62,9 +62,18 @@ const EVENT_SCHEMA_VERSION = 2;
|
||||
const CLIENT_TYPE = "packaged_main";
|
||||
const CAPTURE_SOURCE = "packaged/startup";
|
||||
|
||||
// Replicates apps/daemon/src/telemetry-environment.ts (daemon src, not
|
||||
// importable here) so the packaged main process resolves the same `env` bucket
|
||||
// as daemon-emitted events for a given process environment.
|
||||
// Resolve the same `env` bucket as the daemon events this main process's own
|
||||
// sidecars emit. The daemon child is ALWAYS spawned with NODE_ENV=production
|
||||
// (see spawnSidecarChild in sidecars.ts), so daemon/web events report
|
||||
// `production` — but the packaged MAIN process's own NODE_ENV is unset, so the
|
||||
// old `development` default mislabeled every packaged_runtime_failed as dev
|
||||
// (100% 'development' in prod), hiding real startup crashes from the
|
||||
// env=production dashboards. apps/packaged only ever runs as a packaged build,
|
||||
// so an unset NODE_ENV here means packaged production, not dev; treat anything
|
||||
// that isn't an explicit development marker as production so the two sides
|
||||
// match. Explicit overrides (OD_TELEMETRY_ENV / OPEN_DESIGN_ENV / POSTHOG_ENV /
|
||||
// LANGFUSE_ENVIRONMENT) still win for anyone who needs to force a bucket
|
||||
// (e.g. a maintainer smoke-testing a local packaged build).
|
||||
function resolveTelemetryEnv(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const explicit =
|
||||
env.OD_TELEMETRY_ENV?.trim() ||
|
||||
@@ -72,8 +81,8 @@ function resolveTelemetryEnv(env: NodeJS.ProcessEnv = process.env): string {
|
||||
env.POSTHOG_ENV?.trim() ||
|
||||
env.LANGFUSE_ENVIRONMENT?.trim();
|
||||
if (explicit) return explicit;
|
||||
if (env.NODE_ENV === "production") return "production";
|
||||
return "development";
|
||||
if (env.NODE_ENV === "development") return "development";
|
||||
return "production";
|
||||
}
|
||||
|
||||
// Mirrors apps/daemon/src/analytics.ts randomInsertId. $insert_id is PostHog's
|
||||
|
||||
@@ -248,6 +248,72 @@ describe('captureStartupFailure', () => {
|
||||
expect(typeof p.env).toBe('string');
|
||||
});
|
||||
|
||||
it('labels main-process events production (unset NODE_ENV) so they match daemon events', async () => {
|
||||
// The packaged main process has no NODE_ENV of its own, while the daemon it
|
||||
// spawns runs with NODE_ENV=production. Without this the event reported
|
||||
// `development` — 100% of packaged_runtime_failed were mislabeled dev in
|
||||
// prod and filtered out of the env=production dashboards.
|
||||
const saved = {
|
||||
OD_TELEMETRY_ENV: process.env.OD_TELEMETRY_ENV,
|
||||
OPEN_DESIGN_ENV: process.env.OPEN_DESIGN_ENV,
|
||||
POSTHOG_ENV: process.env.POSTHOG_ENV,
|
||||
LANGFUSE_ENVIRONMENT: process.env.LANGFUSE_ENVIRONMENT,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
};
|
||||
delete process.env.OD_TELEMETRY_ENV;
|
||||
delete process.env.OPEN_DESIGN_ENV;
|
||||
delete process.env.POSTHOG_ENV;
|
||||
delete process.env.LANGFUSE_ENVIRONMENT;
|
||||
delete process.env.NODE_ENV;
|
||||
try {
|
||||
const fetchImpl = vi.fn().mockResolvedValue(new Response('ok'));
|
||||
await captureStartupFailure(
|
||||
{
|
||||
posthogKey: 'phc_test',
|
||||
posthogHost: null,
|
||||
distinctId: 'install-123',
|
||||
event: STARTUP_FAILURE_EVENT,
|
||||
properties: { failure_kind: 'daemon-start' },
|
||||
},
|
||||
{ fetchImpl: fetchImpl as unknown as typeof fetch, now: () => '2026-06-23T00:00:00.000Z' },
|
||||
);
|
||||
const [, init] = fetchImpl.mock.calls[0] as [string, RequestInit];
|
||||
const p = (JSON.parse(init.body as string) as { properties: Record<string, unknown> })
|
||||
.properties;
|
||||
expect(p.env).toBe('production');
|
||||
} finally {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('honors an explicit OD_TELEMETRY_ENV override (local smoke test can force dev)', async () => {
|
||||
const savedOd = process.env.OD_TELEMETRY_ENV;
|
||||
process.env.OD_TELEMETRY_ENV = 'development';
|
||||
try {
|
||||
const fetchImpl = vi.fn().mockResolvedValue(new Response('ok'));
|
||||
await captureStartupFailure(
|
||||
{
|
||||
posthogKey: 'phc_test',
|
||||
posthogHost: null,
|
||||
distinctId: 'install-123',
|
||||
event: STARTUP_FAILURE_EVENT,
|
||||
properties: { failure_kind: 'daemon-start' },
|
||||
},
|
||||
{ fetchImpl: fetchImpl as unknown as typeof fetch, now: () => '2026-06-23T00:00:00.000Z' },
|
||||
);
|
||||
const [, init] = fetchImpl.mock.calls[0] as [string, RequestInit];
|
||||
const p = (JSON.parse(init.body as string) as { properties: Record<string, unknown> })
|
||||
.properties;
|
||||
expect(p.env).toBe('development');
|
||||
} finally {
|
||||
if (savedOd === undefined) delete process.env.OD_TELEMETRY_ENV;
|
||||
else process.env.OD_TELEMETRY_ENV = savedOd;
|
||||
}
|
||||
});
|
||||
|
||||
it('never blocks past the timeout even if fetch hangs forever', async () => {
|
||||
// The critical guarantee for "no startup side-effect": a hung (offline)
|
||||
// request must not wedge the exit. Race must resolve via the timeout.
|
||||
|
||||
Reference in New Issue
Block a user