diff --git a/src/lib/logging/context.ts b/src/lib/logging/context.ts index 1ed5e019c..5ff165dff 100644 --- a/src/lib/logging/context.ts +++ b/src/lib/logging/context.ts @@ -1,6 +1,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import type { WideEvent } from "./types"; import { WideEventBuilder } from "./event-builder"; +import { registerSourcePriorityParseObserver } from "@/lib/validations/source-priority"; /** AsyncLocalStorage fuer den aktuellen Request-Kontext */ export const eventStorage = new AsyncLocalStorage(); @@ -27,3 +28,12 @@ export function annotate(fields: { } } } + +// v1.4.25 Fix-G — wire the schema-parser breadcrumb into the +// AsyncLocalStorage-backed `annotate()` only on the server. The +// validations module stays bundle-safe for client imports; this side +// effect runs the first time any server module imports the logging +// context. +registerSourcePriorityParseObserver((fields) => { + annotate(fields); +}); diff --git a/src/lib/validations/source-priority.ts b/src/lib/validations/source-priority.ts index 0afafe2aa..896650d35 100644 --- a/src/lib/validations/source-priority.ts +++ b/src/lib/validations/source-priority.ts @@ -34,12 +34,41 @@ */ import { z } from "zod/v4"; -import { annotate } from "@/lib/logging/context"; import { measurementSourceEnum, measurementTypeEnum, } from "@/lib/validations/measurement"; +// v1.4.25 Fix-G — `annotate` from `@/lib/logging/context` was the +// canonical breadcrumb for a parse failure, but the helper pulls +// `node:async_hooks` via `AsyncLocalStorage`. Any client component that +// reaches for a type-only export here (e.g. +// `src/components/settings/sources-section.tsx` importing +// `DEFAULT_SOURCE_PRIORITY`) used to drag `node:async_hooks` into the +// browser bundle and break the Turbopack build. The parser stays in the +// shared module — splitting it out would ripple through every callsite — +// so the breadcrumb path now goes through a runtime-resolved callback. +// The server logging module registers the real `annotate` on first import +// (via `registerSourcePriorityParseObserver`); the browser leaves it as +// the no-op default and never traces `node:async_hooks`. +type ParseFailureObserver = (entry: { + meta: Record; +}) => void; +let parseFailureObserver: ParseFailureObserver = () => {}; + +/** + * Server-side opt-in for the parse-failure breadcrumb. Called once from + * `@/lib/logging/context` so a buggy schema-tightening that silently + * wipes a saved ladder still surfaces in ops dashboards. The client + * bundle never imports the logging module, so the observer stays the + * no-op default in the browser. + */ +export function registerSourcePriorityParseObserver( + observer: ParseFailureObserver, +): void { + parseFailureObserver = observer; +} + /** * Device-type tag attached to a `Measurement` row. Mirrors * open-wearables (`watch | band | ring | phone | scale | other`) plus @@ -280,7 +309,7 @@ export function parseSourcePriority(raw: unknown): ResolvedSourcePriority { // priorities reset?" user report. `annotate()` is a no-op outside a // request context, so the static settings-page render and tests // remain side-effect-free. - annotate({ + parseFailureObserver({ meta: { sourcePriority: { parse: "failed",