Skip to content

Commit a245269

Browse files
committed
feat: Add stack trace parsing and options.
1 parent d42ffc0 commit a245269

File tree

4 files changed

+508
-0
lines changed

4 files changed

+508
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import ErrorCollector from '../src/collectors/error';
2+
import parse, { defaultOptions } from '../src/options';
3+
4+
it('handles an empty configuration', () => {
5+
const outOptions = parse({});
6+
expect(outOptions).toEqual(defaultOptions());
7+
});
8+
9+
it('can set each option', () => {
10+
const outOptions = parse({
11+
maxPendingEvents: 1,
12+
breadcrumbs: {
13+
maxBreadcrumbs: 1,
14+
click: false,
15+
evaluations: false,
16+
flagChange: false,
17+
},
18+
collectors: [new ErrorCollector(), new ErrorCollector()],
19+
});
20+
expect(outOptions).toEqual({
21+
maxPendingEvents: 1,
22+
breadcrumbs: {
23+
keyboardInput: true,
24+
maxBreadcrumbs: 1,
25+
click: false,
26+
evaluations: false,
27+
flagChange: false,
28+
http: {
29+
customUrlFilter: undefined,
30+
instrumentFetch: true,
31+
instrumentXhr: true,
32+
},
33+
},
34+
stack: {
35+
source: {
36+
beforeLines: 3,
37+
afterLines: 3,
38+
maxLineLength: 280,
39+
},
40+
},
41+
collectors: [new ErrorCollector(), new ErrorCollector()],
42+
});
43+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
getLines,
3+
getSrcLines,
4+
processUrlToFileName,
5+
TrimOptions,
6+
trimSourceLine,
7+
} from '../../src/stack/StackParser';
8+
9+
it.each([
10+
['http://www.launchdarkly.com', 'http://www.launchdarkly.com/', '(index)'],
11+
['http://www.launchdarkly.com', 'http://www.launchdarkly.com/test/(index)', 'test/(index)'],
12+
['http://www.launchdarkly.com', 'http://www.launchdarkly.com/test.js', 'test.js'],
13+
['http://localhost:8080', 'http://localhost:8080/dist/main.js', 'dist/main.js'],
14+
])('handles URL parsing to file names', (origin: string, url: string, expected: string) => {
15+
expect(processUrlToFileName(url, origin)).toEqual(expected);
16+
});
17+
18+
it.each([
19+
['this is the source line', 5, { maxLength: 10, beforeColumnCharacters: 2 }, 's is the s'],
20+
['this is the source line', 0, { maxLength: 10, beforeColumnCharacters: 2 }, 'this is th'],
21+
['this is the source line', 2, { maxLength: 10, beforeColumnCharacters: 0 }, 'is is the '],
22+
['12345', 0, { maxLength: 5, beforeColumnCharacters: 2 }, '12345'],
23+
['this is the source line', 21, { maxLength: 10, beforeColumnCharacters: 2 }, 'line'],
24+
])(
25+
'trims source lines',
26+
(source: string, column: number, options: TrimOptions, expected: string) => {
27+
expect(trimSourceLine(options, source, column)).toEqual(expected);
28+
},
29+
);
30+
31+
describe('given source lines', () => {
32+
const lines = ['1234567890', 'ABCDEFGHIJ', '0987654321', 'abcdefghij'];
33+
34+
it('can get a range which would underflow the lines', () => {
35+
expect(getLines(-1, 2, lines, (input) => input)).toStrictEqual(['1234567890', 'ABCDEFGHIJ']);
36+
});
37+
38+
it('can get a range which would overflow the lines', () => {
39+
expect(getLines(2, 4, lines, (input) => input)).toStrictEqual(['0987654321', 'abcdefghij']);
40+
});
41+
42+
it('can get a range which is satisfied by the lines', () => {
43+
expect(getLines(0, 4, lines, (input) => input)).toStrictEqual([
44+
'1234567890',
45+
'ABCDEFGHIJ',
46+
'0987654321',
47+
'abcdefghij',
48+
]);
49+
});
50+
});
51+
52+
describe('given an input stack frame', () => {
53+
const inputFrame = {
54+
context: ['1234567890', 'ABCDEFGHIJ', 'the src line', '0987654321', 'abcdefghij'],
55+
column: 0,
56+
};
57+
58+
it('can produce a full stack source in the output frame', () => {
59+
expect(
60+
getSrcLines(inputFrame, {
61+
source: {
62+
beforeLines: 2,
63+
afterLines: 2,
64+
maxLineLength: 280,
65+
},
66+
}),
67+
).toMatchObject({
68+
srcBefore: ['1234567890', 'ABCDEFGHIJ'],
69+
srcLine: 'the src line',
70+
srcAfter: ['0987654321', 'abcdefghij'],
71+
});
72+
});
73+
74+
it('can trim all the lines', () => {
75+
expect(
76+
getSrcLines(inputFrame, {
77+
source: {
78+
beforeLines: 2,
79+
afterLines: 2,
80+
maxLineLength: 1,
81+
},
82+
}),
83+
).toMatchObject({
84+
srcBefore: ['1', 'A'],
85+
srcLine: 't',
86+
srcAfter: ['0', 'a'],
87+
});
88+
});
89+
90+
it('can handle fewer input lines than the expected context', () => {
91+
expect(
92+
getSrcLines(inputFrame, {
93+
source: {
94+
beforeLines: 3,
95+
afterLines: 3,
96+
maxLineLength: 280,
97+
},
98+
}),
99+
).toMatchObject({
100+
srcBefore: ['1234567890', 'ABCDEFGHIJ'],
101+
srcLine: 'the src line',
102+
srcAfter: ['0987654321', 'abcdefghij'],
103+
});
104+
});
105+
106+
it('can handle more input lines than the expected context', () => {
107+
expect(
108+
getSrcLines(inputFrame, {
109+
source: {
110+
beforeLines: 1,
111+
afterLines: 1,
112+
maxLineLength: 280,
113+
},
114+
}),
115+
).toMatchObject({
116+
srcBefore: ['ABCDEFGHIJ'],
117+
srcLine: 'the src line',
118+
srcAfter: ['0987654321'],
119+
});
120+
});
121+
});
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { Collector } from './api/Collector';
2+
import { HttpBreadCrumbOptions, Options, StackOptions, UrlFilter } from './api/Options';
3+
4+
export function defaultOptions(): ParsedOptions {
5+
return {
6+
breadcrumbs: {
7+
maxBreadcrumbs: 50,
8+
evaluations: true,
9+
flagChange: true,
10+
click: true,
11+
keyboardInput: true,
12+
http: {
13+
instrumentFetch: true,
14+
instrumentXhr: true,
15+
},
16+
},
17+
stack: {
18+
source: {
19+
beforeLines: 3,
20+
afterLines: 3,
21+
maxLineLength: 280,
22+
},
23+
},
24+
maxPendingEvents: 100,
25+
collectors: [],
26+
};
27+
}
28+
29+
function itemOrDefault<T>(item: T | undefined, defaultValue: T): T {
30+
if (item !== undefined && item !== null) {
31+
return item;
32+
}
33+
return defaultValue;
34+
}
35+
36+
function parseHttp(
37+
options: HttpBreadCrumbOptions | false | undefined,
38+
defaults: ParsedHttpOptions,
39+
): ParsedHttpOptions {
40+
if (options === false) {
41+
return {
42+
instrumentFetch: false,
43+
instrumentXhr: false,
44+
};
45+
}
46+
47+
// Make sure that the custom filter is at least a function.
48+
const customUrlFilter =
49+
options?.customUrlFilter && typeof options?.customUrlFilter === 'function'
50+
? options.customUrlFilter
51+
: undefined;
52+
53+
// TODO: Logging for incorrect types.
54+
55+
return {
56+
instrumentFetch: itemOrDefault(options?.instrumentFetch, defaults.instrumentFetch),
57+
instrumentXhr: itemOrDefault(options?.instrumentFetch, defaults.instrumentXhr),
58+
customUrlFilter,
59+
};
60+
}
61+
62+
function parseStack(
63+
options: StackOptions | undefined,
64+
defaults: ParsedStackOptions,
65+
): ParsedStackOptions {
66+
return {
67+
source: {
68+
beforeLines: itemOrDefault(options?.source?.beforeLines, defaults.source.beforeLines),
69+
afterLines: itemOrDefault(options?.source?.afterLines, defaults.source.afterLines),
70+
maxLineLength: itemOrDefault(options?.source?.maxLineLength, defaults.source.maxLineLength),
71+
},
72+
};
73+
}
74+
75+
export default function parse(options: Options): ParsedOptions {
76+
const defaults = defaultOptions();
77+
return {
78+
breadcrumbs: {
79+
maxBreadcrumbs: itemOrDefault(
80+
options.breadcrumbs?.maxBreadcrumbs,
81+
defaults.breadcrumbs.maxBreadcrumbs,
82+
),
83+
evaluations: itemOrDefault(
84+
options.breadcrumbs?.evaluations,
85+
defaults.breadcrumbs.evaluations,
86+
),
87+
flagChange: itemOrDefault(options.breadcrumbs?.flagChange, defaults.breadcrumbs.flagChange),
88+
click: itemOrDefault(options.breadcrumbs?.click, defaults.breadcrumbs.click),
89+
keyboardInput: itemOrDefault(
90+
options.breadcrumbs?.keyboardInput,
91+
defaults.breadcrumbs.keyboardInput,
92+
),
93+
http: parseHttp(options.breadcrumbs?.http, defaults.breadcrumbs.http),
94+
},
95+
stack: parseStack(options.stack, defaults.stack),
96+
maxPendingEvents: itemOrDefault(options.maxPendingEvents, defaults.maxPendingEvents),
97+
collectors: [...itemOrDefault(options.collectors, defaults.collectors)],
98+
};
99+
}
100+
101+
export interface ParsedHttpOptions {
102+
/**
103+
* True to instrument fetch and enable fetch breadcrumbs.
104+
*/
105+
instrumentFetch: boolean;
106+
107+
/**
108+
* True to instrument XMLHttpRequests and enable XMLHttpRequests breadcrumbs.
109+
*/
110+
instrumentXhr: boolean;
111+
112+
/**
113+
* Optional custom URL filter.
114+
*/
115+
customUrlFilter?: UrlFilter;
116+
}
117+
118+
export interface ParsedStackOptions {
119+
source: {
120+
/**
121+
* The number of lines captured before the originating line.
122+
*/
123+
beforeLines: number;
124+
125+
/**
126+
* The number of lines captured after the originating line.
127+
*/
128+
afterLines: number;
129+
130+
/**
131+
* The maximum length of source line to include. Lines longer than this will be
132+
* trimmed.
133+
*/
134+
maxLineLength: number;
135+
};
136+
}
137+
138+
export interface ParsedOptions {
139+
/**
140+
* The maximum number of pending events. Events may be captured before the LaunchDarkly
141+
* SDK is initialized and these are stored until they can be sent. This only affects the
142+
* events captured during initialization.
143+
*/
144+
maxPendingEvents: number;
145+
/**
146+
* Properties related to automatic breadcrumb collection.
147+
*/
148+
breadcrumbs: {
149+
/**
150+
* Set the maximum number of breadcrumbs. Defaults to 50.
151+
*/
152+
maxBreadcrumbs: number;
153+
154+
/**
155+
* True to enable automatic evaluation breadcrumbs. Defaults to true.
156+
*/
157+
evaluations: boolean;
158+
159+
/**
160+
* True to enable flag change breadcrumbs. Defaults to true.
161+
*/
162+
flagChange: boolean;
163+
164+
/**
165+
* True to enable click breadcrumbs. Defaults to true.
166+
*/
167+
click: boolean;
168+
169+
/**
170+
* True to enable input breadcrumbs for keypresses. Defaults to true.
171+
*/
172+
keyboardInput?: boolean;
173+
174+
/**
175+
* Settings for http instrumentation and breadcrumbs.
176+
*/
177+
http: ParsedHttpOptions;
178+
};
179+
180+
/**
181+
* Settings which affect call stack capture.
182+
*/
183+
stack: ParsedStackOptions;
184+
185+
/**
186+
* Additional, or custom, collectors.
187+
*/
188+
collectors: Collector[];
189+
}

0 commit comments

Comments
 (0)