@timber-js/app 0.2.0-alpha.200 → 0.2.0-alpha.201
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/bin/timber.mjs +15 -1
- package/dist/_chunks/{actions-d1hCqnU3.js → actions-HUdJADAD.js} +3 -3
- package/dist/_chunks/{actions-d1hCqnU3.js.map → actions-HUdJADAD.js.map} +1 -1
- package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/{cache-api-ByagcC-J.js → cache-api-CAPbZTga.js} +2 -2
- package/dist/_chunks/{cache-api-ByagcC-J.js.map → cache-api-CAPbZTga.js.map} +1 -1
- package/dist/_chunks/{chains-Bpb0W4ax.js → chains-CBNA0Ozj.js} +3 -3
- package/dist/_chunks/{chains-Bpb0W4ax.js.map → chains-CBNA0Ozj.js.map} +1 -1
- package/dist/_chunks/{cli-check-D6VolrDV.js → cli-check-BzMGuIH6.js} +3 -3
- package/dist/_chunks/{cli-check-D6VolrDV.js.map → cli-check-BzMGuIH6.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js → cli-schema-sync-DnXqcIIj.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-D6rO-VcS.js.map → cli-schema-sync-DnXqcIIj.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-DxX1SU0g.js} +3 -3
- package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-DxX1SU0g.js.map} +1 -1
- package/dist/_chunks/{convention-lint-fRkwVwEH.js → convention-lint-DHOFvX5s.js} +5 -95
- package/dist/_chunks/convention-lint-DHOFvX5s.js.map +1 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js +227 -0
- package/dist/_chunks/csp-nonce-hOGniaG4.js.map +1 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js +2288 -0
- package/dist/_chunks/dev-server-DioP7tkQ.js.map +1 -0
- package/dist/_chunks/{error-boundary-BfPHZjm0.js → error-boundary-BQKxl6EX.js} +11 -17
- package/dist/_chunks/{error-boundary-BfPHZjm0.js.map → error-boundary-BQKxl6EX.js.map} +1 -1
- package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-Cv3njEH8.js} +2 -2
- package/dist/_chunks/{graph-cache-CP4GEmf9.js.map → graph-cache-Cv3njEH8.js.map} +1 -1
- package/dist/_chunks/{live-graph-D_2D32Ad.js → live-graph-VjHFF5EV.js} +3 -3
- package/dist/_chunks/{live-graph-D_2D32Ad.js.map → live-graph-VjHFF5EV.js.map} +1 -1
- package/dist/_chunks/{logger-uLBuGKDI.js → logger-DqJ2VoAY.js} +450 -458
- package/dist/_chunks/logger-DqJ2VoAY.js.map +1 -0
- package/dist/_chunks/{segment-keys-lqtdookO.js → metadata-routes-DSDjM_hJ.js} +2 -61
- package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js → poison-scan-lEbz4pQE.js} +2 -2
- package/dist/_chunks/{poison-scan-Bm9Yyqk9.js.map → poison-scan-lEbz4pQE.js.map} +1 -1
- package/dist/_chunks/{scanner-AiazgH_f.js → scanner-B_tnqFcF.js} +37 -137
- package/dist/_chunks/scanner-B_tnqFcF.js.map +1 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
- package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js +122 -0
- package/dist/_chunks/tree-match-CdbvYTBz.js.map +1 -0
- package/dist/_chunks/{walkers-B6XUtmqK.js → walkers-Cm3PC5JT.js} +2 -2
- package/dist/_chunks/{walkers-B6XUtmqK.js.map → walkers-Cm3PC5JT.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +3 -3
- package/dist/cache/index.js +1 -1
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +44 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +20 -1
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +3 -15
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/error-reconstituter.d.ts +4 -4
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/internal.js +1 -1
- package/dist/dev-tools/debug-channel.d.ts +55 -0
- package/dist/dev-tools/debug-channel.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2119
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts +7 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/scanner.d.ts +4 -4
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +2 -2
- package/dist/server/deny-boundary.d.ts +3 -5
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +0 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +5 -12
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +20 -195
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline.d.ts +8 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +15 -13
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +3 -7
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +6 -27
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +1 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +5 -5
- package/docs/api/34-api-config.mdx +2 -2
- package/docs/api/36-cli.mdx +1 -1
- package/docs/learn/12-error-handling.mdx +7 -8
- package/package.json +1 -2
- package/src/cli.ts +62 -8
- package/src/client/browser-entry/action-dispatch.ts +12 -0
- package/src/client/browser-entry/action-queue.ts +70 -8
- package/src/client/browser-entry/rsc-stream.ts +78 -22
- package/src/client/error-boundary.tsx +13 -39
- package/src/client/error-reconstituter.tsx +5 -5
- package/src/dev-tools/debug-channel.ts +151 -0
- package/src/index.ts +8 -12
- package/src/plugins/dev-server.ts +58 -7
- package/src/plugins/mdx.ts +2 -1
- package/src/routing/convention-lint.ts +4 -111
- package/src/routing/scanner.ts +33 -22
- package/src/server/access-gate.tsx +3 -3
- package/src/server/deny-boundary.ts +5 -23
- package/src/server/deny-renderer.ts +4 -7
- package/src/server/error-boundary-wrapper.ts +11 -35
- package/src/server/pipeline.ts +9 -0
- package/src/server/primitives.ts +20 -26
- package/src/server/rsc-entry/error-renderer.ts +15 -43
- package/src/server/rsc-entry/helpers.ts +10 -67
- package/src/server/rsc-entry/index.ts +1 -0
- package/src/server/rsc-entry/render-route.ts +12 -1
- package/src/server/rsc-entry/rsc-stream.ts +27 -24
- package/src/server/rsc-entry/ssr-renderer.ts +7 -1
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +0 -1
- package/dist/_chunks/logger-uLBuGKDI.js.map +0 -1
- package/dist/_chunks/scanner-AiazgH_f.js.map +0 -1
- package/dist/_chunks/segment-keys-lqtdookO.js.map +0 -1
- package/dist/server/utils/mdx-file.d.ts +0 -17
- package/dist/server/utils/mdx-file.d.ts.map +0 -1
- package/src/server/utils/mdx-file.ts +0 -22
|
@@ -4,27 +4,214 @@ import { a as assertValidCookieOptions, i as assertValidCookieName, n as parseSe
|
|
|
4
4
|
import { r as appVisibleSearch } from "./rsc-cache-key-ClUiXQnK.js";
|
|
5
5
|
import { a as mergePreservedSearchParams, o as resolveSegmentParams, r as validateExternalRedirectUrl, t as assertRelativeRedirectPath } from "./href-validation-BIrxavIy.js";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
//#region src/server/
|
|
7
|
+
//#region src/server/tracing.ts
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* request duration. The `waitUntil()` primitive reads from this ALS to
|
|
15
|
-
* dispatch background work to the correct platform API.
|
|
11
|
+
* getTraceId() is always available in server code (middleware, access, components, actions).
|
|
12
|
+
* Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,
|
|
13
|
+
* or a crypto.randomUUID()-derived fallback otherwise.
|
|
16
14
|
*
|
|
17
|
-
*
|
|
15
|
+
* See design/17-logging.md §"trace_id is Always Set"
|
|
18
16
|
*/
|
|
19
17
|
/**
|
|
20
|
-
*
|
|
18
|
+
* Returns the current request's trace ID — always a 32-char lowercase hex string.
|
|
21
19
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).
|
|
21
|
+
* Without OTEL: crypto.randomUUID() with hyphens stripped.
|
|
22
|
+
*
|
|
23
|
+
* Throws if called outside a request context (no ALS store).
|
|
25
24
|
*/
|
|
26
|
-
function
|
|
27
|
-
|
|
25
|
+
function getTraceId() {
|
|
26
|
+
const store = traceAls.getStore();
|
|
27
|
+
if (!store) throw new Error("[timber] getTraceId() called outside of a request context. It can only be used in middleware, access checks, server components, and server actions.");
|
|
28
|
+
return store.traceId;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns the current OTEL span ID if available, undefined otherwise.
|
|
32
|
+
*/
|
|
33
|
+
function getSpanId() {
|
|
34
|
+
return traceAls.getStore()?.spanId;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generate a 32-char lowercase hex ID from crypto.randomUUID().
|
|
38
|
+
* Same format as OTEL trace IDs — zero-friction upgrade path.
|
|
39
|
+
*/
|
|
40
|
+
function generateTraceId() {
|
|
41
|
+
return randomUUID().replace(/-/g, "");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run a callback within a trace context. Used by the pipeline to establish
|
|
45
|
+
* per-request ALS scope.
|
|
46
|
+
*/
|
|
47
|
+
function runWithTraceId(id, fn) {
|
|
48
|
+
return traceAls.run({ traceId: id }, fn);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Replace the trace ID in the current ALS store. Used when OTEL creates
|
|
52
|
+
* a root span and we want to switch from the UUID fallback to the real
|
|
53
|
+
* OTEL trace ID.
|
|
54
|
+
*/
|
|
55
|
+
function replaceTraceId(newTraceId, newSpanId) {
|
|
56
|
+
const store = traceAls.getStore();
|
|
57
|
+
if (store) {
|
|
58
|
+
store.traceId = newTraceId;
|
|
59
|
+
store.spanId = newSpanId;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Update the span ID in the current ALS store. Used when entering a new
|
|
64
|
+
* OTEL span to keep log–trace correlation accurate.
|
|
65
|
+
*/
|
|
66
|
+
function updateSpanId(newSpanId) {
|
|
67
|
+
const store = traceAls.getStore();
|
|
68
|
+
if (store) store.spanId = newSpanId;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get the current trace store, or undefined if outside a request context.
|
|
72
|
+
* Framework-internal — use getTraceId()/getSpanId() in user code.
|
|
73
|
+
*/
|
|
74
|
+
function getTraceStore() {
|
|
75
|
+
return traceAls.getStore();
|
|
76
|
+
}
|
|
77
|
+
var PLATFORM_TRACER_KEY = Symbol.for("timber:platform-tracer");
|
|
78
|
+
/** The registered native platform tracer, if any. */
|
|
79
|
+
function getPlatformTracer() {
|
|
80
|
+
return globalThis[PLATFORM_TRACER_KEY];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Attempt to get the @opentelemetry/api tracer. Returns undefined if the
|
|
84
|
+
* package is not installed or no SDK is registered.
|
|
85
|
+
*
|
|
86
|
+
* timber.js depends on @opentelemetry/api as the vendor-neutral interface.
|
|
87
|
+
* The API is a no-op by default — spans are only emitted when the developer
|
|
88
|
+
* initializes an SDK in register().
|
|
89
|
+
*/
|
|
90
|
+
var _otelApi;
|
|
91
|
+
async function getOtelApi() {
|
|
92
|
+
if (_otelApi === void 0) try {
|
|
93
|
+
_otelApi = await import("@opentelemetry/api");
|
|
94
|
+
} catch {
|
|
95
|
+
_otelApi = null;
|
|
96
|
+
}
|
|
97
|
+
return _otelApi;
|
|
98
|
+
}
|
|
99
|
+
/** OTEL tracer instance, lazily created. */
|
|
100
|
+
var _tracer;
|
|
101
|
+
/**
|
|
102
|
+
* Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.
|
|
103
|
+
*/
|
|
104
|
+
async function getTracer() {
|
|
105
|
+
if (_tracer === void 0) {
|
|
106
|
+
const api = await getOtelApi();
|
|
107
|
+
if (api) _tracer = api.trace.getTracer("timber.js");
|
|
108
|
+
else _tracer = null;
|
|
109
|
+
}
|
|
110
|
+
return _tracer;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Run a function within a framework span. Composes two emission channels:
|
|
114
|
+
*
|
|
115
|
+
* - **Native platform span** — when an adapter registered a PlatformTracer
|
|
116
|
+
* (Cloudflare Workers), the fn is wrapped in platformTracer.enterSpan()
|
|
117
|
+
* so it appears in the platform's native trace view.
|
|
118
|
+
* - **OTEL span** — when an OTEL SDK is active, the fn also runs inside an
|
|
119
|
+
* OTEL span (dual emission). No SDK and no platform tracer = zero overhead.
|
|
120
|
+
*
|
|
121
|
+
* Automatically:
|
|
122
|
+
* - Creates the span as a child of the current context
|
|
123
|
+
* - Updates the ALS span ID for log–trace correlation
|
|
124
|
+
* - Ends the span when the function completes
|
|
125
|
+
* - Records exceptions on error (OTEL channel)
|
|
126
|
+
*/
|
|
127
|
+
async function withSpan(name, attributes, fn) {
|
|
128
|
+
const platformTracer = getPlatformTracer();
|
|
129
|
+
if (!platformTracer) return runOtelSpan(name, attributes, fn);
|
|
130
|
+
return platformTracer.enterSpan(name, async (span) => {
|
|
131
|
+
for (const key of Object.keys(attributes)) span.setAttribute(key, attributes[key]);
|
|
132
|
+
const store = traceAls.getStore();
|
|
133
|
+
const prevPlatformSpan = store?.platformSpan;
|
|
134
|
+
if (store) store.platformSpan = span;
|
|
135
|
+
try {
|
|
136
|
+
return await runOtelSpan(name, attributes, fn);
|
|
137
|
+
} finally {
|
|
138
|
+
if (store) store.platformSpan = prevPlatformSpan;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** The OTEL half of withSpan() — no-op passthrough when no SDK is active. */
|
|
143
|
+
async function runOtelSpan(name, attributes, fn) {
|
|
144
|
+
const tracer = await getTracer();
|
|
145
|
+
if (!tracer) return fn();
|
|
146
|
+
const api = await getOtelApi();
|
|
147
|
+
return tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
148
|
+
const prevSpanId = getSpanId();
|
|
149
|
+
updateSpanId(span.spanContext().spanId);
|
|
150
|
+
try {
|
|
151
|
+
const result = await fn();
|
|
152
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
153
|
+
return result;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
156
|
+
if (error instanceof Error) span.recordException(error);
|
|
157
|
+
throw error;
|
|
158
|
+
} finally {
|
|
159
|
+
span.end();
|
|
160
|
+
updateSpanId(prevSpanId);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Set an attribute on the current active span (if any).
|
|
166
|
+
* Used for setting span attributes after span creation (e.g. timber.result on access spans).
|
|
167
|
+
*/
|
|
168
|
+
async function setSpanAttribute(key, value) {
|
|
169
|
+
const platformSpan = traceAls.getStore()?.platformSpan;
|
|
170
|
+
if (platformSpan) platformSpan.setAttribute(key, value);
|
|
171
|
+
const api = await getOtelApi();
|
|
172
|
+
if (!api) return;
|
|
173
|
+
const activeSpan = api.trace.getActiveSpan();
|
|
174
|
+
if (activeSpan) activeSpan.setAttribute(key, value);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Add a span event to the current active span (if any).
|
|
178
|
+
* Used for timber.cache HIT/MISS events — recorded as span events, not child spans.
|
|
179
|
+
*/
|
|
180
|
+
async function addSpanEvent(name, attributes) {
|
|
181
|
+
const api = await getOtelApi();
|
|
182
|
+
if (!api) return;
|
|
183
|
+
const activeSpan = api.trace.getActiveSpan();
|
|
184
|
+
if (activeSpan) activeSpan.addEvent(name, attributes);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Fire-and-forget span event — no await, no microtask overhead.
|
|
188
|
+
*
|
|
189
|
+
* Used on the cache hot path where awaiting addSpanEvent creates an
|
|
190
|
+
* unnecessary microtask per cache operation. If OTEL is not loaded yet,
|
|
191
|
+
* the event is silently dropped (acceptable for diagnostics).
|
|
192
|
+
*
|
|
193
|
+
* See TIM-370 for perf motivation.
|
|
194
|
+
*/
|
|
195
|
+
function addSpanEventSync(name, attributes) {
|
|
196
|
+
if (!_otelApi) return;
|
|
197
|
+
const activeSpan = _otelApi.trace.getActiveSpan();
|
|
198
|
+
if (activeSpan) activeSpan.addEvent(name, attributes);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Try to extract the OTEL trace ID from the current active span context.
|
|
202
|
+
* Returns undefined if OTEL is not active or no span exists.
|
|
203
|
+
*/
|
|
204
|
+
async function getOtelTraceId() {
|
|
205
|
+
const api = await getOtelApi();
|
|
206
|
+
if (!api) return void 0;
|
|
207
|
+
const activeSpan = api.trace.getActiveSpan();
|
|
208
|
+
if (!activeSpan) return void 0;
|
|
209
|
+
const ctx = activeSpan.spanContext();
|
|
210
|
+
if (!ctx.traceId || ctx.traceId === "00000000000000000000000000000000") return;
|
|
211
|
+
return {
|
|
212
|
+
traceId: ctx.traceId,
|
|
213
|
+
spanId: ctx.spanId
|
|
214
|
+
};
|
|
28
215
|
}
|
|
29
216
|
//#endregion
|
|
30
217
|
//#region src/server/debug.ts
|
|
@@ -132,43 +319,248 @@ function _readTimberDebugEnv() {
|
|
|
132
319
|
return false;
|
|
133
320
|
}
|
|
134
321
|
//#endregion
|
|
135
|
-
//#region src/server/
|
|
322
|
+
//#region src/server/error-formatter.ts
|
|
136
323
|
/**
|
|
137
|
-
*
|
|
138
|
-
* functions with no ALS dependency. Split out of `cookie-context.ts`
|
|
139
|
-
* (TIM-853) so the API surface and the wire-format codecs can each be
|
|
140
|
-
* read on their own.
|
|
324
|
+
* Error Formatter — rewrites SSR/RSC error messages to surface user code.
|
|
141
325
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* is enforced regardless of which path produced the bytes).
|
|
326
|
+
* When React or Vite throw errors during SSR, stack traces reference
|
|
327
|
+
* vendored dependency paths (e.g. `.vite/deps_ssr/@vitejs_plugin-rsc_vendor_...`)
|
|
328
|
+
* and mangled export names (`__vite_ssr_export_default__`). This module
|
|
329
|
+
* rewrites error messages and stack traces to point at user code instead.
|
|
147
330
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* timber's Map-based API and never-throw contract.
|
|
331
|
+
* Dev-only — in production, errors go through the structured logger
|
|
332
|
+
* without formatting.
|
|
151
333
|
*/
|
|
152
334
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* Values are auto-decoded with `decodeURIComponent` so they round-trip
|
|
157
|
-
* losslessly with `getCookies().set()` (which auto-encodes). Malformed
|
|
158
|
-
* `%`-escapes from third-party cookies fall back to the raw byte sequence
|
|
159
|
-
* — the parser must be total over arbitrary inbound headers, including
|
|
160
|
-
* non-conforming values from other servers, browser extensions, etc.
|
|
335
|
+
* Patterns that identify internal Vite/RSC vendor paths in stack traces.
|
|
336
|
+
* These are replaced with human-readable labels.
|
|
161
337
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
338
|
+
var VENDOR_PATH_PATTERNS = [
|
|
339
|
+
{
|
|
340
|
+
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor_react-server-dom[^\s)]+/g,
|
|
341
|
+
replacement: "<react-server-dom>"
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor[^\s)]+/g,
|
|
345
|
+
replacement: "<rsc-vendor>"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
pattern: /node_modules\/\.vite\/deps_ssr\/[^\s)]+/g,
|
|
349
|
+
replacement: "<vite-dep>"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
pattern: /node_modules\/\.vite\/deps\/[^\s)]+/g,
|
|
353
|
+
replacement: "<vite-dep>"
|
|
354
|
+
}
|
|
355
|
+
];
|
|
356
|
+
/**
|
|
357
|
+
* Patterns that identify Vite-mangled export names in error messages.
|
|
358
|
+
*/
|
|
359
|
+
var MANGLED_NAME_PATTERNS = [{
|
|
360
|
+
pattern: /__vite_ssr_export_default__/g,
|
|
361
|
+
replacement: "<default export>"
|
|
362
|
+
}, {
|
|
363
|
+
pattern: /__vite_ssr_export_(\w+)__/g,
|
|
364
|
+
replacement: "<export $1>"
|
|
365
|
+
}];
|
|
366
|
+
/**
|
|
367
|
+
* Rewrite an error's message and stack to replace internal Vite paths
|
|
368
|
+
* and mangled names with human-readable labels.
|
|
369
|
+
*/
|
|
370
|
+
function formatSsrError(error) {
|
|
371
|
+
if (!(error instanceof Error)) return String(error);
|
|
372
|
+
let message = error.message;
|
|
373
|
+
let stack = error.stack ?? "";
|
|
374
|
+
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) message = message.replace(pattern, replacement);
|
|
375
|
+
for (const { pattern, replacement } of VENDOR_PATH_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
376
|
+
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
377
|
+
const hint = extractErrorHint(error.message);
|
|
378
|
+
const parts = [];
|
|
379
|
+
parts.push(message);
|
|
380
|
+
if (hint) parts.push(` → ${hint}`);
|
|
381
|
+
const userFrames = extractUserFrames(stack);
|
|
382
|
+
if (userFrames.length > 0) {
|
|
383
|
+
parts.push("");
|
|
384
|
+
parts.push(" User code in stack:");
|
|
385
|
+
for (const frame of userFrames) parts.push(` ${frame}`);
|
|
386
|
+
}
|
|
387
|
+
return parts.join("\n");
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Extract a human-readable hint from common React/RSC error messages.
|
|
391
|
+
*
|
|
392
|
+
* React error messages contain useful information but the surrounding
|
|
393
|
+
* context (vendor paths, mangled names) obscures it. This extracts the
|
|
394
|
+
* actionable part as a one-line hint.
|
|
395
|
+
*/
|
|
396
|
+
function extractErrorHint(message) {
|
|
397
|
+
if (message.match(/Functions cannot be passed directly to Client Components/)) {
|
|
398
|
+
const propMatch = message.match(/<[^>]*?\s(\w+)=\{function/);
|
|
399
|
+
if (propMatch) return `Prop "${propMatch[1]}" is a function — mark it "use server" or call it before passing`;
|
|
400
|
+
return "A function prop was passed to a Client Component — mark it \"use server\" or call it before passing";
|
|
401
|
+
}
|
|
402
|
+
if (message.includes("Objects are not valid as a React child")) return "An object was rendered as JSX children — convert to string or extract the value";
|
|
403
|
+
const nullRefMatch = message.match(/Cannot read propert(?:y|ies) of (undefined|null) \(reading '(\w+)'\)/);
|
|
404
|
+
if (nullRefMatch) return `Accessed .${nullRefMatch[2]} on ${nullRefMatch[1]} — check that the value exists`;
|
|
405
|
+
const notFnMatch = message.match(/(\w+) is not a function/);
|
|
406
|
+
if (notFnMatch) return `"${notFnMatch[1]}" is not a function — check imports and exports`;
|
|
407
|
+
if (message.includes("Element type is invalid")) return "A component resolved to undefined/null — check default exports and import paths";
|
|
408
|
+
if (message.includes("Invalid hook call")) return "A hook was called outside of a React component render. If this is a 'use client' component, ensure the directive is at the very top of the file (before any imports) and that @vitejs/plugin-rsc is loaded correctly. Barrel re-exports from non-'use client' files do not propagate the directive.";
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Extract stack frames that reference user code (not node_modules,
|
|
413
|
+
* not framework internals).
|
|
414
|
+
*
|
|
415
|
+
* Returns at most 5 frames to keep output concise.
|
|
416
|
+
*/
|
|
417
|
+
function extractUserFrames(stack) {
|
|
418
|
+
const lines = stack.split("\n");
|
|
419
|
+
const userFrames = [];
|
|
420
|
+
for (const line of lines) {
|
|
421
|
+
const trimmed = line.trim();
|
|
422
|
+
if (!trimmed.startsWith("at ")) continue;
|
|
423
|
+
if (trimmed.includes("node_modules") || trimmed.includes("<react-server-dom>") || trimmed.includes("<rsc-vendor>") || trimmed.includes("<vite-dep>") || trimmed.includes("node:internal")) continue;
|
|
424
|
+
userFrames.push(trimmed);
|
|
425
|
+
if (userFrames.length >= 5) break;
|
|
426
|
+
}
|
|
427
|
+
return userFrames;
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/server/default-logger.ts
|
|
431
|
+
/**
|
|
432
|
+
* DefaultLogger — human-readable stderr logging when no custom logger is configured.
|
|
433
|
+
*
|
|
434
|
+
* Ships as the fallback so production deployments always have error visibility,
|
|
435
|
+
* even without an `instrumentation.ts` logger export. Output is one line per
|
|
436
|
+
* event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.
|
|
437
|
+
*
|
|
438
|
+
* Format:
|
|
439
|
+
* [timber] ERROR message key=value key=value trace_id=4bf92f35
|
|
440
|
+
* [timber] WARN message key=value key=value trace_id=4bf92f35
|
|
441
|
+
* [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35
|
|
442
|
+
*
|
|
443
|
+
* Behavior:
|
|
444
|
+
* - Suppressed entirely in dev mode (dev logging handles all output)
|
|
445
|
+
* - `debug` suppressed unless TIMBER_DEBUG is set
|
|
446
|
+
* - Replaced entirely when a custom logger is set via `setLogger()`
|
|
447
|
+
*
|
|
448
|
+
* See design/17-logging.md §"DefaultLogger"
|
|
449
|
+
*/
|
|
450
|
+
/**
|
|
451
|
+
* Format data fields as `key=value` pairs for human-readable output.
|
|
452
|
+
* - `error` key is serialized via formatSsrError for stack trace cleanup
|
|
453
|
+
* - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)
|
|
454
|
+
* - Other values are stringified inline
|
|
455
|
+
*/
|
|
456
|
+
function formatDataFields(data) {
|
|
457
|
+
if (!data) return "";
|
|
458
|
+
const parts = [];
|
|
459
|
+
let traceId;
|
|
460
|
+
for (const [key, value] of Object.entries(data)) {
|
|
461
|
+
if (key === "trace_id") {
|
|
462
|
+
traceId = typeof value === "string" ? value : String(value);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (key === "error") {
|
|
466
|
+
parts.push(`error=${formatSsrError(value)}`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (value === void 0 || value === null) continue;
|
|
470
|
+
parts.push(`${key}=${value}`);
|
|
471
|
+
}
|
|
472
|
+
if (traceId) parts.push(`trace_id=${traceId.slice(0, 8)}`);
|
|
473
|
+
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
474
|
+
}
|
|
475
|
+
/** Pad level string to fixed width for alignment. */
|
|
476
|
+
function padLevel(level) {
|
|
477
|
+
return level.padEnd(5);
|
|
478
|
+
}
|
|
479
|
+
function createDefaultLogger() {
|
|
480
|
+
return {
|
|
481
|
+
error(msg, data) {
|
|
482
|
+
const fields = formatDataFields(data);
|
|
483
|
+
process.stderr.write(`[timber] ${padLevel("ERROR")} ${msg}${fields}\n`);
|
|
484
|
+
},
|
|
485
|
+
warn(msg, data) {
|
|
486
|
+
const fields = formatDataFields(data);
|
|
487
|
+
process.stderr.write(`[timber] ${padLevel("WARN")} ${msg}${fields}\n`);
|
|
488
|
+
},
|
|
489
|
+
info(msg, data) {
|
|
490
|
+
if (isDevMode()) return;
|
|
491
|
+
if (!isDebug()) return;
|
|
492
|
+
const fields = formatDataFields(data);
|
|
493
|
+
process.stderr.write(`[timber] ${padLevel("INFO")} ${msg}${fields}\n`);
|
|
494
|
+
},
|
|
495
|
+
debug(msg, data) {
|
|
496
|
+
if (isDevMode()) return;
|
|
497
|
+
if (!isDebug()) return;
|
|
498
|
+
const fields = formatDataFields(data);
|
|
499
|
+
process.stderr.write(`[timber] ${padLevel("DEBUG")} ${msg}${fields}\n`);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/server/waituntil-bridge.ts
|
|
505
|
+
/**
|
|
506
|
+
* Per-request waitUntil bridge — ALS bridge for platform adapters.
|
|
507
|
+
*
|
|
508
|
+
* The generated entry point (Nitro, Cloudflare) wraps the handler with
|
|
509
|
+
* `runWithWaitUntil`, binding the platform's lifecycle extension function
|
|
510
|
+
* (e.g., h3's `event.waitUntil()` or CF's `ctx.waitUntil()`) for the
|
|
511
|
+
* request duration. The `waitUntil()` primitive reads from this ALS to
|
|
512
|
+
* dispatch background work to the correct platform API.
|
|
513
|
+
*
|
|
514
|
+
* Design doc: design/11-platform.md §"waitUntil()"
|
|
515
|
+
*/
|
|
516
|
+
/**
|
|
517
|
+
* Get the current request's waitUntil function, if available.
|
|
518
|
+
*
|
|
519
|
+
* Returns undefined when no platform adapter has installed a waitUntil
|
|
520
|
+
* handler for the current request (e.g., on platforms that don't support
|
|
521
|
+
* lifecycle extension, or outside a request context).
|
|
522
|
+
*/
|
|
523
|
+
function getWaitUntil() {
|
|
524
|
+
return waitUntilAls.getStore();
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/server/cookie-parsing.ts
|
|
528
|
+
/**
|
|
529
|
+
* Cookie parsing and serialization helpers — pure string ↔ structure
|
|
530
|
+
* functions with no ALS dependency. Split out of `cookie-context.ts`
|
|
531
|
+
* (TIM-853) so the API surface and the wire-format codecs can each be
|
|
532
|
+
* read on their own.
|
|
533
|
+
*
|
|
534
|
+
* The functions in this module are total over arbitrary input. They
|
|
535
|
+
* never throw and never call `assertValid*` (the security validators
|
|
536
|
+
* live in the API surface — `cookie-context.ts` invokes them at every
|
|
537
|
+
* jar entry point so the smuggling-primitive invariant from TIM-868
|
|
538
|
+
* is enforced regardless of which path produced the bytes).
|
|
539
|
+
*
|
|
540
|
+
* Delegates to the `cookie` package (RFC 6265, dependency-free,
|
|
541
|
+
* browser-safe) for wire codecs. Adapts at the boundary to preserve
|
|
542
|
+
* timber's Map-based API and never-throw contract.
|
|
543
|
+
*/
|
|
544
|
+
/**
|
|
545
|
+
* Parse a Cookie header string into a Map of name → value pairs.
|
|
546
|
+
* Follows RFC 6265 §4.2.1: cookies are semicolon-separated key=value pairs.
|
|
547
|
+
*
|
|
548
|
+
* Values are auto-decoded with `decodeURIComponent` so they round-trip
|
|
549
|
+
* losslessly with `getCookies().set()` (which auto-encodes). Malformed
|
|
550
|
+
* `%`-escapes from third-party cookies fall back to the raw byte sequence
|
|
551
|
+
* — the parser must be total over arbitrary inbound headers, including
|
|
552
|
+
* non-conforming values from other servers, browser extensions, etc.
|
|
553
|
+
*/
|
|
554
|
+
function parseCookieHeader(header) {
|
|
555
|
+
const map = /* @__PURE__ */ new Map();
|
|
556
|
+
if (!header) return map;
|
|
557
|
+
const parsed = parseCookie(header);
|
|
558
|
+
for (const name in parsed) {
|
|
559
|
+
const value = parsed[name];
|
|
560
|
+
if (value !== void 0) map.set(name, value);
|
|
561
|
+
}
|
|
562
|
+
return map;
|
|
563
|
+
}
|
|
172
564
|
/**
|
|
173
565
|
* Decode a single cookie value with `decodeURIComponent`, falling back to
|
|
174
566
|
* the raw byte sequence if the input contains a malformed `%`-escape.
|
|
@@ -808,34 +1200,26 @@ var DenySignal = class extends Error {
|
|
|
808
1200
|
* - Inside Suspense (hold window): promoted to pre-flush behavior
|
|
809
1201
|
* - Inside Suspense (after flush): error boundary + noindex meta
|
|
810
1202
|
*
|
|
811
|
-
* Supports both positional and object signatures:
|
|
812
1203
|
* ```ts
|
|
813
|
-
* deny()
|
|
814
|
-
* deny(404)
|
|
815
|
-
* deny(503, {
|
|
816
|
-
* deny({
|
|
1204
|
+
* deny() // 403 (default)
|
|
1205
|
+
* deny(404) // 404
|
|
1206
|
+
* deny(503, { message: 'Maintenance' }) // server-only log message
|
|
1207
|
+
* deny(404, { dangerouslyPassData: { resourceId: params.id } }) // explicit client opt-in
|
|
817
1208
|
* ```
|
|
818
1209
|
*
|
|
819
1210
|
* Accepts any 4xx or 5xx status code. This replaces the need for
|
|
820
1211
|
* `throw new RenderError(...)` in user code — RenderError is now an
|
|
821
1212
|
* internal pipeline detail.
|
|
822
1213
|
*
|
|
823
|
-
* @param
|
|
824
|
-
* @param
|
|
1214
|
+
* @param status - HTTP status code (4xx or 5xx). Default: 403.
|
|
1215
|
+
* @param options - Optional message and/or data to pass to the client.
|
|
825
1216
|
*/
|
|
826
|
-
function deny(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if (
|
|
830
|
-
status = statusOrOptions.status ?? 403;
|
|
831
|
-
resolvedData = statusOrOptions.data;
|
|
832
|
-
} else {
|
|
833
|
-
status = statusOrOptions ?? 403;
|
|
834
|
-
resolvedData = data;
|
|
835
|
-
}
|
|
836
|
-
if (status < 400 || status > 599) throw new Error(`deny() requires a 4xx or 5xx status code, got ${status}.`);
|
|
1217
|
+
function deny(status, options) {
|
|
1218
|
+
const resolvedStatus = status ?? 403;
|
|
1219
|
+
const resolvedData = options?.dangerouslyPassData;
|
|
1220
|
+
if (resolvedStatus < 400 || resolvedStatus > 599) throw new Error(`deny() requires a 4xx or 5xx status code, got ${resolvedStatus}.`);
|
|
837
1221
|
warnIfNotSerializable(resolvedData, "deny()");
|
|
838
|
-
throw new DenySignal(
|
|
1222
|
+
throw new DenySignal(resolvedStatus, resolvedData);
|
|
839
1223
|
}
|
|
840
1224
|
/**
|
|
841
1225
|
* Render-phase signal thrown by `redirect()` and `redirectExternal()`.
|
|
@@ -979,398 +1363,6 @@ function waitUntil(promise) {
|
|
|
979
1363
|
}
|
|
980
1364
|
}
|
|
981
1365
|
//#endregion
|
|
982
|
-
//#region src/server/tracing.ts
|
|
983
|
-
/**
|
|
984
|
-
* Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.
|
|
985
|
-
*
|
|
986
|
-
* getTraceId() is always available in server code (middleware, access, components, actions).
|
|
987
|
-
* Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,
|
|
988
|
-
* or a crypto.randomUUID()-derived fallback otherwise.
|
|
989
|
-
*
|
|
990
|
-
* See design/17-logging.md §"trace_id is Always Set"
|
|
991
|
-
*/
|
|
992
|
-
/**
|
|
993
|
-
* Returns the current request's trace ID — always a 32-char lowercase hex string.
|
|
994
|
-
*
|
|
995
|
-
* With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).
|
|
996
|
-
* Without OTEL: crypto.randomUUID() with hyphens stripped.
|
|
997
|
-
*
|
|
998
|
-
* Throws if called outside a request context (no ALS store).
|
|
999
|
-
*/
|
|
1000
|
-
function getTraceId() {
|
|
1001
|
-
const store = traceAls.getStore();
|
|
1002
|
-
if (!store) throw new Error("[timber] getTraceId() called outside of a request context. It can only be used in middleware, access checks, server components, and server actions.");
|
|
1003
|
-
return store.traceId;
|
|
1004
|
-
}
|
|
1005
|
-
/**
|
|
1006
|
-
* Returns the current OTEL span ID if available, undefined otherwise.
|
|
1007
|
-
*/
|
|
1008
|
-
function getSpanId() {
|
|
1009
|
-
return traceAls.getStore()?.spanId;
|
|
1010
|
-
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Generate a 32-char lowercase hex ID from crypto.randomUUID().
|
|
1013
|
-
* Same format as OTEL trace IDs — zero-friction upgrade path.
|
|
1014
|
-
*/
|
|
1015
|
-
function generateTraceId() {
|
|
1016
|
-
return randomUUID().replace(/-/g, "");
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Run a callback within a trace context. Used by the pipeline to establish
|
|
1020
|
-
* per-request ALS scope.
|
|
1021
|
-
*/
|
|
1022
|
-
function runWithTraceId(id, fn) {
|
|
1023
|
-
return traceAls.run({ traceId: id }, fn);
|
|
1024
|
-
}
|
|
1025
|
-
/**
|
|
1026
|
-
* Replace the trace ID in the current ALS store. Used when OTEL creates
|
|
1027
|
-
* a root span and we want to switch from the UUID fallback to the real
|
|
1028
|
-
* OTEL trace ID.
|
|
1029
|
-
*/
|
|
1030
|
-
function replaceTraceId(newTraceId, newSpanId) {
|
|
1031
|
-
const store = traceAls.getStore();
|
|
1032
|
-
if (store) {
|
|
1033
|
-
store.traceId = newTraceId;
|
|
1034
|
-
store.spanId = newSpanId;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Update the span ID in the current ALS store. Used when entering a new
|
|
1039
|
-
* OTEL span to keep log–trace correlation accurate.
|
|
1040
|
-
*/
|
|
1041
|
-
function updateSpanId(newSpanId) {
|
|
1042
|
-
const store = traceAls.getStore();
|
|
1043
|
-
if (store) store.spanId = newSpanId;
|
|
1044
|
-
}
|
|
1045
|
-
/**
|
|
1046
|
-
* Get the current trace store, or undefined if outside a request context.
|
|
1047
|
-
* Framework-internal — use getTraceId()/getSpanId() in user code.
|
|
1048
|
-
*/
|
|
1049
|
-
function getTraceStore() {
|
|
1050
|
-
return traceAls.getStore();
|
|
1051
|
-
}
|
|
1052
|
-
var PLATFORM_TRACER_KEY = Symbol.for("timber:platform-tracer");
|
|
1053
|
-
/** The registered native platform tracer, if any. */
|
|
1054
|
-
function getPlatformTracer() {
|
|
1055
|
-
return globalThis[PLATFORM_TRACER_KEY];
|
|
1056
|
-
}
|
|
1057
|
-
/**
|
|
1058
|
-
* Attempt to get the @opentelemetry/api tracer. Returns undefined if the
|
|
1059
|
-
* package is not installed or no SDK is registered.
|
|
1060
|
-
*
|
|
1061
|
-
* timber.js depends on @opentelemetry/api as the vendor-neutral interface.
|
|
1062
|
-
* The API is a no-op by default — spans are only emitted when the developer
|
|
1063
|
-
* initializes an SDK in register().
|
|
1064
|
-
*/
|
|
1065
|
-
var _otelApi;
|
|
1066
|
-
async function getOtelApi() {
|
|
1067
|
-
if (_otelApi === void 0) try {
|
|
1068
|
-
_otelApi = await import("@opentelemetry/api");
|
|
1069
|
-
} catch {
|
|
1070
|
-
_otelApi = null;
|
|
1071
|
-
}
|
|
1072
|
-
return _otelApi;
|
|
1073
|
-
}
|
|
1074
|
-
/** OTEL tracer instance, lazily created. */
|
|
1075
|
-
var _tracer;
|
|
1076
|
-
/**
|
|
1077
|
-
* Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.
|
|
1078
|
-
*/
|
|
1079
|
-
async function getTracer() {
|
|
1080
|
-
if (_tracer === void 0) {
|
|
1081
|
-
const api = await getOtelApi();
|
|
1082
|
-
if (api) _tracer = api.trace.getTracer("timber.js");
|
|
1083
|
-
else _tracer = null;
|
|
1084
|
-
}
|
|
1085
|
-
return _tracer;
|
|
1086
|
-
}
|
|
1087
|
-
/**
|
|
1088
|
-
* Run a function within a framework span. Composes two emission channels:
|
|
1089
|
-
*
|
|
1090
|
-
* - **Native platform span** — when an adapter registered a PlatformTracer
|
|
1091
|
-
* (Cloudflare Workers), the fn is wrapped in platformTracer.enterSpan()
|
|
1092
|
-
* so it appears in the platform's native trace view.
|
|
1093
|
-
* - **OTEL span** — when an OTEL SDK is active, the fn also runs inside an
|
|
1094
|
-
* OTEL span (dual emission). No SDK and no platform tracer = zero overhead.
|
|
1095
|
-
*
|
|
1096
|
-
* Automatically:
|
|
1097
|
-
* - Creates the span as a child of the current context
|
|
1098
|
-
* - Updates the ALS span ID for log–trace correlation
|
|
1099
|
-
* - Ends the span when the function completes
|
|
1100
|
-
* - Records exceptions on error (OTEL channel)
|
|
1101
|
-
*/
|
|
1102
|
-
async function withSpan(name, attributes, fn) {
|
|
1103
|
-
const platformTracer = getPlatformTracer();
|
|
1104
|
-
if (!platformTracer) return runOtelSpan(name, attributes, fn);
|
|
1105
|
-
return platformTracer.enterSpan(name, async (span) => {
|
|
1106
|
-
for (const key of Object.keys(attributes)) span.setAttribute(key, attributes[key]);
|
|
1107
|
-
const store = traceAls.getStore();
|
|
1108
|
-
const prevPlatformSpan = store?.platformSpan;
|
|
1109
|
-
if (store) store.platformSpan = span;
|
|
1110
|
-
try {
|
|
1111
|
-
return await runOtelSpan(name, attributes, fn);
|
|
1112
|
-
} finally {
|
|
1113
|
-
if (store) store.platformSpan = prevPlatformSpan;
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
/** The OTEL half of withSpan() — no-op passthrough when no SDK is active. */
|
|
1118
|
-
async function runOtelSpan(name, attributes, fn) {
|
|
1119
|
-
const tracer = await getTracer();
|
|
1120
|
-
if (!tracer) return fn();
|
|
1121
|
-
const api = await getOtelApi();
|
|
1122
|
-
return tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
1123
|
-
const prevSpanId = getSpanId();
|
|
1124
|
-
updateSpanId(span.spanContext().spanId);
|
|
1125
|
-
try {
|
|
1126
|
-
const result = await fn();
|
|
1127
|
-
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
1128
|
-
return result;
|
|
1129
|
-
} catch (error) {
|
|
1130
|
-
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
1131
|
-
if (error instanceof Error) span.recordException(error);
|
|
1132
|
-
throw error;
|
|
1133
|
-
} finally {
|
|
1134
|
-
span.end();
|
|
1135
|
-
updateSpanId(prevSpanId);
|
|
1136
|
-
}
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
/**
|
|
1140
|
-
* Set an attribute on the current active span (if any).
|
|
1141
|
-
* Used for setting span attributes after span creation (e.g. timber.result on access spans).
|
|
1142
|
-
*/
|
|
1143
|
-
async function setSpanAttribute(key, value) {
|
|
1144
|
-
const platformSpan = traceAls.getStore()?.platformSpan;
|
|
1145
|
-
if (platformSpan) platformSpan.setAttribute(key, value);
|
|
1146
|
-
const api = await getOtelApi();
|
|
1147
|
-
if (!api) return;
|
|
1148
|
-
const activeSpan = api.trace.getActiveSpan();
|
|
1149
|
-
if (activeSpan) activeSpan.setAttribute(key, value);
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* Add a span event to the current active span (if any).
|
|
1153
|
-
* Used for timber.cache HIT/MISS events — recorded as span events, not child spans.
|
|
1154
|
-
*/
|
|
1155
|
-
async function addSpanEvent(name, attributes) {
|
|
1156
|
-
const api = await getOtelApi();
|
|
1157
|
-
if (!api) return;
|
|
1158
|
-
const activeSpan = api.trace.getActiveSpan();
|
|
1159
|
-
if (activeSpan) activeSpan.addEvent(name, attributes);
|
|
1160
|
-
}
|
|
1161
|
-
/**
|
|
1162
|
-
* Fire-and-forget span event — no await, no microtask overhead.
|
|
1163
|
-
*
|
|
1164
|
-
* Used on the cache hot path where awaiting addSpanEvent creates an
|
|
1165
|
-
* unnecessary microtask per cache operation. If OTEL is not loaded yet,
|
|
1166
|
-
* the event is silently dropped (acceptable for diagnostics).
|
|
1167
|
-
*
|
|
1168
|
-
* See TIM-370 for perf motivation.
|
|
1169
|
-
*/
|
|
1170
|
-
function addSpanEventSync(name, attributes) {
|
|
1171
|
-
if (!_otelApi) return;
|
|
1172
|
-
const activeSpan = _otelApi.trace.getActiveSpan();
|
|
1173
|
-
if (activeSpan) activeSpan.addEvent(name, attributes);
|
|
1174
|
-
}
|
|
1175
|
-
/**
|
|
1176
|
-
* Try to extract the OTEL trace ID from the current active span context.
|
|
1177
|
-
* Returns undefined if OTEL is not active or no span exists.
|
|
1178
|
-
*/
|
|
1179
|
-
async function getOtelTraceId() {
|
|
1180
|
-
const api = await getOtelApi();
|
|
1181
|
-
if (!api) return void 0;
|
|
1182
|
-
const activeSpan = api.trace.getActiveSpan();
|
|
1183
|
-
if (!activeSpan) return void 0;
|
|
1184
|
-
const ctx = activeSpan.spanContext();
|
|
1185
|
-
if (!ctx.traceId || ctx.traceId === "00000000000000000000000000000000") return;
|
|
1186
|
-
return {
|
|
1187
|
-
traceId: ctx.traceId,
|
|
1188
|
-
spanId: ctx.spanId
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
//#endregion
|
|
1192
|
-
//#region src/server/error-formatter.ts
|
|
1193
|
-
/**
|
|
1194
|
-
* Error Formatter — rewrites SSR/RSC error messages to surface user code.
|
|
1195
|
-
*
|
|
1196
|
-
* When React or Vite throw errors during SSR, stack traces reference
|
|
1197
|
-
* vendored dependency paths (e.g. `.vite/deps_ssr/@vitejs_plugin-rsc_vendor_...`)
|
|
1198
|
-
* and mangled export names (`__vite_ssr_export_default__`). This module
|
|
1199
|
-
* rewrites error messages and stack traces to point at user code instead.
|
|
1200
|
-
*
|
|
1201
|
-
* Dev-only — in production, errors go through the structured logger
|
|
1202
|
-
* without formatting.
|
|
1203
|
-
*/
|
|
1204
|
-
/**
|
|
1205
|
-
* Patterns that identify internal Vite/RSC vendor paths in stack traces.
|
|
1206
|
-
* These are replaced with human-readable labels.
|
|
1207
|
-
*/
|
|
1208
|
-
var VENDOR_PATH_PATTERNS = [
|
|
1209
|
-
{
|
|
1210
|
-
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor_react-server-dom[^\s)]+/g,
|
|
1211
|
-
replacement: "<react-server-dom>"
|
|
1212
|
-
},
|
|
1213
|
-
{
|
|
1214
|
-
pattern: /node_modules\/\.vite\/deps_ssr\/@vitejs_plugin-rsc_vendor[^\s)]+/g,
|
|
1215
|
-
replacement: "<rsc-vendor>"
|
|
1216
|
-
},
|
|
1217
|
-
{
|
|
1218
|
-
pattern: /node_modules\/\.vite\/deps_ssr\/[^\s)]+/g,
|
|
1219
|
-
replacement: "<vite-dep>"
|
|
1220
|
-
},
|
|
1221
|
-
{
|
|
1222
|
-
pattern: /node_modules\/\.vite\/deps\/[^\s)]+/g,
|
|
1223
|
-
replacement: "<vite-dep>"
|
|
1224
|
-
}
|
|
1225
|
-
];
|
|
1226
|
-
/**
|
|
1227
|
-
* Patterns that identify Vite-mangled export names in error messages.
|
|
1228
|
-
*/
|
|
1229
|
-
var MANGLED_NAME_PATTERNS = [{
|
|
1230
|
-
pattern: /__vite_ssr_export_default__/g,
|
|
1231
|
-
replacement: "<default export>"
|
|
1232
|
-
}, {
|
|
1233
|
-
pattern: /__vite_ssr_export_(\w+)__/g,
|
|
1234
|
-
replacement: "<export $1>"
|
|
1235
|
-
}];
|
|
1236
|
-
/**
|
|
1237
|
-
* Rewrite an error's message and stack to replace internal Vite paths
|
|
1238
|
-
* and mangled names with human-readable labels.
|
|
1239
|
-
*/
|
|
1240
|
-
function formatSsrError(error) {
|
|
1241
|
-
if (!(error instanceof Error)) return String(error);
|
|
1242
|
-
let message = error.message;
|
|
1243
|
-
let stack = error.stack ?? "";
|
|
1244
|
-
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) message = message.replace(pattern, replacement);
|
|
1245
|
-
for (const { pattern, replacement } of VENDOR_PATH_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
1246
|
-
for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) stack = stack.replace(pattern, replacement);
|
|
1247
|
-
const hint = extractErrorHint(error.message);
|
|
1248
|
-
const parts = [];
|
|
1249
|
-
parts.push(message);
|
|
1250
|
-
if (hint) parts.push(` → ${hint}`);
|
|
1251
|
-
const userFrames = extractUserFrames(stack);
|
|
1252
|
-
if (userFrames.length > 0) {
|
|
1253
|
-
parts.push("");
|
|
1254
|
-
parts.push(" User code in stack:");
|
|
1255
|
-
for (const frame of userFrames) parts.push(` ${frame}`);
|
|
1256
|
-
}
|
|
1257
|
-
return parts.join("\n");
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Extract a human-readable hint from common React/RSC error messages.
|
|
1261
|
-
*
|
|
1262
|
-
* React error messages contain useful information but the surrounding
|
|
1263
|
-
* context (vendor paths, mangled names) obscures it. This extracts the
|
|
1264
|
-
* actionable part as a one-line hint.
|
|
1265
|
-
*/
|
|
1266
|
-
function extractErrorHint(message) {
|
|
1267
|
-
if (message.match(/Functions cannot be passed directly to Client Components/)) {
|
|
1268
|
-
const propMatch = message.match(/<[^>]*?\s(\w+)=\{function/);
|
|
1269
|
-
if (propMatch) return `Prop "${propMatch[1]}" is a function — mark it "use server" or call it before passing`;
|
|
1270
|
-
return "A function prop was passed to a Client Component — mark it \"use server\" or call it before passing";
|
|
1271
|
-
}
|
|
1272
|
-
if (message.includes("Objects are not valid as a React child")) return "An object was rendered as JSX children — convert to string or extract the value";
|
|
1273
|
-
const nullRefMatch = message.match(/Cannot read propert(?:y|ies) of (undefined|null) \(reading '(\w+)'\)/);
|
|
1274
|
-
if (nullRefMatch) return `Accessed .${nullRefMatch[2]} on ${nullRefMatch[1]} — check that the value exists`;
|
|
1275
|
-
const notFnMatch = message.match(/(\w+) is not a function/);
|
|
1276
|
-
if (notFnMatch) return `"${notFnMatch[1]}" is not a function — check imports and exports`;
|
|
1277
|
-
if (message.includes("Element type is invalid")) return "A component resolved to undefined/null — check default exports and import paths";
|
|
1278
|
-
if (message.includes("Invalid hook call")) return "A hook was called outside of a React component render. If this is a 'use client' component, ensure the directive is at the very top of the file (before any imports) and that @vitejs/plugin-rsc is loaded correctly. Barrel re-exports from non-'use client' files do not propagate the directive.";
|
|
1279
|
-
return null;
|
|
1280
|
-
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Extract stack frames that reference user code (not node_modules,
|
|
1283
|
-
* not framework internals).
|
|
1284
|
-
*
|
|
1285
|
-
* Returns at most 5 frames to keep output concise.
|
|
1286
|
-
*/
|
|
1287
|
-
function extractUserFrames(stack) {
|
|
1288
|
-
const lines = stack.split("\n");
|
|
1289
|
-
const userFrames = [];
|
|
1290
|
-
for (const line of lines) {
|
|
1291
|
-
const trimmed = line.trim();
|
|
1292
|
-
if (!trimmed.startsWith("at ")) continue;
|
|
1293
|
-
if (trimmed.includes("node_modules") || trimmed.includes("<react-server-dom>") || trimmed.includes("<rsc-vendor>") || trimmed.includes("<vite-dep>") || trimmed.includes("node:internal")) continue;
|
|
1294
|
-
userFrames.push(trimmed);
|
|
1295
|
-
if (userFrames.length >= 5) break;
|
|
1296
|
-
}
|
|
1297
|
-
return userFrames;
|
|
1298
|
-
}
|
|
1299
|
-
//#endregion
|
|
1300
|
-
//#region src/server/default-logger.ts
|
|
1301
|
-
/**
|
|
1302
|
-
* DefaultLogger — human-readable stderr logging when no custom logger is configured.
|
|
1303
|
-
*
|
|
1304
|
-
* Ships as the fallback so production deployments always have error visibility,
|
|
1305
|
-
* even without an `instrumentation.ts` logger export. Output is one line per
|
|
1306
|
-
* event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.
|
|
1307
|
-
*
|
|
1308
|
-
* Format:
|
|
1309
|
-
* [timber] ERROR message key=value key=value trace_id=4bf92f35
|
|
1310
|
-
* [timber] WARN message key=value key=value trace_id=4bf92f35
|
|
1311
|
-
* [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35
|
|
1312
|
-
*
|
|
1313
|
-
* Behavior:
|
|
1314
|
-
* - Suppressed entirely in dev mode (dev logging handles all output)
|
|
1315
|
-
* - `debug` suppressed unless TIMBER_DEBUG is set
|
|
1316
|
-
* - Replaced entirely when a custom logger is set via `setLogger()`
|
|
1317
|
-
*
|
|
1318
|
-
* See design/17-logging.md §"DefaultLogger"
|
|
1319
|
-
*/
|
|
1320
|
-
/**
|
|
1321
|
-
* Format data fields as `key=value` pairs for human-readable output.
|
|
1322
|
-
* - `error` key is serialized via formatSsrError for stack trace cleanup
|
|
1323
|
-
* - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)
|
|
1324
|
-
* - Other values are stringified inline
|
|
1325
|
-
*/
|
|
1326
|
-
function formatDataFields(data) {
|
|
1327
|
-
if (!data) return "";
|
|
1328
|
-
const parts = [];
|
|
1329
|
-
let traceId;
|
|
1330
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1331
|
-
if (key === "trace_id") {
|
|
1332
|
-
traceId = typeof value === "string" ? value : String(value);
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
if (key === "error") {
|
|
1336
|
-
parts.push(`error=${formatSsrError(value)}`);
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
if (value === void 0 || value === null) continue;
|
|
1340
|
-
parts.push(`${key}=${value}`);
|
|
1341
|
-
}
|
|
1342
|
-
if (traceId) parts.push(`trace_id=${traceId.slice(0, 8)}`);
|
|
1343
|
-
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
1344
|
-
}
|
|
1345
|
-
/** Pad level string to fixed width for alignment. */
|
|
1346
|
-
function padLevel(level) {
|
|
1347
|
-
return level.padEnd(5);
|
|
1348
|
-
}
|
|
1349
|
-
function createDefaultLogger() {
|
|
1350
|
-
return {
|
|
1351
|
-
error(msg, data) {
|
|
1352
|
-
const fields = formatDataFields(data);
|
|
1353
|
-
process.stderr.write(`[timber] ${padLevel("ERROR")} ${msg}${fields}\n`);
|
|
1354
|
-
},
|
|
1355
|
-
warn(msg, data) {
|
|
1356
|
-
const fields = formatDataFields(data);
|
|
1357
|
-
process.stderr.write(`[timber] ${padLevel("WARN")} ${msg}${fields}\n`);
|
|
1358
|
-
},
|
|
1359
|
-
info(msg, data) {
|
|
1360
|
-
if (isDevMode()) return;
|
|
1361
|
-
if (!isDebug()) return;
|
|
1362
|
-
const fields = formatDataFields(data);
|
|
1363
|
-
process.stderr.write(`[timber] ${padLevel("INFO")} ${msg}${fields}\n`);
|
|
1364
|
-
},
|
|
1365
|
-
debug(msg, data) {
|
|
1366
|
-
if (isDevMode()) return;
|
|
1367
|
-
if (!isDebug()) return;
|
|
1368
|
-
const fields = formatDataFields(data);
|
|
1369
|
-
process.stderr.write(`[timber] ${padLevel("DEBUG")} ${msg}${fields}\n`);
|
|
1370
|
-
}
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
//#endregion
|
|
1374
1366
|
//#region src/server/logger.ts
|
|
1375
1367
|
/**
|
|
1376
1368
|
* Logger — structured logging with environment-aware formatting.
|
|
@@ -1479,6 +1471,6 @@ function swallow(err, reason, opts) {
|
|
|
1479
1471
|
} catch {}
|
|
1480
1472
|
}
|
|
1481
1473
|
//#endregion
|
|
1482
|
-
export {
|
|
1474
|
+
export { getSegmentParams as A, isDebug as B, redirect as C, getHeader as D, applyRequestHeaderOverlay as E, setSegmentParams as F, getOtelTraceId as G, addSpanEvent as H, getCookie as I, replaceTraceId as J, getSpanId as K, getCookieJar as L, runWithRequestContext as M, setMatchedSegmentPath as N, getHeaders as O, setMutableCookieContext as P, getSetCookieHeaders as R, isSsrStreamError as S, waitUntil as T, addSpanEventSync as U, isDevMode as V, generateTraceId as W, setSpanAttribute as X, runWithTraceId as Y, withSpan as Z, RedirectSignal as _, logProxyError as a, isDenySignal as b, logRequestReceived as c, logSwrRefetchFailed as d, logWaitUntilRejected as f, DenySignal as g, swallow as h, logMiddlewareShortCircuit as i, markResponseFlushed as j, getSearchParams as k, logRouteError as l, setLogger as m, logCacheMiss as n, logRenderError as o, logWaitUntilUnsupported as p, getTraceId as q, logMiddlewareError as r, logRequestCompleted as s, getLogger as t, logSlowRequest as u, RenderError as v, redirectExternal as w, isRedirectSignal as x, deny as y, getWaitUntil as z };
|
|
1483
1475
|
|
|
1484
|
-
//# sourceMappingURL=logger-
|
|
1476
|
+
//# sourceMappingURL=logger-DqJ2VoAY.js.map
|