formatting.test.ts•56.4 kB
import { describe, it, expect } from "vitest";
import { formatEventOutput, formatFrameHeader } from "./formatting";
import type { Event } from "../api-client/types";
import {
EventBuilder,
createFrame,
frameFactories,
createStackTrace,
createExceptionValue,
createThread,
testEvents,
createFrameWithContext,
} from "./test-fixtures";
// Helper functions to reduce duplication in event creation
function createPythonExceptionEvent(
errorType: string,
errorMessage: string,
frames: any[],
): Event {
return new EventBuilder("python")
.withException(
createExceptionValue({
type: errorType,
value: errorMessage,
stacktrace: createStackTrace(frames),
}),
)
.build();
}
function createSimpleExceptionEvent(
platform: string,
errorType: string,
errorMessage: string,
frame: any,
): Event {
const builder = new EventBuilder(platform);
// Remove the contexts property to avoid "Additional Context" section
const event = builder
.withException(
createExceptionValue({
type: errorType,
value: errorMessage,
stacktrace: createStackTrace([frame]),
}),
)
.build();
// Remove contexts to match original test expectations
event.contexts = undefined;
return event;
}
describe("formatFrameHeader", () => {
it("uses platform as fallback when language detection fails", () => {
// Frame with no clear language indicators
const unknownFrame = {
filename: "/path/to/file.unknown",
function: "someFunction",
lineNo: 42,
};
// Without platform - should use generic format
expect(formatFrameHeader(unknownFrame)).toBe(
" at someFunction (/path/to/file.unknown:42)",
);
// With platform python - should use Python format
expect(formatFrameHeader(unknownFrame, undefined, "python")).toBe(
' File "/path/to/file.unknown", line 42, in someFunction',
);
// With platform java - should use Java format
expect(formatFrameHeader(unknownFrame, undefined, "java")).toBe(
"at UnknownClass.someFunction(/path/to/file.unknown:42)",
);
});
it("formats Java stack traces correctly", () => {
// With module and filename
const javaFrame1 = {
module: "com.example.ClassName",
function: "methodName",
filename: "ClassName.java",
lineNo: 123,
};
expect(formatFrameHeader(javaFrame1)).toBe(
"at com.example.ClassName.methodName(ClassName.java:123)",
);
// Without filename (common in Java) - needs platform hint
const javaFrame2 = {
module: "com.example.ClassName",
function: "methodName",
lineNo: 123,
};
expect(formatFrameHeader(javaFrame2, undefined, "java")).toBe(
"at com.example.ClassName.methodName(Unknown Source:123)",
);
});
it("formats Python stack traces correctly", () => {
const pythonFrame = {
filename: "/path/to/file.py",
function: "function_name",
lineNo: 42,
};
expect(formatFrameHeader(pythonFrame)).toBe(
' File "/path/to/file.py", line 42, in function_name',
);
// Module only (no filename) - needs platform hint
const pythonModuleFrame = {
module: "mymodule",
function: "function_name",
lineNo: 42,
};
expect(formatFrameHeader(pythonModuleFrame, undefined, "python")).toBe(
' File "mymodule", line 42, in function_name',
);
});
it("formats JavaScript stack traces correctly", () => {
// With column number
const jsFrame1 = {
filename: "/path/to/file.js",
function: "functionName",
lineNo: 10,
colNo: 15,
};
expect(formatFrameHeader(jsFrame1)).toBe(
"/path/to/file.js:10:15 (functionName)",
);
// Without column number but .js extension
const jsFrame2 = {
filename: "/path/to/file.js",
function: "functionName",
lineNo: 10,
};
expect(formatFrameHeader(jsFrame2)).toBe(
"/path/to/file.js:10 (functionName)",
);
// Anonymous function (no function name)
const jsFrame3 = {
filename: "/path/to/file.js",
lineNo: 10,
colNo: 15,
};
expect(formatFrameHeader(jsFrame3)).toBe("/path/to/file.js:10:15");
});
it("formats Ruby stack traces correctly", () => {
const rubyFrame = {
filename: "/path/to/file.rb",
function: "method_name",
lineNo: 42,
};
expect(formatFrameHeader(rubyFrame)).toBe(
" from /path/to/file.rb:42:in `method_name`",
);
// Without function name
const rubyFrame2 = {
filename: "/path/to/file.rb",
lineNo: 42,
};
expect(formatFrameHeader(rubyFrame2)).toBe(
" from /path/to/file.rb:42:in",
);
});
it("formats PHP stack traces correctly", () => {
// With frame index
const phpFrame1 = {
filename: "/path/to/file.php",
function: "functionName",
lineNo: 42,
};
expect(formatFrameHeader(phpFrame1, 0)).toBe(
"#0 /path/to/file.php(42): functionName()",
);
// Without frame index
const phpFrame2 = {
filename: "/path/to/file.php",
function: "functionName",
lineNo: 42,
};
expect(formatFrameHeader(phpFrame2)).toBe(
"/path/to/file.php(42): functionName()",
);
});
it("formats unknown languages with generic format", () => {
const unknownFrame = {
filename: "/path/to/file.unknown",
function: "someFunction",
lineNo: 42,
};
expect(formatFrameHeader(unknownFrame)).toBe(
" at someFunction (/path/to/file.unknown:42)",
);
});
it("prioritizes duck typing over platform when clear indicators exist", () => {
// Java file but platform says python - should use Java format
const javaFrame = {
filename: "Example.java",
module: "com.example.Example",
function: "doSomething",
lineNo: 42,
};
expect(formatFrameHeader(javaFrame, undefined, "python")).toBe(
"at com.example.Example.doSomething(Example.java:42)",
);
// Python file but platform says java - should use Python format
const pythonFrame = {
filename: "/app/example.py",
function: "do_something",
lineNo: 42,
};
expect(formatFrameHeader(pythonFrame, undefined, "java")).toBe(
' File "/app/example.py", line 42, in do_something',
);
});
});
describe("formatEventOutput", () => {
it("formats Java thread stack traces correctly", () => {
const event = testEvents.javaThreadError(
"Cannot use this function, please use update(String sql, PreparedStatementSetter pss) instead",
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
Cannot use this function, please use update(String sql, PreparedStatementSetter pss) instead
\`\`\`
**Thread** (CONTRACT_WORKER)
**Stacktrace:**
\`\`\`
at java.lang.Thread.run(Thread.java:833)
at com.citics.eqd.mq.aeron.AeronServer.lambda$start$3(AeronServer.java:110)
\`\`\`
"
`);
});
it("formats Python exception traces correctly", () => {
const event = testEvents.pythonException("Invalid value");
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: Invalid value
\`\`\`
**Stacktrace:**
\`\`\`
File "/app/main.py", line 42, in process_data
File "/app/utils.py", line 15, in validate
\`\`\`
"
`);
});
it("should render enhanced in-app frame with context lines", () => {
const event = new EventBuilder("python")
.withException(
createExceptionValue({
type: "ValueError",
value: "Something went wrong",
stacktrace: createStackTrace([
createFrame({
filename: "/usr/lib/python3.8/json/__init__.py",
function: "loads",
lineNo: 357,
inApp: false,
}),
createFrameWithContext(
{
filename: "/app/services/payment.py",
function: "process_payment",
lineNo: 42,
},
[
[37, " def process_payment(self, amount, user_id):"],
[38, " user = self.get_user(user_id)"],
[39, " if not user:"],
[40, ' raise ValueError("User not found")'],
[41, " "],
[42, " balance = user.account.balance"],
[43, " if balance < amount:"],
[44, " raise InsufficientFundsError()"],
[45, " "],
[46, " transaction = Transaction(user, amount)"],
],
),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: Something went wrong
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/services/payment.py", line 42, in process_payment
39 │ if not user:
40 │ raise ValueError("User not found")
41 │
→ 42 │ balance = user.account.balance
43 │ if balance < amount:
44 │ raise InsufficientFundsError()
45 │
**Full Stacktrace:**
────────────────
\`\`\`
File "/usr/lib/python3.8/json/__init__.py", line 357, in loads
File "/app/services/payment.py", line 42, in process_payment
balance = user.account.balance
\`\`\`
"
`);
});
it("should render enhanced in-app frame with variables", () => {
const event = new EventBuilder("python")
.withException(
createExceptionValue({
type: "ValueError",
value: "Something went wrong",
stacktrace: createStackTrace([
createFrame({
filename: "/app/services/payment.py",
function: "process_payment",
lineNo: 42,
inApp: true,
vars: {
amount: 150.0,
user_id: "usr_123456",
user: null,
self: { type: "PaymentService", id: 1234 },
},
}),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: Something went wrong
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/services/payment.py", line 42, in process_payment
Local Variables:
├─ amount: 150
├─ user_id: "usr_123456"
├─ user: null
└─ self: {"type":"PaymentService","id":1234}
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/services/payment.py", line 42, in process_payment
\`\`\`
"
`);
});
it("should handle frames without in-app or enhanced data", () => {
const event = new EventBuilder("python")
.withException(
createExceptionValue({
type: "ValueError",
value: "Something went wrong",
stacktrace: createStackTrace([
frameFactories.python({ lineNo: 10, function: "main" }),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: Something went wrong
\`\`\`
**Stacktrace:**
\`\`\`
File "/app/main.py", line 10, in main
\`\`\`
"
`);
});
it("should work with thread interface containing in-app frame", () => {
const event = new EventBuilder("java")
.withThread(
createThread({
id: 1,
crashed: true,
name: "main",
stacktrace: createStackTrace([
frameFactories.java({
module: "java.lang.Thread",
function: "run",
filename: "Thread.java",
lineNo: 748,
inApp: false,
}),
createFrameWithContext(
{
module: "com.example.PaymentService",
function: "processPayment",
filename: "PaymentService.java",
lineNo: 42,
},
[
[40, " User user = getUser(userId);"],
[41, " if (user == null) {"],
[42, " throw new UserNotFoundException(userId);"],
[43, " }"],
[44, " return user.getBalance();"],
],
{
userId: "12345",
user: null,
},
),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"**Thread** (main)
**Most Relevant Frame:**
─────────────────────
at com.example.PaymentService.processPayment(PaymentService.java:42)
40 │ User user = getUser(userId);
41 │ if (user == null) {
→ 42 │ throw new UserNotFoundException(userId);
43 │ }
44 │ return user.getBalance();
Local Variables:
├─ userId: "12345"
└─ user: null
**Full Stacktrace:**
────────────────
\`\`\`
at java.lang.Thread.run(Thread.java:748)
at com.example.PaymentService.processPayment(PaymentService.java:42)
throw new UserNotFoundException(userId);
\`\`\`
"
`);
});
describe("Enhanced frame rendering variations", () => {
it("should handle Python format with enhanced frame", () => {
const event = createSimpleExceptionEvent(
"python",
"AttributeError",
"'NoneType' object has no attribute 'balance'",
createFrameWithContext(
{
filename: "/app/models/user.py",
function: "get_balance",
lineNo: 25,
},
[
[23, " def get_balance(self):"],
[24, " # This will fail if account is None"],
[25, " return self.account.balance"],
[26, ""],
[27, " def set_balance(self, amount):"],
],
{
self: { id: 123, account: null },
},
),
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
AttributeError: 'NoneType' object has no attribute 'balance'
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/models/user.py", line 25, in get_balance
23 │ def get_balance(self):
24 │ # This will fail if account is None
→ 25 │ return self.account.balance
26 │
27 │ def set_balance(self, amount):
Local Variables:
└─ self: {"id":123,"account":null}
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/models/user.py", line 25, in get_balance
return self.account.balance
\`\`\`
"
`);
});
it("should handle JavaScript format with enhanced frame", () => {
const event = createSimpleExceptionEvent(
"javascript",
"TypeError",
"Cannot read property 'name' of undefined",
createFrameWithContext(
{
filename: "/src/components/UserProfile.tsx",
function: "UserProfile",
lineNo: 15,
colNo: 28,
},
[
[
13,
"export const UserProfile: React.FC<Props> = ({ userId }) => {",
],
[14, " const user = useUser(userId);"],
[15, " const displayName = user.profile.name;"],
[16, " "],
[17, " return ("],
],
{
userId: "usr_123",
user: undefined,
displayName: undefined,
},
),
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
TypeError: Cannot read property 'name' of undefined
\`\`\`
**Most Relevant Frame:**
─────────────────────
/src/components/UserProfile.tsx:15:28 (UserProfile)
13 │ export const UserProfile: React.FC<Props> = ({ userId }) => {
14 │ const user = useUser(userId);
→ 15 │ const displayName = user.profile.name;
16 │
17 │ return (
Local Variables:
├─ userId: "usr_123"
├─ user: undefined
└─ displayName: undefined
**Full Stacktrace:**
────────────────
\`\`\`
/src/components/UserProfile.tsx:15:28 (UserProfile)
const displayName = user.profile.name;
\`\`\`
"
`);
});
it("should handle Ruby format with enhanced frame", () => {
const event = new EventBuilder("ruby")
.withException(
createExceptionValue({
type: "NoMethodError",
value: "undefined method `charge' for nil:NilClass",
stacktrace: createStackTrace([
createFrameWithContext(
{
filename: "/app/services/payment_service.rb",
function: "process_payment",
lineNo: 8,
},
[
[6, " def process_payment(amount)"],
[7, " payment_method = user.payment_method"],
[8, " payment_method.charge(amount)"],
[9, " rescue => e"],
[10, " Rails.logger.error(e)"],
],
{
amount: 99.99,
payment_method: null,
},
),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
NoMethodError: undefined method \`charge' for nil:NilClass
\`\`\`
**Most Relevant Frame:**
─────────────────────
from /app/services/payment_service.rb:8:in \`process_payment\`
6 │ def process_payment(amount)
7 │ payment_method = user.payment_method
→ 8 │ payment_method.charge(amount)
9 │ rescue => e
10 │ Rails.logger.error(e)
Local Variables:
├─ amount: 99.99
└─ payment_method: null
**Full Stacktrace:**
────────────────
\`\`\`
from /app/services/payment_service.rb:8:in \`process_payment\`
payment_method.charge(amount)
\`\`\`
"
`);
});
it("should handle PHP format with enhanced frame", () => {
const event = new EventBuilder("php")
.withException(
createExceptionValue({
type: "Error",
value: "Call to a member function getName() on null",
stacktrace: createStackTrace([
createFrameWithContext(
{
filename: "/var/www/app/User.php",
function: "getDisplayName",
lineNo: 45,
},
[
[43, " public function getDisplayName() {"],
[44, " $profile = $this->getProfile();"],
[45, " return $profile->getName();"],
[46, " }"],
],
{
profile: null,
},
),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
Error: Call to a member function getName() on null
\`\`\`
**Most Relevant Frame:**
─────────────────────
/var/www/app/User.php(45): getDisplayName()
43 │ public function getDisplayName() {
44 │ $profile = $this->getProfile();
→ 45 │ return $profile->getName();
46 │ }
Local Variables:
└─ profile: null
**Full Stacktrace:**
────────────────
\`\`\`
/var/www/app/User.php(45): getDisplayName()
return $profile->getName();
\`\`\`
"
`);
});
it("should handle frame with context but no vars", () => {
const event = new EventBuilder("python")
.withException(
createExceptionValue({
type: "ValueError",
value: "Invalid configuration",
stacktrace: createStackTrace([
createFrameWithContext(
{
filename: "/app/config.py",
function: "load_config",
lineNo: 12,
},
[
[10, "def load_config():"],
[11, " if not os.path.exists(CONFIG_FILE):"],
[12, " raise ValueError('Invalid configuration')"],
[13, " with open(CONFIG_FILE) as f:"],
[14, " return json.load(f)"],
],
),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: Invalid configuration
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/config.py", line 12, in load_config
10 │ def load_config():
11 │ if not os.path.exists(CONFIG_FILE):
→ 12 │ raise ValueError('Invalid configuration')
13 │ with open(CONFIG_FILE) as f:
14 │ return json.load(f)
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/config.py", line 12, in load_config
raise ValueError('Invalid configuration')
\`\`\`
"
`);
});
it("should handle frame with vars but no context", () => {
const event = createSimpleExceptionEvent(
"python",
"TypeError",
"unsupported operand type(s)",
createFrame({
filename: "/app/calculator.py",
function: "divide",
lineNo: 5,
inApp: true,
vars: {
numerator: 10,
denominator: "0",
result: undefined,
},
}),
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
TypeError: unsupported operand type(s)
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/calculator.py", line 5, in divide
Local Variables:
├─ numerator: 10
├─ denominator: "0"
└─ result: undefined
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/calculator.py", line 5, in divide
\`\`\`
"
`);
});
it("should handle complex variable types", () => {
const event = createSimpleExceptionEvent(
"python",
"KeyError",
"'missing_key'",
createFrame({
filename: "/app/processor.py",
function: "process_data",
lineNo: 30,
inApp: true,
vars: {
string_var: "hello world",
number_var: 42,
float_var: 3.14,
bool_var: true,
null_var: null,
undefined_var: undefined,
array_var: [1, 2, 3],
object_var: { type: "User", id: 123 },
nested_object: {
user: { name: "John", age: 30 },
settings: { theme: "dark" },
},
empty_string: "",
zero: 0,
false_bool: false,
long_string:
"This is a very long string that should be handled properly in the output",
},
}),
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
KeyError: 'missing_key'
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/processor.py", line 30, in process_data
Local Variables:
├─ string_var: "hello world"
├─ number_var: 42
├─ float_var: 3.14
├─ bool_var: true
├─ null_var: null
├─ undefined_var: undefined
├─ array_var: [1,2,3]
├─ object_var: {"type":"User","id":123}
├─ nested_object: {"user":{"name":"John","age":30},"settings":{"theme":"dark"}}
├─ empty_string: ""
├─ zero: 0
├─ false_bool: false
└─ long_string: "This is a very long string that should be handled properly in the output"
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/processor.py", line 30, in process_data
\`\`\`
"
`);
});
it("should truncate very long objects and arrays", () => {
const event = new EventBuilder("python")
.withException(
createExceptionValue({
type: "ValueError",
value: "Data processing error",
stacktrace: createStackTrace([
createFrame({
filename: "/app/processor.py",
function: "process_batch",
lineNo: 45,
inApp: true,
vars: {
small_array: [1, 2, 3],
large_array: Array(100)
.fill(0)
.map((_, i) => i),
small_object: { name: "test", value: 123 },
large_object: {
data: Array(50)
.fill(0)
.reduce(
(acc, _, i) => {
acc[`field${i}`] = `value${i}`;
return acc;
},
{} as Record<string, string>,
),
},
},
}),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: Data processing error
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/processor.py", line 45, in process_batch
Local Variables:
├─ small_array: [1,2,3]
├─ large_array: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26, ...]
├─ small_object: {"name":"test","value":123}
└─ large_object: {"data":{"field0":"value0","field1":"value1","field2":"value2", ...}
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/processor.py", line 45, in process_batch
\`\`\`
"
`);
});
it("should show proper truncation format", () => {
const event = new EventBuilder("javascript")
.withException(
createExceptionValue({
type: "Error",
value: "Test error",
stacktrace: createStackTrace([
createFrame({
filename: "/app/test.js",
function: "test",
lineNo: 1,
inApp: true,
vars: {
shortArray: [1, 2, 3],
// This will be over 80 chars when stringified
longArray: [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
],
shortObject: { a: 1, b: 2 },
// This will be over 80 chars when stringified
longObject: {
field1: "value1",
field2: "value2",
field3: "value3",
field4: "value4",
field5: "value5",
field6: "value6",
field7: "value7",
field8: "value8",
},
},
}),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
Error: Test error
\`\`\`
**Most Relevant Frame:**
─────────────────────
/app/test.js:1 (test)
Local Variables:
├─ shortArray: [1,2,3]
├─ longArray: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27, ...]
├─ shortObject: {"a":1,"b":2}
└─ longObject: {"field1":"value1","field2":"value2","field3":"value3","field4":"value4", ...}
**Full Stacktrace:**
────────────────
\`\`\`
/app/test.js:1 (test)
\`\`\`
"
`);
});
it("should handle circular references gracefully", () => {
const circular: any = { name: "test" };
circular.self = circular;
const event = new EventBuilder("javascript")
.withException(
createExceptionValue({
type: "TypeError",
value: "Circular reference detected",
stacktrace: createStackTrace([
createFrame({
filename: "/app/utils.js",
function: "serialize",
lineNo: 10,
inApp: true,
vars: {
normal: { a: 1, b: 2 },
circular: circular,
},
}),
]),
}),
)
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
TypeError: Circular reference detected
\`\`\`
**Most Relevant Frame:**
─────────────────────
/app/utils.js:10 (serialize)
Local Variables:
├─ normal: {"a":1,"b":2}
└─ circular: <object>
**Full Stacktrace:**
────────────────
\`\`\`
/app/utils.js:10 (serialize)
\`\`\`
"
`);
});
it("should handle empty vars object", () => {
const event = createSimpleExceptionEvent(
"python",
"RuntimeError",
"Something went wrong",
createFrame({
filename: "/app/main.py",
function: "main",
lineNo: 1,
inApp: true,
vars: {},
}),
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
RuntimeError: Something went wrong
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/main.py", line 1, in main
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/main.py", line 1, in main
\`\`\`
"
`);
});
it("should handle large context with proper windowing", () => {
const event = createSimpleExceptionEvent(
"python",
"IndexError",
"list index out of range",
createFrameWithContext(
{
filename: "/app/processor.py",
function: "process_items",
lineNo: 50,
},
[
[45, " # Setup phase"],
[46, " items = get_items()"],
[47, " results = []"],
[48, " "],
[49, " # This line causes the error"],
[50, " first_item = items[0]"],
[51, " "],
[52, " # Process items"],
[53, " for item in items:"],
[54, " results.append(process(item))"],
[55, " return results"],
],
{
items: [],
},
),
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
IndexError: list index out of range
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/processor.py", line 50, in process_items
47 │ results = []
48 │
49 │ # This line causes the error
→ 50 │ first_item = items[0]
51 │
52 │ # Process items
53 │ for item in items:
Local Variables:
└─ items: []
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/processor.py", line 50, in process_items
first_item = items[0]
\`\`\`
"
`);
});
it("should handle context at beginning of file", () => {
const event = createSimpleExceptionEvent(
"python",
"ImportError",
"No module named 'missing_module'",
createFrameWithContext(
{
filename: "/app/startup.py",
function: "<module>",
lineNo: 2,
},
[
[1, "import os"],
[2, "import missing_module"],
[3, "import json"],
[4, ""],
[5, "def main():"],
],
),
);
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ImportError: No module named 'missing_module'
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/startup.py", line 2, in <module>
1 │ import os
→ 2 │ import missing_module
3 │ import json
4 │
5 │ def main():
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/startup.py", line 2, in <module>
import missing_module
\`\`\`
"
`);
});
});
describe("Chained exceptions", () => {
it("should render multiple chained exceptions", () => {
const event = new EventBuilder("python")
.withChainedExceptions([
createExceptionValue({
type: "KeyError",
value: "'user_id'",
stacktrace: createStackTrace([
createFrame({
filename: "/app/database.py",
function: "get_user",
lineNo: 15,
inApp: true,
}),
]),
}),
createExceptionValue({
type: "ValueError",
value: "User not found",
stacktrace: createStackTrace([
createFrame({
filename: "/app/services.py",
function: "process_user",
lineNo: 25,
inApp: true,
}),
]),
}),
createExceptionValue({
type: "HTTPError",
value: "500 Internal Server Error",
stacktrace: createStackTrace([
createFrame({
filename: "/app/handlers.py",
function: "handle_request",
lineNo: 42,
inApp: true,
}),
]),
}),
])
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
HTTPError: 500 Internal Server Error
\`\`\`
**Stacktrace:**
\`\`\`
File "/app/handlers.py", line 42, in handle_request
\`\`\`
**During handling of the above exception, another exception occurred:**
### ValueError: User not found
**Stacktrace:**
\`\`\`
File "/app/services.py", line 25, in process_user
\`\`\`
**During handling of the above exception, another exception occurred:**
### KeyError: 'user_id'
**Stacktrace:**
\`\`\`
File "/app/database.py", line 15, in get_user
\`\`\`
"
`);
});
it("should render chained exceptions with enhanced frame on outermost exception", () => {
const event = new EventBuilder("python")
.withChainedExceptions([
createExceptionValue({
type: "KeyError",
value: "'user_id'",
stacktrace: createStackTrace([
createFrameWithContext(
{
filename: "/app/database.py",
function: "get_user",
lineNo: 15,
inApp: true,
},
[
[13, "def get_user(data):"],
[14, " # This will fail if user_id is missing"],
[15, " user_id = data['user_id']"],
[16, " return db.find_user(user_id)"],
],
{
data: {},
},
),
]),
}),
createExceptionValue({
type: "ValueError",
value: "User not found",
stacktrace: createStackTrace([
createFrameWithContext(
{
filename: "/app/services.py",
function: "process_user",
lineNo: 25,
inApp: true,
},
[
[23, " try:"],
[24, " user = get_user(request_data)"],
[25, " except KeyError:"],
[26, " raise ValueError('User not found')"],
],
{
request_data: {},
},
),
]),
}),
])
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: User not found
\`\`\`
**Most Relevant Frame:**
─────────────────────
File "/app/services.py", line 25, in process_user
23 │ try:
24 │ user = get_user(request_data)
→ 25 │ except KeyError:
26 │ raise ValueError('User not found')
Local Variables:
└─ request_data: {}
**Full Stacktrace:**
────────────────
\`\`\`
File "/app/services.py", line 25, in process_user
except KeyError:
\`\`\`
**During handling of the above exception, another exception occurred:**
### KeyError: 'user_id'
**Stacktrace:**
\`\`\`
File "/app/database.py", line 15, in get_user
user_id = data['user_id']
\`\`\`
"
`);
});
it("should handle single exception in values array (not chained)", () => {
const event = new EventBuilder("python")
.withChainedExceptions([
createExceptionValue({
type: "RuntimeError",
value: "Something went wrong",
stacktrace: createStackTrace([
createFrame({
filename: "/app/main.py",
function: "main",
lineNo: 10,
inApp: true,
}),
]),
}),
])
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
RuntimeError: Something went wrong
\`\`\`
**Stacktrace:**
\`\`\`
File "/app/main.py", line 10, in main
\`\`\`
"
`);
});
it("should use Java-style 'Caused by' for Java platform", () => {
const event = new EventBuilder("java")
.withChainedExceptions([
createExceptionValue({
type: "SQLException",
value: "Database connection failed",
stacktrace: createStackTrace([
frameFactories.java({
module: "com.example.db.DatabaseConnector",
function: "connect",
filename: "DatabaseConnector.java",
lineNo: 45,
}),
]),
}),
createExceptionValue({
type: "RuntimeException",
value: "Failed to initialize service",
stacktrace: createStackTrace([
frameFactories.java({
module: "com.example.service.UserService",
function: "initialize",
filename: "UserService.java",
lineNo: 23,
}),
]),
}),
])
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
RuntimeException: Failed to initialize service
\`\`\`
**Stacktrace:**
\`\`\`
at com.example.service.UserService.initialize(UserService.java:23)
\`\`\`
**Caused by:**
### SQLException: Database connection failed
**Stacktrace:**
\`\`\`
at com.example.db.DatabaseConnector.connect(DatabaseConnector.java:45)
\`\`\`
"
`);
});
it("should use C#-style arrow notation for dotnet platform", () => {
const event = new EventBuilder("csharp")
.withChainedExceptions([
createExceptionValue({
type: "ArgumentNullException",
value: "Value cannot be null. (Parameter 'userId')",
stacktrace: createStackTrace([
createFrame({
filename: "UserRepository.cs",
function: "GetUserById",
lineNo: 15,
inApp: true,
}),
]),
}),
createExceptionValue({
type: "ApplicationException",
value: "Failed to load user profile",
stacktrace: createStackTrace([
createFrame({
filename: "UserService.cs",
function: "LoadProfile",
lineNo: 42,
inApp: true,
}),
]),
}),
])
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ApplicationException: Failed to load user profile
\`\`\`
**Stacktrace:**
\`\`\`
at LoadProfile (UserService.cs:42)
\`\`\`
**---> Inner Exception:**
### ArgumentNullException: Value cannot be null. (Parameter 'userId')
**Stacktrace:**
\`\`\`
at GetUserById (UserRepository.cs:15)
\`\`\`
"
`);
});
it("should handle child exception without stacktrace", () => {
const event = new EventBuilder("python")
.withChainedExceptions([
createExceptionValue({
type: "KeyError",
value: "'missing_key'",
// No stacktrace for child exception
stacktrace: undefined,
}),
createExceptionValue({
type: "ValueError",
value: "Data processing failed",
stacktrace: createStackTrace([
createFrame({
filename: "/app/processor.py",
function: "process_data",
lineNo: 42,
inApp: true,
}),
]),
}),
])
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"### Error
\`\`\`
ValueError: Data processing failed
\`\`\`
**Stacktrace:**
\`\`\`
File "/app/processor.py", line 42, in process_data
\`\`\`
**During handling of the above exception, another exception occurred:**
### KeyError: 'missing_key'
**Stacktrace:**
\`\`\`
No stacktrace available
\`\`\`
"
`);
});
});
describe("Performance issue formatting", () => {
it("should format N+1 query issue with evidence data", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "N+1 Query",
culprit: "SELECT * FROM users WHERE id = %s",
type: 1006, // Performance issue type code
issueType: "performance_n_plus_one_db_queries",
evidenceData: {
parentSpanIds: ["span_123"],
parentSpan: "GET /api/users",
repeatingSpansCompact: ["SELECT * FROM users WHERE id = %s"],
numberRepeatingSpans: "5",
offenderSpanIds: [
"span_456",
"span_457",
"span_458",
"span_459",
"span_460",
],
transactionName: "/api/users",
op: "db",
},
evidenceDisplay: [
{
name: "Offending Spans",
value: "UserService.get_users",
important: true,
},
],
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"**Parent Operation:**
GET /api/users
### Repeated Database Queries
**Query executed 5 times:**
\`\`\`sql
SELECT * FROM users WHERE id = %s
\`\`\`
**Transaction:**
/api/users
**Offending Spans:**
UserService.get_users
"
`);
});
it("should format N+1 query issue with spans data fallback", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "N+1 Query detected",
culprit: "database query",
})
.withEntry({
type: "spans",
data: [
{
op: "db.query",
description: "SELECT * FROM posts WHERE user_id = 1",
timestamp: 100.5,
start_timestamp: 100.0,
},
{
op: "db.query",
description: "SELECT * FROM posts WHERE user_id = 2",
timestamp: 101.0,
start_timestamp: 100.5,
},
{
op: "db.query",
description: "SELECT * FROM posts WHERE user_id = 3",
timestamp: 101.5,
start_timestamp: 101.0,
},
{
op: "http.client",
description: "GET /api/external",
timestamp: 102.0,
start_timestamp: 101.5,
},
],
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`""`);
});
it("should format transaction event with non-repeated database queries", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "Slow DB Query",
culprit: "database",
})
.withEntry({
type: "spans",
data: [
{
op: "db.query",
description: "SELECT COUNT(*) FROM users",
timestamp: 100.5,
start_timestamp: 100.0,
},
{
op: "db.query",
description: "SELECT * FROM settings WHERE key = 'theme'",
timestamp: 101.0,
start_timestamp: 100.5,
},
],
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`""`);
});
it("should format evidence with operation and offenderSpanIds", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "N+1 Query",
culprit: "database",
issueType: "performance_n_plus_one_db_queries",
evidenceData: {
op: "db",
offenderSpanIds: ["span_1", "span_2", "span_3", "span_4", "span_5"],
numberRepeatingSpans: "5",
},
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`""`);
});
it("should render span tree for N+1 queries with evidence and spans data", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "N+1 Query",
culprit: "SELECT * FROM users WHERE id = %s",
issueType: "performance_n_plus_one_db_queries",
evidenceData: {
parentSpanIds: ["parent123"],
parentSpan: "GET /api/users",
offenderSpanIds: ["span1", "span2", "span3"],
repeatingSpansCompact: ["SELECT * FROM users WHERE id = %s"],
numberRepeatingSpans: "3",
op: "db",
},
})
.withEntry({
type: "spans",
data: [
{
span_id: "parent123",
op: "http.server",
description: "GET /users",
timestamp: 1722963600.25,
start_timestamp: 1722963600.0,
},
{
span_id: "span1",
op: "db.query",
description: "SELECT * FROM users WHERE id = 1",
timestamp: 1722963600.013,
start_timestamp: 1722963600.01,
},
{
span_id: "span2",
op: "db.query",
description: "SELECT * FROM users WHERE id = 2",
timestamp: 1722963600.018,
start_timestamp: 1722963600.014,
},
{
span_id: "span3",
op: "db.query",
description: "SELECT * FROM users WHERE id = 3",
timestamp: 1722963600.027,
start_timestamp: 1722963600.019,
},
],
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"**Parent Operation:**
GET /api/users
### Repeated Database Queries
**Query executed 3 times:**
\`\`\`sql
SELECT * FROM users WHERE id = %s
\`\`\`
### Span Tree (Limited to 10 spans)
\`\`\`
GET /users [parent12 · http.server · 250ms]
├─ SELECT * FROM users WHERE id = 1 [span1 · db.query · 3ms] [N+1]
├─ SELECT * FROM users WHERE id = 2 [span2 · db.query · 4ms] [N+1]
└─ SELECT * FROM users WHERE id = 3 [span3 · db.query · 8ms] [N+1]
\`\`\`
"
`);
});
it("should render span tree using duration fields when timestamps are missing", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "N+1 Query",
issueType: "performance_n_plus_one_db_queries",
evidenceData: {
parentSpanIds: ["parentDur"],
parentSpan: "GET /api/durations",
offenderSpanIds: ["spanA", "spanB"],
repeatingSpansCompact: [
"SELECT * FROM durations WHERE bucket = %s",
],
numberRepeatingSpans: "2",
},
})
.withEntry({
type: "spans",
data: [
{
span_id: "parentDur",
op: "http.server",
description: "GET /durations",
duration: 1250,
},
{
span_id: "spanA",
op: "db.query",
description: "SELECT * FROM durations WHERE bucket = 'fast'",
duration: 0.5,
},
{
span_id: "spanB",
op: "db.query",
description: "SELECT * FROM durations WHERE bucket = 'slow'",
duration: 1500,
},
],
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"**Parent Operation:**
GET /api/durations
### Repeated Database Queries
**Query executed 2 times:**
\`\`\`sql
SELECT * FROM durations WHERE bucket = %s
\`\`\`
### Span Tree (Limited to 10 spans)
\`\`\`
GET /durations [parentDu · http.server · 1250ms]
├─ SELECT * FROM durations WHERE bucket = 'fast' [spanA · db.query · 1ms] [N+1]
└─ SELECT * FROM durations WHERE bucket = 'slow' [spanB · db.query · 1500ms] [N+1]
\`\`\`
"
`);
});
it("should handle transaction event without performance data", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "Generic Performance Issue",
culprit: "slow endpoint",
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`""`);
});
it("should handle evidence data without repeating_spans", () => {
const event = new EventBuilder("python")
.withType("transaction")
.withOccurrence({
issueTitle: "Performance Issue",
culprit: "database",
issueType: "performance_slow_db_query", // A different type that we don't fully handle yet
evidenceData: {
parentSpan: "GET /api/data",
transactionName: "/api/data",
},
evidenceDisplay: [
{
name: "Source Location",
value: "DataService.fetch",
important: true,
},
],
})
.build();
const output = formatEventOutput(event);
expect(output).toMatchInlineSnapshot(`
"**Parent Operation:**
GET /api/data
**Transaction:**
/api/data
**Source Location:**
DataService.fetch
"
`);
});
});
});