within-sdk 1.0.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/LICENSE +1 -0
- package/README.md +77 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +243 -0
- package/dist/middleware.d.ts +40 -0
- package/dist/middleware.js +562 -0
- package/dist/modules/compatibility.d.ts +18 -0
- package/dist/modules/compatibility.js +99 -0
- package/dist/modules/constants.d.ts +12 -0
- package/dist/modules/constants.js +12 -0
- package/dist/modules/context-parameters.d.ts +13 -0
- package/dist/modules/context-parameters.js +74 -0
- package/dist/modules/diagnostics.d.ts +7 -0
- package/dist/modules/diagnostics.js +12 -0
- package/dist/modules/eventQueue.d.ts +32 -0
- package/dist/modules/eventQueue.js +234 -0
- package/dist/modules/exceptions.d.ts +13 -0
- package/dist/modules/exceptions.js +730 -0
- package/dist/modules/exporters/datadog.d.ts +17 -0
- package/dist/modules/exporters/datadog.js +166 -0
- package/dist/modules/exporters/otlp.d.ts +13 -0
- package/dist/modules/exporters/otlp.js +140 -0
- package/dist/modules/exporters/posthog.d.ts +32 -0
- package/dist/modules/exporters/posthog.js +272 -0
- package/dist/modules/exporters/sentry.d.ts +27 -0
- package/dist/modules/exporters/sentry.js +386 -0
- package/dist/modules/exporters/trace-context.d.ts +8 -0
- package/dist/modules/exporters/trace-context.js +27 -0
- package/dist/modules/index.d.ts +8 -0
- package/dist/modules/index.js +8 -0
- package/dist/modules/internal.d.ts +33 -0
- package/dist/modules/internal.js +195 -0
- package/dist/modules/logging.d.ts +2 -0
- package/dist/modules/logging.js +74 -0
- package/dist/modules/mcp-sdk-compat.d.ts +32 -0
- package/dist/modules/mcp-sdk-compat.js +111 -0
- package/dist/modules/privacy.d.ts +15 -0
- package/dist/modules/privacy.js +179 -0
- package/dist/modules/redaction.d.ts +11 -0
- package/dist/modules/redaction.js +81 -0
- package/dist/modules/sanitization.d.ts +9 -0
- package/dist/modules/sanitization.js +111 -0
- package/dist/modules/session.d.ts +22 -0
- package/dist/modules/session.js +109 -0
- package/dist/modules/telemetry.d.ts +8 -0
- package/dist/modules/telemetry.js +53 -0
- package/dist/modules/tools.d.ts +25 -0
- package/dist/modules/tools.js +119 -0
- package/dist/modules/tracing.d.ts +4 -0
- package/dist/modules/tracing.js +263 -0
- package/dist/modules/tracingV2.d.ts +2 -0
- package/dist/modules/tracingV2.js +300 -0
- package/dist/modules/truncation.d.ts +24 -0
- package/dist/modules/truncation.js +303 -0
- package/dist/modules/validation.d.ts +6 -0
- package/dist/modules/validation.js +51 -0
- package/dist/network.d.ts +83 -0
- package/dist/network.js +411 -0
- package/dist/proxy.d.ts +31 -0
- package/dist/proxy.js +158 -0
- package/dist/thirdparty/ksuid/base-convert-int-array.js +49 -0
- package/dist/thirdparty/ksuid/base62.js +24 -0
- package/dist/thirdparty/ksuid/index.d.ts +30 -0
- package/dist/thirdparty/ksuid/index.js +201 -0
- package/dist/types.d.ts +197 -0
- package/dist/types.js +5 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.js +139 -0
- package/package.json +43 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
// Lazy-loaded fs module for context_line extraction (Node.js only)
|
|
3
|
+
// Edge environments don't have filesystem access
|
|
4
|
+
let fsModule = null;
|
|
5
|
+
let fsInitAttempted = false;
|
|
6
|
+
function getFsSync() {
|
|
7
|
+
if (!fsInitAttempted) {
|
|
8
|
+
fsInitAttempted = true;
|
|
9
|
+
try {
|
|
10
|
+
// Use createRequire for ESM compatibility
|
|
11
|
+
// Works in Node.js ESM/CJS, fails gracefully in Workers/edge environments
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
fsModule = require("fs");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
fsModule = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return fsModule;
|
|
20
|
+
}
|
|
21
|
+
// Maximum number of exceptions to capture in a cause chain
|
|
22
|
+
const MAX_EXCEPTION_CHAIN_DEPTH = 10;
|
|
23
|
+
// Maximum number of stack frames to capture per exception
|
|
24
|
+
const MAX_STACK_FRAMES = 50;
|
|
25
|
+
/**
|
|
26
|
+
* Captures detailed exception information including stack traces and cause chains.
|
|
27
|
+
*
|
|
28
|
+
* This function extracts error metadata (type, message, stack trace) and recursively
|
|
29
|
+
* unwraps Error.cause chains. It parses V8 stack traces into structured frames and
|
|
30
|
+
* detects whether each frame is user code (in_app: true) or library code (in_app: false).
|
|
31
|
+
*
|
|
32
|
+
* @param error - The error to capture (can be Error, string, object, or any value)
|
|
33
|
+
* @param contextStack - Optional Error object to use for stack context (for validation errors)
|
|
34
|
+
* @returns ErrorData object with structured error information
|
|
35
|
+
*/
|
|
36
|
+
export function captureException(error, contextStack) {
|
|
37
|
+
// Handle CallToolResult objects (SDK 1.21.0+ converts errors to these)
|
|
38
|
+
if (isCallToolResult(error)) {
|
|
39
|
+
return captureCallToolResultError(error, contextStack);
|
|
40
|
+
}
|
|
41
|
+
// Handle non-Error objects
|
|
42
|
+
if (!(error instanceof Error)) {
|
|
43
|
+
return {
|
|
44
|
+
message: stringifyNonError(error),
|
|
45
|
+
type: undefined,
|
|
46
|
+
platform: "javascript",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const errorData = {
|
|
50
|
+
message: error.message || "",
|
|
51
|
+
type: error.name || error.constructor?.name || undefined,
|
|
52
|
+
platform: "javascript",
|
|
53
|
+
};
|
|
54
|
+
// Capture stack trace if available
|
|
55
|
+
if (error.stack) {
|
|
56
|
+
errorData.stack = error.stack;
|
|
57
|
+
errorData.frames = parseV8StackTrace(error.stack);
|
|
58
|
+
}
|
|
59
|
+
// Unwrap Error.cause chain
|
|
60
|
+
const chainedErrors = unwrapErrorCauses(error);
|
|
61
|
+
if (chainedErrors.length > 0) {
|
|
62
|
+
errorData.chained_errors = chainedErrors;
|
|
63
|
+
}
|
|
64
|
+
return errorData;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parses V8 stack trace string into structured StackFrame array.
|
|
68
|
+
*
|
|
69
|
+
* V8 stack traces have the format:
|
|
70
|
+
* Error: message
|
|
71
|
+
* at functionName (filename:line:col)
|
|
72
|
+
* at Object.method (filename:line:col)
|
|
73
|
+
* ...
|
|
74
|
+
*
|
|
75
|
+
* This function handles various V8 format variations including:
|
|
76
|
+
* - Regular functions: "at functionName (file:10:5)"
|
|
77
|
+
* - Anonymous functions: "at file:10:5"
|
|
78
|
+
* - Async functions: "at async functionName (file:10:5)"
|
|
79
|
+
* - Object methods: "at Object.method (file:10:5)"
|
|
80
|
+
* - Native code: "at Array.map (native)"
|
|
81
|
+
*
|
|
82
|
+
* @param stackTrace - Raw V8 stack trace string from Error.stack
|
|
83
|
+
* @returns Array of parsed StackFrame objects (limited to MAX_STACK_FRAMES)
|
|
84
|
+
*/
|
|
85
|
+
function parseV8StackTrace(stackTrace) {
|
|
86
|
+
const frames = [];
|
|
87
|
+
const lines = stackTrace.split("\n");
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
// Skip the first line (error message) and empty lines
|
|
90
|
+
if (!line.trim().startsWith("at ")) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const frame = parseV8StackFrame(line.trim());
|
|
94
|
+
if (frame) {
|
|
95
|
+
addContextToFrame(frame);
|
|
96
|
+
frames.push(frame);
|
|
97
|
+
}
|
|
98
|
+
// Limit number of frames
|
|
99
|
+
if (frames.length >= MAX_STACK_FRAMES) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return frames;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Adds context_line to a stack frame by reading the source file.
|
|
107
|
+
*
|
|
108
|
+
* This function extracts the line of code where the error occurred by:
|
|
109
|
+
* 1. Reading the source file using abs_path
|
|
110
|
+
* 2. Extracting the line at the specified line number
|
|
111
|
+
* 3. Setting the context_line field on the frame
|
|
112
|
+
*
|
|
113
|
+
* Only extracts context for user code (in_app: true)
|
|
114
|
+
* If the file cannot be read or the line number is invalid, context_line remains undefined.
|
|
115
|
+
*
|
|
116
|
+
* @param frame - The StackFrame to add context to (modified in place)
|
|
117
|
+
* @returns The modified StackFrame
|
|
118
|
+
*/
|
|
119
|
+
function addContextToFrame(frame) {
|
|
120
|
+
if (!frame.in_app || !frame.abs_path || !frame.lineno) {
|
|
121
|
+
return frame;
|
|
122
|
+
}
|
|
123
|
+
// Get fs module lazily - returns null in edge environments
|
|
124
|
+
const fs = getFsSync();
|
|
125
|
+
if (!fs) {
|
|
126
|
+
return frame; // File reading not available in this environment
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const source = fs.readFileSync(frame.abs_path, "utf8");
|
|
130
|
+
const lines = source.split("\n");
|
|
131
|
+
const lineIndex = frame.lineno - 1; // Convert to 0-based index
|
|
132
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
133
|
+
frame.context_line = lines[lineIndex];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// File not found or not readable - silently skip
|
|
138
|
+
}
|
|
139
|
+
return frame;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parses a location string from a V8 stack frame.
|
|
143
|
+
*
|
|
144
|
+
* Handles different location formats:
|
|
145
|
+
* - "fileName:lineNumber:columnNumber" - normal file location
|
|
146
|
+
* - "eval at functionName (location)" - eval'd code (recursively unwraps)
|
|
147
|
+
* - "native" - V8 internal code
|
|
148
|
+
* - "unknown location" - location unavailable
|
|
149
|
+
*
|
|
150
|
+
* @param location - Location string from stack frame
|
|
151
|
+
* @returns Object with filename, abs_path, and optional lineno/colno, or null if unparseable
|
|
152
|
+
*/
|
|
153
|
+
function parseLocation(location) {
|
|
154
|
+
// Handle special cases first
|
|
155
|
+
if (location === "native") {
|
|
156
|
+
return { filename: "native", abs_path: "native" };
|
|
157
|
+
}
|
|
158
|
+
if (location === "unknown location") {
|
|
159
|
+
return { filename: "<unknown>", abs_path: "<unknown>" };
|
|
160
|
+
}
|
|
161
|
+
// Handle eval locations
|
|
162
|
+
if (location.startsWith("eval at ")) {
|
|
163
|
+
return parseEvalOrigin(location);
|
|
164
|
+
}
|
|
165
|
+
// Handle normal location format: fileName:lineNumber:columnNumber
|
|
166
|
+
const match = location.match(/^(.+):(\d+):(\d+)$/);
|
|
167
|
+
if (match) {
|
|
168
|
+
const [, filename, lineStr, colStr] = match;
|
|
169
|
+
return {
|
|
170
|
+
filename: makeRelativePath(filename),
|
|
171
|
+
abs_path: filename,
|
|
172
|
+
lineno: parseInt(lineStr, 10),
|
|
173
|
+
colno: parseInt(colStr, 10),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Recursively unwraps eval location chains to extract the underlying file location.
|
|
180
|
+
*
|
|
181
|
+
* Eval locations have the format: "eval at functionName (location), <anonymous>:line:col"
|
|
182
|
+
* where location can be another eval or a file location.
|
|
183
|
+
*
|
|
184
|
+
* V8 formats:
|
|
185
|
+
* - "eval at Bar.z (myscript.js:10:3)" → extract myscript.js:10:3
|
|
186
|
+
* - "eval at Foo (eval at Bar (file.js:10:3)), <anonymous>:5:2" → extract file.js:10:3
|
|
187
|
+
*
|
|
188
|
+
* @param evalLocation - Eval location string starting with "eval at "
|
|
189
|
+
* @returns Object with extracted file location, or null if unparseable
|
|
190
|
+
*/
|
|
191
|
+
function parseEvalOrigin(evalLocation) {
|
|
192
|
+
// V8 format: "eval at functionName (parentLocation), <anonymous>:line:col"
|
|
193
|
+
// or simpler: "eval at functionName (parentLocation)"
|
|
194
|
+
//
|
|
195
|
+
// Strategy: Find balanced parentheses to extract the parent location,
|
|
196
|
+
// then recursively parse it to find the actual file.
|
|
197
|
+
// First, check if there's a comma separating eval chain from eval code location
|
|
198
|
+
// Format: "eval at FUNC (...), <anonymous>:line:col"
|
|
199
|
+
// We want to extract just the "eval at FUNC (...)" part
|
|
200
|
+
let evalChainPart = evalLocation;
|
|
201
|
+
const commaIndex = findCommaAfterBalancedParens(evalLocation);
|
|
202
|
+
if (commaIndex !== -1) {
|
|
203
|
+
evalChainPart = evalLocation.substring(0, commaIndex);
|
|
204
|
+
}
|
|
205
|
+
// Match "eval at <anything> (<innerLocation>)"
|
|
206
|
+
const match = evalChainPart.match(/^eval at (.+?) \((.+)\)$/);
|
|
207
|
+
if (!match) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const innerLocation = match[2];
|
|
211
|
+
// Recursively parse the inner location
|
|
212
|
+
if (innerLocation.startsWith("eval at ")) {
|
|
213
|
+
return parseEvalOrigin(innerLocation);
|
|
214
|
+
}
|
|
215
|
+
// Base case: parse as normal location
|
|
216
|
+
const locationMatch = innerLocation.match(/^(.+):(\d+):(\d+)$/);
|
|
217
|
+
if (locationMatch) {
|
|
218
|
+
const [, filename, lineStr, colStr] = locationMatch;
|
|
219
|
+
return {
|
|
220
|
+
filename: makeRelativePath(filename),
|
|
221
|
+
abs_path: filename,
|
|
222
|
+
lineno: parseInt(lineStr, 10),
|
|
223
|
+
colno: parseInt(colStr, 10),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Finds the index of the comma that appears after balanced parentheses.
|
|
230
|
+
*
|
|
231
|
+
* For "eval at f (eval at g (x)), <anonymous>:1:2", returns the index of the comma
|
|
232
|
+
* after the closing ")" and before "<anonymous>".
|
|
233
|
+
*
|
|
234
|
+
* @param str - String to search
|
|
235
|
+
* @returns Index of comma, or -1 if not found
|
|
236
|
+
*/
|
|
237
|
+
function findCommaAfterBalancedParens(str) {
|
|
238
|
+
let depth = 0;
|
|
239
|
+
let foundOpenParen = false;
|
|
240
|
+
for (let i = 0; i < str.length; i++) {
|
|
241
|
+
if (str[i] === "(") {
|
|
242
|
+
depth++;
|
|
243
|
+
foundOpenParen = true;
|
|
244
|
+
}
|
|
245
|
+
else if (str[i] === ")") {
|
|
246
|
+
depth--;
|
|
247
|
+
if (depth === 0 && foundOpenParen) {
|
|
248
|
+
// Found the closing paren of the eval at (...) part
|
|
249
|
+
for (let j = i + 1; j < str.length; j++) {
|
|
250
|
+
if (str[j] === ",") {
|
|
251
|
+
return j;
|
|
252
|
+
}
|
|
253
|
+
else if (str[j] !== " ") {
|
|
254
|
+
// Non-comma, non-space character found, no comma separator
|
|
255
|
+
return -1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return -1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Parses a single V8 stack frame line into a StackFrame object.
|
|
266
|
+
*
|
|
267
|
+
* Handles multiple V8 stack frame formats:
|
|
268
|
+
* - "at functionName (filename:line:col)"
|
|
269
|
+
* - "at filename:line:col" (top-level code)
|
|
270
|
+
* - "at async functionName (filename:line:col)"
|
|
271
|
+
* - "at Object.method (filename:line:col)"
|
|
272
|
+
* - "at Module._compile (node:internal/...)" (internal modules)
|
|
273
|
+
* - "at functionName (eval at ...)" (eval'd code)
|
|
274
|
+
* - "at functionName (native)" (native code)
|
|
275
|
+
*
|
|
276
|
+
* @param line - Single line from V8 stack trace (trimmed, starts with "at ")
|
|
277
|
+
* @returns Parsed StackFrame or null if line cannot be parsed
|
|
278
|
+
*/
|
|
279
|
+
function parseV8StackFrame(line) {
|
|
280
|
+
// Remove "at " prefix
|
|
281
|
+
const withoutAt = line.substring(3);
|
|
282
|
+
// Try to extract function name and location
|
|
283
|
+
// Format 1: "functionName (location)"
|
|
284
|
+
// Location can be: filename:line:col, eval at ..., native, unknown location
|
|
285
|
+
const matchWithFunction = withoutAt.match(/^(.+?)\s+\((.+)\)$/);
|
|
286
|
+
if (matchWithFunction) {
|
|
287
|
+
const [, functionName, location] = matchWithFunction;
|
|
288
|
+
const parsedLocation = parseLocation(location);
|
|
289
|
+
if (parsedLocation) {
|
|
290
|
+
return {
|
|
291
|
+
function: functionName.trim(),
|
|
292
|
+
filename: parsedLocation.filename,
|
|
293
|
+
abs_path: parsedLocation.abs_path,
|
|
294
|
+
lineno: parsedLocation.lineno,
|
|
295
|
+
colno: parsedLocation.colno,
|
|
296
|
+
in_app: isInApp(parsedLocation.abs_path),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Format 2: "location" (no function name, top-level code)
|
|
301
|
+
// Try to parse as location directly
|
|
302
|
+
const parsedLocation = parseLocation(withoutAt);
|
|
303
|
+
if (parsedLocation) {
|
|
304
|
+
return {
|
|
305
|
+
function: "<anonymous>",
|
|
306
|
+
filename: parsedLocation.filename,
|
|
307
|
+
abs_path: parsedLocation.abs_path,
|
|
308
|
+
lineno: parsedLocation.lineno,
|
|
309
|
+
colno: parsedLocation.colno,
|
|
310
|
+
in_app: isInApp(parsedLocation.abs_path),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// Format 3: Unparseable
|
|
314
|
+
// Fallback for formats we don't recognize
|
|
315
|
+
return {
|
|
316
|
+
function: withoutAt,
|
|
317
|
+
filename: "<unknown>",
|
|
318
|
+
in_app: false,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Determines if a file path represents user code (in_app: true) or library code (in_app: false).
|
|
323
|
+
*
|
|
324
|
+
* Library code is identified by:
|
|
325
|
+
* - Paths containing "/node_modules/"
|
|
326
|
+
* - Node.js internal modules (e.g., "node:internal/...")
|
|
327
|
+
* - Native code
|
|
328
|
+
*
|
|
329
|
+
* @param filename - File path from stack frame
|
|
330
|
+
* @returns true if user code, false if library code
|
|
331
|
+
*/
|
|
332
|
+
function isInApp(filename) {
|
|
333
|
+
// Exclude node_modules
|
|
334
|
+
if (filename.includes("/node_modules/") ||
|
|
335
|
+
filename.includes("\\node_modules\\")) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
// Exclude Node.js internal modules (node:internal/...)
|
|
339
|
+
if (filename.startsWith("node:")) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
// Exclude native code
|
|
343
|
+
if (filename === "native" || filename === "<unknown>") {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Normalizes URL schemes to regular file paths.
|
|
350
|
+
*
|
|
351
|
+
* Handles file:// URLs commonly seen in ESM modules and local testing:
|
|
352
|
+
* - "file:///Users/john/project/src/index.ts" → "/Users/john/project/src/index.ts"
|
|
353
|
+
* - "file:///C:/projects/app/src/index.ts" → "C:/projects/app/src/index.ts"
|
|
354
|
+
*
|
|
355
|
+
* @param filename - File path that may be a file:// URL
|
|
356
|
+
* @returns Clean file path without URL scheme
|
|
357
|
+
*/
|
|
358
|
+
function normalizeUrl(filename) {
|
|
359
|
+
// Handle file:// URLs (common in ESM modules and local testing)
|
|
360
|
+
if (filename.startsWith("file://")) {
|
|
361
|
+
let result = filename.substring(7); // Remove "file://"
|
|
362
|
+
// Ensure Unix paths start with /
|
|
363
|
+
if (!result.startsWith("/") && !result.match(/^[A-Za-z]:/)) {
|
|
364
|
+
result = "/" + result;
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
return filename;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Normalizes Node.js internal module paths for consistent error grouping.
|
|
372
|
+
*
|
|
373
|
+
* Examples:
|
|
374
|
+
* - "node:internal/modules/cjs/loader" → "node:internal"
|
|
375
|
+
* - "node:fs/promises" → "node:fs"
|
|
376
|
+
* - "node:fs" → "node:fs" (unchanged)
|
|
377
|
+
*
|
|
378
|
+
* @param filename - File path that may be a Node.js internal module
|
|
379
|
+
* @returns Simplified module path or original filename
|
|
380
|
+
*/
|
|
381
|
+
function normalizeNodeInternals(filename) {
|
|
382
|
+
if (filename.startsWith("node:internal")) {
|
|
383
|
+
return "node:internal";
|
|
384
|
+
}
|
|
385
|
+
if (filename.startsWith("node:")) {
|
|
386
|
+
// Extract just the module name: node:fs/promises → node:fs
|
|
387
|
+
const parts = filename.split("/");
|
|
388
|
+
return parts[0];
|
|
389
|
+
}
|
|
390
|
+
return filename;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Strips user-specific and system path prefixes.
|
|
394
|
+
*
|
|
395
|
+
* Removes prefixes like:
|
|
396
|
+
* - /Users/username/ → ~/
|
|
397
|
+
* - /home/username/ → ~/
|
|
398
|
+
* - C:\Users\username\ → ~\
|
|
399
|
+
* - C:/Users/username/ → ~/ (mixed separators)
|
|
400
|
+
*
|
|
401
|
+
* @param path - File path to normalize
|
|
402
|
+
* @returns Path with system prefixes removed
|
|
403
|
+
*/
|
|
404
|
+
function stripSystemPrefixes(path) {
|
|
405
|
+
// Unix/macOS: /Users/username/
|
|
406
|
+
path = path.replace(/^\/Users\/[^/]+\//, "~/");
|
|
407
|
+
// Linux: /home/username/
|
|
408
|
+
path = path.replace(/^\/home\/[^/]+\//, "~/");
|
|
409
|
+
// Windows: C:\Users\username\ or C:/Users/username/ (with any separator)
|
|
410
|
+
path = path.replace(/^[A-Za-z]:[\\\/]Users[\\\/][^\\\/]+[\\\/]/, "~/");
|
|
411
|
+
return path;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Normalizes node_modules paths to be consistent across deployments.
|
|
415
|
+
*
|
|
416
|
+
* Extracts only the package-relative portion of the path:
|
|
417
|
+
* - /Users/john/project/node_modules/express/lib/router.js → node_modules/express/lib/router.js
|
|
418
|
+
* - /app/node_modules/@scope/pkg/index.js → node_modules/@scope/pkg/index.js
|
|
419
|
+
*
|
|
420
|
+
* @param path - File path that may contain node_modules
|
|
421
|
+
* @returns Normalized node_modules path or original path
|
|
422
|
+
*/
|
|
423
|
+
function normalizeNodeModules(path) {
|
|
424
|
+
// Find the last occurrence of /node_modules/ or \node_modules\
|
|
425
|
+
const unixIndex = path.lastIndexOf("/node_modules/");
|
|
426
|
+
const winIndex = path.lastIndexOf("\\node_modules\\");
|
|
427
|
+
if (unixIndex !== -1) {
|
|
428
|
+
return path.substring(unixIndex + 1); // +1 to exclude leading slash
|
|
429
|
+
}
|
|
430
|
+
if (winIndex !== -1) {
|
|
431
|
+
return path.substring(winIndex + 1).replace(/\\/g, "/");
|
|
432
|
+
}
|
|
433
|
+
return path;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Strips common deployment-specific path prefixes.
|
|
437
|
+
*
|
|
438
|
+
* Removes prefixes like:
|
|
439
|
+
* - /var/www/app/ → ""
|
|
440
|
+
* - /app/ → ""
|
|
441
|
+
* - /opt/project/ → ""
|
|
442
|
+
* - /var/task/ → "" (AWS Lambda)
|
|
443
|
+
* - /usr/src/app/ → "" (Docker)
|
|
444
|
+
*
|
|
445
|
+
* @param path - File path to normalize
|
|
446
|
+
* @returns Path with deployment prefixes removed
|
|
447
|
+
*/
|
|
448
|
+
function stripDeploymentPaths(path) {
|
|
449
|
+
// Common deployment paths
|
|
450
|
+
const deploymentPrefixes = [
|
|
451
|
+
/^\/var\/www\/[^/]+\//, // Apache/nginx: /var/www/myapp/
|
|
452
|
+
/^\/var\/task\//, // AWS Lambda: /var/task/
|
|
453
|
+
/^\/usr\/src\/app\//, // Docker: /usr/src/app/
|
|
454
|
+
/^\/app\//, // Heroku, Docker, generic: /app/
|
|
455
|
+
/^\/opt\/[^/]+\//, // Optional software: /opt/myapp/
|
|
456
|
+
/^\/srv\/[^/]+\//, // Service data: /srv/myapp/
|
|
457
|
+
];
|
|
458
|
+
for (const prefix of deploymentPrefixes) {
|
|
459
|
+
path = path.replace(prefix, "");
|
|
460
|
+
}
|
|
461
|
+
return path;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Finds project-relative path using common project boundary markers.
|
|
465
|
+
*
|
|
466
|
+
* Looks for markers like /src/, /lib/, /dist/, /build/ and extracts the path
|
|
467
|
+
* from that marker onwards:
|
|
468
|
+
* - /Users/john/project/src/components/Button.tsx → src/components/Button.tsx
|
|
469
|
+
* - /app/dist/index.js → dist/index.js
|
|
470
|
+
*
|
|
471
|
+
* Priority order: looks for primary markers first (src, lib, dist, build),
|
|
472
|
+
* then secondary markers. Uses the highest-priority marker found.
|
|
473
|
+
*
|
|
474
|
+
* @param path - File path to search for project boundaries
|
|
475
|
+
* @returns Project-relative path or original path if no marker found
|
|
476
|
+
*/
|
|
477
|
+
function findProjectPath(path) {
|
|
478
|
+
// Project boundary markers in priority order
|
|
479
|
+
// Primary markers (most likely to be project root)
|
|
480
|
+
const primaryMarkers = ["/src/", "/lib/", "/dist/", "/build/"];
|
|
481
|
+
// Secondary markers (could be subdirectories)
|
|
482
|
+
const secondaryMarkers = [
|
|
483
|
+
"/app/",
|
|
484
|
+
"/components/",
|
|
485
|
+
"/pages/",
|
|
486
|
+
"/api/",
|
|
487
|
+
"/utils/",
|
|
488
|
+
"/services/",
|
|
489
|
+
"/modules/",
|
|
490
|
+
];
|
|
491
|
+
// Check primary markers first
|
|
492
|
+
for (const marker of primaryMarkers) {
|
|
493
|
+
const index = path.lastIndexOf(marker);
|
|
494
|
+
if (index !== -1) {
|
|
495
|
+
return path.substring(index + 1); // +1 to remove leading slash
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// If no primary marker, check secondary markers
|
|
499
|
+
for (const marker of secondaryMarkers) {
|
|
500
|
+
const index = path.lastIndexOf(marker);
|
|
501
|
+
if (index !== -1) {
|
|
502
|
+
return path.substring(index + 1);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return path;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Converts absolute file paths to normalized relative paths for consistent error grouping.
|
|
509
|
+
*
|
|
510
|
+
* This function performs comprehensive path normalization to ensure errors from the same
|
|
511
|
+
* code location group together regardless of deployment environment, user directories,
|
|
512
|
+
* or system-specific paths. The original absolute path is always preserved in abs_path.
|
|
513
|
+
*
|
|
514
|
+
* Normalization steps:
|
|
515
|
+
* 1. Normalize URL schemes (file://, etc.) - must be first to strip URL prefixes
|
|
516
|
+
* 2. Preserve special paths (already relative, Node internals, etc.)
|
|
517
|
+
* 3. Normalize path separators to forward slashes (for consistent processing)
|
|
518
|
+
* 4. Normalize Node.js internal modules (node:internal/*, node:fs/*)
|
|
519
|
+
* 5. Normalize node_modules paths to package-relative format
|
|
520
|
+
* 6. Strip user home directories (/Users/*, /home/*, C:\Users\*)
|
|
521
|
+
* 7. Strip deployment-specific paths (/var/www/*, /app/, AWS Lambda, Docker)
|
|
522
|
+
* 8. Strip current working directory
|
|
523
|
+
* 9. Find project boundaries (/src/, /lib/, /dist/, etc.)
|
|
524
|
+
* 10. Remove leading slashes for clean relative paths
|
|
525
|
+
*
|
|
526
|
+
* @param filename - Absolute or relative file path from stack trace
|
|
527
|
+
* @returns Normalized relative path for error grouping
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* makeRelativePath('/Users/john/project/src/index.ts')
|
|
531
|
+
* // Returns: 'src/index.ts'
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* makeRelativePath('/home/ubuntu/app/node_modules/express/lib/router.js')
|
|
535
|
+
* // Returns: 'node_modules/express/lib/router.js'
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* makeRelativePath('/var/www/myapp/dist/server.js')
|
|
539
|
+
* // Returns: 'dist/server.js'
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* makeRelativePath('node:internal/modules/cjs/loader')
|
|
543
|
+
* // Returns: 'node:internal'
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
* makeRelativePath('C:\\Users\\John\\projects\\myapp\\src\\index.ts')
|
|
547
|
+
* // Returns: 'src/index.ts'
|
|
548
|
+
*/
|
|
549
|
+
function makeRelativePath(filename) {
|
|
550
|
+
let result = filename;
|
|
551
|
+
// Step 1: Normalize URL schemes (file://, etc.)
|
|
552
|
+
result = normalizeUrl(result);
|
|
553
|
+
// Step 2: Handle already-relative paths and special cases
|
|
554
|
+
if (!result.startsWith("/") && !result.match(/^[A-Za-z]:\\/)) {
|
|
555
|
+
// Already relative or special path (native, <unknown>, etc.)
|
|
556
|
+
// Still normalize Node internals
|
|
557
|
+
if (result.startsWith("node:")) {
|
|
558
|
+
return normalizeNodeInternals(result);
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
// Step 3: Normalize path separators early for consistent processing
|
|
563
|
+
result = result.replace(/\\/g, "/");
|
|
564
|
+
// Step 4: Normalize Node.js internal modules (should be rare at this point)
|
|
565
|
+
if (result.startsWith("node:")) {
|
|
566
|
+
return normalizeNodeInternals(result);
|
|
567
|
+
}
|
|
568
|
+
// Step 5: Handle node_modules specially - preserve package structure
|
|
569
|
+
if (result.includes("/node_modules/")) {
|
|
570
|
+
return normalizeNodeModules(result);
|
|
571
|
+
}
|
|
572
|
+
// Step 6: Strip user home directories
|
|
573
|
+
result = stripSystemPrefixes(result);
|
|
574
|
+
// Step 7: Strip deployment-specific paths
|
|
575
|
+
result = stripDeploymentPaths(result);
|
|
576
|
+
// Step 8: Strip current working directory (if available)
|
|
577
|
+
// process.cwd() may not be available in edge environments
|
|
578
|
+
let cwd = null;
|
|
579
|
+
try {
|
|
580
|
+
if (typeof process !== "undefined" && typeof process.cwd === "function") {
|
|
581
|
+
cwd = process.cwd();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// process.cwd() not available in this environment
|
|
586
|
+
}
|
|
587
|
+
if (cwd && result.startsWith(cwd)) {
|
|
588
|
+
result = result.substring(cwd.length + 1); // +1 to remove leading /
|
|
589
|
+
}
|
|
590
|
+
// Step 9: Find project boundaries if still absolute-looking
|
|
591
|
+
// Also apply to tilde paths that might have project markers after the tilde
|
|
592
|
+
if (result.startsWith("/") || result.match(/^[A-Za-z]:[/]/)) {
|
|
593
|
+
result = findProjectPath(result);
|
|
594
|
+
}
|
|
595
|
+
else if (result.startsWith("~")) {
|
|
596
|
+
// For tilde paths, strip the tilde and find markers in the remaining path
|
|
597
|
+
const withoutTilde = result.substring(2); // Remove ~/
|
|
598
|
+
const projectPath = findProjectPath("/" + withoutTilde);
|
|
599
|
+
// If a marker was found (path changed), use it; otherwise keep the tilde version
|
|
600
|
+
if (projectPath !== "/" + withoutTilde) {
|
|
601
|
+
result = projectPath;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Step 10: Remove leading slash if present (prefer relative paths)
|
|
605
|
+
if (result.startsWith("/")) {
|
|
606
|
+
result = result.substring(1);
|
|
607
|
+
}
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Recursively unwraps Error.cause chain and returns array of chained errors.
|
|
612
|
+
*
|
|
613
|
+
* Error.cause is a standard JavaScript feature that allows chaining errors:
|
|
614
|
+
* const cause = new Error("Root cause");
|
|
615
|
+
* const error = new Error("Wrapper error", { cause });
|
|
616
|
+
*
|
|
617
|
+
* This function extracts all errors in the cause chain up to MAX_EXCEPTION_CHAIN_DEPTH.
|
|
618
|
+
*
|
|
619
|
+
* @param error - Error object to unwrap
|
|
620
|
+
* @returns Array of ChainedErrorData objects representing the error chain
|
|
621
|
+
*/
|
|
622
|
+
function unwrapErrorCauses(error) {
|
|
623
|
+
const chainedErrors = [];
|
|
624
|
+
const seenErrors = new Set();
|
|
625
|
+
let currentError = error.cause;
|
|
626
|
+
let depth = 0;
|
|
627
|
+
while (currentError && depth < MAX_EXCEPTION_CHAIN_DEPTH) {
|
|
628
|
+
// If cause is not an Error, stringify it and stop
|
|
629
|
+
if (!(currentError instanceof Error)) {
|
|
630
|
+
chainedErrors.push({
|
|
631
|
+
message: stringifyNonError(currentError),
|
|
632
|
+
type: undefined,
|
|
633
|
+
});
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
// Check for circular reference
|
|
637
|
+
if (seenErrors.has(currentError)) {
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
seenErrors.add(currentError);
|
|
641
|
+
const chainedErrorData = {
|
|
642
|
+
message: currentError.message || "",
|
|
643
|
+
type: currentError.name || currentError.constructor?.name || "Error",
|
|
644
|
+
};
|
|
645
|
+
if (currentError.stack) {
|
|
646
|
+
chainedErrorData.stack = currentError.stack;
|
|
647
|
+
chainedErrorData.frames = parseV8StackTrace(currentError.stack);
|
|
648
|
+
}
|
|
649
|
+
chainedErrors.push(chainedErrorData);
|
|
650
|
+
// Move to next cause in chain
|
|
651
|
+
currentError = currentError.cause;
|
|
652
|
+
depth++;
|
|
653
|
+
}
|
|
654
|
+
return chainedErrors;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Detects if a value is a CallToolResult object (SDK 1.21.0+ error format).
|
|
658
|
+
*
|
|
659
|
+
* SDK 1.21.0+ converts errors to CallToolResult format:
|
|
660
|
+
* { content: [{ type: "text", text: "error message" }], isError: true }
|
|
661
|
+
*
|
|
662
|
+
* @param value - Value to check
|
|
663
|
+
* @returns True if value is a CallToolResult object
|
|
664
|
+
*/
|
|
665
|
+
function isCallToolResult(value) {
|
|
666
|
+
return (value !== null &&
|
|
667
|
+
typeof value === "object" &&
|
|
668
|
+
"isError" in value &&
|
|
669
|
+
"content" in value &&
|
|
670
|
+
Array.isArray(value.content));
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Extracts error information from CallToolResult objects.
|
|
674
|
+
*
|
|
675
|
+
* SDK 1.21.0+ converts errors to CallToolResult, losing original stack traces.
|
|
676
|
+
* This extracts the error message from the content array.
|
|
677
|
+
*
|
|
678
|
+
* @param result - CallToolResult object with error
|
|
679
|
+
* @param _contextStack - Optional Error object for stack context (unused, kept for compatibility)
|
|
680
|
+
* @returns ErrorData with extracted message (no stack trace)
|
|
681
|
+
*/
|
|
682
|
+
function captureCallToolResultError(result, _contextStack) {
|
|
683
|
+
// Extract message from content array
|
|
684
|
+
const message = result.content
|
|
685
|
+
?.filter((c) => c.type === "text")
|
|
686
|
+
.map((c) => c.text)
|
|
687
|
+
.join(" ")
|
|
688
|
+
.trim() || "Unknown error";
|
|
689
|
+
const errorData = {
|
|
690
|
+
message,
|
|
691
|
+
type: undefined, // Can't determine actual type from CallToolResult
|
|
692
|
+
platform: "javascript",
|
|
693
|
+
// No stack or frames - SDK stripped the original error information
|
|
694
|
+
};
|
|
695
|
+
return errorData;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Converts non-Error objects to string representation for error messages.
|
|
699
|
+
*
|
|
700
|
+
* In JavaScript, anything can be thrown (not just Error objects):
|
|
701
|
+
* throw "string error";
|
|
702
|
+
* throw { code: 404 };
|
|
703
|
+
* throw null;
|
|
704
|
+
*
|
|
705
|
+
* This function handles these cases by converting them to meaningful strings.
|
|
706
|
+
*
|
|
707
|
+
* @param value - Non-Error value that was thrown
|
|
708
|
+
* @returns String representation of the value
|
|
709
|
+
*/
|
|
710
|
+
function stringifyNonError(value) {
|
|
711
|
+
if (value === null) {
|
|
712
|
+
return "null";
|
|
713
|
+
}
|
|
714
|
+
if (value === undefined) {
|
|
715
|
+
return "undefined";
|
|
716
|
+
}
|
|
717
|
+
if (typeof value === "string") {
|
|
718
|
+
return value;
|
|
719
|
+
}
|
|
720
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
721
|
+
return String(value);
|
|
722
|
+
}
|
|
723
|
+
// Try to stringify objects with fallback
|
|
724
|
+
try {
|
|
725
|
+
return JSON.stringify(value);
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return String(value);
|
|
729
|
+
}
|
|
730
|
+
}
|