Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Move source loading to seperate file
  • Loading branch information
timfish committed Jun 21, 2021
commit 8b30a9a107c517365720aa58fc35a204b634a591
6 changes: 3 additions & 3 deletions packages/node/src/backend.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BaseBackend } from '@sentry/core';
import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types';
import { Dsn } from '@sentry/utils';
import { readFile } from 'fs';

import { eventFromException, eventFromMessage } from './eventbuilder';
import { readFilesAddPrePostContext } from './sources';
import { HTTPSTransport, HTTPTransport } from './transports';
import { NodeOptions } from './types';

Expand All @@ -17,14 +17,14 @@ export class NodeBackend extends BaseBackend<NodeOptions> {
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
public eventFromException(exception: any, hint?: EventHint): PromiseLike<Event> {
return eventFromException(this._options, exception, hint, readFile);
return eventFromException(this._options, exception, hint, readFilesAddPrePostContext);
}

/**
* @inheritDoc
*/
public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike<Event> {
return eventFromMessage(this._options, message, level, hint, readFile);
return eventFromMessage(this._options, message, level, hint, readFilesAddPrePostContext);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions packages/node/src/eventbuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SyncPromise,
} from '@sentry/utils';

import { extractStackFromError, parseError, parseStack, prepareFramesForEvent, ReadFileFn } from './parsers';
import { extractStackFromError, parseError, parseStack, prepareFramesForEvent, ReadFilesFn } from './parsers';
import { NodeOptions } from './types';

/**
Expand All @@ -22,7 +22,7 @@ export function eventFromException(
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
exception: any,
hint?: EventHint,
readFile?: ReadFileFn,
readFiles?: ReadFilesFn,
): PromiseLike<Event> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let ex: any = exception;
Expand Down Expand Up @@ -55,7 +55,7 @@ export function eventFromException(
}

return new SyncPromise<Event>((resolve, reject) =>
parseError(ex as Error, readFile, options)
parseError(ex as Error, readFiles, options)
.then(event => {
addExceptionTypeValue(event, undefined, undefined);
addExceptionMechanism(event, mechanism);
Expand All @@ -78,7 +78,7 @@ export function eventFromMessage(
message: string,
level: Severity = Severity.Info,
hint?: EventHint,
readFile?: ReadFileFn,
readFiles?: ReadFilesFn,
): PromiseLike<Event> {
const event: Event = {
event_id: hint && hint.event_id,
Expand All @@ -89,7 +89,7 @@ export function eventFromMessage(
return new SyncPromise<Event>(resolve => {
if (options.attachStacktrace && hint && hint.syntheticException) {
const stack = hint.syntheticException ? extractStackFromError(hint.syntheticException) : [];
void parseStack(stack, readFile, options)
void parseStack(stack, readFiles, options)
.then(frames => {
event.stacktrace = {
frames: prepareFramesForEvent(frames),
Expand Down
124 changes: 13 additions & 111 deletions packages/node/src/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import { Event, Exception, ExtendedError, StackFrame } from '@sentry/types';
import { addContextToFrame, basename, dirname, SyncPromise } from '@sentry/utils';
import { LRUMap } from 'lru_map';
import { basename, dirname, SyncPromise } from '@sentry/utils';

import * as stacktrace from './stacktrace';
import { NodeOptions } from './types';

const DEFAULT_LINES_OF_CONTEXT: number = 7;
const FILE_CONTENT_CACHE = new LRUMap<string, string | null>(100);

export type ReadFileFn = (path: string, callback: (err: Error | null, data: Buffer) => void) => void;

/**
* Resets the file cache. Exists for testing purposes.
* @hidden
*/
export function resetFileContentCache(): void {
FILE_CONTENT_CACHE.clear();
}
export type ReadFilesFn = (
filesToRead: string[],
frames: StackFrame[],
linesOfContext: number,
) => PromiseLike<StackFrame[]>;

/** JSDoc */
function getFunction(frame: stacktrace.StackFrame): string {
Expand Down Expand Up @@ -63,65 +57,6 @@ function getModule(filename: string, base?: string): string {
return file;
}

/**
* This function reads file contents and caches them in a global LRU cache.
* Returns a Promise filepath => content array for all files that we were able to read.
*
* @param filenames Array of filepaths to read content from.
*/
function readSourceFiles(filenames: string[], readFile: ReadFileFn): PromiseLike<{ [key: string]: string | null }> {
// we're relying on filenames being de-duped already
if (filenames.length === 0) {
return SyncPromise.resolve({});
}

return new SyncPromise<{
[key: string]: string | null;
}>(resolve => {
const sourceFiles: {
[key: string]: string | null;
} = {};

let count = 0;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < filenames.length; i++) {
const filename = filenames[i];

const cache = FILE_CONTENT_CACHE.get(filename);
// We have a cache hit
if (cache !== undefined) {
// If it's not null (which means we found a file and have a content)
// we set the content and return it later.
if (cache !== null) {
sourceFiles[filename] = cache;
}
// eslint-disable-next-line no-plusplus
count++;
// In any case we want to skip here then since we have a content already or we couldn't
// read the file and don't want to try again.
if (count === filenames.length) {
resolve(sourceFiles);
}
continue;
}

readFile(filename, (err: Error | null, data: Buffer) => {
const content = err ? null : data.toString();
sourceFiles[filename] = content;

// We always want to set the cache, even to null which means there was an error reading the file.
// We do not want to try to read the file again.
FILE_CONTENT_CACHE.set(filename, content);
// eslint-disable-next-line no-plusplus
count++;
if (count === filenames.length) {
resolve(sourceFiles);
}
});
}
});
}

/**
* @hidden
*/
Expand All @@ -138,7 +73,7 @@ export function extractStackFromError(error: Error): stacktrace.StackFrame[] {
*/
export function parseStack(
stack: stacktrace.StackFrame[],
readFile?: ReadFileFn,
readFiles?: ReadFilesFn,
options?: NodeOptions,
): PromiseLike<StackFrame[]> {
const filesToRead: string[] = [];
Expand Down Expand Up @@ -184,9 +119,9 @@ export function parseStack(
return SyncPromise.resolve(frames);
}

if (readFile) {
if (readFiles) {
try {
return addPrePostContext(filesToRead, frames, linesOfContext, readFile);
return readFiles(filesToRead, frames, linesOfContext);
} catch (_) {
// This happens in electron for example where we are not able to read files from asar.
// So it's fine, we recover be just returning all frames without pre/post context.
Expand All @@ -196,51 +131,18 @@ export function parseStack(
return SyncPromise.resolve(frames);
}

/**
* This function tries to read the source files + adding pre and post context (source code)
* to a frame.
* @param filesToRead string[] of filepaths
* @param frames StackFrame[] containg all frames
*/
function addPrePostContext(
filesToRead: string[],
frames: StackFrame[],
linesOfContext: number,
readFile: ReadFileFn,
): PromiseLike<StackFrame[]> {
return new SyncPromise<StackFrame[]>(resolve =>
readSourceFiles(filesToRead, readFile).then(sourceFiles => {
const result = frames.map(frame => {
if (frame.filename && sourceFiles[frame.filename]) {
try {
const lines = (sourceFiles[frame.filename] as string).split('\n');

addContextToFrame(lines, frame, linesOfContext);
} catch (e) {
// anomaly, being defensive in case
// unlikely to ever happen in practice but can definitely happen in theory
}
}
return frame;
});

resolve(result);
}),
);
}

/**
* @hidden
*/
export function getExceptionFromError(
error: Error,
readFile?: ReadFileFn,
readFiles?: ReadFilesFn,
options?: NodeOptions,
): PromiseLike<Exception> {
const name = error.name || error.constructor.name;
const stack = extractStackFromError(error);
return new SyncPromise<Exception>(resolve =>
parseStack(stack, readFile, options).then(frames => {
parseStack(stack, readFiles, options).then(frames => {
const result = {
stacktrace: {
frames: prepareFramesForEvent(frames),
Expand All @@ -256,9 +158,9 @@ export function getExceptionFromError(
/**
* @hidden
*/
export function parseError(error: ExtendedError, readFile?: ReadFileFn, options?: NodeOptions): PromiseLike<Event> {
export function parseError(error: ExtendedError, readFiles?: ReadFilesFn, options?: NodeOptions): PromiseLike<Event> {
return new SyncPromise<Event>(resolve =>
getExceptionFromError(error, readFile, options).then((exception: Exception) => {
getExceptionFromError(error, readFiles, options).then((exception: Exception) => {
resolve({
exception: {
values: [exception],
Expand Down
105 changes: 105 additions & 0 deletions packages/node/src/sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { StackFrame } from '@sentry/types';
import { addContextToFrame, SyncPromise } from '@sentry/utils';
import { readFile } from 'fs';
import { LRUMap } from 'lru_map';

const FILE_CONTENT_CACHE = new LRUMap<string, string | null>(100);

/**
* Resets the file cache. Exists for testing purposes.
* @hidden
*/
export function resetFileContentCache(): void {
FILE_CONTENT_CACHE.clear();
}

/**
* This function tries to read the source files + adding pre and post context (source code)
* to a frame.
* @param filesToRead string[] of filepaths
* @param frames StackFrame[] containg all frames
*/
export function readFilesAddPrePostContext(
filesToRead: string[],
frames: StackFrame[],
linesOfContext: number,
): PromiseLike<StackFrame[]> {
return new SyncPromise<StackFrame[]>(resolve =>
readSourceFiles(filesToRead).then(sourceFiles => {
const result = frames.map(frame => {
if (frame.filename && sourceFiles[frame.filename]) {
try {
const lines = (sourceFiles[frame.filename] as string).split('\n');

addContextToFrame(lines, frame, linesOfContext);
} catch (e) {
// anomaly, being defensive in case
// unlikely to ever happen in practice but can definitely happen in theory
}
}
return frame;
});

resolve(result);
}),
);
}

/**
* This function reads file contents and caches them in a global LRU cache.
* Returns a Promise filepath => content array for all files that we were able to read.
*
* @param filenames Array of filepaths to read content from.
*/
function readSourceFiles(filenames: string[]): PromiseLike<{ [key: string]: string | null }> {
// we're relying on filenames being de-duped already
if (filenames.length === 0) {
return SyncPromise.resolve({});
}

return new SyncPromise<{
[key: string]: string | null;
}>(resolve => {
const sourceFiles: {
[key: string]: string | null;
} = {};

let count = 0;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < filenames.length; i++) {
const filename = filenames[i];

const cache = FILE_CONTENT_CACHE.get(filename);
// We have a cache hit
if (cache !== undefined) {
// If it's not null (which means we found a file and have a content)
// we set the content and return it later.
if (cache !== null) {
sourceFiles[filename] = cache;
}
// eslint-disable-next-line no-plusplus
count++;
// In any case we want to skip here then since we have a content already or we couldn't
// read the file and don't want to try again.
if (count === filenames.length) {
resolve(sourceFiles);
}
continue;
}

readFile(filename, (err: Error | null, data: Buffer) => {
const content = err ? null : data.toString();
sourceFiles[filename] = content;

// We always want to set the cache, even to null which means there was an error reading the file.
// We do not want to try to read the file again.
FILE_CONTENT_CACHE.set(filename, content);
// eslint-disable-next-line no-plusplus
count++;
if (count === filenames.length) {
resolve(sourceFiles);
}
});
}
});
}
Loading