light-sentry-sdk/tests/stacktrace.test.ts

361 lines
12 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { parseStackTrace } from '../src/utils/stacktrace';
describe('stacktrace', () => {
// Mock location for jsdom environment
beforeEach(() => {
delete (global as any).location;
});
describe('parseStackTrace', () => {
it('should return empty array for undefined stack', () => {
expect(parseStackTrace(undefined)).toEqual([]);
});
it('should return empty array for empty string', () => {
expect(parseStackTrace('')).toEqual([]);
});
it('should parse standard Chrome/Node.js stack trace format', () => {
// Stack trace: newest call first (Function1), then Function2, then Function3
// After reverse, order becomes Function3, Function2, Function1
const stack = `Error
at Function3 (http://example.com/js/index.js:30:5)
at Function2 (http://example.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(3);
// After reverse, Function1 should be first (oldest call in the trace)
expect(frames[0]).toEqual({
filename: 'http://example.com/js/app.js',
function: 'Function1',
lineno: 10,
colno: 15,
in_app: true,
});
expect(frames[1]).toEqual({
filename: 'http://example.com/js/main.js',
function: 'Function2',
lineno: 25,
colno: 10,
in_app: true,
});
expect(frames[2]).toEqual({
filename: 'http://example.com/js/index.js',
function: 'Function3',
lineno: 30,
colno: 5,
in_app: true,
});
});
it('should parse V8 format with @ separator', () => {
// V8 format requires 'at' prefix: at Function1@path:line:col
const stack = `Error
at Function1@http://example.com/js/app.js:10:15
at Function2@http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(2);
// After reverse, order is reversed
expect(frames[0]).toEqual({
filename: 'http://example.com/js/main.js',
function: 'Function2',
lineno: 25,
colno: 10,
in_app: true,
});
expect(frames[1]).toEqual({
filename: 'http://example.com/js/app.js',
function: 'Function1',
lineno: 10,
colno: 15,
in_app: true,
});
});
it('should parse URL-only format (no function name)', () => {
const stack = `Error
at http://example.com/js/app.js:10:15
at http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(2);
expect(frames[0]).toEqual({
filename: 'http://example.com/js/main.js',
lineno: 25,
colno: 10,
in_app: true,
});
expect(frames[1]).toEqual({
filename: 'http://example.com/js/app.js',
lineno: 10,
colno: 15,
in_app: true,
});
});
it('should mark node_modules frames as in_app: false when captured', () => {
// When captureNodeModules is true, node_modules frames are included
const stack = `Error
at Function1 (http://example.com/node_modules/dep.js:10:15)
at Function2 (http://example.com/js/app.js:25:10)`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
// After reverse, order is Function2, Function1
expect(frames).toHaveLength(2);
expect(frames[0].in_app).toBe(true); // Function2 (app code)
expect(frames[1].in_app).toBe(false); // Function1 (node_modules)
});
it('should skip node_modules frames when captureNodeModules is false', () => {
const stack = `Error
at Function1 (http://example.com/node_modules/dep.js:10:15)
at Function2 (http://example.com/js/app.js:25:10)
at Function3 (http://example.com/node_modules/other.js:30:5)`;
const frames = parseStackTrace(stack, { captureNodeModules: false });
// After reverse: Function3, Function2, Function1, then filter node_modules
expect(frames).toHaveLength(1);
expect(frames[0].function).toBe('Function2');
});
it('should include node_modules when captureNodeModules is true', () => {
const stack = `Error
at Function1 (http://example.com/node_modules/dep.js:10:15)
at Function2 (http://example.com/js/app.js:25:10)`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
expect(frames).toHaveLength(2);
});
it('should not include column when captureColumn is false', () => {
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack, { captureColumn: false });
expect(frames).toHaveLength(1);
expect(frames[0].colno).toBeUndefined();
});
it('should not apply relativePathOnly when origin does not match', () => {
// In jsdom, location.origin would be 'http://localhost:3000' or similar
// so example.com URLs won't match
const stack = `Error
at Function2 (http://other.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack, { relativePathOnly: true });
// URLs don't match location.origin, so they remain unchanged
// After reverse: Function1, Function2
expect(frames[0].filename).toBe('http://example.com/js/app.js');
expect(frames[1].filename).toBe('http://other.com/js/main.js');
});
it('should limit frames with maxFrames option', () => {
const stack = `Error
at Function4 (http://example.com/js/index.js:40:0)
at Function3 (http://example.com/js/util.js:35:0)
at Function2 (http://example.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
// After reverse: Function1, Function2, Function3, Function4
// maxFrames=2 returns first 2: Function1, Function2
const frames = parseStackTrace(stack, { maxFrames: 2 });
expect(frames).toHaveLength(2);
expect(frames[0].function).toBe('Function1');
expect(frames[1].function).toBe('Function2');
});
it('should return all frames when maxFrames is 0 or undefined', () => {
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)
at Function2 (http://example.com/js/main.js:25:10)`;
const frames1 = parseStackTrace(stack, { maxFrames: 0 });
const frames2 = parseStackTrace(stack, { maxFrames: undefined });
expect(frames1).toHaveLength(2);
expect(frames2).toHaveLength(2);
});
it('should handle anonymous functions in V8 format', () => {
// V8 format requires 'at' prefix: at <anonymous>@path:line:col
const stack = `Error
at <anonymous>@http://example.com/js/app.js:10:15`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(1);
expect(frames[0].function).toBe('<anonymous>');
});
it('should handle stack with extra whitespace', () => {
// Note: complex whitespace may cause regex to fail, test with standard format
const stack = `Error
at Function2 (http://example.com/js/main.js:25:10)
at Function1 (http://example.com/js/app.js:10:15)`;
const frames = parseStackTrace(stack);
// After reverse: Function1, Function2
expect(frames).toHaveLength(2);
expect(frames[0].function).toBe('Function1');
expect(frames[1].function).toBe('Function2');
});
it('should handle file:// URLs', () => {
const stack = `Error
at Function1 (file:///Users/name/project/app.js:10:15)`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(1);
expect(frames[0].filename).toBe('file:///Users/name/project/app.js');
expect(frames[0].in_app).toBe(true);
});
it('should handle mixed format stacks', () => {
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)
at Function2@http://example.com/js/main.js:25:10
at http://example.com/js/index.js:30:5`;
const frames = parseStackTrace(stack);
expect(frames).toHaveLength(3);
});
it('should handle V8 format with captureColumn: false', () => {
const stack = `Error
at Function1@http://example.com/js/app.js:10:15`;
const frames = parseStackTrace(stack, { captureColumn: false });
expect(frames).toHaveLength(1);
expect(frames[0].colno).toBeUndefined();
expect(frames[0].function).toBe('Function1');
});
it('should handle URL-only format with captureColumn: false', () => {
const stack = `Error
at http://example.com/js/app.js:10:15`;
const frames = parseStackTrace(stack, { captureColumn: false });
expect(frames).toHaveLength(1);
expect(frames[0].colno).toBeUndefined();
});
it('should skip node_modules in V8 format', () => {
const stack = `Error
at Function1@http://example.com/node_modules/dep.js:10:15
at Function2@http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: false });
expect(frames).toHaveLength(1);
expect(frames[0].function).toBe('Function2');
});
it('should skip node_modules in URL-only format', () => {
const stack = `Error
at http://example.com/node_modules/dep.js:10:15
at http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: false });
expect(frames).toHaveLength(1);
});
it('should include node_modules in V8 format when captureNodeModules is true', () => {
const stack = `Error
at Function1@http://example.com/node_modules/dep.js:10:15
at Function2@http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
expect(frames).toHaveLength(2);
expect(frames[0].in_app).toBe(true);
expect(frames[1].in_app).toBe(false);
});
it('should include node_modules in URL-only format when captureNodeModules is true', () => {
const stack = `Error
at http://example.com/node_modules/dep.js:10:15
at http://example.com/js/app.js:25:10`;
const frames = parseStackTrace(stack, { captureNodeModules: true });
expect(frames).toHaveLength(2);
});
it('should apply relativePathOnly to V8 format', () => {
// Set up location mock
const mockLocation = {
protocol: 'http:',
host: 'example.com',
};
(global as any).location = mockLocation;
const stack = `Error
at Function1@http://example.com/js/app.js:10:15
at Function2@http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack, { relativePathOnly: true });
expect(frames).toHaveLength(2);
expect(frames[0].filename).toBe('/js/main.js');
expect(frames[1].filename).toBe('/js/app.js');
delete (global as any).location;
});
it('should apply relativePathOnly to URL-only format', () => {
const mockLocation = {
protocol: 'http:',
host: 'example.com',
};
(global as any).location = mockLocation;
const stack = `Error
at http://example.com/js/app.js:10:15
at http://example.com/js/main.js:25:10`;
const frames = parseStackTrace(stack, { relativePathOnly: true });
expect(frames).toHaveLength(2);
expect(frames[0].filename).toBe('/js/main.js');
expect(frames[1].filename).toBe('/js/app.js');
delete (global as any).location;
});
it('should return reversed frames (newest call first after reversal)', () => {
// In standard stack traces, the first line after "Error" is the most recent call
const stack = `Error
at Function1 (http://example.com/js/app.js:10:15)
at Function2 (http://example.com/js/main.js:25:10)
at Function3 (http://example.com/js/index.js:30:5)`;
const frames = parseStackTrace(stack);
// After reverse of [Function1, Function2, Function3], we get [Function3, Function2, Function1]
// This puts the oldest call first
expect(frames[0].function).toBe('Function3');
expect(frames[1].function).toBe('Function2');
expect(frames[2].function).toBe('Function1');
});
});
});