Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/modules/exceptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ErrorData, StackFrame, ChainedErrorData } from "../types.js";
import { readFileSync } from "fs";

// Maximum number of exceptions to capture in a cause chain
const MAX_EXCEPTION_CHAIN_DEPTH = 10;
Expand All @@ -22,12 +23,14 @@ export function captureException(error: unknown): ErrorData {
return {
message: stringifyNonError(error),
type: "NonError",
platform: "javascript",
};
}

const errorData: ErrorData = {
message: error.message || "",
type: error.name || error.constructor?.name || "Error",
platform: "javascript",
};

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

const frame = parseV8StackFrame(line.trim());
if (frame) {
addContextToFrame(frame);
frames.push(frame);
}

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

/**
* Adds context_line to a stack frame by reading the source file.
*
* This function extracts the line of code where the error occurred by:
* 1. Reading the source file using abs_path
* 2. Extracting the line at the specified line number
* 3. Setting the context_line field on the frame
*
* Only extracts context for user code (in_app: true)
* If the file cannot be read or the line number is invalid, context_line remains undefined.
*
* @param frame - The StackFrame to add context to (modified in place)
* @returns The modified StackFrame
*/
function addContextToFrame(frame: StackFrame): StackFrame {
if (!frame.in_app || !frame.abs_path || !frame.lineno) {
return frame;
}

try {
const source = readFileSync(frame.abs_path, "utf8");
const lines = source.split("\n");
const lineIndex = frame.lineno - 1; // Convert to 0-based index

if (lineIndex >= 0 && lineIndex < lines.length) {
frame.context_line = lines[lineIndex];
}
} catch {
// File not found or not readable - silently skip
}

return frame;
}

/**
* Parses a location string from a V8 stack frame.
*
Expand Down
61 changes: 61 additions & 0 deletions src/tests/exceptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ describe("captureException", () => {
expect(result.message).toBe("Custom error message");
expect(result.type).toBe("CustomError");
});

it("should always set platform to 'javascript'", () => {
const error = new Error("Test error");
const result = captureException(error);

expect(result.platform).toBe("javascript");
});

it("should set platform to 'javascript' for non-Error objects", () => {
const result = captureException("string error");

expect(result.platform).toBe("javascript");
});
});

describe("stack trace parsing", () => {
Expand Down Expand Up @@ -161,6 +174,54 @@ describe("captureException", () => {
expect(result.frames).toBeDefined();
expect(result.frames!.length).toBeLessThanOrEqual(50);
});

it("should capture context_line for in_app frames", () => {
// This test throws a real error, so context_line should be captured
const error = new Error("Test error");
const result = captureException(error);

expect(result.frames).toBeDefined();
const inAppFrames = result.frames!.filter((frame) => frame.in_app);
expect(inAppFrames.length).toBeGreaterThan(0);

// At least one in_app frame should have context_line
const hasContextLine = inAppFrames.some(
(frame) => frame.context_line !== undefined,
);
expect(hasContextLine).toBe(true);
});

it("should NOT capture context_line for library code (in_app: false)", () => {
// Create a mock stack trace with node_modules
const error = new Error("Test");
error.stack = `Error: Test
at libFunction (/app/node_modules/some-lib/index.js:42:10)
at internal (node:internal/process:123:45)`;

const result = captureException(error);

expect(result.frames).toBeDefined();
// All frames should be library code and should NOT have context_line
result.frames!.forEach((frame) => {
expect(frame.in_app).toBe(false);
expect(frame.context_line).toBeUndefined();
});
});

it("should handle missing files gracefully when extracting context_line", () => {
// Create a mock stack trace with a non-existent file
const error = new Error("Test");
error.stack = `Error: Test
at testFunction (/nonexistent/file/path.ts:10:5)`;

const result = captureException(error);

expect(result.frames).toBeDefined();
expect(result.frames!.length).toBe(1);
// Frame should be in_app but context_line should be undefined (file not found)
expect(result.frames![0].in_app).toBe(true);
expect(result.frames![0].context_line).toBeUndefined();
});
});

describe("Error.cause chain", () => {
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export interface StackFrame {
colno?: number;
in_app: boolean;
abs_path?: string;
context_line?: string; // The line of code where the error occurred
}

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