Skip to content

Commit 8d14c6e

Browse files
committed
fix: Move limiter to node, Add frameContextLines option
1 parent 277c9a7 commit 8d14c6e

File tree

8 files changed

+72
-22
lines changed

8 files changed

+72
-22
lines changed

packages/node/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@sentry/types": "4.4.2",
2121
"@sentry/utils": "4.4.2",
2222
"@types/stack-trace": "0.0.29",
23+
"async-limiter": "^1.0.0",
2324
"cookie": "0.3.1",
2425
"https-proxy-agent": "^2.2.1",
2526
"lsmod": "1.0.0",

packages/node/src/backend.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export interface NodeOptions extends Options {
3030

3131
/** HTTPS proxy certificates path */
3232
caCerts?: string;
33+
34+
/** Sets the number of context lines for each frame when loading a file. */
35+
frameContextLines?: number;
3336
}
3437

3538
/** The Sentry Node SDK Backend. */
@@ -65,7 +68,7 @@ export class NodeBackend extends BaseBackend<NodeOptions> {
6568
}
6669
}
6770

68-
const event: SentryEvent = await parseError(ex as Error);
71+
const event: SentryEvent = await parseError(ex as Error, this.options);
6972

7073
return {
7174
...event,
@@ -89,7 +92,7 @@ export class NodeBackend extends BaseBackend<NodeOptions> {
8992

9093
if (this.options.attachStacktrace && hint && hint.syntheticException) {
9194
const stack = hint.syntheticException ? await extractStackFromError(hint.syntheticException) : [];
92-
const frames = await parseStack(stack);
95+
const frames = await parseStack(stack, this.options);
9396
event.stacktrace = {
9497
frames: prepareFramesForEvent(frames),
9598
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
declare module 'lsmod';
22
declare module 'https-proxy-agent';
3+
declare module 'async-limiter';

packages/node/src/parsers.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { SentryEvent, SentryException, StackFrame } from '@sentry/types';
22
import { readFileAsync } from '@sentry/utils/fs';
33
import { basename, dirname } from '@sentry/utils/path';
44
import { snipLine } from '@sentry/utils/string';
5+
import * as Limiter from 'async-limiter';
56
import * as stacktrace from 'stack-trace';
7+
import { NodeOptions } from './backend';
68

7-
const LINES_OF_CONTEXT: number = 7;
9+
// tslint:disable-next-line:no-unsafe-any
10+
const fsLimiter = new Limiter({ concurrency: 25 });
11+
const DEFAULT_LINES_OF_CONTEXT: number = 7;
812

913
/**
1014
* Just an Error object with arbitrary attributes attached to it.
@@ -75,7 +79,7 @@ async function readSourceFiles(
7579
filenames.map(async filename => {
7680
let content;
7781
try {
78-
content = await readFileAsync(filename);
82+
content = await readFileAsync(filename, fsLimiter);
7983
} catch (_) {
8084
// unsure what to add here as the file is unreadable
8185
content = null;
@@ -99,8 +103,12 @@ export async function extractStackFromError(error: Error): Promise<stacktrace.St
99103
}
100104

101105
/** JSDoc */
102-
export async function parseStack(stack: stacktrace.StackFrame[]): Promise<StackFrame[]> {
106+
export async function parseStack(stack: stacktrace.StackFrame[], options?: NodeOptions): Promise<StackFrame[]> {
103107
const filesToRead: string[] = [];
108+
109+
const linesOfContext =
110+
options && options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
111+
104112
const frames: StackFrame[] = stack.map(frame => {
105113
const parsedFrame: StackFrame = {
106114
colno: frame.getColumnNumber(),
@@ -126,16 +134,21 @@ export async function parseStack(stack: stacktrace.StackFrame[]): Promise<StackF
126134
if (parsedFrame.filename) {
127135
parsedFrame.module = getModule(parsedFrame.filename);
128136

129-
if (!isInternal) {
137+
if (!isInternal && linesOfContext > 0) {
130138
filesToRead.push(parsedFrame.filename);
131139
}
132140
}
133141

134142
return parsedFrame;
135143
});
136144

145+
// We do an early return if we do not want to fetch context liens
146+
if (linesOfContext <= 0) {
147+
return frames;
148+
}
149+
137150
try {
138-
return await addPrePostContext(filesToRead, frames);
151+
return await addPrePostContext(filesToRead, frames, linesOfContext);
139152
} catch (_) {
140153
// This happens in electron for example where we are not able to read files from asar.
141154
// So it's fine, we recover be just returning all frames without pre/post context.
@@ -149,21 +162,25 @@ export async function parseStack(stack: stacktrace.StackFrame[]): Promise<StackF
149162
* @param filesToRead string[] of filepaths
150163
* @param frames StackFrame[] containg all frames
151164
*/
152-
async function addPrePostContext(filesToRead: string[], frames: StackFrame[]): Promise<StackFrame[]> {
165+
async function addPrePostContext(
166+
filesToRead: string[],
167+
frames: StackFrame[],
168+
linesOfContext: number,
169+
): Promise<StackFrame[]> {
153170
const sourceFiles = await readSourceFiles(filesToRead);
154171
return frames.map(frame => {
155172
if (frame.filename && sourceFiles[frame.filename]) {
156173
try {
157174
const lines = sourceFiles[frame.filename].split('\n');
158175

159176
frame.pre_context = lines
160-
.slice(Math.max(0, (frame.lineno || 0) - (LINES_OF_CONTEXT + 1)), (frame.lineno || 0) - 1)
177+
.slice(Math.max(0, (frame.lineno || 0) - (linesOfContext + 1)), (frame.lineno || 0) - 1)
161178
.map((line: string) => snipLine(line, 0));
162179

163180
frame.context_line = snipLine(lines[(frame.lineno || 0) - 1], frame.colno || 0);
164181

165182
frame.post_context = lines
166-
.slice(frame.lineno || 0, (frame.lineno || 0) + LINES_OF_CONTEXT)
183+
.slice(frame.lineno || 0, (frame.lineno || 0) + linesOfContext)
167184
.map((line: string) => snipLine(line, 0));
168185
} catch (e) {
169186
// anomaly, being defensive in case
@@ -175,10 +192,10 @@ async function addPrePostContext(filesToRead: string[], frames: StackFrame[]): P
175192
}
176193

177194
/** JSDoc */
178-
export async function getExceptionFromError(error: Error): Promise<SentryException> {
195+
export async function getExceptionFromError(error: Error, options?: NodeOptions): Promise<SentryException> {
179196
const name = error.name || error.constructor.name;
180197
const stack = await extractStackFromError(error);
181-
const frames = await parseStack(stack);
198+
const frames = await parseStack(stack, options);
182199

183200
return {
184201
stacktrace: {
@@ -190,9 +207,9 @@ export async function getExceptionFromError(error: Error): Promise<SentryExcepti
190207
}
191208

192209
/** JSDoc */
193-
export async function parseError(error: ExtendedError): Promise<SentryEvent> {
210+
export async function parseError(error: ExtendedError, options?: NodeOptions): Promise<SentryEvent> {
194211
const name = error.name || error.constructor.name;
195-
const exception = await getExceptionFromError(error);
212+
const exception = await getExceptionFromError(error, options);
196213
return {
197214
exception: {
198215
values: [exception],

packages/node/test/index.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,14 @@ describe('SentryNode', () => {
151151
});
152152

153153
test('capture an exception', done => {
154-
expect.assertions(6);
154+
expect.assertions(8);
155155
getCurrentHub().bindClient(
156156
new NodeClient({
157157
beforeSend: (event: SentryEvent) => {
158158
expect(event.tags).toEqual({ test: '1' });
159159
expect(event.exception).not.toBeUndefined();
160+
expect(event.exception!.values![0].stacktrace!.frames![2].pre_context).not.toBeUndefined();
161+
expect(event.exception!.values![0].stacktrace!.frames![2].post_context).not.toBeUndefined();
160162
expect(event.exception!.values![0]).not.toBeUndefined();
161163
expect(event.exception!.values![0].type).toBe('Error');
162164
expect(event.exception!.values![0].value).toBe('test');
@@ -177,6 +179,36 @@ describe('SentryNode', () => {
177179
}
178180
});
179181

182+
test('capture an exception no pre/post context', done => {
183+
expect.assertions(8);
184+
getCurrentHub().bindClient(
185+
new NodeClient({
186+
beforeSend: (event: SentryEvent) => {
187+
expect(event.tags).toEqual({ test: '1' });
188+
expect(event.exception).not.toBeUndefined();
189+
expect(event.exception!.values![0].stacktrace!.frames![2].pre_context).toBeUndefined();
190+
expect(event.exception!.values![0].stacktrace!.frames![2].post_context).toBeUndefined();
191+
expect(event.exception!.values![0]).not.toBeUndefined();
192+
expect(event.exception!.values![0].type).toBe('Error');
193+
expect(event.exception!.values![0].value).toBe('test');
194+
expect(event.exception!.values![0].stacktrace).toBeTruthy();
195+
done();
196+
return event;
197+
},
198+
dsn,
199+
frameContextLines: 0,
200+
}),
201+
);
202+
configureScope((scope: Scope) => {
203+
scope.setTag('test', '1');
204+
});
205+
try {
206+
throw new Error('test');
207+
} catch (e) {
208+
captureException(e);
209+
}
210+
});
211+
180212
test('capture a message', done => {
181213
expect.assertions(2);
182214
getCurrentHub().bindClient(

packages/utils/src/declarations.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/utils/src/fs.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
import * as Limiter from 'async-limiter';
21
import { mkdir, mkdirSync, readFile, statSync } from 'fs';
32
import { dirname, resolve } from 'path';
43

5-
// tslint:disable-next-line:no-unsafe-any
6-
const fsLimiter = new Limiter({ concurrency: 25 });
74
const _0777 = parseInt('0777', 8);
85

96
/**
107
* Asynchronously reads given files content.
118
*
129
* @param path A relative or absolute path to the file
10+
* @param fsLimiter A limiter instance that prevents excessive file access
1311
* @returns A Promise that resolves when the file has been read.
1412
*/
15-
export async function readFileAsync(path: string): Promise<Error | string> {
13+
export async function readFileAsync(path: string, fsLimiter: any): Promise<Error | string> {
1614
// We cannot use util.promisify here because that was only introduced in Node
1715
// 8 and we need to support older Node versions.
1816
return new Promise<Error | string>((res, reject) => {
1917
// tslint:disable-next-line:no-unsafe-any
2018
fsLimiter.push((done: () => void) => {
2119
readFile(path, 'utf8', (err, data) => {
2220
done();
23-
2421
if (err) {
2522
reject(err);
2623
} else {

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1299,7 +1299,7 @@ async-each@^1.0.0:
12991299
version "1.0.1"
13001300
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
13011301

1302-
async-limiter@~1.0.0:
1302+
async-limiter@^1.0.0, async-limiter@~1.0.0:
13031303
version "1.0.0"
13041304
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
13051305

0 commit comments

Comments
 (0)