Skip to content

Commit 79a109c

Browse files
authored
ref(node): Include source context lines for all exceptions (#4734)
- Ensures source context is added to **all** exceptions! - `readSourceFiles` becomes `readSourceFile` - Since we already have an LRU, nothing is gained by loading files in groups
1 parent 76acf2f commit 79a109c

File tree

3 files changed

+74
-44
lines changed

3 files changed

+74
-44
lines changed

packages/node/src/integrations/contextlines.ts

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getCurrentHub } from '@sentry/core';
2-
import { Event, EventProcessor, Integration } from '@sentry/types';
2+
import { Event, EventProcessor, Integration, StackFrame } from '@sentry/types';
33
import { addContextToFrame } from '@sentry/utils';
44
import { readFile } from 'fs';
55
import { LRUMap } from 'lru_map';
@@ -73,23 +73,26 @@ export class ContextLines implements Integration {
7373

7474
/** Processes an event and adds context lines */
7575
public async addSourceContext(event: Event, contextLines: number): Promise<Event> {
76-
const frames = event.exception?.values?.[0].stacktrace?.frames;
77-
78-
if (frames && contextLines > 0) {
79-
const filenames: Set<string> = new Set();
80-
81-
for (const frame of frames) {
82-
if (frame.filename) {
83-
filenames.add(frame.filename);
76+
if (contextLines > 0 && event.exception?.values) {
77+
for (const exception of event.exception.values) {
78+
if (exception.stacktrace?.frames) {
79+
await this._addSourceContextToFrames(exception.stacktrace.frames, contextLines);
8480
}
8581
}
82+
}
83+
84+
return event;
85+
}
8686

87-
const sourceFiles = await readSourceFiles(filenames);
87+
/** Adds context lines to frames */
88+
public async _addSourceContextToFrames(frames: StackFrame[], contextLines: number): Promise<void> {
89+
for (const frame of frames) {
90+
if (frame.filename) {
91+
const sourceFile = await _readSourceFile(frame.filename);
8892

89-
for (const frame of frames) {
90-
if (frame.filename && sourceFiles[frame.filename]) {
93+
if (sourceFile) {
9194
try {
92-
const lines = (sourceFiles[frame.filename] as string).split('\n');
95+
const lines = sourceFile.split('\n');
9396
addContextToFrame(lines, frame, contextLines);
9497
} catch (e) {
9598
// anomaly, being defensive in case
@@ -98,43 +101,28 @@ export class ContextLines implements Integration {
98101
}
99102
}
100103
}
101-
102-
return event;
103104
}
104105
}
105106

106107
/**
107-
* This function reads file contents and caches them in a global LRU cache.
108+
* Reads file contents and caches them in a global LRU cache.
108109
*
109-
* @param filenames Array of filepaths to read content from.
110+
* @param filename filepath to read content from.
110111
*/
111-
async function readSourceFiles(filenames: Set<string>): Promise<Record<string, string | null>> {
112-
const sourceFiles: Record<string, string | null> = {};
113-
114-
for (const filename of filenames) {
115-
const cachedFile = FILE_CONTENT_CACHE.get(filename);
116-
// We have a cache hit
117-
if (cachedFile !== undefined) {
118-
// If stored value is null, it means that we already tried, but couldn't read the content of the file. Skip.
119-
if (cachedFile === null) {
120-
continue;
121-
}
122-
123-
// Otherwise content is there, so reuse cached value.
124-
sourceFiles[filename] = cachedFile;
125-
continue;
126-
}
127-
128-
let content: string | null = null;
129-
try {
130-
content = await readTextFileAsync(filename);
131-
} catch (_) {
132-
//
133-
}
112+
async function _readSourceFile(filename: string): Promise<string | null> {
113+
const cachedFile = FILE_CONTENT_CACHE.get(filename);
114+
// We have a cache hit
115+
if (cachedFile !== undefined) {
116+
return cachedFile;
117+
}
134118

135-
FILE_CONTENT_CACHE.set(filename, content);
136-
sourceFiles[filename] = content;
119+
let content: string | null = null;
120+
try {
121+
content = await readTextFileAsync(filename);
122+
} catch (_) {
123+
//
137124
}
138125

139-
return sourceFiles;
126+
FILE_CONTENT_CACHE.set(filename, content);
127+
return content;
140128
}

packages/node/src/sdk.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export const defaultIntegrations = [
1212
// Common
1313
new CoreIntegrations.InboundFilters(),
1414
new CoreIntegrations.FunctionToString(),
15-
new ContextLines(),
1615
// Native Wrappers
1716
new Console(),
1817
new Http(),
@@ -21,6 +20,8 @@ export const defaultIntegrations = [
2120
new OnUnhandledRejection(),
2221
// Misc
2322
new LinkedErrors(),
23+
// ContextLines must come after LinkedErrors so that context is added to linked errors
24+
new ContextLines(),
2425
];
2526

2627
/**

packages/node/test/index.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Scope,
1717
} from '../src';
1818
import { NodeBackend } from '../src/backend';
19+
import { LinkedErrors } from '../src/integrations';
1920

2021
jest.mock('@sentry/core', () => {
2122
const original = jest.requireActual('@sentry/core');
@@ -198,6 +199,46 @@ describe('SentryNode', () => {
198199
}
199200
});
200201

202+
test.only('capture a linked exception with pre/post context', done => {
203+
expect.assertions(15);
204+
getCurrentHub().bindClient(
205+
new NodeClient({
206+
integrations: i => [new LinkedErrors(), ...i],
207+
beforeSend: (event: Event) => {
208+
expect(event.exception).not.toBeUndefined();
209+
expect(event.exception!.values![1]).not.toBeUndefined();
210+
expect(event.exception!.values![1].stacktrace!).not.toBeUndefined();
211+
expect(event.exception!.values![1].stacktrace!.frames![1]).not.toBeUndefined();
212+
expect(event.exception!.values![1].stacktrace!.frames![1].pre_context).not.toBeUndefined();
213+
expect(event.exception!.values![1].stacktrace!.frames![1].post_context).not.toBeUndefined();
214+
expect(event.exception!.values![1].type).toBe('Error');
215+
expect(event.exception!.values![1].value).toBe('test');
216+
217+
expect(event.exception!.values![0]).not.toBeUndefined();
218+
expect(event.exception!.values![0].stacktrace!).not.toBeUndefined();
219+
expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined();
220+
expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined();
221+
expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined();
222+
expect(event.exception!.values![0].type).toBe('Error');
223+
expect(event.exception!.values![0].value).toBe('cause');
224+
done();
225+
return null;
226+
},
227+
dsn,
228+
}),
229+
);
230+
try {
231+
throw new Error('test');
232+
} catch (e) {
233+
try {
234+
throw new Error('cause');
235+
} catch (c) {
236+
e.cause = c;
237+
captureException(e);
238+
}
239+
}
240+
});
241+
201242
test('capture a message', done => {
202243
expect.assertions(2);
203244
getCurrentHub().bindClient(

0 commit comments

Comments
 (0)