@sentry/node
Version:
Official Sentry SDK for Node.js
165 lines (140 loc) • 5.78 kB
JavaScript
var {
_optionalChain
} = require('@sentry/utils/cjs/buildPolyfills');
Object.defineProperty(exports, '__esModule', { value: true });
const utils = require('@sentry/utils');
const fs = require('fs');
const lru_map = require('lru_map');
const FILE_CONTENT_CACHE = new lru_map.LRUMap(100);
const DEFAULT_LINES_OF_CONTEXT = 7;
// TODO: Replace with promisify when minimum supported node >= v8
function readTextFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
/** Add node modules / packages to the event */
class ContextLines {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'ContextLines';}
/**
* @inheritDoc
*/
__init() {this.name = ContextLines.id;}
constructor( _options = {}) {this._options = _options;ContextLines.prototype.__init.call(this);}
/** Get's the number of context lines to add */
get _contextLines() {
return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
}
/**
* @inheritDoc
*/
setupOnce(addGlobalEventProcessor, getCurrentHub) {
addGlobalEventProcessor(event => {
const self = getCurrentHub().getIntegration(ContextLines);
if (!self) {
return event;
}
return this.addSourceContext(event);
});
}
/** Processes an event and adds context lines */
async addSourceContext(event) {
// keep a lookup map of which files we've already enqueued to read,
// so we don't enqueue the same file multiple times which would cause multiple i/o reads
const enqueuedReadSourceFileTasks = {};
const readSourceFileTasks = [];
if (this._contextLines > 0 && _optionalChain([event, 'access', _2 => _2.exception, 'optionalAccess', _3 => _3.values])) {
for (const exception of event.exception.values) {
if (!_optionalChain([exception, 'access', _4 => _4.stacktrace, 'optionalAccess', _5 => _5.frames])) {
continue;
}
// We want to iterate in reverse order as calling cache.get will bump the file in our LRU cache.
// This ends up prioritizes source context for frames at the top of the stack instead of the bottom.
for (let i = exception.stacktrace.frames.length - 1; i >= 0; i--) {
const frame = exception.stacktrace.frames[i];
// Call cache.get to bump the file to the top of the cache and ensure we have not already
// enqueued a read operation for this filename
if (
frame.filename &&
!enqueuedReadSourceFileTasks[frame.filename] &&
!FILE_CONTENT_CACHE.get(frame.filename)
) {
readSourceFileTasks.push(_readSourceFile(frame.filename));
enqueuedReadSourceFileTasks[frame.filename] = 1;
}
}
}
}
// check if files to read > 0, if so, await all of them to be read before adding source contexts.
// Normally, Promise.all here could be short circuited if one of the promises rejects, but we
// are guarding from that by wrapping the i/o read operation in a try/catch.
if (readSourceFileTasks.length > 0) {
await Promise.all(readSourceFileTasks);
}
// Perform the same loop as above, but this time we can assume all files are in the cache
// and attempt to add source context to frames.
if (this._contextLines > 0 && _optionalChain([event, 'access', _6 => _6.exception, 'optionalAccess', _7 => _7.values])) {
for (const exception of event.exception.values) {
if (exception.stacktrace && exception.stacktrace.frames) {
await this.addSourceContextToFrames(exception.stacktrace.frames);
}
}
}
return event;
}
/** Adds context lines to frames */
addSourceContextToFrames(frames) {
for (const frame of frames) {
// Only add context if we have a filename and it hasn't already been added
if (frame.filename && frame.context_line === undefined) {
const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename);
if (sourceFileLines) {
try {
utils.addContextToFrame(sourceFileLines, frame, this._contextLines);
} catch (e) {
// anomaly, being defensive in case
// unlikely to ever happen in practice but can definitely happen in theory
}
}
}
}
}
}ContextLines.__initStatic();
/**
* Reads file contents and caches them in a global LRU cache.
* If reading fails, mark the file as null in the cache so we don't try again.
*
* @param filename filepath to read content from.
*/
async function _readSourceFile(filename) {
const cachedFile = FILE_CONTENT_CACHE.get(filename);
// We have already attempted to read this file and failed, do not try again
if (cachedFile === null) {
return null;
}
// We have a cache hit, return it
if (cachedFile !== undefined) {
return cachedFile;
}
// Guard from throwing if readFile fails, this enables us to use Promise.all and
// not have it short circuiting if one of the promises rejects + since context lines are added
// on a best effort basis, we want to throw here anyways.
// If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it
let content = null;
try {
const rawFileContents = await readTextFileAsync(filename);
content = rawFileContents.split('\n');
} catch (_) {
// if we fail, we will mark the file as null in the cache and short circuit next time we try to read it
}
FILE_CONTENT_CACHE.set(filename, content);
return content;
}
exports.ContextLines = ContextLines;
//# sourceMappingURL=contextlines.js.map