@glasstrace/sdk 1.9.1 → 1.10.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/dist/async-context/index.cjs +44 -0
- package/dist/async-context/index.cjs.map +1 -1
- package/dist/async-context/index.js +2 -2
- package/dist/{chunk-JHUNLPSS.js → chunk-6RKS3DNA.js} +45 -1
- package/dist/{chunk-JHUNLPSS.js.map → chunk-6RKS3DNA.js.map} +1 -1
- package/dist/{chunk-HD6JIFKN.js → chunk-BSVWJSVX.js} +2 -2
- package/dist/{chunk-QOHKZOKB.js → chunk-D54FMQHF.js} +2 -2
- package/dist/chunk-I2DVVSKW.js +419 -0
- package/dist/chunk-I2DVVSKW.js.map +1 -0
- package/dist/{chunk-H6WJ63X2.js → chunk-M5GO2SSO.js} +2 -2
- package/dist/{chunk-2F2MGFLO.js → chunk-OXA4IHQX.js} +39 -405
- package/dist/chunk-OXA4IHQX.js.map +1 -0
- package/dist/{chunk-QEXRCXSY.js → chunk-OXM2BZMF.js} +2 -2
- package/dist/{chunk-M6EWJCAT.js → chunk-QVTONMVZ.js} +2 -2
- package/dist/{chunk-DKV53A2C.js → chunk-RL43PU2X.js} +2 -2
- package/dist/{chunk-GWIEUBFR.js → chunk-UMGZJYC4.js} +3 -3
- package/dist/{chunk-QXITSNYM.js → chunk-XG6WR2KS.js} +3 -3
- package/dist/cli/init.cjs +4 -4
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +7 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp-add.cjs +1 -1
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +3 -3
- package/dist/cli/mcp-add.js.map +1 -1
- package/dist/cli/uninit.js +3 -3
- package/dist/cli/upgrade-instructions.cjs +1 -1
- package/dist/cli/upgrade-instructions.cjs.map +1 -1
- package/dist/cli/upgrade-instructions.js +3 -3
- package/dist/cli/upgrade-instructions.js.map +1 -1
- package/dist/cli/validate.cjs.map +1 -1
- package/dist/cli/validate.js +2 -2
- package/dist/edge-entry.cjs +44 -0
- package/dist/edge-entry.cjs.map +1 -1
- package/dist/edge-entry.js +4 -4
- package/dist/index.cjs +58 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.cjs +44 -0
- package/dist/middleware/index.cjs.map +1 -1
- package/dist/middleware/index.js +2 -2
- package/dist/node-entry.cjs +58 -5
- package/dist/node-entry.cjs.map +1 -1
- package/dist/node-entry.js +12 -10
- package/dist/node-subpath.cjs.map +1 -1
- package/dist/node-subpath.js +3 -3
- package/dist/{source-map-uploader-MMJ2WCL4.js → source-map-uploader-CLYCE2TZ.js} +3 -3
- package/dist/trpc/index.cjs +15164 -503
- package/dist/trpc/index.cjs.map +1 -1
- package/dist/trpc/index.d.cts +62 -1
- package/dist/trpc/index.d.ts +62 -1
- package/dist/trpc/index.js +200 -1
- package/dist/trpc/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2F2MGFLO.js.map +0 -1
- /package/dist/{chunk-HD6JIFKN.js.map → chunk-BSVWJSVX.js.map} +0 -0
- /package/dist/{chunk-QOHKZOKB.js.map → chunk-D54FMQHF.js.map} +0 -0
- /package/dist/{chunk-H6WJ63X2.js.map → chunk-M5GO2SSO.js.map} +0 -0
- /package/dist/{chunk-QEXRCXSY.js.map → chunk-OXM2BZMF.js.map} +0 -0
- /package/dist/{chunk-M6EWJCAT.js.map → chunk-QVTONMVZ.js.map} +0 -0
- /package/dist/{chunk-DKV53A2C.js.map → chunk-RL43PU2X.js.map} +0 -0
- /package/dist/{chunk-GWIEUBFR.js.map → chunk-UMGZJYC4.js.map} +0 -0
- /package/dist/{chunk-QXITSNYM.js.map → chunk-XG6WR2KS.js.map} +0 -0
- /package/dist/{source-map-uploader-MMJ2WCL4.js.map → source-map-uploader-CLYCE2TZ.js.map} +0 -0
package/dist/trpc/index.d.cts
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
import { AttributeValue } from './common/Attributes';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for {@link wrapBatchedHttpHandler}.
|
|
5
|
+
*/
|
|
6
|
+
interface WrapBatchedHttpHandlerOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The HTTP path prefix where the tRPC handler is mounted. Defaults
|
|
9
|
+
* to `/api/trpc/` (the most common Next.js tRPC mount path).
|
|
10
|
+
*
|
|
11
|
+
* Apps that mount tRPC at a different path (e.g. `/trpc/`,
|
|
12
|
+
* `/api/v2/trpc/`) MUST pass their actual base path here — the
|
|
13
|
+
* wrapper does NOT auto-detect to avoid both false matches
|
|
14
|
+
* (unrelated routes containing `trpc`) and missed matches (custom
|
|
15
|
+
* mounts). Per DISC-1215, the tRPC base path is configurable on
|
|
16
|
+
* the user side; this option propagates that decision.
|
|
17
|
+
*
|
|
18
|
+
* Callers MAY supply the path with or without a trailing `/` —
|
|
19
|
+
* the wrapper normalizes by appending `/` when missing. The
|
|
20
|
+
* normalized form (with trailing `/`) is what's used for prefix
|
|
21
|
+
* matching, so it can't accidentally match prefix substrings
|
|
22
|
+
* (e.g., `/api/trpc-internal/...` does not match `/api/trpc/`).
|
|
23
|
+
* Apps SHOULD pass the trailing slash explicitly; the runtime
|
|
24
|
+
* normalization is a safety net, not a documented affordance.
|
|
25
|
+
*/
|
|
26
|
+
basePath?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a tRPC HTTP handler so batched requests get per-member span
|
|
30
|
+
* attribution via `tracedMiddleware`.
|
|
31
|
+
*
|
|
32
|
+
* The wrapper inspects the incoming request's URL on each call. If
|
|
33
|
+
* the URL matches the batch pattern at the configured base path,
|
|
34
|
+
* the wrapper parses the comma-joined procedure list, builds a
|
|
35
|
+
* {@link BatchEnvelope}, and runs the underlying handler inside
|
|
36
|
+
* the envelope's `AsyncLocalStorage` scope. `tracedMiddleware`
|
|
37
|
+
* (which the user's tRPC procedure chain must already include)
|
|
38
|
+
* reads the envelope and adds the batch attributes to each member
|
|
39
|
+
* span.
|
|
40
|
+
*
|
|
41
|
+
* Non-batched requests (no `batch=` query param OR a URL whose
|
|
42
|
+
* pathname doesn't match the configured `basePath`) pass through
|
|
43
|
+
* to the underlying handler unchanged — the trace shape is
|
|
44
|
+
* identical to today's behavior. **Single-procedure URLs with
|
|
45
|
+
* `batch=1` ARE treated as batches** (a one-member batch is still
|
|
46
|
+
* a batch in tRPC's protocol semantics; the wrapper builds a
|
|
47
|
+
* single-element envelope and `tracedMiddleware` labels the one
|
|
48
|
+
* member span with `member_index: 0`).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { wrapBatchedHttpHandler } from "@glasstrace/sdk/trpc";
|
|
53
|
+
* import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
54
|
+
*
|
|
55
|
+
* const handler = (req: Request) =>
|
|
56
|
+
* fetchRequestHandler({ endpoint: "/api/trpc", req, router });
|
|
57
|
+
*
|
|
58
|
+
* export const POST = wrapBatchedHttpHandler(handler);
|
|
59
|
+
* export const GET = wrapBatchedHttpHandler(handler);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare function wrapBatchedHttpHandler<H extends (...args: never[]) => unknown>(handler: H, options?: WrapBatchedHttpHandlerOptions): H;
|
|
63
|
+
|
|
3
64
|
/**
|
|
4
65
|
* tRPC middleware-chain instrumentation for Glasstrace.
|
|
5
66
|
*
|
|
@@ -162,4 +223,4 @@ interface TracedMiddlewareOptions {
|
|
|
162
223
|
*/
|
|
163
224
|
declare function tracedMiddleware<T extends MiddlewareFunction>(options: TracedMiddlewareOptions, middleware: T): T;
|
|
164
225
|
|
|
165
|
-
export { type MiddlewareFunction, type TracedMiddlewareOptions, tracedMiddleware };
|
|
226
|
+
export { type MiddlewareFunction, type TracedMiddlewareOptions, type WrapBatchedHttpHandlerOptions, tracedMiddleware, wrapBatchedHttpHandler };
|
package/dist/trpc/index.d.ts
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
import { AttributeValue } from './common/Attributes';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for {@link wrapBatchedHttpHandler}.
|
|
5
|
+
*/
|
|
6
|
+
interface WrapBatchedHttpHandlerOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The HTTP path prefix where the tRPC handler is mounted. Defaults
|
|
9
|
+
* to `/api/trpc/` (the most common Next.js tRPC mount path).
|
|
10
|
+
*
|
|
11
|
+
* Apps that mount tRPC at a different path (e.g. `/trpc/`,
|
|
12
|
+
* `/api/v2/trpc/`) MUST pass their actual base path here — the
|
|
13
|
+
* wrapper does NOT auto-detect to avoid both false matches
|
|
14
|
+
* (unrelated routes containing `trpc`) and missed matches (custom
|
|
15
|
+
* mounts). Per DISC-1215, the tRPC base path is configurable on
|
|
16
|
+
* the user side; this option propagates that decision.
|
|
17
|
+
*
|
|
18
|
+
* Callers MAY supply the path with or without a trailing `/` —
|
|
19
|
+
* the wrapper normalizes by appending `/` when missing. The
|
|
20
|
+
* normalized form (with trailing `/`) is what's used for prefix
|
|
21
|
+
* matching, so it can't accidentally match prefix substrings
|
|
22
|
+
* (e.g., `/api/trpc-internal/...` does not match `/api/trpc/`).
|
|
23
|
+
* Apps SHOULD pass the trailing slash explicitly; the runtime
|
|
24
|
+
* normalization is a safety net, not a documented affordance.
|
|
25
|
+
*/
|
|
26
|
+
basePath?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a tRPC HTTP handler so batched requests get per-member span
|
|
30
|
+
* attribution via `tracedMiddleware`.
|
|
31
|
+
*
|
|
32
|
+
* The wrapper inspects the incoming request's URL on each call. If
|
|
33
|
+
* the URL matches the batch pattern at the configured base path,
|
|
34
|
+
* the wrapper parses the comma-joined procedure list, builds a
|
|
35
|
+
* {@link BatchEnvelope}, and runs the underlying handler inside
|
|
36
|
+
* the envelope's `AsyncLocalStorage` scope. `tracedMiddleware`
|
|
37
|
+
* (which the user's tRPC procedure chain must already include)
|
|
38
|
+
* reads the envelope and adds the batch attributes to each member
|
|
39
|
+
* span.
|
|
40
|
+
*
|
|
41
|
+
* Non-batched requests (no `batch=` query param OR a URL whose
|
|
42
|
+
* pathname doesn't match the configured `basePath`) pass through
|
|
43
|
+
* to the underlying handler unchanged — the trace shape is
|
|
44
|
+
* identical to today's behavior. **Single-procedure URLs with
|
|
45
|
+
* `batch=1` ARE treated as batches** (a one-member batch is still
|
|
46
|
+
* a batch in tRPC's protocol semantics; the wrapper builds a
|
|
47
|
+
* single-element envelope and `tracedMiddleware` labels the one
|
|
48
|
+
* member span with `member_index: 0`).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { wrapBatchedHttpHandler } from "@glasstrace/sdk/trpc";
|
|
53
|
+
* import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
54
|
+
*
|
|
55
|
+
* const handler = (req: Request) =>
|
|
56
|
+
* fetchRequestHandler({ endpoint: "/api/trpc", req, router });
|
|
57
|
+
*
|
|
58
|
+
* export const POST = wrapBatchedHttpHandler(handler);
|
|
59
|
+
* export const GET = wrapBatchedHttpHandler(handler);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare function wrapBatchedHttpHandler<H extends (...args: never[]) => unknown>(handler: H, options?: WrapBatchedHttpHandlerOptions): H;
|
|
63
|
+
|
|
3
64
|
/**
|
|
4
65
|
* tRPC middleware-chain instrumentation for Glasstrace.
|
|
5
66
|
*
|
|
@@ -162,4 +223,4 @@ interface TracedMiddlewareOptions {
|
|
|
162
223
|
*/
|
|
163
224
|
declare function tracedMiddleware<T extends MiddlewareFunction>(options: TracedMiddlewareOptions, middleware: T): T;
|
|
164
225
|
|
|
165
|
-
export { type MiddlewareFunction, type TracedMiddlewareOptions, tracedMiddleware };
|
|
226
|
+
export { type MiddlewareFunction, type TracedMiddlewareOptions, type WrapBatchedHttpHandlerOptions, tracedMiddleware, wrapBatchedHttpHandler };
|
package/dist/trpc/index.js
CHANGED
|
@@ -1,9 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
emitLifecycleEvent
|
|
3
|
+
} from "../chunk-I2DVVSKW.js";
|
|
4
|
+
import "../chunk-CL3OVHPO.js";
|
|
1
5
|
import {
|
|
2
6
|
SpanStatusCode,
|
|
3
7
|
trace
|
|
4
8
|
} from "../chunk-DQ25VOKK.js";
|
|
9
|
+
import {
|
|
10
|
+
sdkLog
|
|
11
|
+
} from "../chunk-YG3X7TUI.js";
|
|
12
|
+
import "../chunk-VUZCLMIX.js";
|
|
13
|
+
import {
|
|
14
|
+
GLASSTRACE_ATTRIBUTE_NAMES
|
|
15
|
+
} from "../chunk-6RKS3DNA.js";
|
|
5
16
|
import "../chunk-NSBPE2FW.js";
|
|
6
17
|
|
|
18
|
+
// src/trpc/batch-context.ts
|
|
19
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
|
+
var _als = new AsyncLocalStorage();
|
|
21
|
+
function buildEnvelope(procedures) {
|
|
22
|
+
const allNames = procedures.map((m) => m.name);
|
|
23
|
+
const positions = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const member of procedures) {
|
|
25
|
+
let list = positions.get(member.name);
|
|
26
|
+
if (list === void 0) {
|
|
27
|
+
list = [];
|
|
28
|
+
positions.set(member.name, list);
|
|
29
|
+
}
|
|
30
|
+
list.push(member.index);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
procedures,
|
|
34
|
+
allNames,
|
|
35
|
+
nameToPositions: positions,
|
|
36
|
+
nameCounters: /* @__PURE__ */ new Map()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function withBatchEnvelope(envelope, fn) {
|
|
40
|
+
return _als.run(envelope, fn);
|
|
41
|
+
}
|
|
42
|
+
function getBatchEnvelope() {
|
|
43
|
+
return _als.getStore();
|
|
44
|
+
}
|
|
45
|
+
function resolveBatchMember(procedureName) {
|
|
46
|
+
const envelope = _als.getStore();
|
|
47
|
+
if (envelope === void 0) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
const positions = envelope.nameToPositions.get(procedureName);
|
|
51
|
+
if (positions === void 0) {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
const occurrence = envelope.nameCounters.get(procedureName) ?? 0;
|
|
55
|
+
if (occurrence >= positions.length) {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
envelope.nameCounters.set(procedureName, occurrence + 1);
|
|
59
|
+
return {
|
|
60
|
+
envelope,
|
|
61
|
+
index: positions[occurrence],
|
|
62
|
+
allNames: envelope.allNames
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/trpc/batch-handler.ts
|
|
67
|
+
var _malformedUrlWarned = false;
|
|
68
|
+
function wrapBatchedHttpHandler(handler, options) {
|
|
69
|
+
const rawBasePath = options?.basePath ?? "/api/trpc/";
|
|
70
|
+
const basePath = rawBasePath.endsWith("/") ? rawBasePath : `${rawBasePath}/`;
|
|
71
|
+
const wrapped = ((...args) => {
|
|
72
|
+
const url = extractRequestUrl(args[0]);
|
|
73
|
+
if (url === void 0) {
|
|
74
|
+
return handler(...args);
|
|
75
|
+
}
|
|
76
|
+
const envelope = parseBatchUrl(url, basePath);
|
|
77
|
+
if (envelope === void 0) {
|
|
78
|
+
return handler(...args);
|
|
79
|
+
}
|
|
80
|
+
return withBatchEnvelope(
|
|
81
|
+
envelope,
|
|
82
|
+
() => handler(...args)
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
return wrapped;
|
|
86
|
+
}
|
|
87
|
+
function extractRequestUrl(arg) {
|
|
88
|
+
if (typeof arg !== "object" || arg === null) {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
const direct = readUrlFromRequest(arg);
|
|
92
|
+
if (direct !== void 0) {
|
|
93
|
+
return direct;
|
|
94
|
+
}
|
|
95
|
+
const reqWrapper = arg.req;
|
|
96
|
+
if (reqWrapper !== void 0 && reqWrapper !== null && typeof reqWrapper === "object") {
|
|
97
|
+
const wrapped = readUrlFromRequest(reqWrapper);
|
|
98
|
+
if (wrapped !== void 0) {
|
|
99
|
+
return wrapped;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const nextUrl = arg.nextUrl;
|
|
103
|
+
if (nextUrl !== void 0 && nextUrl !== null && typeof nextUrl === "object") {
|
|
104
|
+
const href = nextUrl.href;
|
|
105
|
+
if (typeof href === "string") {
|
|
106
|
+
return href;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
function readUrlFromRequest(req) {
|
|
112
|
+
const originalUrl = req.originalUrl;
|
|
113
|
+
if (typeof originalUrl === "string" && originalUrl.length > 0) {
|
|
114
|
+
return originalUrl;
|
|
115
|
+
}
|
|
116
|
+
const baseUrl = req.baseUrl;
|
|
117
|
+
const url = req.url;
|
|
118
|
+
if (typeof baseUrl === "string" && baseUrl.length > 0 && typeof url === "string") {
|
|
119
|
+
return baseUrl + url;
|
|
120
|
+
}
|
|
121
|
+
if (typeof url === "string") {
|
|
122
|
+
return url;
|
|
123
|
+
}
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
function parseBatchUrl(url, basePath) {
|
|
127
|
+
let pathname;
|
|
128
|
+
let search;
|
|
129
|
+
try {
|
|
130
|
+
const parsed = new URL(url, "http://glasstrace.invalid/");
|
|
131
|
+
pathname = parsed.pathname;
|
|
132
|
+
search = parsed.search;
|
|
133
|
+
} catch {
|
|
134
|
+
rateLimitWarn(`malformed URL: cannot parse "${url}"`);
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
if (!/[?&]batch=/.test(search)) {
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
if (!pathname.startsWith(basePath)) {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
const procSegment = pathname.slice(basePath.length);
|
|
144
|
+
if (procSegment.length === 0) {
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
let decoded;
|
|
148
|
+
try {
|
|
149
|
+
decoded = decodeURIComponent(procSegment);
|
|
150
|
+
} catch {
|
|
151
|
+
rateLimitWarn(`malformed batch URL: ${procSegment}`);
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
const names = decoded.split(",").filter((s) => s.length > 0);
|
|
155
|
+
if (names.length === 0) {
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
const procedures = names.map((name, index) => ({
|
|
159
|
+
name,
|
|
160
|
+
index
|
|
161
|
+
}));
|
|
162
|
+
return buildEnvelope(procedures);
|
|
163
|
+
}
|
|
164
|
+
function rateLimitWarn(reason) {
|
|
165
|
+
if (_malformedUrlWarned) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
_malformedUrlWarned = true;
|
|
169
|
+
sdkLog(
|
|
170
|
+
"warn",
|
|
171
|
+
`[glasstrace] wrapBatchedHttpHandler: ${reason}; falling back to pass-through. Subsequent malformed-URL warnings suppressed for this process.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
7
175
|
// src/trpc/index.ts
|
|
8
176
|
var TRACER_NAME = "@glasstrace/sdk/trpc";
|
|
9
177
|
function tracedMiddleware(options, middleware) {
|
|
@@ -19,16 +187,46 @@ function tracedMiddleware(options, middleware) {
|
|
|
19
187
|
if (options.attributes) {
|
|
20
188
|
span.setAttributes(options.attributes);
|
|
21
189
|
}
|
|
190
|
+
let procedurePath;
|
|
22
191
|
if (mwOpts && typeof mwOpts === "object") {
|
|
23
192
|
const path = mwOpts.path;
|
|
24
193
|
if (typeof path === "string") {
|
|
25
194
|
span.setAttribute("trpc.path", path);
|
|
195
|
+
procedurePath = path;
|
|
26
196
|
}
|
|
27
197
|
const type = mwOpts.type;
|
|
28
198
|
if (type === "query" || type === "mutation" || type === "subscription") {
|
|
29
199
|
span.setAttribute("trpc.type", type);
|
|
30
200
|
}
|
|
31
201
|
}
|
|
202
|
+
if (procedurePath !== void 0) {
|
|
203
|
+
const resolved = resolveBatchMember(procedurePath);
|
|
204
|
+
if (resolved !== void 0) {
|
|
205
|
+
span.setAttribute(
|
|
206
|
+
GLASSTRACE_ATTRIBUTE_NAMES.TRPC_BATCH_MEMBER_INDEX,
|
|
207
|
+
resolved.index
|
|
208
|
+
);
|
|
209
|
+
span.setAttribute(
|
|
210
|
+
GLASSTRACE_ATTRIBUTE_NAMES.TRPC_BATCH_MEMBER_PROCEDURES,
|
|
211
|
+
resolved.allNames
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
const envelope = getBatchEnvelope();
|
|
215
|
+
if (envelope !== void 0) {
|
|
216
|
+
emitLifecycleEvent("otel:trpc_batch_member_mismatch", {
|
|
217
|
+
procedureName: procedurePath,
|
|
218
|
+
// Use the envelope's precomputed allNames cache
|
|
219
|
+
// rather than rebuilding `procedures.map(...)` on
|
|
220
|
+
// every mismatch — the rebuild was the residual
|
|
221
|
+
// O(N) waste from the original implementation that
|
|
222
|
+
// the precomputed-cache fix in batch-context.ts is
|
|
223
|
+
// designed to eliminate.
|
|
224
|
+
batchMembers: envelope.allNames,
|
|
225
|
+
spanId: span.spanContext().spanId
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
32
230
|
const result = await middleware(mwOpts);
|
|
33
231
|
if (result !== null && typeof result === "object" && result.ok === false) {
|
|
34
232
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
@@ -60,6 +258,7 @@ function tracedMiddleware(options, middleware) {
|
|
|
60
258
|
return wrapped;
|
|
61
259
|
}
|
|
62
260
|
export {
|
|
63
|
-
tracedMiddleware
|
|
261
|
+
tracedMiddleware,
|
|
262
|
+
wrapBatchedHttpHandler
|
|
64
263
|
};
|
|
65
264
|
//# sourceMappingURL=index.js.map
|
package/dist/trpc/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/trpc/index.ts"],"sourcesContent":["/**\n * tRPC middleware-chain instrumentation for Glasstrace.\n *\n * Subpath: `@glasstrace/sdk/trpc`\n *\n * This module exposes {@link tracedMiddleware}, a thin wrapper that turns\n * a user-supplied tRPC middleware function into a span-emitting middleware\n * function. Each invocation of the wrapped middleware opens a child span\n * (via {@link https://opentelemetry.io/docs/specs/otel/trace/api/#starting-a-new-active-span | tracer.startActiveSpan})\n * under whatever active OTel context the runtime exposes when the tRPC\n * dispatcher calls the middleware. In a typical Next.js / Node HTTP server\n * deployment that active context is the HTTP server span, so middleware\n * spans land as children of the HTTP span automatically — no manual\n * parent plumbing required.\n *\n * The helper does not import the `@trpc/server` runtime; it consumes the\n * middleware function shape structurally so that:\n *\n * 1. Projects that do not use tRPC pay no runtime cost (the subpath is\n * excluded from the root barrel and tree-shakeable on its own).\n * 2. The same helper works against `@trpc/server@^10.0.0` and\n * `@trpc/server@^11.0.0` without two parallel implementations.\n *\n * The wrapped function preserves the user's call-site type (`T`) so that\n * tRPC's procedure-builder context narrowing (e.g., adding a `session`\n * field across `.use()` chains) continues to flow through.\n *\n * Compatibility with the existing `glasstrace.trpc.procedure` URL-derived\n * attribute (DISC-1215, shipped) is by construction: that attribute is\n * attached to the **parent** HTTP span at exporter time, never to a\n * middleware child span. Middleware spans only carry `trpc.path` and\n * `trpc.type` (forwarded from the middleware options) plus whatever\n * caller-supplied attributes the {@link TracedMiddlewareOptions.attributes}\n * field carries.\n *\n * @module @glasstrace/sdk/trpc\n */\n\nimport {\n trace,\n SpanStatusCode,\n type AttributeValue,\n} from \"@opentelemetry/api\";\n\n/**\n * Permissive structural bound for a tRPC middleware function. The shape\n * is the intersection of `@trpc/server@^10` and `@trpc/server@^11`'s\n * middleware signature: an async function taking a single options object\n * and returning a thenable middleware result.\n *\n * The `opts` parameter is typed `any` so any user-narrowed middleware\n * (with strongly-typed `ctx` / `input` / `meta`) is assignable, and the\n * return type is `Promise<unknown>` so both v10's\n * `Promise<MiddlewareResult<...>>` and v11's identically-named result\n * shape (with extra fields) are accepted without import-time coupling\n * to either major version.\n *\n * Exported so consumers can reference it for type-inference assertions\n * (e.g., proving a strongly-typed middleware fits the bound) without\n * having to recreate the structural shape. The runtime contract is\n * fixed by the `@trpc/server` versions in the peer-dependency range.\n */\n// The `any` here is load-bearing: a tighter bound would reject either\n// v10 or v11 middleware shapes, or both, because tRPC narrows `ctx`,\n// `input`, and `meta` to user-supplied types via generics. Capturing\n// `T extends MiddlewareFunction` preserves that narrowing through the\n// wrapper's `: T` return type (see the type-inference fixture in\n// tests/unit/sdk/trpc/traced-middleware-types.test.ts).\n//\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type MiddlewareFunction = (opts: any) => Promise<unknown>;\n\n/**\n * Options for {@link tracedMiddleware}.\n *\n * @example\n * ```ts\n * import { tracedMiddleware } from \"@glasstrace/sdk/trpc\";\n *\n * const isAuthed = t.middleware(\n * tracedMiddleware(\n * { name: \"isAuthed\", attributes: { \"auth.required\": true } },\n * async ({ ctx, next }) => {\n * if (!ctx.session) throw new TRPCError({ code: \"UNAUTHORIZED\" });\n * return next({ ctx: { ...ctx, session: ctx.session } });\n * },\n * ),\n * );\n * ```\n */\nexport interface TracedMiddlewareOptions {\n /**\n * Span name. Required. Used as the OTel span name; appears in trace\n * timelines and is the primary identifier surfaced by enrichment when\n * a middleware step short-circuits (e.g., auth failure).\n *\n * Must be a non-empty string. Names should be stable across runs so\n * enrichment can reason about middleware identity (e.g., \"isAuthed\",\n * \"isPro\"); avoid embedding request data in the name.\n */\n name: string;\n /**\n * Optional attributes attached to the span before the wrapped\n * middleware body runs. Forwarded to OTel as-is via\n * `span.setAttributes(...)`. The SDK does not redact, sanitize, or\n * scan values here — callers must avoid placing tokens, credentials,\n * or other sensitive data in `attributes`.\n *\n * Sensitive request/response data is captured through the gated\n * `glasstrace.error.response_body` path (see DISC-1216), not through\n * this surface.\n */\n attributes?: Record<string, AttributeValue>;\n}\n\n/**\n * Module-level OTel tracer name for the tRPC subpath. Resolves through\n * the global `ProxyTracerProvider` so it inherits whatever provider the\n * SDK has detected or registered (Glasstrace's enriching exporter,\n * Sentry's processor in coexistence mode, Datadog's processor, etc.).\n *\n * Re-resolved on every call site rather than cached at module top-level\n * so that the test harness's `trace.setGlobalTracerProvider` can be\n * picked up after this module is imported. (Caching the tracer at module\n * top-level would race against test harness setup and produce stale\n * no-op spans for the very first test.)\n */\nconst TRACER_NAME = \"@glasstrace/sdk/trpc\";\n\n/**\n * Wrap a tRPC middleware function in an OTel span.\n *\n * Each call to the returned middleware:\n *\n * 1. Opens a span named `options.name` under the active OTel context\n * (typically the HTTP server span). The span inherits `traceId` and\n * parent `spanId` automatically — no manual context plumbing.\n * 2. Sets caller-supplied {@link TracedMiddlewareOptions.attributes}\n * plus `trpc.path` and `trpc.type` (forwarded from the middleware\n * options) on the span before calling the wrapped middleware body.\n * 3. Lets the wrapped middleware run with the new span as the active\n * span (so any `tracer.startActiveSpan` calls inside the body open\n * grandchild spans under the middleware span).\n * 4. On a thrown error: records the exception via `span.recordException`\n * and sets `span.status` to `ERROR` with the error's message; rethrows.\n * 5. On a returned `{ ok: false, error }` middleware result (tRPC's\n * short-circuit shape): sets `span.status` to `ERROR` without\n * `recordException` (no `Error` object to record).\n * 6. On a successful `{ ok: true, ... }` result: leaves the span status\n * as `UNSET` (per OTel instrumentation-library guidance — explicit\n * `OK` here would shadow downstream consumers attempting their own\n * status transitions).\n * 7. Always ends the span (`span.end()`), even on `throw` or `return`.\n *\n * Type-inference: the returned function preserves the input function's\n * type `T`, so tRPC's procedure-builder context narrowing flows through\n * unchanged. See `sdk-trpc.md` §3.3 for the recommended call pattern.\n *\n * @param options - Span name and optional pre-start attributes.\n * @param middleware - The user's tRPC middleware function. Must be\n * structurally compatible with `@trpc/server@^10` or `@trpc/server@^11`.\n * @returns The wrapped middleware function with the same call signature\n * and return type as `middleware`.\n *\n * @example\n * ```ts\n * // trpc.ts — user's project\n * import { initTRPC, TRPCError } from \"@trpc/server\";\n * import { tracedMiddleware } from \"@glasstrace/sdk/trpc\";\n *\n * interface MyContext { session?: { userId: string }; tier?: string }\n * const t = initTRPC.context<MyContext>().create();\n *\n * const isAuthed = t.middleware(\n * tracedMiddleware({ name: \"isAuthed\" }, async ({ ctx, next }) => {\n * if (!ctx.session) throw new TRPCError({ code: \"UNAUTHORIZED\" });\n * return next({ ctx: { ...ctx, session: ctx.session } });\n * }),\n * );\n *\n * const isPro = t.middleware(\n * tracedMiddleware({ name: \"isPro\" }, async ({ ctx, next }) => {\n * if (ctx.tier !== \"pro\") throw new TRPCError({ code: \"FORBIDDEN\" });\n * return next();\n * }),\n * );\n *\n * export const proProcedure = t.procedure.use(isAuthed).use(isPro);\n * ```\n */\nexport function tracedMiddleware<T extends MiddlewareFunction>(\n options: TracedMiddlewareOptions,\n middleware: T,\n): T {\n // Validate the span name eagerly so a mis-typed call site fails at\n // wrapper-construction time (typically at module load) rather than at\n // first request, when the failure is harder to diagnose. The structural\n // bound only enforces shape, not value-level invariants.\n if (typeof options.name !== \"string\" || options.name.length === 0) {\n throw new TypeError(\n \"tracedMiddleware: options.name must be a non-empty string\",\n );\n }\n\n // The wrapped function. Capture `options` and `middleware` lexically;\n // do not read them from `this` since tRPC invokes middleware as a\n // plain function (not a method).\n const wrapped = async (mwOpts: Parameters<T>[0]): Promise<unknown> => {\n const tracer = trace.getTracer(TRACER_NAME);\n return tracer.startActiveSpan(options.name, async (span) => {\n try {\n // Set caller-supplied attributes first so they appear on the\n // span before any internal attribute we add below. Caller-supplied\n // attributes are forwarded as-is — no redaction or scanning (see\n // TracedMiddlewareOptions.attributes JSDoc).\n if (options.attributes) {\n span.setAttributes(options.attributes);\n }\n // Forward the tRPC-provided `path` and `type` so consumers (the\n // enriching exporter, third-party UIs) can correlate the\n // middleware span back to its procedure without joining against\n // the parent HTTP span. Both fields are documented as Tier 2\n // heuristics in `sdk-trpc.md` §4.\n if (mwOpts && typeof mwOpts === \"object\") {\n const path = (mwOpts as { path?: unknown }).path;\n if (typeof path === \"string\") {\n span.setAttribute(\"trpc.path\", path);\n }\n const type = (mwOpts as { type?: unknown }).type;\n if (\n type === \"query\" ||\n type === \"mutation\" ||\n type === \"subscription\"\n ) {\n span.setAttribute(\"trpc.type\", type);\n }\n }\n\n const result = await middleware(mwOpts);\n\n // tRPC's middleware result is a discriminated union:\n // { ok: true, ... } — successful pass-through\n // { ok: false, error, ... } — middleware short-circuited with\n // an explicit error envelope\n //\n // The error envelope is the path users hit when they call\n // `next()` and the next link returns ok:false; from the wrapper's\n // perspective the middleware did not throw, but the request did\n // fail. Mark the span ERROR so the exporter and downstream UIs\n // surface the failure, but do not call `recordException` —\n // there is no `Error` object to record.\n if (\n result !== null &&\n typeof result === \"object\" &&\n (result as { ok?: unknown }).ok === false\n ) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n\n return result;\n } catch (error) {\n // Thrown error path. `recordException` produces an OTel\n // exception event with the error name, message, and stack;\n // `setStatus({ code: ERROR, message })` lets standard OTel UIs\n // display the error message inline with the span.\n //\n // OpenTelemetry's `Span.recordException` accepts only\n // `Exception = string | Error` — a non-Error, non-string\n // throwable (e.g. a plain object, number, or symbol thrown by\n // user code via valid JavaScript) can cause `recordException`\n // to throw, which would otherwise leave the span status UNSET\n // even though the request failed. Normalize the throwable\n // first, then guard `recordException` and `setStatus` in\n // independent try/catch blocks so a failure inside one cannot\n // block the other from running. The user's original `error`\n // value is preserved verbatim for the `throw error` re-raise\n // below — wrapping is purely a span-side normalization.\n const normalized: Error | string =\n error instanceof Error\n ? error\n : typeof error === \"string\"\n ? error\n : new Error(String(error));\n const statusMessage =\n normalized instanceof Error ? normalized.message : normalized;\n try {\n span.recordException(normalized);\n } catch {\n // Swallow — instrumentation must never replace the user's\n // error with its own. The span is still ended in `finally`.\n }\n try {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: statusMessage,\n });\n } catch {\n // Swallow — see comment above. Independent from the\n // recordException try/catch so a failing recordException\n // does not prevent the ERROR status from being recorded.\n }\n throw error;\n } finally {\n // Always end the span. `try/finally` covers both the success\n // and throw paths; the `return result` above happens inside the\n // try, so finally still runs before the value is yielded.\n // Defensively suppress any throw from `span.end()` so a\n // misbehaving OTel impl cannot replace the wrapped middleware's\n // return value (or thrown error) with an unrelated one.\n try {\n span.end();\n } catch {\n // Span lifecycle errors are always non-fatal at this layer.\n }\n }\n });\n };\n\n // The `T` cast preserves the user's function type at the call site\n // even though our wrapper widens parameters to `Parameters<T>[0]` and\n // return to `Promise<unknown>` internally. This is the load-bearing\n // type-inference contract documented in `sdk-trpc.md` §3.3 and\n // verified by `tests/unit/sdk/trpc/traced-middleware-types.test.ts`.\n return wrapped as T;\n}\n"],"mappings":";;;;;;;AA+HA,IAAM,cAAc;AA+Db,SAAS,iBACd,SACA,YACG;AAKH,MAAI,OAAO,QAAQ,SAAS,YAAY,QAAQ,KAAK,WAAW,GAAG;AACjE,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAKA,QAAM,UAAU,OAAO,WAA+C;AACpE,UAAM,SAAS,MAAM,UAAU,WAAW;AAC1C,WAAO,OAAO,gBAAgB,QAAQ,MAAM,OAAO,SAAS;AAC1D,UAAI;AAKF,YAAI,QAAQ,YAAY;AACtB,eAAK,cAAc,QAAQ,UAAU;AAAA,QACvC;AAMA,YAAI,UAAU,OAAO,WAAW,UAAU;AACxC,gBAAM,OAAQ,OAA8B;AAC5C,cAAI,OAAO,SAAS,UAAU;AAC5B,iBAAK,aAAa,aAAa,IAAI;AAAA,UACrC;AACA,gBAAM,OAAQ,OAA8B;AAC5C,cACE,SAAS,WACT,SAAS,cACT,SAAS,gBACT;AACA,iBAAK,aAAa,aAAa,IAAI;AAAA,UACrC;AAAA,QACF;AAEA,cAAM,SAAS,MAAM,WAAW,MAAM;AAatC,YACE,WAAW,QACX,OAAO,WAAW,YACjB,OAA4B,OAAO,OACpC;AACA,eAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;AAAA,QAC/C;AAEA,eAAO;AAAA,MACT,SAAS,OAAO;AAiBd,cAAM,aACJ,iBAAiB,QACb,QACA,OAAO,UAAU,WACf,QACA,IAAI,MAAM,OAAO,KAAK,CAAC;AAC/B,cAAM,gBACJ,sBAAsB,QAAQ,WAAW,UAAU;AACrD,YAAI;AACF,eAAK,gBAAgB,UAAU;AAAA,QACjC,QAAQ;AAAA,QAGR;AACA,YAAI;AACF,eAAK,UAAU;AAAA,YACb,MAAM,eAAe;AAAA,YACrB,SAAS;AAAA,UACX,CAAC;AAAA,QACH,QAAQ;AAAA,QAIR;AACA,cAAM;AAAA,MACR,UAAE;AAOA,YAAI;AACF,eAAK,IAAI;AAAA,QACX,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAOA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/trpc/batch-context.ts","../../src/trpc/batch-handler.ts","../../src/trpc/index.ts"],"sourcesContent":["/**\n * Per-request batch envelope for tRPC HTTP-batch dispatch (SDK-052 /\n * Wave 16B / advances DISC-1534 SDK-side slice).\n *\n * The envelope is set by `wrapBatchedHttpHandler` at the outer HTTP\n * boundary and read by `tracedMiddleware` inside each member's\n * dispatch — propagation is via Node `AsyncLocalStorage` so the\n * envelope flows through tRPC's `Promise.all` member dispatch\n * without coupling to tRPC's `createContext` API (which has subtle\n * shape differences between v10 and v11).\n *\n * **Per-procedure-name occurrence counter is mutable.** Batches can\n * include the same procedure name more than once; the middleware\n * tracks how many invocations it has seen per name and maps the\n * N-th invocation to the N-th positional occurrence in\n * `procedures` (positional dispatch index, NOT name-only matching).\n * The counter mutation is scoped to the single-request envelope and\n * therefore safe under tRPC's per-batch dispatch ordering — see\n * §6 of the SDK-052 brief for the full contract.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * One member of a batched tRPC HTTP request, captured at URL-parse\n * time. `name` is the procedure name as it appears in the\n * comma-joined URL segment; `index` is its zero-based positional\n * order in that segment. Duplicate names across the array are\n * permitted — `index` is what disambiguates them.\n */\nexport interface BatchMember {\n readonly name: string;\n readonly index: number;\n}\n\n/**\n * Request-scoped envelope set by `wrapBatchedHttpHandler` for the\n * duration of one batched HTTP request. Read by `tracedMiddleware`\n * via `getBatchEnvelope()`.\n *\n * **Performance contract:** `allNames` and `nameToPositions` are\n * precomputed at envelope construction so per-member span\n * attribution stays O(1) per `resolveBatchMember` invocation.\n * Without these caches the resolver would scan `procedures` and\n * rebuild the names list on every call, making total work O(N^2)\n * for an N-member batch on a hot request path.\n */\nexport interface BatchEnvelope {\n /** Ordered procedure list as parsed from the URL. */\n readonly procedures: ReadonlyArray<BatchMember>;\n /**\n * Pre-materialized names list — the same value passed onto each\n * member span as `glasstrace.trpc.batch.member_procedures`. It's\n * a real `string[]` (not a derived `map(...)` view) so OTel's\n * span-attribute setter accepts it without a synthetic copy on\n * every invocation, and so the OTel typed-array contract is\n * satisfied with a concrete mutable array (per Copilot review).\n */\n readonly allNames: string[];\n /**\n * Index from procedure-name → ordered list of positional indices\n * in `procedures` where that name appears. Built once at envelope\n * construction so `resolveBatchMember` can look up the N-th\n * positional match for the N-th invocation in O(1).\n */\n readonly nameToPositions: ReadonlyMap<string, ReadonlyArray<number>>;\n /**\n * Per-name occurrence counter. Mutated by `tracedMiddleware` as\n * each invocation maps itself to the next positional member of\n * the same name. Initialized to an empty Map; entries default to\n * 0 the first time a name is queried.\n */\n readonly nameCounters: Map<string, number>;\n}\n\nconst _als = new AsyncLocalStorage<BatchEnvelope>();\n\n/**\n * Construct a `BatchEnvelope` from an ordered list of member names.\n * Pre-computes `allNames` and `nameToPositions` so subsequent\n * `resolveBatchMember` calls run in O(1) regardless of batch size.\n */\nexport function buildEnvelope(\n procedures: ReadonlyArray<BatchMember>,\n): BatchEnvelope {\n const allNames: string[] = procedures.map((m) => m.name);\n const positions = new Map<string, number[]>();\n for (const member of procedures) {\n let list = positions.get(member.name);\n if (list === undefined) {\n list = [];\n positions.set(member.name, list);\n }\n list.push(member.index);\n }\n return {\n procedures,\n allNames,\n nameToPositions: positions,\n nameCounters: new Map<string, number>(),\n };\n}\n\n/**\n * Run `fn` with `envelope` set as the request-scoped batch envelope.\n * Used by `wrapBatchedHttpHandler`. Promises returned by `fn` (or\n * any async work it spawns) inherit the envelope via\n * `AsyncLocalStorage` propagation.\n */\nexport function withBatchEnvelope<T>(\n envelope: BatchEnvelope,\n fn: () => T,\n): T {\n return _als.run(envelope, fn);\n}\n\n/**\n * Returns the current request's batch envelope, or `undefined` when\n * no envelope is in scope (the non-batched path or apps not using\n * `wrapBatchedHttpHandler`).\n */\nexport function getBatchEnvelope(): BatchEnvelope | undefined {\n return _als.getStore();\n}\n\n/**\n * Resolve the next positional member for a given procedure name and\n * advance the counter. Returns `undefined` when no envelope is in\n * scope, or when the name doesn't appear in the envelope, or when\n * the call's occurrence count exceeds the positional matches\n * available (the failure mode that triggers\n * `otel:trpc_batch_member_mismatch`).\n *\n * **Side effect:** advances `envelope.nameCounters` on success. The\n * mutation is scoped to the per-request envelope and therefore safe\n * under tRPC's batch-dispatch ordering — repeated invocations of\n * the same name within a single request consume positional matches\n * in order.\n *\n * **Performance:** O(1) per invocation. Uses the precomputed\n * `nameToPositions` index built by `buildEnvelope`.\n */\nexport function resolveBatchMember(\n procedureName: string,\n):\n | { envelope: BatchEnvelope; index: number; allNames: string[] }\n | undefined {\n const envelope = _als.getStore();\n if (envelope === undefined) {\n return undefined;\n }\n const positions = envelope.nameToPositions.get(procedureName);\n if (positions === undefined) {\n return undefined;\n }\n const occurrence = envelope.nameCounters.get(procedureName) ?? 0;\n if (occurrence >= positions.length) {\n return undefined;\n }\n envelope.nameCounters.set(procedureName, occurrence + 1);\n return {\n envelope,\n index: positions[occurrence]!,\n allNames: envelope.allNames,\n };\n}\n","/**\n * `wrapBatchedHttpHandler` — opt-in HTTP-handler wrapper that\n * inspects incoming tRPC batch URLs and sets a request-scoped\n * batch envelope so `tracedMiddleware` can label each member span\n * with `glasstrace.trpc.batch.member_index` /\n * `glasstrace.trpc.batch.member_procedures` (SDK-052 / Wave 16B,\n * advances DISC-1534 SDK-side slice).\n *\n * **Opt-in design (v1):** the wrapper is a separate exported helper\n * that apps wire into their tRPC handler explicitly. Apps NOT using\n * the wrapper (or apps NOT using `tracedMiddleware`) see no trace-\n * shape change. A future brief may add auto-detection.\n *\n * **Cross-version compatibility:** v10 and v11 both fire\n * `tracedMiddleware` per procedure during batch dispatch. The\n * envelope is propagated via `AsyncLocalStorage` rather than tRPC's\n * `createContext` shape (which differs between major versions).\n *\n * **Failure modes are non-throwing:** a malformed URL, unparseable\n * batch segment, or unsupported request shape causes the wrapper to\n * fall through to the underlying handler unchanged. The trace shape\n * remains identical to today's behavior (no per-member spans).\n */\nimport { sdkLog } from \"../console-capture.js\";\nimport {\n buildEnvelope,\n withBatchEnvelope,\n type BatchEnvelope,\n type BatchMember,\n} from \"./batch-context.js\";\n\n/**\n * Configuration for {@link wrapBatchedHttpHandler}.\n */\nexport interface WrapBatchedHttpHandlerOptions {\n /**\n * The HTTP path prefix where the tRPC handler is mounted. Defaults\n * to `/api/trpc/` (the most common Next.js tRPC mount path).\n *\n * Apps that mount tRPC at a different path (e.g. `/trpc/`,\n * `/api/v2/trpc/`) MUST pass their actual base path here — the\n * wrapper does NOT auto-detect to avoid both false matches\n * (unrelated routes containing `trpc`) and missed matches (custom\n * mounts). Per DISC-1215, the tRPC base path is configurable on\n * the user side; this option propagates that decision.\n *\n * Callers MAY supply the path with or without a trailing `/` —\n * the wrapper normalizes by appending `/` when missing. The\n * normalized form (with trailing `/`) is what's used for prefix\n * matching, so it can't accidentally match prefix substrings\n * (e.g., `/api/trpc-internal/...` does not match `/api/trpc/`).\n * Apps SHOULD pass the trailing slash explicitly; the runtime\n * normalization is a safety net, not a documented affordance.\n */\n basePath?: string;\n}\n\n/**\n * Whether the wrapper has logged a malformed-URL warning for the\n * current process. Rate-limits to one warning per session so a\n * misconfigured base path doesn't flood logs on a hot request path.\n */\nlet _malformedUrlWarned = false;\n\n/**\n * Wrap a tRPC HTTP handler so batched requests get per-member span\n * attribution via `tracedMiddleware`.\n *\n * The wrapper inspects the incoming request's URL on each call. If\n * the URL matches the batch pattern at the configured base path,\n * the wrapper parses the comma-joined procedure list, builds a\n * {@link BatchEnvelope}, and runs the underlying handler inside\n * the envelope's `AsyncLocalStorage` scope. `tracedMiddleware`\n * (which the user's tRPC procedure chain must already include)\n * reads the envelope and adds the batch attributes to each member\n * span.\n *\n * Non-batched requests (no `batch=` query param OR a URL whose\n * pathname doesn't match the configured `basePath`) pass through\n * to the underlying handler unchanged — the trace shape is\n * identical to today's behavior. **Single-procedure URLs with\n * `batch=1` ARE treated as batches** (a one-member batch is still\n * a batch in tRPC's protocol semantics; the wrapper builds a\n * single-element envelope and `tracedMiddleware` labels the one\n * member span with `member_index: 0`).\n *\n * @example\n * ```ts\n * import { wrapBatchedHttpHandler } from \"@glasstrace/sdk/trpc\";\n * import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\n *\n * const handler = (req: Request) =>\n * fetchRequestHandler({ endpoint: \"/api/trpc\", req, router });\n *\n * export const POST = wrapBatchedHttpHandler(handler);\n * export const GET = wrapBatchedHttpHandler(handler);\n * ```\n */\nexport function wrapBatchedHttpHandler<\n H extends (...args: never[]) => unknown,\n>(handler: H, options?: WrapBatchedHttpHandlerOptions): H {\n const rawBasePath = options?.basePath ?? \"/api/trpc/\";\n // Normalize: ensure trailing slash so prefix matching is exact.\n const basePath = rawBasePath.endsWith(\"/\") ? rawBasePath : `${rawBasePath}/`;\n\n const wrapped = ((...args: Parameters<H>): ReturnType<H> => {\n const url = extractRequestUrl(args[0]);\n if (url === undefined) {\n return handler(...args) as ReturnType<H>;\n }\n const envelope = parseBatchUrl(url, basePath);\n if (envelope === undefined) {\n return handler(...args) as ReturnType<H>;\n }\n return withBatchEnvelope(envelope, () =>\n handler(...args),\n ) as ReturnType<H>;\n }) as H;\n\n return wrapped;\n}\n\n/**\n * Extracts the request URL from various tRPC handler argument\n * shapes. Returns `undefined` for shapes the wrapper doesn't\n * recognize (in which case the wrapper falls through to a no-op\n * pass-through — never throws).\n *\n * Express-mounting awareness: when an Express app mounts the\n * tRPC handler with `app.use('/api/trpc', ...)`, the framework\n * rewrites `req.url` to strip the mount prefix — so a request to\n * `/api/trpc/polls.get?batch=1` arrives at the handler with\n * `req.url === '/polls.get?batch=1'` and `req.originalUrl ===\n * '/api/trpc/polls.get?batch=1'`. The wrapper prefers\n * `originalUrl` (and `baseUrl + url` as a secondary fallback) so\n * the basePath match against `/api/trpc/` succeeds for Express\n * users without forcing them to mount-aware-configure the wrapper.\n *\n * Supported shapes (checked in this preference order):\n * - Express `Request`: `.originalUrl` (mount-aware)\n * - Express `Request`: `.baseUrl + .url` reconstruction\n * - Web `Request` / Next.js `NextRequest`: `.url`\n * - Next.js `NextRequest`: `.nextUrl.href` (fallback)\n * - tRPC's own `{ req, res }` envelope: `req.originalUrl` /\n * `req.url` via the same precedence\n */\nfunction extractRequestUrl(arg: unknown): string | undefined {\n if (typeof arg !== \"object\" || arg === null) {\n return undefined;\n }\n // Try the request object directly first.\n const direct = readUrlFromRequest(arg);\n if (direct !== undefined) {\n return direct;\n }\n // Some tRPC adapters wrap the request in `{ req, res }`.\n const reqWrapper = (arg as { req?: unknown }).req;\n if (\n reqWrapper !== undefined &&\n reqWrapper !== null &&\n typeof reqWrapper === \"object\"\n ) {\n const wrapped = readUrlFromRequest(reqWrapper);\n if (wrapped !== undefined) {\n return wrapped;\n }\n }\n // NextRequest exposes `nextUrl.href` as a final fallback.\n const nextUrl = (arg as { nextUrl?: { href?: unknown } }).nextUrl;\n if (\n nextUrl !== undefined &&\n nextUrl !== null &&\n typeof nextUrl === \"object\"\n ) {\n const href = nextUrl.href;\n if (typeof href === \"string\") {\n return href;\n }\n }\n return undefined;\n}\n\n/**\n * Reads the most-correct URL from a request-shaped object,\n * preferring `originalUrl` (Express mount-aware) over `url`. Falls\n * back to `baseUrl + url` reconstruction when only those are\n * available.\n */\nfunction readUrlFromRequest(req: object): string | undefined {\n // Express request: originalUrl is the un-rewritten request path\n // including any mount prefix that was stripped from `url`.\n const originalUrl = (req as { originalUrl?: unknown }).originalUrl;\n if (typeof originalUrl === \"string\" && originalUrl.length > 0) {\n return originalUrl;\n }\n // Reconstruct from baseUrl + url for Express-mounted handlers\n // that don't expose originalUrl (rare, but defensively handled).\n const baseUrl = (req as { baseUrl?: unknown }).baseUrl;\n const url = (req as { url?: unknown }).url;\n if (\n typeof baseUrl === \"string\" &&\n baseUrl.length > 0 &&\n typeof url === \"string\"\n ) {\n return baseUrl + url;\n }\n // Web Request / Next.js NextRequest / un-mounted Node http:\n // plain `.url` carries the full request path.\n if (typeof url === \"string\") {\n return url;\n }\n return undefined;\n}\n\n/**\n * Parses a request URL into a `BatchEnvelope` if it's a tRPC batch\n * request at the configured base path. Returns `undefined` for\n * non-batch URLs, malformed URLs, or non-matching base paths.\n *\n * Detection rules:\n * 1. URL must match `<basePath><proc-list>?...` — base path is a\n * prefix of the path (after URL parsing strips host/scheme).\n * 2. The URL must carry a `batch=` query parameter (any value;\n * `batch=1` is the canonical form).\n * 3. The procedure list is the URL segment between `<basePath>`\n * and the next `?` or `#`. It MAY contain a single name (a\n * single-procedure batch is still a batch in tRPC's protocol\n * semantics — `batch=1` with one input).\n */\nfunction parseBatchUrl(\n url: string,\n basePath: string,\n): BatchEnvelope | undefined {\n let pathname: string;\n let search: string;\n try {\n // Use a placeholder origin so relative URLs (path-only) parse\n // identically to absolute URLs. The placeholder is discarded —\n // we only consume `pathname` and `search`.\n const parsed = new URL(url, \"http://glasstrace.invalid/\");\n pathname = parsed.pathname;\n search = parsed.search;\n } catch {\n rateLimitWarn(`malformed URL: cannot parse \"${url}\"`);\n return undefined;\n }\n\n // Detect batch via the `batch=` query parameter. Use string\n // search (NOT URLSearchParams) because tRPC's wire format uses\n // `?batch=1` and we want presence detection, not a specific value.\n if (!/[?&]batch=/.test(search)) {\n return undefined;\n }\n\n // Match base path as a prefix.\n if (!pathname.startsWith(basePath)) {\n return undefined;\n }\n\n // Procedure-list segment is everything between basePath and the\n // end of pathname. (search/hash were already split off.)\n const procSegment = pathname.slice(basePath.length);\n if (procSegment.length === 0) {\n return undefined;\n }\n\n // Decode percent-escapes so procedure names with `.` survive (the\n // tRPC client URL-encodes `.` → `%2E` in some configurations);\n // post-decode, we split on `,` to recover the member list.\n let decoded: string;\n try {\n decoded = decodeURIComponent(procSegment);\n } catch {\n rateLimitWarn(`malformed batch URL: ${procSegment}`);\n return undefined;\n }\n\n const names = decoded.split(\",\").filter((s) => s.length > 0);\n if (names.length === 0) {\n return undefined;\n }\n\n const procedures: BatchMember[] = names.map((name, index) => ({\n name,\n index,\n }));\n\n return buildEnvelope(procedures);\n}\n\n/**\n * Emit a warning to stderr at most once per process. Rate-limited\n * to avoid log floods on a hot request path when a wrapper\n * misconfiguration affects every request.\n */\nfunction rateLimitWarn(reason: string): void {\n if (_malformedUrlWarned) {\n return;\n }\n _malformedUrlWarned = true;\n sdkLog(\n \"warn\",\n `[glasstrace] wrapBatchedHttpHandler: ${reason}; falling back to pass-through. Subsequent malformed-URL warnings suppressed for this process.`,\n );\n}\n\n/**\n * Reset the rate-limit guard. Test-only export.\n */\nexport function _resetBatchHandlerForTesting(): void {\n _malformedUrlWarned = false;\n}\n","/**\n * tRPC middleware-chain instrumentation for Glasstrace.\n *\n * Subpath: `@glasstrace/sdk/trpc`\n *\n * This module exposes {@link tracedMiddleware}, a thin wrapper that turns\n * a user-supplied tRPC middleware function into a span-emitting middleware\n * function. Each invocation of the wrapped middleware opens a child span\n * (via {@link https://opentelemetry.io/docs/specs/otel/trace/api/#starting-a-new-active-span | tracer.startActiveSpan})\n * under whatever active OTel context the runtime exposes when the tRPC\n * dispatcher calls the middleware. In a typical Next.js / Node HTTP server\n * deployment that active context is the HTTP server span, so middleware\n * spans land as children of the HTTP span automatically — no manual\n * parent plumbing required.\n *\n * The helper does not import the `@trpc/server` runtime; it consumes the\n * middleware function shape structurally so that:\n *\n * 1. Projects that do not use tRPC pay no runtime cost (the subpath is\n * excluded from the root barrel and tree-shakeable on its own).\n * 2. The same helper works against `@trpc/server@^10.0.0` and\n * `@trpc/server@^11.0.0` without two parallel implementations.\n *\n * The wrapped function preserves the user's call-site type (`T`) so that\n * tRPC's procedure-builder context narrowing (e.g., adding a `session`\n * field across `.use()` chains) continues to flow through.\n *\n * Compatibility with the existing `glasstrace.trpc.procedure` URL-derived\n * attribute (DISC-1215, shipped) is by construction: that attribute is\n * attached to the **parent** HTTP span at exporter time, never to a\n * middleware child span. Middleware spans only carry `trpc.path` and\n * `trpc.type` (forwarded from the middleware options) plus whatever\n * caller-supplied attributes the {@link TracedMiddlewareOptions.attributes}\n * field carries.\n *\n * @module @glasstrace/sdk/trpc\n */\n\nimport {\n trace,\n SpanStatusCode,\n type AttributeValue,\n} from \"@opentelemetry/api\";\nimport { GLASSTRACE_ATTRIBUTE_NAMES } from \"@glasstrace/protocol\";\nimport { emitLifecycleEvent } from \"../lifecycle.js\";\nimport { getBatchEnvelope, resolveBatchMember } from \"./batch-context.js\";\n\nexport {\n wrapBatchedHttpHandler,\n type WrapBatchedHttpHandlerOptions,\n} from \"./batch-handler.js\";\n\n/**\n * Permissive structural bound for a tRPC middleware function. The shape\n * is the intersection of `@trpc/server@^10` and `@trpc/server@^11`'s\n * middleware signature: an async function taking a single options object\n * and returning a thenable middleware result.\n *\n * The `opts` parameter is typed `any` so any user-narrowed middleware\n * (with strongly-typed `ctx` / `input` / `meta`) is assignable, and the\n * return type is `Promise<unknown>` so both v10's\n * `Promise<MiddlewareResult<...>>` and v11's identically-named result\n * shape (with extra fields) are accepted without import-time coupling\n * to either major version.\n *\n * Exported so consumers can reference it for type-inference assertions\n * (e.g., proving a strongly-typed middleware fits the bound) without\n * having to recreate the structural shape. The runtime contract is\n * fixed by the `@trpc/server` versions in the peer-dependency range.\n */\n// The `any` here is load-bearing: a tighter bound would reject either\n// v10 or v11 middleware shapes, or both, because tRPC narrows `ctx`,\n// `input`, and `meta` to user-supplied types via generics. Capturing\n// `T extends MiddlewareFunction` preserves that narrowing through the\n// wrapper's `: T` return type (see the type-inference fixture in\n// tests/unit/sdk/trpc/traced-middleware-types.test.ts).\n//\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type MiddlewareFunction = (opts: any) => Promise<unknown>;\n\n/**\n * Options for {@link tracedMiddleware}.\n *\n * @example\n * ```ts\n * import { tracedMiddleware } from \"@glasstrace/sdk/trpc\";\n *\n * const isAuthed = t.middleware(\n * tracedMiddleware(\n * { name: \"isAuthed\", attributes: { \"auth.required\": true } },\n * async ({ ctx, next }) => {\n * if (!ctx.session) throw new TRPCError({ code: \"UNAUTHORIZED\" });\n * return next({ ctx: { ...ctx, session: ctx.session } });\n * },\n * ),\n * );\n * ```\n */\nexport interface TracedMiddlewareOptions {\n /**\n * Span name. Required. Used as the OTel span name; appears in trace\n * timelines and is the primary identifier surfaced by enrichment when\n * a middleware step short-circuits (e.g., auth failure).\n *\n * Must be a non-empty string. Names should be stable across runs so\n * enrichment can reason about middleware identity (e.g., \"isAuthed\",\n * \"isPro\"); avoid embedding request data in the name.\n */\n name: string;\n /**\n * Optional attributes attached to the span before the wrapped\n * middleware body runs. Forwarded to OTel as-is via\n * `span.setAttributes(...)`. The SDK does not redact, sanitize, or\n * scan values here — callers must avoid placing tokens, credentials,\n * or other sensitive data in `attributes`.\n *\n * Sensitive request/response data is captured through the gated\n * `glasstrace.error.response_body` path (see DISC-1216), not through\n * this surface.\n */\n attributes?: Record<string, AttributeValue>;\n}\n\n/**\n * Module-level OTel tracer name for the tRPC subpath. Resolves through\n * the global `ProxyTracerProvider` so it inherits whatever provider the\n * SDK has detected or registered (Glasstrace's enriching exporter,\n * Sentry's processor in coexistence mode, Datadog's processor, etc.).\n *\n * Re-resolved on every call site rather than cached at module top-level\n * so that the test harness's `trace.setGlobalTracerProvider` can be\n * picked up after this module is imported. (Caching the tracer at module\n * top-level would race against test harness setup and produce stale\n * no-op spans for the very first test.)\n */\nconst TRACER_NAME = \"@glasstrace/sdk/trpc\";\n\n/**\n * Wrap a tRPC middleware function in an OTel span.\n *\n * Each call to the returned middleware:\n *\n * 1. Opens a span named `options.name` under the active OTel context\n * (typically the HTTP server span). The span inherits `traceId` and\n * parent `spanId` automatically — no manual context plumbing.\n * 2. Sets caller-supplied {@link TracedMiddlewareOptions.attributes}\n * plus `trpc.path` and `trpc.type` (forwarded from the middleware\n * options) on the span before calling the wrapped middleware body.\n * 3. Lets the wrapped middleware run with the new span as the active\n * span (so any `tracer.startActiveSpan` calls inside the body open\n * grandchild spans under the middleware span).\n * 4. On a thrown error: records the exception via `span.recordException`\n * and sets `span.status` to `ERROR` with the error's message; rethrows.\n * 5. On a returned `{ ok: false, error }` middleware result (tRPC's\n * short-circuit shape): sets `span.status` to `ERROR` without\n * `recordException` (no `Error` object to record).\n * 6. On a successful `{ ok: true, ... }` result: leaves the span status\n * as `UNSET` (per OTel instrumentation-library guidance — explicit\n * `OK` here would shadow downstream consumers attempting their own\n * status transitions).\n * 7. Always ends the span (`span.end()`), even on `throw` or `return`.\n *\n * Type-inference: the returned function preserves the input function's\n * type `T`, so tRPC's procedure-builder context narrowing flows through\n * unchanged. See `sdk-trpc.md` §3.3 for the recommended call pattern.\n *\n * @param options - Span name and optional pre-start attributes.\n * @param middleware - The user's tRPC middleware function. Must be\n * structurally compatible with `@trpc/server@^10` or `@trpc/server@^11`.\n * @returns The wrapped middleware function with the same call signature\n * and return type as `middleware`.\n *\n * @example\n * ```ts\n * // trpc.ts — user's project\n * import { initTRPC, TRPCError } from \"@trpc/server\";\n * import { tracedMiddleware } from \"@glasstrace/sdk/trpc\";\n *\n * interface MyContext { session?: { userId: string }; tier?: string }\n * const t = initTRPC.context<MyContext>().create();\n *\n * const isAuthed = t.middleware(\n * tracedMiddleware({ name: \"isAuthed\" }, async ({ ctx, next }) => {\n * if (!ctx.session) throw new TRPCError({ code: \"UNAUTHORIZED\" });\n * return next({ ctx: { ...ctx, session: ctx.session } });\n * }),\n * );\n *\n * const isPro = t.middleware(\n * tracedMiddleware({ name: \"isPro\" }, async ({ ctx, next }) => {\n * if (ctx.tier !== \"pro\") throw new TRPCError({ code: \"FORBIDDEN\" });\n * return next();\n * }),\n * );\n *\n * export const proProcedure = t.procedure.use(isAuthed).use(isPro);\n * ```\n */\nexport function tracedMiddleware<T extends MiddlewareFunction>(\n options: TracedMiddlewareOptions,\n middleware: T,\n): T {\n // Validate the span name eagerly so a mis-typed call site fails at\n // wrapper-construction time (typically at module load) rather than at\n // first request, when the failure is harder to diagnose. The structural\n // bound only enforces shape, not value-level invariants.\n if (typeof options.name !== \"string\" || options.name.length === 0) {\n throw new TypeError(\n \"tracedMiddleware: options.name must be a non-empty string\",\n );\n }\n\n // The wrapped function. Capture `options` and `middleware` lexically;\n // do not read them from `this` since tRPC invokes middleware as a\n // plain function (not a method).\n const wrapped = async (mwOpts: Parameters<T>[0]): Promise<unknown> => {\n const tracer = trace.getTracer(TRACER_NAME);\n return tracer.startActiveSpan(options.name, async (span) => {\n try {\n // Set caller-supplied attributes first so they appear on the\n // span before any internal attribute we add below. Caller-supplied\n // attributes are forwarded as-is — no redaction or scanning (see\n // TracedMiddlewareOptions.attributes JSDoc).\n if (options.attributes) {\n span.setAttributes(options.attributes);\n }\n // Forward the tRPC-provided `path` and `type` so consumers (the\n // enriching exporter, third-party UIs) can correlate the\n // middleware span back to its procedure without joining against\n // the parent HTTP span. Both fields are documented as Tier 2\n // heuristics in `sdk-trpc.md` §4.\n let procedurePath: string | undefined;\n if (mwOpts && typeof mwOpts === \"object\") {\n const path = (mwOpts as { path?: unknown }).path;\n if (typeof path === \"string\") {\n span.setAttribute(\"trpc.path\", path);\n procedurePath = path;\n }\n const type = (mwOpts as { type?: unknown }).type;\n if (\n type === \"query\" ||\n type === \"mutation\" ||\n type === \"subscription\"\n ) {\n span.setAttribute(\"trpc.type\", type);\n }\n }\n\n // SDK-052 / Wave 16B — when this invocation runs under a\n // `wrapBatchedHttpHandler` envelope, label the span with its\n // positional batch-member index and the full member-procedures\n // list. Positional matching disambiguates batches that include\n // the same procedure name multiple times. When no envelope is\n // present (the non-batched path or apps not using the\n // wrapper), this branch is a no-op and the span shape is\n // unchanged from today.\n if (procedurePath !== undefined) {\n const resolved = resolveBatchMember(procedurePath);\n if (resolved !== undefined) {\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.TRPC_BATCH_MEMBER_INDEX,\n resolved.index,\n );\n span.setAttribute(\n GLASSTRACE_ATTRIBUTE_NAMES.TRPC_BATCH_MEMBER_PROCEDURES,\n resolved.allNames,\n );\n } else {\n // The envelope might exist but the procedure name doesn't\n // map — emit the mismatch event for observability and\n // proceed without batch attributes (trace shape preserved).\n // We only emit the event when we can confirm an envelope\n // exists; otherwise this is the non-batched path (no\n // envelope at all) and silence is correct.\n const envelope = getBatchEnvelope();\n if (envelope !== undefined) {\n emitLifecycleEvent(\"otel:trpc_batch_member_mismatch\", {\n procedureName: procedurePath,\n // Use the envelope's precomputed allNames cache\n // rather than rebuilding `procedures.map(...)` on\n // every mismatch — the rebuild was the residual\n // O(N) waste from the original implementation that\n // the precomputed-cache fix in batch-context.ts is\n // designed to eliminate.\n batchMembers: envelope.allNames,\n spanId: span.spanContext().spanId,\n });\n }\n }\n }\n\n const result = await middleware(mwOpts);\n\n // tRPC's middleware result is a discriminated union:\n // { ok: true, ... } — successful pass-through\n // { ok: false, error, ... } — middleware short-circuited with\n // an explicit error envelope\n //\n // The error envelope is the path users hit when they call\n // `next()` and the next link returns ok:false; from the wrapper's\n // perspective the middleware did not throw, but the request did\n // fail. Mark the span ERROR so the exporter and downstream UIs\n // surface the failure, but do not call `recordException` —\n // there is no `Error` object to record.\n if (\n result !== null &&\n typeof result === \"object\" &&\n (result as { ok?: unknown }).ok === false\n ) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n\n return result;\n } catch (error) {\n // Thrown error path. `recordException` produces an OTel\n // exception event with the error name, message, and stack;\n // `setStatus({ code: ERROR, message })` lets standard OTel UIs\n // display the error message inline with the span.\n //\n // OpenTelemetry's `Span.recordException` accepts only\n // `Exception = string | Error` — a non-Error, non-string\n // throwable (e.g. a plain object, number, or symbol thrown by\n // user code via valid JavaScript) can cause `recordException`\n // to throw, which would otherwise leave the span status UNSET\n // even though the request failed. Normalize the throwable\n // first, then guard `recordException` and `setStatus` in\n // independent try/catch blocks so a failure inside one cannot\n // block the other from running. The user's original `error`\n // value is preserved verbatim for the `throw error` re-raise\n // below — wrapping is purely a span-side normalization.\n const normalized: Error | string =\n error instanceof Error\n ? error\n : typeof error === \"string\"\n ? error\n : new Error(String(error));\n const statusMessage =\n normalized instanceof Error ? normalized.message : normalized;\n try {\n span.recordException(normalized);\n } catch {\n // Swallow — instrumentation must never replace the user's\n // error with its own. The span is still ended in `finally`.\n }\n try {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: statusMessage,\n });\n } catch {\n // Swallow — see comment above. Independent from the\n // recordException try/catch so a failing recordException\n // does not prevent the ERROR status from being recorded.\n }\n throw error;\n } finally {\n // Always end the span. `try/finally` covers both the success\n // and throw paths; the `return result` above happens inside the\n // try, so finally still runs before the value is yielded.\n // Defensively suppress any throw from `span.end()` so a\n // misbehaving OTel impl cannot replace the wrapped middleware's\n // return value (or thrown error) with an unrelated one.\n try {\n span.end();\n } catch {\n // Span lifecycle errors are always non-fatal at this layer.\n }\n }\n });\n };\n\n // The `T` cast preserves the user's function type at the call site\n // even though our wrapper widens parameters to `Parameters<T>[0]` and\n // return to `Promise<unknown>` internally. This is the load-bearing\n // type-inference contract documented in `sdk-trpc.md` §3.3 and\n // verified by `tests/unit/sdk/trpc/traced-middleware-types.test.ts`.\n return wrapped as T;\n}\n"],"mappings":"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","names":[]}
|