Skip to content

Commit 98729f8

Browse files
authored
fix: add platform field and context_line extraction (#13)
1 parent 83e0339 commit 98729f8

File tree

3 files changed

+101
-0
lines changed

3 files changed

+101
-0
lines changed

src/modules/exceptions.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ErrorData, StackFrame, ChainedErrorData } from "../types.js";
2+
import { readFileSync } from "fs";
23

34
// Maximum number of exceptions to capture in a cause chain
45
const MAX_EXCEPTION_CHAIN_DEPTH = 10;
@@ -22,12 +23,14 @@ export function captureException(error: unknown): ErrorData {
2223
return {
2324
message: stringifyNonError(error),
2425
type: "NonError",
26+
platform: "javascript",
2527
};
2628
}
2729

2830
const errorData: ErrorData = {
2931
message: error.message || "",
3032
type: error.name || error.constructor?.name || "Error",
33+
platform: "javascript",
3134
};
3235

3336
// Capture stack trace if available
@@ -76,6 +79,7 @@ function parseV8StackTrace(stackTrace: string): StackFrame[] {
7679

7780
const frame = parseV8StackFrame(line.trim());
7881
if (frame) {
82+
addContextToFrame(frame);
7983
frames.push(frame);
8084
}
8185

@@ -88,6 +92,40 @@ function parseV8StackTrace(stackTrace: string): StackFrame[] {
8892
return frames;
8993
}
9094

95+
/**
96+
* Adds context_line to a stack frame by reading the source file.
97+
*
98+
* This function extracts the line of code where the error occurred by:
99+
* 1. Reading the source file using abs_path
100+
* 2. Extracting the line at the specified line number
101+
* 3. Setting the context_line field on the frame
102+
*
103+
* Only extracts context for user code (in_app: true)
104+
* If the file cannot be read or the line number is invalid, context_line remains undefined.
105+
*
106+
* @param frame - The StackFrame to add context to (modified in place)
107+
* @returns The modified StackFrame
108+
*/
109+
function addContextToFrame(frame: StackFrame): StackFrame {
110+
if (!frame.in_app || !frame.abs_path || !frame.lineno) {
111+
return frame;
112+
}
113+
114+
try {
115+
const source = readFileSync(frame.abs_path, "utf8");
116+
const lines = source.split("\n");
117+
const lineIndex = frame.lineno - 1; // Convert to 0-based index
118+
119+
if (lineIndex >= 0 && lineIndex < lines.length) {
120+
frame.context_line = lines[lineIndex];
121+
}
122+
} catch {
123+
// File not found or not readable - silently skip
124+
}
125+
126+
return frame;
127+
}
128+
91129
/**
92130
* Parses a location string from a V8 stack frame.
93131
*

src/tests/exceptions.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ describe("captureException", () => {
4444
expect(result.message).toBe("Custom error message");
4545
expect(result.type).toBe("CustomError");
4646
});
47+
48+
it("should always set platform to 'javascript'", () => {
49+
const error = new Error("Test error");
50+
const result = captureException(error);
51+
52+
expect(result.platform).toBe("javascript");
53+
});
54+
55+
it("should set platform to 'javascript' for non-Error objects", () => {
56+
const result = captureException("string error");
57+
58+
expect(result.platform).toBe("javascript");
59+
});
4760
});
4861

4962
describe("stack trace parsing", () => {
@@ -161,6 +174,54 @@ describe("captureException", () => {
161174
expect(result.frames).toBeDefined();
162175
expect(result.frames!.length).toBeLessThanOrEqual(50);
163176
});
177+
178+
it("should capture context_line for in_app frames", () => {
179+
// This test throws a real error, so context_line should be captured
180+
const error = new Error("Test error");
181+
const result = captureException(error);
182+
183+
expect(result.frames).toBeDefined();
184+
const inAppFrames = result.frames!.filter((frame) => frame.in_app);
185+
expect(inAppFrames.length).toBeGreaterThan(0);
186+
187+
// At least one in_app frame should have context_line
188+
const hasContextLine = inAppFrames.some(
189+
(frame) => frame.context_line !== undefined,
190+
);
191+
expect(hasContextLine).toBe(true);
192+
});
193+
194+
it("should NOT capture context_line for library code (in_app: false)", () => {
195+
// Create a mock stack trace with node_modules
196+
const error = new Error("Test");
197+
error.stack = `Error: Test
198+
at libFunction (/app/node_modules/some-lib/index.js:42:10)
199+
at internal (node:internal/process:123:45)`;
200+
201+
const result = captureException(error);
202+
203+
expect(result.frames).toBeDefined();
204+
// All frames should be library code and should NOT have context_line
205+
result.frames!.forEach((frame) => {
206+
expect(frame.in_app).toBe(false);
207+
expect(frame.context_line).toBeUndefined();
208+
});
209+
});
210+
211+
it("should handle missing files gracefully when extracting context_line", () => {
212+
// Create a mock stack trace with a non-existent file
213+
const error = new Error("Test");
214+
error.stack = `Error: Test
215+
at testFunction (/nonexistent/file/path.ts:10:5)`;
216+
217+
const result = captureException(error);
218+
219+
expect(result.frames).toBeDefined();
220+
expect(result.frames!.length).toBe(1);
221+
// Frame should be in_app but context_line should be undefined (file not found)
222+
expect(result.frames![0].in_app).toBe(true);
223+
expect(result.frames![0].context_line).toBeUndefined();
224+
});
164225
});
165226

166227
describe("Error.cause chain", () => {

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export interface StackFrame {
179179
colno?: number;
180180
in_app: boolean;
181181
abs_path?: string;
182+
context_line?: string; // The line of code where the error occurred
182183
}
183184

184185
export interface ChainedErrorData {
@@ -194,4 +195,5 @@ export interface ErrorData {
194195
stack?: string; // Full stack trace string
195196
frames?: StackFrame[]; // Parsed stack frames
196197
chained_errors?: ChainedErrorData[];
198+
platform?: string; // Platform identifier (e.g., "javascript", "node")
197199
}

0 commit comments

Comments
 (0)