Skip to content

Commit 3657d2e

Browse files
authored
ref(node): Add source code context when using LinkedErrors (#4753)
- Modifies the `ContextLines` integration so it can be used from elsewhere with it's options - In the `LinkedErrors` integration, if the `ContextLines` integration is enabled, adds context to linked errors
1 parent 43fa409 commit 3657d2e

File tree

5 files changed

+37
-23
lines changed

5 files changed

+37
-23
lines changed

packages/node/src/integrations/contextlines.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,8 @@ export class ContextLines implements Integration {
5151

5252
public constructor(private readonly _options: ContextLinesOptions = {}) {}
5353

54-
/**
55-
* @inheritDoc
56-
*/
57-
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
54+
/** Get's the number of context lines to add */
55+
private get _contextLines(): number {
5856
// This is only here to copy frameContextLines from init options if it hasn't
5957
// been set via this integrations constructor.
6058
//
@@ -65,18 +63,22 @@ export class ContextLines implements Integration {
6563
this._options.frameContextLines = initOptions?.frameContextLines;
6664
}
6765

68-
const contextLines =
69-
this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
66+
return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
67+
}
7068

71-
addGlobalEventProcessor(event => this.addSourceContext(event, contextLines));
69+
/**
70+
* @inheritDoc
71+
*/
72+
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
73+
addGlobalEventProcessor(event => this.addSourceContext(event));
7274
}
7375

7476
/** Processes an event and adds context lines */
75-
public async addSourceContext(event: Event, contextLines: number): Promise<Event> {
76-
if (contextLines > 0 && event.exception?.values) {
77+
public async addSourceContext(event: Event): Promise<Event> {
78+
if (this._contextLines > 0 && event.exception?.values) {
7779
for (const exception of event.exception.values) {
7880
if (exception.stacktrace?.frames) {
79-
await this._addSourceContextToFrames(exception.stacktrace.frames, contextLines);
81+
await this.addSourceContextToFrames(exception.stacktrace.frames);
8082
}
8183
}
8284
}
@@ -85,9 +87,12 @@ export class ContextLines implements Integration {
8587
}
8688

8789
/** Adds context lines to frames */
88-
public async _addSourceContextToFrames(frames: StackFrame[], contextLines: number): Promise<void> {
90+
public async addSourceContextToFrames(frames: StackFrame[]): Promise<void> {
91+
const contextLines = this._contextLines;
92+
8993
for (const frame of frames) {
90-
if (frame.filename) {
94+
// Only add context if we have a filename and it hasn't already been added
95+
if (frame.filename && frame.context_line === undefined) {
9196
const sourceFile = await _readSourceFile(frame.filename);
9297

9398
if (sourceFile) {

packages/node/src/integrations/linkederrors.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry
33
import { isInstanceOf, resolvedSyncPromise, SyncPromise } from '@sentry/utils';
44

55
import { exceptionFromError } from '../eventbuilder';
6+
import { ContextLines } from './contextlines';
67

78
const DEFAULT_KEY = 'cause';
89
const DEFAULT_LIMIT = 5;
@@ -76,14 +77,21 @@ export class LinkedErrors implements Integration {
7677
/**
7778
* @inheritDoc
7879
*/
79-
private _walkErrorTree(error: ExtendedError, key: string, stack: Exception[] = []): PromiseLike<Exception[]> {
80+
private async _walkErrorTree(error: ExtendedError, key: string, stack: Exception[] = []): Promise<Exception[]> {
8081
if (!isInstanceOf(error[key], Error) || stack.length + 1 >= this._limit) {
81-
return resolvedSyncPromise(stack);
82+
return Promise.resolve(stack);
8283
}
8384

8485
const exception = exceptionFromError(error[key]);
8586

86-
return new SyncPromise<Exception[]>((resolve, reject) => {
87+
// If the ContextLines integration is enabled, we add source code context to linked errors
88+
// because we can't guarantee the order that integrations are run.
89+
const contextLines = getCurrentHub().getIntegration(ContextLines);
90+
if (contextLines && exception.stacktrace?.frames) {
91+
await contextLines.addSourceContextToFrames(exception.stacktrace.frames);
92+
}
93+
94+
return new Promise<Exception[]>((resolve, reject) => {
8795
void this._walkErrorTree(error[key], key, [exception, ...stack])
8896
.then(resolve)
8997
.then(null, () => {

packages/node/src/sdk.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const defaultIntegrations = [
1212
// Common
1313
new CoreIntegrations.InboundFilters(),
1414
new CoreIntegrations.FunctionToString(),
15+
new ContextLines(),
1516
// Native Wrappers
1617
new Console(),
1718
new Http(),
@@ -20,8 +21,6 @@ export const defaultIntegrations = [
2021
new OnUnhandledRejection(),
2122
// Misc
2223
new LinkedErrors(),
23-
// ContextLines must come after LinkedErrors so that context is added to linked errors
24-
new ContextLines(),
2524
];
2625

2726
/**

packages/node/test/context-lines.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ describe('ContextLines', () => {
99
let readFileSpy: jest.SpyInstance;
1010
let contextLines: ContextLines;
1111

12-
async function addContext(frames: StackFrame[], lines: number = 7): Promise<void> {
13-
await contextLines.addSourceContext({ exception: { values: [{ stacktrace: { frames } }] } }, lines);
12+
async function addContext(frames: StackFrame[]): Promise<void> {
13+
await contextLines.addSourceContext({ exception: { values: [{ stacktrace: { frames } }] } });
1414
}
1515

1616
beforeEach(() => {
@@ -97,10 +97,12 @@ describe('ContextLines', () => {
9797
});
9898

9999
test('parseStack with no context', async () => {
100+
contextLines = new ContextLines({ frameContextLines: 0 });
101+
100102
expect.assertions(1);
101103
const frames = parseStackFrames(new Error('test'));
102104

103-
await addContext(frames, 0);
105+
await addContext(frames);
104106
expect(readFileSpy).toHaveBeenCalledTimes(0);
105107
});
106108
});

packages/node/test/index.test.ts

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

2121
jest.mock('@sentry/core', () => {
2222
const original = jest.requireActual('@sentry/core');
@@ -199,11 +199,11 @@ describe('SentryNode', () => {
199199
}
200200
});
201201

202-
test.only('capture a linked exception with pre/post context', done => {
202+
test('capture a linked exception with pre/post context', done => {
203203
expect.assertions(15);
204204
getCurrentHub().bindClient(
205205
new NodeClient({
206-
integrations: i => [new LinkedErrors(), ...i],
206+
integrations: [new ContextLines(), new LinkedErrors()],
207207
beforeSend: (event: Event) => {
208208
expect(event.exception).not.toBeUndefined();
209209
expect(event.exception!.values![1]).not.toBeUndefined();

0 commit comments

Comments
 (0)