@reliantlabs/forge-web-runtime 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,9 @@ import {
13
13
  RouteGuard, // signed-in render gate
14
14
  Resource, // the list tristate + cursor pagination
15
15
  reportException, // record a caught error with the browser telemetry SDK
16
+ track, // record a product event (funnel step) on the current session
17
+ setTelemetryUser, // attach an opaque user id to every span and replay
18
+ disableTelemetry, // stop recording for the rest of the page load (consent)
16
19
  formatMinorUnits, // exact money display; parseMinorUnits for input
17
20
  } from "@reliantlabs/forge-web-runtime";
18
21
  ```
@@ -90,6 +93,49 @@ build — it is no longer "these are `.tsx` files" but "these modules carry
90
93
  `"use client"` and Next has to process them rather than treat the package as
91
94
  an opaque external."
92
95
 
96
+ ## Browser telemetry (`/telemetry`)
97
+
98
+ `initBrowserTelemetry` starts the HyperDX browser SDK once per page load
99
+ (errors, console, fetch/XHR, page loads, web vitals, optional replay) and ships
100
+ to this origin's `/_otel`. It is a no-op without a DOM or an endpoint and never
101
+ throws.
102
+
103
+ ```ts
104
+ import { initBrowserTelemetry } from "@reliantlabs/forge-web-runtime/telemetry";
105
+
106
+ await initBrowserTelemetry({
107
+ serviceName: "web",
108
+ serviceVersion: import.meta.env.VITE_APP_VERSION, // the forge build id
109
+ frontend: "web", // resource attribute forge.frontend
110
+ endpoint: "/_otel",
111
+ consoleCapture: false, // log/info/warn/debug only; console.error(err) is still an error
112
+ replay: true,
113
+ replaySampleRate: 0.1, // 0..1 of SESSIONS; decided once, held in sessionStorage
114
+ });
115
+ ```
116
+
117
+ `track`, `setTelemetryUser` and `disableTelemetry` come from the barrel, so a
118
+ component can call them without importing the SDK. They are safe anywhere: no-ops
119
+ on the server, and calls made while the SDK chunk is still loading are held
120
+ (at most 100) and replayed in order.
121
+
122
+ - `track(name, props?)` — one `custom-action` span. At most 20 props; keys
123
+ `[a-z0-9_.]{1,64}`; values string, number or boolean; strings are redacted for
124
+ credential shapes and cut to 256 chars. Not for user content.
125
+ - `setTelemetryUser(id | null)` — the `userId` global attribute. Opaque id only,
126
+ never an email. `null` clears it.
127
+ - `disableTelemetry()` — stops the replay recorder and silences `track`,
128
+ `setTelemetryUser` and `reportException` until the next page load. It cannot
129
+ recall spans already batched, and the SDK's auto-instrumentation has no off
130
+ switch once running: an app that must send nothing without consent should not
131
+ call `initBrowserTelemetry` before it has it.
132
+
133
+ Privacy is forced, not configurable: when replay records, all text and inputs
134
+ are masked, canvas is never recorded, `img, picture, video, audio, iframe,
135
+ object, embed` are blocked, images are not inlined and fonts are not collected.
136
+ fetch/XHR spans have their `http.url` / `url.full` stripped of query string and
137
+ fragment; request and response bodies and headers are not captured.
138
+
93
139
  ## The DOM-free interceptors entry point
94
140
 
95
141
  ```ts
package/dist/devlog.js CHANGED
@@ -58,6 +58,7 @@
58
58
  // It is also reversible: uninstallDevLogging() restores the original console
59
59
  // methods, and the scaffolded call site is an ordinary line in main.tsx /
60
60
  // providers.tsx that a project can delete outright.
61
+ import { serverTraceIdOf } from "./trace-id.js";
61
62
  /** Console methods this module mirrors. */
62
63
  const LEVELS = ["log", "info", "warn", "error", "debug"];
63
64
  export const DEV_LOG_ENDPOINT = "/__forge/log";
@@ -145,9 +146,17 @@ function capture(level, args) {
145
146
  droppedReason = "buffer full";
146
147
  return;
147
148
  }
149
+ // A failed RPC's error names the trace the server recorded it in: put it on
150
+ // the line, so one grep of the trace id finds the browser's report next to
151
+ // the server's logs in .forge/logs/<env>/.
152
+ let msg = args.map(render).join(" ");
153
+ const traceId = args.map(serverTraceIdOf).find((id) => id !== null);
154
+ if (traceId && !msg.includes(traceId)) {
155
+ msg += ` trace_id=${traceId}`;
156
+ }
148
157
  const entry = {
149
158
  level,
150
- msg: truncate(args.map(render).join(" ")),
159
+ msg: truncate(msg),
151
160
  };
152
161
  const wasEmpty = buffer.length === 0;
153
162
  buffer.push(entry);
@@ -1 +1 @@
1
- {"version":3,"file":"devlog.js","sourceRoot":"","sources":["../src/devlog.ts"],"names":[],"mappings":"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","sourcesContent":["// Dev-only browser log forwarding.\n//\n// A browser's console is a dead end: an agent debugging a scaffolded frontend\n// cannot open devtools, so every console line and — worse — every uncaught\n// error is invisible to it. The backend has no such gap (structured slog, the\n// observe ComponentChain, and forge's dev loop already tees each process's\n// stdout to .forge/logs/<env>/). This closes the frontend half by POSTing\n// browser console output to a dev-server endpoint that prints it, so it lands\n// in .forge/logs/<env>/frontend_<name>.log next to the server logs.\n//\n// ── Why this batches (protocol v2) ───────────────────────────────────────\n//\n// The first version sent one fetch POST per console call. Measured in a real\n// app that logs freely, that is ~80 requests/s at light load and 200+/s in a\n// burst, and those requests are not free bystanders: they share the browser's\n// six-connections-per-origin pool and the dev server's single event loop with\n// the app's own RPCs. In the same session, RPC latency averaged 37ms in quiet\n// seconds and 127ms in seconds carrying 200+ log lines. Worse, `keepalive`\n// requests draw on a 64 KiB in-flight browser quota shared across the page, so\n// a burst does not just slow things down — some of it gets rejected, and the\n// original `.catch(() => {})` made that indistinguishable from silence.\n//\n// So entries are buffered and flushed as a batch:\n//\n// - every FLUSH_INTERVAL_MS, timer armed when an entry lands in an empty\n// buffer (the common case: a few lines coalesce into one request),\n// - immediately once buffered output reaches MAX_BATCH_BYTES,\n// - on a microtask for anything error-level, so a crash never waits 250ms\n// while a same-tick burst still coalesces,\n// - synchronously via sendBeacon on pagehide / visibilitychange→hidden,\n// which is the only way output survives a navigation or a crash-reload.\n//\n// At most ONE request is in flight. Entries logged during it keep buffering and\n// go out when it settles, which is what makes the printed order the log order —\n// six parallel POSTs would interleave on the server.\n//\n// Nothing is ever dropped silently. A full buffer, a rejected request and an\n// over-quota request all increment a counter, and the next batch to leave\n// carries a synthetic warn line saying how many lines were lost and why. The\n// failure this replaces was a dead sink that looked exactly like working code.\n//\n// ── Legacy endpoints ─────────────────────────────────────────────────────\n//\n// The receiving endpoint is a scaffold-once file the PROJECT owns (a Vite\n// plugin / a Next.js route handler), so upgrading this package through npm will\n// routinely meet an endpoint that predates batching. Such an endpoint answers\n// 204 and prints one empty line for the whole batch — total, silent loss. A v2\n// endpoint therefore returns `X-Forge-Devlog: 2`, and a 2xx without it makes\n// this client fall back permanently to one v1 POST per line (plus one warn\n// line telling the developer to update the endpoint).\n//\n// DEV ONLY, and structurally so. The receiving endpoint is a Vite plugin\n// gated `apply: \"serve\"` / a Next.js route behind a NODE_ENV check, so it does\n// not exist in a production build; installDevLogging() additionally no-ops\n// unless it is handed dev: true. A production bundle therefore has no\n// endpoint to post to AND no code that would post.\n//\n// It is also reversible: uninstallDevLogging() restores the original console\n// methods, and the scaffolded call site is an ordinary line in main.tsx /\n// providers.tsx that a project can delete outright.\n\n/** Console methods this module mirrors. */\nconst LEVELS = [\"log\", \"info\", \"warn\", \"error\", \"debug\"] as const;\n\ntype Level = (typeof LEVELS)[number];\n\n/**\n * One log line on the wire.\n *\n * Also the whole v1 body: a legacy endpoint receives exactly this object, which\n * is why the shape is unchanged and still exported.\n */\nexport interface DevLogPayload {\n level: Level | \"error\";\n msg: string;\n}\n\n/** The v2 body: many lines, one request. */\nexport interface DevLogBatch {\n entries: DevLogPayload[];\n}\n\nexport interface DevLoggingOptions {\n /**\n * Whether to install at all. Pass `import.meta.env.DEV` (Vite) or\n * `process.env.NODE_ENV !== \"production\"` (Next.js). When false this is a\n * no-op, so the call site needs no conditional of its own.\n */\n dev: boolean;\n /** Endpoint the dev server serves. Defaults to the forge convention. */\n endpoint?: string;\n /**\n * Also keep writing to the real console. Default true — the browser devtools\n * experience should be unchanged; this feature ADDS a sink, it does not move\n * one.\n */\n mirrorToConsole?: boolean;\n}\n\nexport const DEV_LOG_ENDPOINT = \"/__forge/log\";\n\n/** Response header a batching endpoint sets. Its ABSENCE selects v1 fallback. */\nconst PROTOCOL_HEADER = \"X-Forge-Devlog\";\n\n/** Cap a single line so a runaway loop cannot fill the disk. Matches the endpoints. */\nconst MAX_LINE = 8_000;\n\n/** How long a line may wait for company before it is sent. */\nconst FLUSH_INTERVAL_MS = 250;\n\n/**\n * Per-request ceiling. Deliberately half of the browser's 64 KiB shared\n * `keepalive` in-flight quota: with one request in flight, a batch cannot be\n * the thing that pushes the page over it.\n */\nconst MAX_BATCH_BYTES = 32 * 1024;\n\n/** Buffer ceiling. Past it entries are dropped and counted, never queued forever. */\nconst MAX_BUFFER_ENTRIES = 5_000;\n\nconst original: Partial<Record<Level, (...args: unknown[]) => void>> = {};\nlet installed = false;\n\n// Guards against infinite recursion. The forwarder calls fetch(), and anything\n// that logs inside fetch (an interceptor, a polyfill, a devtools extension)\n// would otherwise re-enter the override forever. Such a line is still mirrored\n// to the real console — it is only forwarding that is suppressed.\nlet forwarding = false;\n\nlet endpointUrl = DEV_LOG_ENDPOINT;\nlet buffer: DevLogPayload[] = [];\nlet bufferedBytes = 0;\nlet flushTimer: ReturnType<typeof setTimeout> | undefined;\nlet requestInFlight = false;\nlet errorFlushQueued = false;\nlet droppedCount = 0;\nlet droppedReason = \"\";\n/** Set for good once an endpoint is known not to understand batches. */\nlet legacyEndpoint = false;\n\n/**\n * Render one console argument as a string.\n *\n * Circular structures and BigInt are the two cases a plain JSON.stringify\n * throws on, and the old fallback to String(value) turned both into\n * \"[object Object]\" — deleting exactly the shape you were trying to read. The\n * replacer keeps the object and names the cycle instead.\n */\nfunction render(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (value instanceof Error) {\n return `${value.name}: ${value.message}${\n value.stack ? `\\n${value.stack}` : \"\"\n }`;\n }\n try {\n const seen = new WeakSet<object>();\n return (\n JSON.stringify(value, (_key, val: unknown) => {\n if (typeof val === \"bigint\") return `${val}n`;\n if (typeof val === \"object\" && val !== null) {\n if (seen.has(val)) return \"[Circular]\";\n seen.add(val);\n }\n return val;\n }) ?? String(value)\n );\n } catch {\n // DOM nodes, getters that throw — never let logging throw.\n return String(value);\n }\n}\n\nfunction truncate(msg: string): string {\n return msg.length > MAX_LINE ? `${msg.slice(0, MAX_LINE)}… (truncated)` : msg;\n}\n\nfunction entryBytes(entry: DevLogPayload): number {\n return JSON.stringify(entry).length;\n}\n\n/**\n * Buffer one line and decide whether it can wait.\n *\n * Arguments are rendered HERE rather than at flush time: a console argument is\n * a live object, and by the time a batch leaves 250ms later the array it logged\n * may have been mutated or emptied. The snapshot is the value at the call.\n */\nfunction capture(level: DevLogPayload[\"level\"], args: unknown[]): void {\n if (forwarding) return;\n\n if (buffer.length >= MAX_BUFFER_ENTRIES) {\n droppedCount += 1;\n droppedReason = \"buffer full\";\n return;\n }\n\n const entry: DevLogPayload = {\n level,\n msg: truncate(args.map(render).join(\" \")),\n };\n const wasEmpty = buffer.length === 0;\n buffer.push(entry);\n bufferedBytes += entryBytes(entry);\n\n if (bufferedBytes >= MAX_BATCH_BYTES) {\n flush();\n return;\n }\n\n if (level === \"error\") {\n // An error is what someone is waiting for; it must not sit in a timer. The\n // microtask (rather than a direct flush) lets a same-tick burst — a failing\n // render logging three times — still leave as one request.\n if (!errorFlushQueued) {\n errorFlushQueued = true;\n queueMicrotask(() => {\n errorFlushQueued = false;\n flush();\n });\n }\n return;\n }\n\n if (wasEmpty) armTimer();\n}\n\nfunction armTimer(): void {\n if (flushTimer !== undefined) return;\n flushTimer = setTimeout(() => {\n flushTimer = undefined;\n flush();\n }, FLUSH_INTERVAL_MS);\n}\n\nfunction clearFlushTimer(): void {\n if (flushTimer === undefined) return;\n clearTimeout(flushTimer);\n flushTimer = undefined;\n}\n\n/** Pull entries off the front of the buffer without exceeding MAX_BATCH_BYTES. */\nfunction takeBatch(limit = MAX_BATCH_BYTES): DevLogPayload[] {\n const batch: DevLogPayload[] = [];\n let size = '{\"entries\":[]}'.length;\n while (buffer.length > 0) {\n const entry = buffer[0] as DevLogPayload;\n const cost = entryBytes(entry) + (batch.length > 0 ? 1 : 0);\n // Always take at least one entry: a single line that somehow exceeds the\n // limit must still leave, or it stalls every line behind it forever.\n if (batch.length > 0 && size + cost > limit) break;\n buffer.shift();\n bufferedBytes -= entryBytes(entry);\n batch.push(entry);\n size += cost;\n }\n if (buffer.length === 0) bufferedBytes = 0;\n return batch;\n}\n\n/**\n * Prepend the \"we lost lines\" notice, if any, so it travels with the next batch\n * rather than needing a request of its own.\n */\nfunction drainDroppedNotice(): void {\n if (droppedCount === 0) return;\n const notice: DevLogPayload = {\n level: \"warn\",\n msg: `[forge-devlog] dropped ${droppedCount} lines (${droppedReason})`,\n };\n buffer.unshift(notice);\n bufferedBytes += entryBytes(notice);\n droppedCount = 0;\n droppedReason = \"\";\n}\n\nfunction post(body: string): Promise<Response> {\n // The recursion guard must wrap the CALL, not the promise: a fetch polyfill\n // that logs does so synchronously from inside fetch().\n forwarding = true;\n try {\n return fetch(endpointUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n // A log emitted during unload (the interesting case for a crash on\n // navigation) is cancelled without this.\n keepalive: true,\n // Never let log forwarding trip a credentials or CORS policy.\n credentials: \"omit\",\n });\n } finally {\n forwarding = false;\n }\n}\n\n/**\n * Send one request's worth of buffered output, if nothing is in flight.\n *\n * The single-flight rule is what guarantees the dev server prints lines in the\n * order they were logged, and it doubles as backpressure: a slow endpoint makes\n * batches bigger rather than making requests pile up.\n */\nfunction flush(): void {\n if (requestInFlight) return;\n drainDroppedNotice();\n if (buffer.length === 0) return;\n clearFlushTimer();\n\n const batch = legacyEndpoint ? takeBatch(0) : takeBatch();\n if (batch.length === 0) return;\n\n const body = legacyEndpoint\n ? JSON.stringify(batch[0])\n : JSON.stringify({ entries: batch } satisfies DevLogBatch);\n\n requestInFlight = true;\n post(body).then(\n (response) => {\n requestInFlight = false;\n if (!response.ok) {\n countDropped(batch.length, `endpoint returned ${response.status}`);\n } else if (!legacyEndpoint && !response.headers.get(PROTOCOL_HEADER)) {\n adoptLegacyMode(batch);\n }\n if (buffer.length > 0 || droppedCount > 0) flush();\n },\n () => {\n requestInFlight = false;\n countDropped(batch.length, \"request failed\");\n if (buffer.length > 0) flush();\n },\n );\n}\n\nfunction countDropped(count: number, reason: string): void {\n droppedCount += count;\n droppedReason = reason;\n}\n\n/**\n * A 2xx with no protocol header came from an endpoint written before batching.\n * It parsed `{entries:[...]}` as a v1 body, found no `msg`, and printed one\n * empty line — so this batch is lost unless it is re-sent one line at a time,\n * and so is everything after it.\n */\nfunction adoptLegacyMode(lost: DevLogPayload[]): void {\n legacyEndpoint = true;\n const notice: DevLogPayload = {\n level: \"warn\",\n msg:\n \"[forge-devlog] this dev server's /__forge/log endpoint predates batched \" +\n \"posts; update it (forge skill: debug) — falling back to one request per line\",\n };\n buffer.unshift(notice, ...lost);\n bufferedBytes += [notice, ...lost].reduce((n, e) => n + entryBytes(e), 0);\n}\n\n/**\n * Last-chance flush on page teardown.\n *\n * fetch — even with keepalive — is not reliable here, and an async flush is\n * hopeless: the event loop may not run again. sendBeacon hands the bytes to the\n * browser to deliver after the document is gone, which is precisely the case\n * where the last few log lines explain what happened.\n */\nfunction flushOnUnload(): void {\n drainDroppedNotice();\n if (buffer.length === 0) return;\n\n const canBeacon =\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\";\n if (!canBeacon) {\n // No beacon: keepalive fetch is the only remaining option, and one request\n // has a better chance of leaving than a queue of them.\n flush();\n return;\n }\n\n forwarding = true;\n try {\n while (buffer.length > 0) {\n const batch = legacyEndpoint ? takeBatch(0) : takeBatch();\n const body = legacyEndpoint\n ? JSON.stringify(batch[0])\n : JSON.stringify({ entries: batch } satisfies DevLogBatch);\n // text/plain is not a decoration: sendBeacon with application/json is a\n // non-simple request and would need a CORS preflight it cannot make. The\n // endpoints parse the body regardless of Content-Type for this reason.\n const blob = new Blob([body], { type: \"text/plain;charset=UTF-8\" });\n if (!navigator.sendBeacon(endpointUrl, blob)) {\n // The beacon queue is full (64 KiB per origin). Nothing later will fit\n // either, so stop and account for what is left.\n countDropped(batch.length + buffer.length, \"beacon queue full\");\n buffer = [];\n bufferedBytes = 0;\n return;\n }\n }\n } finally {\n forwarding = false;\n }\n}\n\nfunction onWindowError(event: ErrorEvent): void {\n capture(\"error\", [\n event.error ??\n `${event.message} @ ${event.filename}:${event.lineno}:${event.colno}`,\n ]);\n}\n\nfunction onRejection(event: PromiseRejectionEvent): void {\n capture(\"error\", [\"unhandled rejection:\", event.reason]);\n}\n\nlet errorHandler: ((e: ErrorEvent) => void) | undefined;\nlet rejectionHandler: ((e: PromiseRejectionEvent) => void) | undefined;\nlet pageHideHandler: (() => void) | undefined;\nlet visibilityHandler: (() => void) | undefined;\n\n/**\n * Mirror console output and uncaught errors to the dev server, which prints\n * them into .forge/logs/<env>/frontend_<name>.log.\n *\n * The window listeners are the point of the feature as much as the console\n * override is: an uncaught TypeError or a rejected promise produces no console\n * call of its own, and those are exactly the failures worth seeing.\n *\n * Safe to call more than once; the second call is a no-op.\n */\nexport function installDevLogging(options: DevLoggingOptions): void {\n // `dev` is read off `options` rather than destructured with a default, and\n // checked FIRST. A bundler constant-folds `import.meta.env.DEV` to false at\n // the CALL SITE and can then drop the argument entirely, leaving a bare\n // `installDevLogging({})` — with a defaulted `dev` that call would install\n // the override in a production bundle. `options?.dev !== true` is therefore\n // fail-closed: absent, undefined, or anything non-true means do nothing.\n if (options?.dev !== true) return;\n if (installed) return;\n if (typeof window === \"undefined\") return; // SSR / prerender pass\n\n const { endpoint = DEV_LOG_ENDPOINT, mirrorToConsole = true } = options;\n installed = true;\n endpointUrl = endpoint;\n buffer = [];\n bufferedBytes = 0;\n droppedCount = 0;\n droppedReason = \"\";\n legacyEndpoint = false;\n requestInFlight = false;\n\n for (const level of LEVELS) {\n // Keep the ORIGINAL reference, not a bound copy: uninstall must restore\n // console.log to the identical function it replaced, so a second consumer\n // holding that reference (or a test asserting on it) sees no difference.\n const previous = console[level] as (...args: unknown[]) => void;\n original[level] = previous;\n console[level] = (...args: unknown[]) => {\n if (mirrorToConsole) previous.apply(console, args);\n capture(level, args);\n };\n }\n\n errorHandler = (e) => onWindowError(e);\n rejectionHandler = (e) => onRejection(e);\n pageHideHandler = () => flushOnUnload();\n window.addEventListener(\"error\", errorHandler);\n window.addEventListener(\"unhandledrejection\", rejectionHandler);\n window.addEventListener(\"pagehide\", pageHideHandler);\n\n if (typeof document !== \"undefined\") {\n // visibilitychange→hidden is the only teardown signal mobile Safari\n // reliably fires; pagehide alone loses the whole buffer there.\n visibilityHandler = () => {\n if (document.visibilityState === \"hidden\") flushOnUnload();\n };\n document.addEventListener(\"visibilitychange\", visibilityHandler);\n }\n}\n\n/** Restore the original console methods and remove the window listeners. */\nexport function uninstallDevLogging(): void {\n if (!installed) return;\n\n // Flush before restoring: whatever is buffered was logged while this was\n // installed, and a teardown is no reason to lose it.\n clearFlushTimer();\n flush();\n\n for (const level of LEVELS) {\n const previous = original[level];\n if (previous) console[level] = previous;\n delete original[level];\n }\n if (errorHandler) window.removeEventListener(\"error\", errorHandler);\n if (rejectionHandler)\n window.removeEventListener(\"unhandledrejection\", rejectionHandler);\n if (pageHideHandler) window.removeEventListener(\"pagehide\", pageHideHandler);\n if (visibilityHandler && typeof document !== \"undefined\")\n document.removeEventListener(\"visibilitychange\", visibilityHandler);\n errorHandler = undefined;\n rejectionHandler = undefined;\n pageHideHandler = undefined;\n visibilityHandler = undefined;\n installed = false;\n}\n\n/** Whether the console override is currently installed. Exposed for tests. */\nexport function devLoggingInstalled(): boolean {\n return installed;\n}\n"]}
1
+ {"version":3,"file":"devlog.js","sourceRoot":"","sources":["../src/devlog.ts"],"names":[],"mappings":"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","sourcesContent":["// Dev-only browser log forwarding.\n//\n// A browser's console is a dead end: an agent debugging a scaffolded frontend\n// cannot open devtools, so every console line and — worse — every uncaught\n// error is invisible to it. The backend has no such gap (structured slog, the\n// observe ComponentChain, and forge's dev loop already tees each process's\n// stdout to .forge/logs/<env>/). This closes the frontend half by POSTing\n// browser console output to a dev-server endpoint that prints it, so it lands\n// in .forge/logs/<env>/frontend_<name>.log next to the server logs.\n//\n// ── Why this batches (protocol v2) ───────────────────────────────────────\n//\n// The first version sent one fetch POST per console call. Measured in a real\n// app that logs freely, that is ~80 requests/s at light load and 200+/s in a\n// burst, and those requests are not free bystanders: they share the browser's\n// six-connections-per-origin pool and the dev server's single event loop with\n// the app's own RPCs. In the same session, RPC latency averaged 37ms in quiet\n// seconds and 127ms in seconds carrying 200+ log lines. Worse, `keepalive`\n// requests draw on a 64 KiB in-flight browser quota shared across the page, so\n// a burst does not just slow things down — some of it gets rejected, and the\n// original `.catch(() => {})` made that indistinguishable from silence.\n//\n// So entries are buffered and flushed as a batch:\n//\n// - every FLUSH_INTERVAL_MS, timer armed when an entry lands in an empty\n// buffer (the common case: a few lines coalesce into one request),\n// - immediately once buffered output reaches MAX_BATCH_BYTES,\n// - on a microtask for anything error-level, so a crash never waits 250ms\n// while a same-tick burst still coalesces,\n// - synchronously via sendBeacon on pagehide / visibilitychange→hidden,\n// which is the only way output survives a navigation or a crash-reload.\n//\n// At most ONE request is in flight. Entries logged during it keep buffering and\n// go out when it settles, which is what makes the printed order the log order —\n// six parallel POSTs would interleave on the server.\n//\n// Nothing is ever dropped silently. A full buffer, a rejected request and an\n// over-quota request all increment a counter, and the next batch to leave\n// carries a synthetic warn line saying how many lines were lost and why. The\n// failure this replaces was a dead sink that looked exactly like working code.\n//\n// ── Legacy endpoints ─────────────────────────────────────────────────────\n//\n// The receiving endpoint is a scaffold-once file the PROJECT owns (a Vite\n// plugin / a Next.js route handler), so upgrading this package through npm will\n// routinely meet an endpoint that predates batching. Such an endpoint answers\n// 204 and prints one empty line for the whole batch — total, silent loss. A v2\n// endpoint therefore returns `X-Forge-Devlog: 2`, and a 2xx without it makes\n// this client fall back permanently to one v1 POST per line (plus one warn\n// line telling the developer to update the endpoint).\n//\n// DEV ONLY, and structurally so. The receiving endpoint is a Vite plugin\n// gated `apply: \"serve\"` / a Next.js route behind a NODE_ENV check, so it does\n// not exist in a production build; installDevLogging() additionally no-ops\n// unless it is handed dev: true. A production bundle therefore has no\n// endpoint to post to AND no code that would post.\n//\n// It is also reversible: uninstallDevLogging() restores the original console\n// methods, and the scaffolded call site is an ordinary line in main.tsx /\n// providers.tsx that a project can delete outright.\n\nimport { serverTraceIdOf } from \"./trace-id.js\";\n\n/** Console methods this module mirrors. */\nconst LEVELS = [\"log\", \"info\", \"warn\", \"error\", \"debug\"] as const;\n\ntype Level = (typeof LEVELS)[number];\n\n/**\n * One log line on the wire.\n *\n * Also the whole v1 body: a legacy endpoint receives exactly this object, which\n * is why the shape is unchanged and still exported.\n */\nexport interface DevLogPayload {\n level: Level | \"error\";\n msg: string;\n}\n\n/** The v2 body: many lines, one request. */\nexport interface DevLogBatch {\n entries: DevLogPayload[];\n}\n\nexport interface DevLoggingOptions {\n /**\n * Whether to install at all. Pass `import.meta.env.DEV` (Vite) or\n * `process.env.NODE_ENV !== \"production\"` (Next.js). When false this is a\n * no-op, so the call site needs no conditional of its own.\n */\n dev: boolean;\n /** Endpoint the dev server serves. Defaults to the forge convention. */\n endpoint?: string;\n /**\n * Also keep writing to the real console. Default true — the browser devtools\n * experience should be unchanged; this feature ADDS a sink, it does not move\n * one.\n */\n mirrorToConsole?: boolean;\n}\n\nexport const DEV_LOG_ENDPOINT = \"/__forge/log\";\n\n/** Response header a batching endpoint sets. Its ABSENCE selects v1 fallback. */\nconst PROTOCOL_HEADER = \"X-Forge-Devlog\";\n\n/** Cap a single line so a runaway loop cannot fill the disk. Matches the endpoints. */\nconst MAX_LINE = 8_000;\n\n/** How long a line may wait for company before it is sent. */\nconst FLUSH_INTERVAL_MS = 250;\n\n/**\n * Per-request ceiling. Deliberately half of the browser's 64 KiB shared\n * `keepalive` in-flight quota: with one request in flight, a batch cannot be\n * the thing that pushes the page over it.\n */\nconst MAX_BATCH_BYTES = 32 * 1024;\n\n/** Buffer ceiling. Past it entries are dropped and counted, never queued forever. */\nconst MAX_BUFFER_ENTRIES = 5_000;\n\nconst original: Partial<Record<Level, (...args: unknown[]) => void>> = {};\nlet installed = false;\n\n// Guards against infinite recursion. The forwarder calls fetch(), and anything\n// that logs inside fetch (an interceptor, a polyfill, a devtools extension)\n// would otherwise re-enter the override forever. Such a line is still mirrored\n// to the real console — it is only forwarding that is suppressed.\nlet forwarding = false;\n\nlet endpointUrl = DEV_LOG_ENDPOINT;\nlet buffer: DevLogPayload[] = [];\nlet bufferedBytes = 0;\nlet flushTimer: ReturnType<typeof setTimeout> | undefined;\nlet requestInFlight = false;\nlet errorFlushQueued = false;\nlet droppedCount = 0;\nlet droppedReason = \"\";\n/** Set for good once an endpoint is known not to understand batches. */\nlet legacyEndpoint = false;\n\n/**\n * Render one console argument as a string.\n *\n * Circular structures and BigInt are the two cases a plain JSON.stringify\n * throws on, and the old fallback to String(value) turned both into\n * \"[object Object]\" — deleting exactly the shape you were trying to read. The\n * replacer keeps the object and names the cycle instead.\n */\nfunction render(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (value instanceof Error) {\n return `${value.name}: ${value.message}${\n value.stack ? `\\n${value.stack}` : \"\"\n }`;\n }\n try {\n const seen = new WeakSet<object>();\n return (\n JSON.stringify(value, (_key, val: unknown) => {\n if (typeof val === \"bigint\") return `${val}n`;\n if (typeof val === \"object\" && val !== null) {\n if (seen.has(val)) return \"[Circular]\";\n seen.add(val);\n }\n return val;\n }) ?? String(value)\n );\n } catch {\n // DOM nodes, getters that throw — never let logging throw.\n return String(value);\n }\n}\n\nfunction truncate(msg: string): string {\n return msg.length > MAX_LINE ? `${msg.slice(0, MAX_LINE)}… (truncated)` : msg;\n}\n\nfunction entryBytes(entry: DevLogPayload): number {\n return JSON.stringify(entry).length;\n}\n\n/**\n * Buffer one line and decide whether it can wait.\n *\n * Arguments are rendered HERE rather than at flush time: a console argument is\n * a live object, and by the time a batch leaves 250ms later the array it logged\n * may have been mutated or emptied. The snapshot is the value at the call.\n */\nfunction capture(level: DevLogPayload[\"level\"], args: unknown[]): void {\n if (forwarding) return;\n\n if (buffer.length >= MAX_BUFFER_ENTRIES) {\n droppedCount += 1;\n droppedReason = \"buffer full\";\n return;\n }\n\n // A failed RPC's error names the trace the server recorded it in: put it on\n // the line, so one grep of the trace id finds the browser's report next to\n // the server's logs in .forge/logs/<env>/.\n let msg = args.map(render).join(\" \");\n const traceId = args.map(serverTraceIdOf).find((id) => id !== null);\n if (traceId && !msg.includes(traceId)) {\n msg += ` trace_id=${traceId}`;\n }\n const entry: DevLogPayload = {\n level,\n msg: truncate(msg),\n };\n const wasEmpty = buffer.length === 0;\n buffer.push(entry);\n bufferedBytes += entryBytes(entry);\n\n if (bufferedBytes >= MAX_BATCH_BYTES) {\n flush();\n return;\n }\n\n if (level === \"error\") {\n // An error is what someone is waiting for; it must not sit in a timer. The\n // microtask (rather than a direct flush) lets a same-tick burst — a failing\n // render logging three times — still leave as one request.\n if (!errorFlushQueued) {\n errorFlushQueued = true;\n queueMicrotask(() => {\n errorFlushQueued = false;\n flush();\n });\n }\n return;\n }\n\n if (wasEmpty) armTimer();\n}\n\nfunction armTimer(): void {\n if (flushTimer !== undefined) return;\n flushTimer = setTimeout(() => {\n flushTimer = undefined;\n flush();\n }, FLUSH_INTERVAL_MS);\n}\n\nfunction clearFlushTimer(): void {\n if (flushTimer === undefined) return;\n clearTimeout(flushTimer);\n flushTimer = undefined;\n}\n\n/** Pull entries off the front of the buffer without exceeding MAX_BATCH_BYTES. */\nfunction takeBatch(limit = MAX_BATCH_BYTES): DevLogPayload[] {\n const batch: DevLogPayload[] = [];\n let size = '{\"entries\":[]}'.length;\n while (buffer.length > 0) {\n const entry = buffer[0] as DevLogPayload;\n const cost = entryBytes(entry) + (batch.length > 0 ? 1 : 0);\n // Always take at least one entry: a single line that somehow exceeds the\n // limit must still leave, or it stalls every line behind it forever.\n if (batch.length > 0 && size + cost > limit) break;\n buffer.shift();\n bufferedBytes -= entryBytes(entry);\n batch.push(entry);\n size += cost;\n }\n if (buffer.length === 0) bufferedBytes = 0;\n return batch;\n}\n\n/**\n * Prepend the \"we lost lines\" notice, if any, so it travels with the next batch\n * rather than needing a request of its own.\n */\nfunction drainDroppedNotice(): void {\n if (droppedCount === 0) return;\n const notice: DevLogPayload = {\n level: \"warn\",\n msg: `[forge-devlog] dropped ${droppedCount} lines (${droppedReason})`,\n };\n buffer.unshift(notice);\n bufferedBytes += entryBytes(notice);\n droppedCount = 0;\n droppedReason = \"\";\n}\n\nfunction post(body: string): Promise<Response> {\n // The recursion guard must wrap the CALL, not the promise: a fetch polyfill\n // that logs does so synchronously from inside fetch().\n forwarding = true;\n try {\n return fetch(endpointUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n // A log emitted during unload (the interesting case for a crash on\n // navigation) is cancelled without this.\n keepalive: true,\n // Never let log forwarding trip a credentials or CORS policy.\n credentials: \"omit\",\n });\n } finally {\n forwarding = false;\n }\n}\n\n/**\n * Send one request's worth of buffered output, if nothing is in flight.\n *\n * The single-flight rule is what guarantees the dev server prints lines in the\n * order they were logged, and it doubles as backpressure: a slow endpoint makes\n * batches bigger rather than making requests pile up.\n */\nfunction flush(): void {\n if (requestInFlight) return;\n drainDroppedNotice();\n if (buffer.length === 0) return;\n clearFlushTimer();\n\n const batch = legacyEndpoint ? takeBatch(0) : takeBatch();\n if (batch.length === 0) return;\n\n const body = legacyEndpoint\n ? JSON.stringify(batch[0])\n : JSON.stringify({ entries: batch } satisfies DevLogBatch);\n\n requestInFlight = true;\n post(body).then(\n (response) => {\n requestInFlight = false;\n if (!response.ok) {\n countDropped(batch.length, `endpoint returned ${response.status}`);\n } else if (!legacyEndpoint && !response.headers.get(PROTOCOL_HEADER)) {\n adoptLegacyMode(batch);\n }\n if (buffer.length > 0 || droppedCount > 0) flush();\n },\n () => {\n requestInFlight = false;\n countDropped(batch.length, \"request failed\");\n if (buffer.length > 0) flush();\n },\n );\n}\n\nfunction countDropped(count: number, reason: string): void {\n droppedCount += count;\n droppedReason = reason;\n}\n\n/**\n * A 2xx with no protocol header came from an endpoint written before batching.\n * It parsed `{entries:[...]}` as a v1 body, found no `msg`, and printed one\n * empty line — so this batch is lost unless it is re-sent one line at a time,\n * and so is everything after it.\n */\nfunction adoptLegacyMode(lost: DevLogPayload[]): void {\n legacyEndpoint = true;\n const notice: DevLogPayload = {\n level: \"warn\",\n msg:\n \"[forge-devlog] this dev server's /__forge/log endpoint predates batched \" +\n \"posts; update it (forge skill: debug) — falling back to one request per line\",\n };\n buffer.unshift(notice, ...lost);\n bufferedBytes += [notice, ...lost].reduce((n, e) => n + entryBytes(e), 0);\n}\n\n/**\n * Last-chance flush on page teardown.\n *\n * fetch — even with keepalive — is not reliable here, and an async flush is\n * hopeless: the event loop may not run again. sendBeacon hands the bytes to the\n * browser to deliver after the document is gone, which is precisely the case\n * where the last few log lines explain what happened.\n */\nfunction flushOnUnload(): void {\n drainDroppedNotice();\n if (buffer.length === 0) return;\n\n const canBeacon =\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\";\n if (!canBeacon) {\n // No beacon: keepalive fetch is the only remaining option, and one request\n // has a better chance of leaving than a queue of them.\n flush();\n return;\n }\n\n forwarding = true;\n try {\n while (buffer.length > 0) {\n const batch = legacyEndpoint ? takeBatch(0) : takeBatch();\n const body = legacyEndpoint\n ? JSON.stringify(batch[0])\n : JSON.stringify({ entries: batch } satisfies DevLogBatch);\n // text/plain is not a decoration: sendBeacon with application/json is a\n // non-simple request and would need a CORS preflight it cannot make. The\n // endpoints parse the body regardless of Content-Type for this reason.\n const blob = new Blob([body], { type: \"text/plain;charset=UTF-8\" });\n if (!navigator.sendBeacon(endpointUrl, blob)) {\n // The beacon queue is full (64 KiB per origin). Nothing later will fit\n // either, so stop and account for what is left.\n countDropped(batch.length + buffer.length, \"beacon queue full\");\n buffer = [];\n bufferedBytes = 0;\n return;\n }\n }\n } finally {\n forwarding = false;\n }\n}\n\nfunction onWindowError(event: ErrorEvent): void {\n capture(\"error\", [\n event.error ??\n `${event.message} @ ${event.filename}:${event.lineno}:${event.colno}`,\n ]);\n}\n\nfunction onRejection(event: PromiseRejectionEvent): void {\n capture(\"error\", [\"unhandled rejection:\", event.reason]);\n}\n\nlet errorHandler: ((e: ErrorEvent) => void) | undefined;\nlet rejectionHandler: ((e: PromiseRejectionEvent) => void) | undefined;\nlet pageHideHandler: (() => void) | undefined;\nlet visibilityHandler: (() => void) | undefined;\n\n/**\n * Mirror console output and uncaught errors to the dev server, which prints\n * them into .forge/logs/<env>/frontend_<name>.log.\n *\n * The window listeners are the point of the feature as much as the console\n * override is: an uncaught TypeError or a rejected promise produces no console\n * call of its own, and those are exactly the failures worth seeing.\n *\n * Safe to call more than once; the second call is a no-op.\n */\nexport function installDevLogging(options: DevLoggingOptions): void {\n // `dev` is read off `options` rather than destructured with a default, and\n // checked FIRST. A bundler constant-folds `import.meta.env.DEV` to false at\n // the CALL SITE and can then drop the argument entirely, leaving a bare\n // `installDevLogging({})` — with a defaulted `dev` that call would install\n // the override in a production bundle. `options?.dev !== true` is therefore\n // fail-closed: absent, undefined, or anything non-true means do nothing.\n if (options?.dev !== true) return;\n if (installed) return;\n if (typeof window === \"undefined\") return; // SSR / prerender pass\n\n const { endpoint = DEV_LOG_ENDPOINT, mirrorToConsole = true } = options;\n installed = true;\n endpointUrl = endpoint;\n buffer = [];\n bufferedBytes = 0;\n droppedCount = 0;\n droppedReason = \"\";\n legacyEndpoint = false;\n requestInFlight = false;\n\n for (const level of LEVELS) {\n // Keep the ORIGINAL reference, not a bound copy: uninstall must restore\n // console.log to the identical function it replaced, so a second consumer\n // holding that reference (or a test asserting on it) sees no difference.\n const previous = console[level] as (...args: unknown[]) => void;\n original[level] = previous;\n console[level] = (...args: unknown[]) => {\n if (mirrorToConsole) previous.apply(console, args);\n capture(level, args);\n };\n }\n\n errorHandler = (e) => onWindowError(e);\n rejectionHandler = (e) => onRejection(e);\n pageHideHandler = () => flushOnUnload();\n window.addEventListener(\"error\", errorHandler);\n window.addEventListener(\"unhandledrejection\", rejectionHandler);\n window.addEventListener(\"pagehide\", pageHideHandler);\n\n if (typeof document !== \"undefined\") {\n // visibilitychange→hidden is the only teardown signal mobile Safari\n // reliably fires; pagehide alone loses the whole buffer there.\n visibilityHandler = () => {\n if (document.visibilityState === \"hidden\") flushOnUnload();\n };\n document.addEventListener(\"visibilitychange\", visibilityHandler);\n }\n}\n\n/** Restore the original console methods and remove the window listeners. */\nexport function uninstallDevLogging(): void {\n if (!installed) return;\n\n // Flush before restoring: whatever is buffered was logged while this was\n // installed, and a teardown is no reason to lose it.\n clearFlushTimer();\n flush();\n\n for (const level of LEVELS) {\n const previous = original[level];\n if (previous) console[level] = previous;\n delete original[level];\n }\n if (errorHandler) window.removeEventListener(\"error\", errorHandler);\n if (rejectionHandler)\n window.removeEventListener(\"unhandledrejection\", rejectionHandler);\n if (pageHideHandler) window.removeEventListener(\"pagehide\", pageHideHandler);\n if (visibilityHandler && typeof document !== \"undefined\")\n document.removeEventListener(\"visibilitychange\", visibilityHandler);\n errorHandler = undefined;\n rejectionHandler = undefined;\n pageHideHandler = undefined;\n visibilityHandler = undefined;\n installed = false;\n}\n\n/** Whether the console override is currently installed. Exposed for tests. */\nexport function devLoggingInstalled(): boolean {\n return installed;\n}\n"]}
@@ -22,6 +22,11 @@ export declare function setExceptionReporter(next: ExceptionReporter | null): vo
22
22
  * boundary-caught error to console.error itself, so the recorder installed
23
23
  * by `/telemetry` skips an error the SDK has already seen: one failure is
24
24
  * one record, never two.
25
+ *
26
+ * A failed RPC's error (ConnectClientError) carries the trace the server
27
+ * recorded it in; it is added as the `server.trace_id` attribute, so the
28
+ * browser record links to the server's spans and logs. An attribute of that
29
+ * name passed by the caller wins.
25
30
  */
26
31
  export declare function reportException(error: unknown, attributes?: ExceptionAttributes): boolean;
27
32
  export {};
@@ -10,6 +10,7 @@
10
10
  // SDK has initialised. Until then — SSR, telemetry off, SDK still loading —
11
11
  // reportException is a no-op, which is what makes it safe to call from any
12
12
  // catch block unconditionally.
13
+ import { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from "./trace-id.js";
13
14
  /**
14
15
  * Reports which recorder is installed, if any. reportException's return
15
16
  * value is "the telemetry SDK has this error" — true both when it was just
@@ -39,13 +40,21 @@ export function setExceptionReporter(next) {
39
40
  * boundary-caught error to console.error itself, so the recorder installed
40
41
  * by `/telemetry` skips an error the SDK has already seen: one failure is
41
42
  * one record, never two.
43
+ *
44
+ * A failed RPC's error (ConnectClientError) carries the trace the server
45
+ * recorded it in; it is added as the `server.trace_id` attribute, so the
46
+ * browser record links to the server's spans and logs. An attribute of that
47
+ * name passed by the caller wins.
42
48
  */
43
49
  export function reportException(error, attributes) {
44
50
  if (!reporter) {
45
51
  return false;
46
52
  }
47
53
  try {
48
- reporter(error, attributes);
54
+ const traceId = serverTraceIdOf(error);
55
+ reporter(error, traceId
56
+ ? { [SERVER_TRACE_ID_ATTRIBUTE]: traceId, ...attributes }
57
+ : attributes);
49
58
  }
50
59
  catch {
51
60
  // Telemetry must never break the app.
@@ -1 +1 @@
1
- {"version":3,"file":"error-reporter.js","sourceRoot":"","sources":["../src/error-reporter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,iEAAiE;AACjE,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,8EAA8E;AAC9E,yEAAyE;AACzE,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,+BAA+B;AAU/B;;;;GAIG;AAEH,IAAI,QAAQ,GAA6B,IAAI,CAAC;AAE9C;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAA8B;IACjE,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAc,EACd,UAAgC;IAEhC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// The seam between \"something threw\" and \"somebody recorded it\".\n//\n// The error boundary and the Next.js error files live in the barrel, which\n// every frontend imports — including the Vite SPA and React Native ones that\n// never install the HyperDX browser SDK. So the barrel cannot import the SDK.\n// It imports this dependency-free registry instead, and the `/telemetry`\n// subpath (which does import the SDK) installs the real recorder here once the\n// SDK has initialised. Until then — SSR, telemetry off, SDK still loading —\n// reportException is a no-op, which is what makes it safe to call from any\n// catch block unconditionally.\n\n/** Span/log attribute values the recorder accepts. */\nexport type ExceptionAttributes = Record<string, string | number | boolean>;\n\ntype ExceptionReporter = (\n error: unknown,\n attributes?: ExceptionAttributes,\n) => void;\n\n/**\n * Reports which recorder is installed, if any. reportException's return\n * value is \"the telemetry SDK has this error\" — true both when it was just\n * recorded and when it had already been recorded by another route.\n */\n\nlet reporter: ExceptionReporter | null = null;\n\n/**\n * Install (or clear, with null) the recorder reportException forwards to.\n * Called by initBrowserTelemetry; an app has no reason to call it directly.\n */\nexport function setExceptionReporter(next: ExceptionReporter | null): void {\n reporter = next;\n}\n\n/**\n * reportException hands a caught error to the browser telemetry SDK, if one\n * is running. Never throws: reporting an error must not become a second\n * error.\n *\n * Returns true when the SDK has the error and false when no SDK is running —\n * so a caller can fall back to console.error and an error never vanishes:\n *\n * if (!reportException(error)) console.error(error);\n *\n * Use it where React or Next catch an error for you (error boundaries,\n * `error.tsx`). The SDK already records uncaught exceptions, unhandled\n * rejections and anything passed to console.error, and React logs a\n * boundary-caught error to console.error itself, so the recorder installed\n * by `/telemetry` skips an error the SDK has already seen: one failure is\n * one record, never two.\n */\nexport function reportException(\n error: unknown,\n attributes?: ExceptionAttributes,\n): boolean {\n if (!reporter) {\n return false;\n }\n try {\n reporter(error, attributes);\n } catch {\n // Telemetry must never break the app.\n }\n return true;\n}\n"]}
1
+ {"version":3,"file":"error-reporter.js","sourceRoot":"","sources":["../src/error-reporter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,iEAAiE;AACjE,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,8EAA8E;AAC9E,yEAAyE;AACzE,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,+BAA+B;AAE/B,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAU3E;;;;GAIG;AAEH,IAAI,QAAQ,GAA6B,IAAI,CAAC;AAE9C;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAA8B;IACjE,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAc,EACd,UAAgC;IAEhC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACvC,QAAQ,CACN,KAAK,EACL,OAAO;YACL,CAAC,CAAC,EAAE,CAAC,yBAAyB,CAAC,EAAE,OAAO,EAAE,GAAG,UAAU,EAAE;YACzD,CAAC,CAAC,UAAU,CACf,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// The seam between \"something threw\" and \"somebody recorded it\".\n//\n// The error boundary and the Next.js error files live in the barrel, which\n// every frontend imports — including the Vite SPA and React Native ones that\n// never install the HyperDX browser SDK. So the barrel cannot import the SDK.\n// It imports this dependency-free registry instead, and the `/telemetry`\n// subpath (which does import the SDK) installs the real recorder here once the\n// SDK has initialised. Until then — SSR, telemetry off, SDK still loading —\n// reportException is a no-op, which is what makes it safe to call from any\n// catch block unconditionally.\n\nimport { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from \"./trace-id.js\";\n\n/** Span/log attribute values the recorder accepts. */\nexport type ExceptionAttributes = Record<string, string | number | boolean>;\n\ntype ExceptionReporter = (\n error: unknown,\n attributes?: ExceptionAttributes,\n) => void;\n\n/**\n * Reports which recorder is installed, if any. reportException's return\n * value is \"the telemetry SDK has this error\" — true both when it was just\n * recorded and when it had already been recorded by another route.\n */\n\nlet reporter: ExceptionReporter | null = null;\n\n/**\n * Install (or clear, with null) the recorder reportException forwards to.\n * Called by initBrowserTelemetry; an app has no reason to call it directly.\n */\nexport function setExceptionReporter(next: ExceptionReporter | null): void {\n reporter = next;\n}\n\n/**\n * reportException hands a caught error to the browser telemetry SDK, if one\n * is running. Never throws: reporting an error must not become a second\n * error.\n *\n * Returns true when the SDK has the error and false when no SDK is running —\n * so a caller can fall back to console.error and an error never vanishes:\n *\n * if (!reportException(error)) console.error(error);\n *\n * Use it where React or Next catch an error for you (error boundaries,\n * `error.tsx`). The SDK already records uncaught exceptions, unhandled\n * rejections and anything passed to console.error, and React logs a\n * boundary-caught error to console.error itself, so the recorder installed\n * by `/telemetry` skips an error the SDK has already seen: one failure is\n * one record, never two.\n *\n * A failed RPC's error (ConnectClientError) carries the trace the server\n * recorded it in; it is added as the `server.trace_id` attribute, so the\n * browser record links to the server's spans and logs. An attribute of that\n * name passed by the caller wins.\n */\nexport function reportException(\n error: unknown,\n attributes?: ExceptionAttributes,\n): boolean {\n if (!reporter) {\n return false;\n }\n try {\n const traceId = serverTraceIdOf(error);\n reporter(\n error,\n traceId\n ? { [SERVER_TRACE_ID_ATTRIBUTE]: traceId, ...attributes }\n : attributes,\n );\n } catch {\n // Telemetry must never break the app.\n }\n return true;\n}\n"]}
package/dist/errors.d.ts CHANGED
@@ -52,6 +52,14 @@ export declare class ConnectClientError extends Error {
52
52
  * never off `message` — message text is display-only.
53
53
  */
54
54
  readonly reason: string | null;
55
+ /**
56
+ * The trace the failed request was recorded in: the server's
57
+ * `traceresponse` when it sent one, else the trace id of the traceparent
58
+ * this runtime sent, else null (the request never left the browser). Put it
59
+ * in a bug report, a toast's "copy details", or a support ticket — it is the
60
+ * key to every server span and log line of that request.
61
+ */
62
+ readonly traceId: string | null;
55
63
  constructor(args: {
56
64
  message: string;
57
65
  code: string;
@@ -59,6 +67,7 @@ export declare class ConnectClientError extends Error {
59
67
  status: number;
60
68
  retryable: boolean;
61
69
  reason?: string | null;
70
+ traceId?: string | null;
62
71
  cause?: unknown;
63
72
  });
64
73
  /**
@@ -80,11 +89,12 @@ export declare function isRetryableCode(code: Code): boolean;
80
89
  /**
81
90
  * normalizeError coerces any thrown value into a ConnectClientError. Already-
82
91
  * normalized errors pass through unchanged so the interceptor chain never
83
- * double-wraps.
92
+ * double-wraps. `requestTraceId` is the trace id of the traceparent the
93
+ * request carried, used when the server sent no `traceresponse`.
84
94
  *
85
95
  * Both `instanceof` checks below match on SHAPE rather than class identity —
86
96
  * ours via ConnectClientError's Symbol.hasInstance, ConnectError's via the one
87
97
  * @connectrpc/connect ships — so neither branch is missed when a second copy
88
98
  * of either package lands in the bundle.
89
99
  */
90
- export declare function normalizeError(err: unknown): ConnectClientError;
100
+ export declare function normalizeError(err: unknown, requestTraceId?: string | null): ConnectClientError;
package/dist/errors.js CHANGED
@@ -7,6 +7,7 @@
7
7
  // code name, an HTTP-ish status, a retryability verdict, and a
8
8
  // human-presentable message.
9
9
  import { Code, ConnectError } from "@connectrpc/connect";
10
+ import { TRACERESPONSE_HEADER, traceIdFrom } from "./trace.js";
10
11
  /**
11
12
  * FORGE_ERROR_REASON_HEADER is the Connect trailer/metadata key carrying a
12
13
  * stable, machine-readable reason code (snake_case) the backend sets for
@@ -115,6 +116,14 @@ export class ConnectClientError extends Error {
115
116
  * never off `message` — message text is display-only.
116
117
  */
117
118
  reason;
119
+ /**
120
+ * The trace the failed request was recorded in: the server's
121
+ * `traceresponse` when it sent one, else the trace id of the traceparent
122
+ * this runtime sent, else null (the request never left the browser). Put it
123
+ * in a bug report, a toast's "copy details", or a support ticket — it is the
124
+ * key to every server span and log line of that request.
125
+ */
126
+ traceId;
118
127
  constructor(args) {
119
128
  super(args.message, { cause: args.cause });
120
129
  this.name = "ConnectClientError";
@@ -123,6 +132,7 @@ export class ConnectClientError extends Error {
123
132
  this.status = args.status;
124
133
  this.retryable = args.retryable;
125
134
  this.reason = args.reason ?? null;
135
+ this.traceId = args.traceId ?? null;
126
136
  }
127
137
  /**
128
138
  * `instanceof` matches on SHAPE, not on class identity — so every check
@@ -147,14 +157,15 @@ export function isRetryableCode(code) {
147
157
  /**
148
158
  * normalizeError coerces any thrown value into a ConnectClientError. Already-
149
159
  * normalized errors pass through unchanged so the interceptor chain never
150
- * double-wraps.
160
+ * double-wraps. `requestTraceId` is the trace id of the traceparent the
161
+ * request carried, used when the server sent no `traceresponse`.
151
162
  *
152
163
  * Both `instanceof` checks below match on SHAPE rather than class identity —
153
164
  * ours via ConnectClientError's Symbol.hasInstance, ConnectError's via the one
154
165
  * @connectrpc/connect ships — so neither branch is missed when a second copy
155
166
  * of either package lands in the bundle.
156
167
  */
157
- export function normalizeError(err) {
168
+ export function normalizeError(err, requestTraceId = null) {
158
169
  if (err instanceof ConnectClientError) {
159
170
  return err;
160
171
  }
@@ -169,6 +180,9 @@ export function normalizeError(err) {
169
180
  retryable: isRetryableCode(err.code),
170
181
  // Stable machine reason from backend metadata (backend owns the set).
171
182
  reason: err.metadata.get(FORGE_ERROR_REASON_HEADER) || null,
183
+ // The server's own span when it answered; the trace this runtime asked
184
+ // for when it did not (a proxy error, an older server).
185
+ traceId: traceIdFrom(err.metadata.get(TRACERESPONSE_HEADER)) ?? requestTraceId,
172
186
  cause: err,
173
187
  });
174
188
  }
@@ -184,6 +198,7 @@ export function normalizeError(err) {
184
198
  status: networkFault ? 503 : 500,
185
199
  retryable: networkFault,
186
200
  reason: null,
201
+ traceId: requestTraceId,
187
202
  cause: err,
188
203
  });
189
204
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Typed client errors. Connect surfaces failures as ConnectError with a\n// numeric Code; the rest of the app should reason about a stable, typed\n// shape instead of sniffing status codes at every call site. normalizeError\n// maps any thrown value into a ConnectClientError carrying the canonical\n// code name, an HTTP-ish status, a retryability verdict, and a\n// human-presentable message.\nimport { Code, ConnectError } from \"@connectrpc/connect\";\n\n/**\n * FORGE_ERROR_REASON_HEADER is the Connect trailer/metadata key carrying a\n * stable, machine-readable reason code (snake_case) the backend sets for\n * every error. UI/routing keys off `error.reason` instead of matching\n * human-readable message strings — the message text is display-only and\n * may change. The backend owns the setter; the frontend only reads it.\n */\nexport const FORGE_ERROR_REASON_HEADER = \"x-forge-error-reason\";\n\n/**\n * stripServerFraming removes the transport framing that must never surface\n * to a user: the leading \"[code] \" gRPC-code prefix connect-es puts on\n * `ConnectError.message`. What survives is the message the server wrote,\n * which is exactly what a user should read.\n *\n * This is the SINGLE implementation — a project never wants a different\n * answer here, and a copy frozen into a scaffold could never be corrected\n * once shipped. Idempotent: framing that has already been stripped strips\n * to itself.\n */\nexport function stripServerFraming(message: string): string {\n return message.replace(/^\\[[a-z_]+\\]\\s*/i, \"\").trim();\n}\n\n/** The copy shown when an error carries no presentable message of its own. */\nconst DEFAULT_USER_MESSAGE = \"Something went wrong. Please try again.\";\n\n/**\n * userMessage — turn any thrown value into copy fit for end users.\n *\n * `ConnectError.message` prefixes the gRPC code (\"[not_found] no such task\");\n * `rawMessage` is the server's human-readable text without that framing, and\n * stripServerFraming drops the prefix from whichever of the two is available.\n * Use this everywhere an error is SHOWN (banners, toasts) instead of\n * `err.message`.\n *\n * Pass `fallback` to override the default copy for a surface that wants its\n * own wording.\n */\nexport function userMessage(\n err: unknown,\n fallback: string = DEFAULT_USER_MESSAGE,\n): string {\n if (err instanceof ConnectError) {\n return stripServerFraming(err.rawMessage || err.message) || fallback;\n }\n if (err instanceof Error) {\n return stripServerFraming(err.message) || fallback;\n }\n return err == null ? fallback : stripServerFraming(String(err)) || fallback;\n}\n\n/** Connect codes worth a transparent transport-level retry. */\nconst RETRYABLE = new Set<Code>([\n Code.Unavailable,\n Code.DeadlineExceeded,\n Code.Aborted,\n Code.ResourceExhausted,\n]);\n\n/** Rough Connect-code → HTTP-status mapping for display/telemetry only. */\nconst HTTP_STATUS: Partial<Record<Code, number>> = {\n [Code.InvalidArgument]: 400,\n [Code.Unauthenticated]: 401,\n [Code.PermissionDenied]: 403,\n [Code.NotFound]: 404,\n [Code.AlreadyExists]: 409,\n [Code.ResourceExhausted]: 429,\n [Code.Unimplemented]: 501,\n [Code.Unavailable]: 503,\n [Code.DeadlineExceeded]: 504,\n [Code.Internal]: 500,\n};\n\n/**\n * isConnectClientErrorShape — the STRUCTURAL test behind\n * {@link ConnectClientError}'s `instanceof`.\n *\n * A class object is per module instance, not per package. npm nests a\n * transitive dependency on any version conflict, and a bundler can pull one\n * package in as both ESM and CJS, so a tree holding two copies of this\n * package holds two distinct ConnectClientError classes. Matching on shape\n * instead of identity is what makes the two agree: the shape is the\n * contract, the constructor is not.\n */\nfunction isConnectClientErrorShape(v: unknown): v is ConnectClientError {\n if (!(v instanceof Error)) return false;\n if (Object.getPrototypeOf(v) === ConnectClientError.prototype) return true;\n const e = v as Partial<ConnectClientError>;\n return (\n v.name === \"ConnectClientError\" &&\n typeof e.code === \"string\" &&\n typeof e.status === \"number\" &&\n typeof e.retryable === \"boolean\" &&\n (typeof e.reason === \"string\" || e.reason === null)\n );\n}\n\n/**\n * ConnectClientError is the single typed error shape the app catches. It is\n * a plain Error subclass so it works with React Query, error boundaries, and\n * `instanceof` alike.\n */\nexport class ConnectClientError extends Error {\n /** Canonical Connect code name, e.g. \"unavailable\", \"not_found\". */\n readonly code: string;\n /** Numeric Connect code (Code enum), or undefined for non-Connect errors. */\n readonly connectCode?: Code;\n /** Approximate HTTP status, for telemetry and generic UI. */\n readonly status: number;\n /** True when a transparent retry is reasonable (transient failures). */\n readonly retryable: boolean;\n /**\n * Stable machine-readable reason code from the backend\n * (x-forge-error-reason metadata), or null. Key UI/routing off THIS,\n * never off `message` — message text is display-only.\n */\n readonly reason: string | null;\n\n constructor(args: {\n message: string;\n code: string;\n connectCode?: Code;\n status: number;\n retryable: boolean;\n reason?: string | null;\n cause?: unknown;\n }) {\n super(args.message, { cause: args.cause });\n this.name = \"ConnectClientError\";\n this.code = args.code;\n this.connectCode = args.connectCode;\n this.status = args.status;\n this.retryable = args.retryable;\n this.reason = args.reason ?? null;\n }\n\n /**\n * `instanceof` matches on SHAPE, not on class identity — so every check\n * spelled `err instanceof ConnectClientError`, here and in app code, keeps\n * working when two copies of this package end up in one bundle (see\n * {@link isConnectClientErrorShape}). Without it, normalizeError's\n * pass-through guard misses an already-normalized error from the other\n * copy; that error's `name` is \"ConnectClientError\" rather than\n * \"ConnectError\", so it misses the ConnectError branch too and is re-wrapped\n * through the generic Error branch — silently downgrading every error in the\n * app to code \"unknown\", status 500, no connectCode and no reason.\n * @connectrpc/connect does the same on ConnectError, for the same reason.\n */\n static [Symbol.hasInstance](v: unknown): boolean {\n return isConnectClientErrorShape(v);\n }\n}\n\n/** isRetryable reports whether a Connect code merits a transport retry. */\nexport function isRetryableCode(code: Code): boolean {\n return RETRYABLE.has(code);\n}\n\n/**\n * normalizeError coerces any thrown value into a ConnectClientError. Already-\n * normalized errors pass through unchanged so the interceptor chain never\n * double-wraps.\n *\n * Both `instanceof` checks below match on SHAPE rather than class identity —\n * ours via ConnectClientError's Symbol.hasInstance, ConnectError's via the one\n * @connectrpc/connect ships — so neither branch is missed when a second copy\n * of either package lands in the bundle.\n */\nexport function normalizeError(err: unknown): ConnectClientError {\n if (err instanceof ConnectClientError) {\n return err;\n }\n\n if (err instanceof ConnectError) {\n return new ConnectClientError({\n // Strip the \"[code] \" prefix so the message is user-presentable at\n // every layer.\n message: stripServerFraming(err.rawMessage || err.message),\n code: Code[err.code]?.toLowerCase() ?? \"unknown\",\n connectCode: err.code,\n status: HTTP_STATUS[err.code] ?? 500,\n retryable: isRetryableCode(err.code),\n // Stable machine reason from backend metadata (backend owns the set).\n reason: err.metadata.get(FORGE_ERROR_REASON_HEADER) || null,\n cause: err,\n });\n }\n\n if (err instanceof Error) {\n // A network fault (fetch rejected: offline, DNS, CORS preflight, TLS)\n // surfaces as a TypeError — that, and only that, is a safe transparent\n // retry. A generic Error is NOT blanket-retried (blanket-retrying every\n // bare Error widened the blast radius to non-idempotent failures).\n const networkFault = err instanceof TypeError;\n return new ConnectClientError({\n message: stripServerFraming(err.message),\n code: networkFault ? \"unavailable\" : \"unknown\",\n status: networkFault ? 503 : 500,\n retryable: networkFault,\n reason: null,\n cause: err,\n });\n }\n\n return new ConnectClientError({\n message:\n typeof err === \"string\" ? stripServerFraming(err) : \"Unknown error\",\n code: \"unknown\",\n status: 500,\n retryable: false,\n reason: null,\n cause: err,\n });\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Typed client errors. Connect surfaces failures as ConnectError with a\n// numeric Code; the rest of the app should reason about a stable, typed\n// shape instead of sniffing status codes at every call site. normalizeError\n// maps any thrown value into a ConnectClientError carrying the canonical\n// code name, an HTTP-ish status, a retryability verdict, and a\n// human-presentable message.\nimport { Code, ConnectError } from \"@connectrpc/connect\";\n\nimport { TRACERESPONSE_HEADER, traceIdFrom } from \"./trace.js\";\n\n/**\n * FORGE_ERROR_REASON_HEADER is the Connect trailer/metadata key carrying a\n * stable, machine-readable reason code (snake_case) the backend sets for\n * every error. UI/routing keys off `error.reason` instead of matching\n * human-readable message strings — the message text is display-only and\n * may change. The backend owns the setter; the frontend only reads it.\n */\nexport const FORGE_ERROR_REASON_HEADER = \"x-forge-error-reason\";\n\n/**\n * stripServerFraming removes the transport framing that must never surface\n * to a user: the leading \"[code] \" gRPC-code prefix connect-es puts on\n * `ConnectError.message`. What survives is the message the server wrote,\n * which is exactly what a user should read.\n *\n * This is the SINGLE implementation — a project never wants a different\n * answer here, and a copy frozen into a scaffold could never be corrected\n * once shipped. Idempotent: framing that has already been stripped strips\n * to itself.\n */\nexport function stripServerFraming(message: string): string {\n return message.replace(/^\\[[a-z_]+\\]\\s*/i, \"\").trim();\n}\n\n/** The copy shown when an error carries no presentable message of its own. */\nconst DEFAULT_USER_MESSAGE = \"Something went wrong. Please try again.\";\n\n/**\n * userMessage — turn any thrown value into copy fit for end users.\n *\n * `ConnectError.message` prefixes the gRPC code (\"[not_found] no such task\");\n * `rawMessage` is the server's human-readable text without that framing, and\n * stripServerFraming drops the prefix from whichever of the two is available.\n * Use this everywhere an error is SHOWN (banners, toasts) instead of\n * `err.message`.\n *\n * Pass `fallback` to override the default copy for a surface that wants its\n * own wording.\n */\nexport function userMessage(\n err: unknown,\n fallback: string = DEFAULT_USER_MESSAGE,\n): string {\n if (err instanceof ConnectError) {\n return stripServerFraming(err.rawMessage || err.message) || fallback;\n }\n if (err instanceof Error) {\n return stripServerFraming(err.message) || fallback;\n }\n return err == null ? fallback : stripServerFraming(String(err)) || fallback;\n}\n\n/** Connect codes worth a transparent transport-level retry. */\nconst RETRYABLE = new Set<Code>([\n Code.Unavailable,\n Code.DeadlineExceeded,\n Code.Aborted,\n Code.ResourceExhausted,\n]);\n\n/** Rough Connect-code → HTTP-status mapping for display/telemetry only. */\nconst HTTP_STATUS: Partial<Record<Code, number>> = {\n [Code.InvalidArgument]: 400,\n [Code.Unauthenticated]: 401,\n [Code.PermissionDenied]: 403,\n [Code.NotFound]: 404,\n [Code.AlreadyExists]: 409,\n [Code.ResourceExhausted]: 429,\n [Code.Unimplemented]: 501,\n [Code.Unavailable]: 503,\n [Code.DeadlineExceeded]: 504,\n [Code.Internal]: 500,\n};\n\n/**\n * isConnectClientErrorShape — the STRUCTURAL test behind\n * {@link ConnectClientError}'s `instanceof`.\n *\n * A class object is per module instance, not per package. npm nests a\n * transitive dependency on any version conflict, and a bundler can pull one\n * package in as both ESM and CJS, so a tree holding two copies of this\n * package holds two distinct ConnectClientError classes. Matching on shape\n * instead of identity is what makes the two agree: the shape is the\n * contract, the constructor is not.\n */\nfunction isConnectClientErrorShape(v: unknown): v is ConnectClientError {\n if (!(v instanceof Error)) return false;\n if (Object.getPrototypeOf(v) === ConnectClientError.prototype) return true;\n const e = v as Partial<ConnectClientError>;\n return (\n v.name === \"ConnectClientError\" &&\n typeof e.code === \"string\" &&\n typeof e.status === \"number\" &&\n typeof e.retryable === \"boolean\" &&\n (typeof e.reason === \"string\" || e.reason === null)\n );\n}\n\n/**\n * ConnectClientError is the single typed error shape the app catches. It is\n * a plain Error subclass so it works with React Query, error boundaries, and\n * `instanceof` alike.\n */\nexport class ConnectClientError extends Error {\n /** Canonical Connect code name, e.g. \"unavailable\", \"not_found\". */\n readonly code: string;\n /** Numeric Connect code (Code enum), or undefined for non-Connect errors. */\n readonly connectCode?: Code;\n /** Approximate HTTP status, for telemetry and generic UI. */\n readonly status: number;\n /** True when a transparent retry is reasonable (transient failures). */\n readonly retryable: boolean;\n /**\n * Stable machine-readable reason code from the backend\n * (x-forge-error-reason metadata), or null. Key UI/routing off THIS,\n * never off `message` — message text is display-only.\n */\n readonly reason: string | null;\n /**\n * The trace the failed request was recorded in: the server's\n * `traceresponse` when it sent one, else the trace id of the traceparent\n * this runtime sent, else null (the request never left the browser). Put it\n * in a bug report, a toast's \"copy details\", or a support ticket — it is the\n * key to every server span and log line of that request.\n */\n readonly traceId: string | null;\n\n constructor(args: {\n message: string;\n code: string;\n connectCode?: Code;\n status: number;\n retryable: boolean;\n reason?: string | null;\n traceId?: string | null;\n cause?: unknown;\n }) {\n super(args.message, { cause: args.cause });\n this.name = \"ConnectClientError\";\n this.code = args.code;\n this.connectCode = args.connectCode;\n this.status = args.status;\n this.retryable = args.retryable;\n this.reason = args.reason ?? null;\n this.traceId = args.traceId ?? null;\n }\n\n /**\n * `instanceof` matches on SHAPE, not on class identity — so every check\n * spelled `err instanceof ConnectClientError`, here and in app code, keeps\n * working when two copies of this package end up in one bundle (see\n * {@link isConnectClientErrorShape}). Without it, normalizeError's\n * pass-through guard misses an already-normalized error from the other\n * copy; that error's `name` is \"ConnectClientError\" rather than\n * \"ConnectError\", so it misses the ConnectError branch too and is re-wrapped\n * through the generic Error branch — silently downgrading every error in the\n * app to code \"unknown\", status 500, no connectCode and no reason.\n * @connectrpc/connect does the same on ConnectError, for the same reason.\n */\n static [Symbol.hasInstance](v: unknown): boolean {\n return isConnectClientErrorShape(v);\n }\n}\n\n/** isRetryable reports whether a Connect code merits a transport retry. */\nexport function isRetryableCode(code: Code): boolean {\n return RETRYABLE.has(code);\n}\n\n/**\n * normalizeError coerces any thrown value into a ConnectClientError. Already-\n * normalized errors pass through unchanged so the interceptor chain never\n * double-wraps. `requestTraceId` is the trace id of the traceparent the\n * request carried, used when the server sent no `traceresponse`.\n *\n * Both `instanceof` checks below match on SHAPE rather than class identity —\n * ours via ConnectClientError's Symbol.hasInstance, ConnectError's via the one\n * @connectrpc/connect ships — so neither branch is missed when a second copy\n * of either package lands in the bundle.\n */\nexport function normalizeError(\n err: unknown,\n requestTraceId: string | null = null,\n): ConnectClientError {\n if (err instanceof ConnectClientError) {\n return err;\n }\n\n if (err instanceof ConnectError) {\n return new ConnectClientError({\n // Strip the \"[code] \" prefix so the message is user-presentable at\n // every layer.\n message: stripServerFraming(err.rawMessage || err.message),\n code: Code[err.code]?.toLowerCase() ?? \"unknown\",\n connectCode: err.code,\n status: HTTP_STATUS[err.code] ?? 500,\n retryable: isRetryableCode(err.code),\n // Stable machine reason from backend metadata (backend owns the set).\n reason: err.metadata.get(FORGE_ERROR_REASON_HEADER) || null,\n // The server's own span when it answered; the trace this runtime asked\n // for when it did not (a proxy error, an older server).\n traceId:\n traceIdFrom(err.metadata.get(TRACERESPONSE_HEADER)) ?? requestTraceId,\n cause: err,\n });\n }\n\n if (err instanceof Error) {\n // A network fault (fetch rejected: offline, DNS, CORS preflight, TLS)\n // surfaces as a TypeError — that, and only that, is a safe transparent\n // retry. A generic Error is NOT blanket-retried (blanket-retrying every\n // bare Error widened the blast radius to non-idempotent failures).\n const networkFault = err instanceof TypeError;\n return new ConnectClientError({\n message: stripServerFraming(err.message),\n code: networkFault ? \"unavailable\" : \"unknown\",\n status: networkFault ? 503 : 500,\n retryable: networkFault,\n reason: null,\n traceId: requestTraceId,\n cause: err,\n });\n }\n\n return new ConnectClientError({\n message:\n typeof err === \"string\" ? stripServerFraming(err) : \"Unknown error\",\n code: \"unknown\",\n status: 500,\n retryable: false,\n reason: null,\n cause: err,\n });\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export { createBasePath, joinBasePath, normalizeBasePath, type BasePathHelpers,
23
23
  */
24
24
  export { basisPointsToInput, currencyMinorDigits, formatBasisPoints, formatMinorUnits, minorUnitsToInput, parseBasisPoints, parseMinorUnits, type BasisPointsOptions, type MoneyOptions, type ParseBasisPointsOptions, type ParseMoneyOptions, } from "./money.js";
25
25
  /** W3C trace-context propagation. */
26
- export { freshTraceparent, injectTraceContext, setTraceSampled, TRACEPARENT_RE, } from "./trace.js";
26
+ export { freshTraceparent, injectTraceContext, setTraceSampled, traceIdFrom, TRACEPARENT_RE, TRACERESPONSE_HEADER, } from "./trace.js";
27
27
  /** Session: the signed-in user, derived from the app's auth state and JWT claims. */
28
28
  export { decodeJwt, SessionProvider, useSession, type JwtClaims, type Session, type SessionAuth, } from "./session.js";
29
29
  /** The app-shell provider set: session + error boundary + toast host in one. */
@@ -42,6 +42,16 @@ export { Resource, type ResourceColumn, type ResourceProps, type ResourceStatus,
42
42
  * through the "/telemetry" subpath, never the barrel.
43
43
  */
44
44
  export { reportException, setExceptionReporter, type ExceptionAttributes, } from "./error-reporter.js";
45
+ /** The server trace id a failed RPC's error carries (ConnectClientError.traceId). */
46
+ export { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from "./trace-id.js";
47
+ /**
48
+ * Product events, the signed-in user and the consent kill-switch for browser
49
+ * telemetry. Like reportException these are safe to call anywhere — no-ops on
50
+ * the server and when telemetry is off — and calls made while the SDK is still
51
+ * loading are held and replayed. The SDK itself arrives through the
52
+ * "/telemetry" subpath, never the barrel.
53
+ */
54
+ export { disableTelemetry, setTelemetryUser, track, type TrackProps, type TrackValue, } from "./track.js";
45
55
  /**
46
56
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
47
57
  * to the dev server, which prints them into
package/dist/index.js CHANGED
@@ -41,7 +41,7 @@ export { createBasePath, joinBasePath, normalizeBasePath, } from "./basepath.js"
41
41
  */
42
42
  export { basisPointsToInput, currencyMinorDigits, formatBasisPoints, formatMinorUnits, minorUnitsToInput, parseBasisPoints, parseMinorUnits, } from "./money.js";
43
43
  /** W3C trace-context propagation. */
44
- export { freshTraceparent, injectTraceContext, setTraceSampled, TRACEPARENT_RE, } from "./trace.js";
44
+ export { freshTraceparent, injectTraceContext, setTraceSampled, traceIdFrom, TRACEPARENT_RE, TRACERESPONSE_HEADER, } from "./trace.js";
45
45
  /** Session: the signed-in user, derived from the app's auth state and JWT claims. */
46
46
  export { decodeJwt, SessionProvider, useSession, } from "./session.js";
47
47
  /** The app-shell provider set: session + error boundary + toast host in one. */
@@ -60,6 +60,16 @@ export { Resource, } from "./resource.js";
60
60
  * through the "/telemetry" subpath, never the barrel.
61
61
  */
62
62
  export { reportException, setExceptionReporter, } from "./error-reporter.js";
63
+ /** The server trace id a failed RPC's error carries (ConnectClientError.traceId). */
64
+ export { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from "./trace-id.js";
65
+ /**
66
+ * Product events, the signed-in user and the consent kill-switch for browser
67
+ * telemetry. Like reportException these are safe to call anywhere — no-ops on
68
+ * the server and when telemetry is off — and calls made while the SDK is still
69
+ * loading are held and replayed. The SDK itself arrives through the
70
+ * "/telemetry" subpath, never the barrel.
71
+ */
72
+ export { disableTelemetry, setTelemetryUser, track, } from "./track.js";
63
73
  /**
64
74
  * Dev-only browser log forwarding: mirrors console output and uncaught errors
65
75
  * to the dev server, which prints them into
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,4CAA4C;AAE5C,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,sEAAsE;AAEtE,mCAAmC;AACnC,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,YAAY,EACZ,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAKhB,MAAM,YAAY,CAAC;AAEpB,qCAAqC;AACrC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,qFAAqF;AACrF,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,GAIX,MAAM,cAAc,CAAC;AAEtB,gFAAgF;AAChF,OAAO,EAAE,YAAY,EAA0B,MAAM,gBAAgB,CAAC;AAEtE,+EAA+E;AAC/E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,yEAAyE;AACzE,OAAO,EACL,gBAAgB,GAMjB,MAAM,iBAAiB,CAAC;AAEzB,gEAAgE;AAChE,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,uFAAuF;AACvF,OAAO,EACL,QAAQ,GAIT,MAAM,eAAe,CAAC;AAEvB;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,GAErB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,aAAa,CAAC","sourcesContent":["// @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Public barrel. Import batteries from \"@reliantlabs/forge-web-runtime\".\n//\n// Everything here is MECHANISM: the transport interceptor stack, the session,\n// the error boundary, the toast queue, the <Resource> tristate ladder, W3C\n// trace propagation, client RUM. Nothing here reaches into an application —\n// app-shaped values (auth state, event subscriptions, toast markup) arrive as\n// props, so an app can restyle or replace its own files freely.\n//\n// Presentation that a project is meant to own — the component library under\n// src/components/ui, globals.css, nav, layout — deliberately stays in the\n// scaffold and is NOT part of this package.\n\n// The per-group docs below are JSDoc, not `//`, on purpose: TypeScript's\n// declaration emit preserves JSDoc and DROPS line comments, and dist/ is now\n// the whole of what this package publishes. `forge project libraries` reads\n// this inventory out of the published barrel, and a consumer's editor shows\n// it on hover — both would get symbols with no explanation otherwise.\n\n/** Transport interceptor stack. */\nexport {\n authInterceptor,\n buildRuntimeInterceptors,\n errorNormalizeInterceptor,\n headerInterceptor,\n retryInterceptor,\n traceInterceptor,\n type RuntimeInterceptorConfig,\n} from \"./interceptors.js\";\n\n/**\n * Typed client errors, and the backend-framing strippers every surface that\n * SHOWS an error goes through.\n */\nexport {\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n stripServerFraming,\n userMessage,\n} from \"./errors.js\";\n\n/**\n * Mount-prefix helpers, for the URLs Next.js's router never sees — absolute\n * URLs handed to external systems that round-trip back into the app.\n *\n * Barrel-exported rather than behind a subpath: three string functions with\n * no imports, so there is no dependency footprint for a subpath to fence off.\n */\nexport {\n createBasePath,\n joinBasePath,\n normalizeBasePath,\n type BasePathHelpers,\n} from \"./basepath.js\";\n\n/**\n * Money and basis points as integers: format minor units through Intl, and\n * parse form input back to minor units without rounding.\n *\n * The web counterpart of forge/pkg/money — an int64 count of the currency's\n * minor unit plus an ISO 4217 code, exact past 2^53. Barrel-exported for the\n * same reason as the base-path helpers: it imports nothing.\n */\nexport {\n basisPointsToInput,\n currencyMinorDigits,\n formatBasisPoints,\n formatMinorUnits,\n minorUnitsToInput,\n parseBasisPoints,\n parseMinorUnits,\n type BasisPointsOptions,\n type MoneyOptions,\n type ParseBasisPointsOptions,\n type ParseMoneyOptions,\n} from \"./money.js\";\n\n/** W3C trace-context propagation. */\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n TRACEPARENT_RE,\n} from \"./trace.js\";\n\n/** Session: the signed-in user, derived from the app's auth state and JWT claims. */\nexport {\n decodeJwt,\n SessionProvider,\n useSession,\n type JwtClaims,\n type Session,\n type SessionAuth,\n} from \"./session.js\";\n\n/** The app-shell provider set: session + error boundary + toast host in one. */\nexport { RuntimeShell, type RuntimeShellProps } from \"./providers.js\";\n\n/** Error boundary with a designed crash fallback, for a route or a subtree. */\nexport {\n DefaultErrorFallback,\n RuntimeErrorBoundary,\n} from \"./error-boundary.js\";\n\n/** The toast queue. The app supplies the event source and the markup. */\nexport {\n RuntimeToastHost,\n type RuntimeToast,\n type RuntimeToastHostProps,\n type RuntimeToastPayload,\n type RuntimeToastSink,\n type RuntimeToastVariant,\n} from \"./toast-host.js\";\n\n/** Route guarding: render a page only to a signed-in caller. */\nexport { RouteGuard, type RouteGuardProps } from \"./route-guard.js\";\n\n/** Generic data-table container: the tristate ladder, filter and cursor pagination. */\nexport {\n Resource,\n type ResourceColumn,\n type ResourceProps,\n type ResourceStatus,\n} from \"./resource.js\";\n\n/**\n * Record a caught error with the browser telemetry SDK, when one is running.\n * A no-op otherwise, so it is safe in any catch block. The SDK itself arrives\n * through the \"/telemetry\" subpath, never the barrel.\n */\nexport {\n reportException,\n setExceptionReporter,\n type ExceptionAttributes,\n} from \"./error-reporter.js\";\n\n/**\n * Dev-only browser log forwarding: mirrors console output and uncaught errors\n * to the dev server, which prints them into\n * `.forge/logs/<env>/frontend_<name>.log`. No-ops unless `dev: true`, and the\n * receiving endpoint exists only in a dev server — so a production bundle\n * neither posts nor has anywhere to post to.\n */\nexport {\n DEV_LOG_ENDPOINT,\n devLoggingInstalled,\n installDevLogging,\n uninstallDevLogging,\n type DevLoggingOptions,\n type DevLogPayload,\n} from \"./devlog.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,gEAAgE;AAChE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,4CAA4C;AAE5C,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,sEAAsE;AAEtE,mCAAmC;AACnC,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,WAAW,GACZ,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,OAAO,EACL,cAAc,EACd,YAAY,EACZ,iBAAiB,GAElB,MAAM,eAAe,CAAC;AAEvB;;;;;;;GAOG;AACH,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAKhB,MAAM,YAAY,CAAC;AAEpB,qCAAqC;AACrC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,cAAc,EACd,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,qFAAqF;AACrF,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,GAIX,MAAM,cAAc,CAAC;AAEtB,gFAAgF;AAChF,OAAO,EAAE,YAAY,EAA0B,MAAM,gBAAgB,CAAC;AAEtE,+EAA+E;AAC/E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,yEAAyE;AACzE,OAAO,EACL,gBAAgB,GAMjB,MAAM,iBAAiB,CAAC;AAEzB,gEAAgE;AAChE,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,uFAAuF;AACvF,OAAO,EACL,QAAQ,GAIT,MAAM,eAAe,CAAC;AAEvB;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,oBAAoB,GAErB,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE3E;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,GAGN,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,aAAa,CAAC","sourcesContent":["// @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Public barrel. Import batteries from \"@reliantlabs/forge-web-runtime\".\n//\n// Everything here is MECHANISM: the transport interceptor stack, the session,\n// the error boundary, the toast queue, the <Resource> tristate ladder, W3C\n// trace propagation, client RUM. Nothing here reaches into an application —\n// app-shaped values (auth state, event subscriptions, toast markup) arrive as\n// props, so an app can restyle or replace its own files freely.\n//\n// Presentation that a project is meant to own — the component library under\n// src/components/ui, globals.css, nav, layout — deliberately stays in the\n// scaffold and is NOT part of this package.\n\n// The per-group docs below are JSDoc, not `//`, on purpose: TypeScript's\n// declaration emit preserves JSDoc and DROPS line comments, and dist/ is now\n// the whole of what this package publishes. `forge project libraries` reads\n// this inventory out of the published barrel, and a consumer's editor shows\n// it on hover — both would get symbols with no explanation otherwise.\n\n/** Transport interceptor stack. */\nexport {\n authInterceptor,\n buildRuntimeInterceptors,\n errorNormalizeInterceptor,\n headerInterceptor,\n retryInterceptor,\n traceInterceptor,\n type RuntimeInterceptorConfig,\n} from \"./interceptors.js\";\n\n/**\n * Typed client errors, and the backend-framing strippers every surface that\n * SHOWS an error goes through.\n */\nexport {\n ConnectClientError,\n FORGE_ERROR_REASON_HEADER,\n isRetryableCode,\n normalizeError,\n stripServerFraming,\n userMessage,\n} from \"./errors.js\";\n\n/**\n * Mount-prefix helpers, for the URLs Next.js's router never sees — absolute\n * URLs handed to external systems that round-trip back into the app.\n *\n * Barrel-exported rather than behind a subpath: three string functions with\n * no imports, so there is no dependency footprint for a subpath to fence off.\n */\nexport {\n createBasePath,\n joinBasePath,\n normalizeBasePath,\n type BasePathHelpers,\n} from \"./basepath.js\";\n\n/**\n * Money and basis points as integers: format minor units through Intl, and\n * parse form input back to minor units without rounding.\n *\n * The web counterpart of forge/pkg/money — an int64 count of the currency's\n * minor unit plus an ISO 4217 code, exact past 2^53. Barrel-exported for the\n * same reason as the base-path helpers: it imports nothing.\n */\nexport {\n basisPointsToInput,\n currencyMinorDigits,\n formatBasisPoints,\n formatMinorUnits,\n minorUnitsToInput,\n parseBasisPoints,\n parseMinorUnits,\n type BasisPointsOptions,\n type MoneyOptions,\n type ParseBasisPointsOptions,\n type ParseMoneyOptions,\n} from \"./money.js\";\n\n/** W3C trace-context propagation. */\nexport {\n freshTraceparent,\n injectTraceContext,\n setTraceSampled,\n traceIdFrom,\n TRACEPARENT_RE,\n TRACERESPONSE_HEADER,\n} from \"./trace.js\";\n\n/** Session: the signed-in user, derived from the app's auth state and JWT claims. */\nexport {\n decodeJwt,\n SessionProvider,\n useSession,\n type JwtClaims,\n type Session,\n type SessionAuth,\n} from \"./session.js\";\n\n/** The app-shell provider set: session + error boundary + toast host in one. */\nexport { RuntimeShell, type RuntimeShellProps } from \"./providers.js\";\n\n/** Error boundary with a designed crash fallback, for a route or a subtree. */\nexport {\n DefaultErrorFallback,\n RuntimeErrorBoundary,\n} from \"./error-boundary.js\";\n\n/** The toast queue. The app supplies the event source and the markup. */\nexport {\n RuntimeToastHost,\n type RuntimeToast,\n type RuntimeToastHostProps,\n type RuntimeToastPayload,\n type RuntimeToastSink,\n type RuntimeToastVariant,\n} from \"./toast-host.js\";\n\n/** Route guarding: render a page only to a signed-in caller. */\nexport { RouteGuard, type RouteGuardProps } from \"./route-guard.js\";\n\n/** Generic data-table container: the tristate ladder, filter and cursor pagination. */\nexport {\n Resource,\n type ResourceColumn,\n type ResourceProps,\n type ResourceStatus,\n} from \"./resource.js\";\n\n/**\n * Record a caught error with the browser telemetry SDK, when one is running.\n * A no-op otherwise, so it is safe in any catch block. The SDK itself arrives\n * through the \"/telemetry\" subpath, never the barrel.\n */\nexport {\n reportException,\n setExceptionReporter,\n type ExceptionAttributes,\n} from \"./error-reporter.js\";\n\n/** The server trace id a failed RPC's error carries (ConnectClientError.traceId). */\nexport { SERVER_TRACE_ID_ATTRIBUTE, serverTraceIdOf } from \"./trace-id.js\";\n\n/**\n * Product events, the signed-in user and the consent kill-switch for browser\n * telemetry. Like reportException these are safe to call anywhere — no-ops on\n * the server and when telemetry is off — and calls made while the SDK is still\n * loading are held and replayed. The SDK itself arrives through the\n * \"/telemetry\" subpath, never the barrel.\n */\nexport {\n disableTelemetry,\n setTelemetryUser,\n track,\n type TrackProps,\n type TrackValue,\n} from \"./track.js\";\n\n/**\n * Dev-only browser log forwarding: mirrors console output and uncaught errors\n * to the dev server, which prints them into\n * `.forge/logs/<env>/frontend_<name>.log`. No-ops unless `dev: true`, and the\n * receiving endpoint exists only in a dev server — so a production bundle\n * neither posts nor has anywhere to post to.\n */\nexport {\n DEV_LOG_ENDPOINT,\n devLoggingInstalled,\n installDevLogging,\n uninstallDevLogging,\n type DevLoggingOptions,\n type DevLogPayload,\n} from \"./devlog.js\";\n"]}
@@ -24,6 +24,10 @@ export declare function headerInterceptor(cfg: RuntimeInterceptorConfig): Interc
24
24
  /**
25
25
  * Propagates a W3C traceparent so the backend (otelconnect + otelhttp) joins
26
26
  * the same distributed trace. Always emits a valid header — see trace.ts.
27
+ *
28
+ * While browser telemetry is still starting it waits (bounded; see
29
+ * telemetry-gate.ts) so the call is made through the SDK's patched fetch and
30
+ * its traceparent names a span the browser really exports.
27
31
  */
28
32
  export declare function traceInterceptor(): Interceptor;
29
33
  /** Converts any thrown failure into a typed ConnectClientError. */
@@ -46,4 +50,4 @@ export declare function retryInterceptor(cfg: RuntimeInterceptorConfig): Interce
46
50
  */
47
51
  export declare function buildRuntimeInterceptors(cfg?: RuntimeInterceptorConfig): Interceptor[];
48
52
  export { ConnectClientError, FORGE_ERROR_REASON_HEADER, isRetryableCode, normalizeError, } from "./errors.js";
49
- export { freshTraceparent, injectTraceContext, setTraceSampled, TRACEPARENT_RE, } from "./trace.js";
53
+ export { freshTraceparent, injectTraceContext, setTraceSampled, traceIdFrom, TRACEPARENT_RE, TRACERESPONSE_HEADER, } from "./trace.js";