contextlines.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { _optionalChain } from '@sentry/utils';
  2. import { readFile } from 'fs';
  3. import { defineIntegration, convertIntegrationFnToClass } from '@sentry/core';
  4. import { LRUMap, addContextToFrame } from '@sentry/utils';
  5. const FILE_CONTENT_CACHE = new LRUMap(100);
  6. const DEFAULT_LINES_OF_CONTEXT = 7;
  7. const INTEGRATION_NAME = 'ContextLines';
  8. // TODO: Replace with promisify when minimum supported node >= v8
  9. function readTextFileAsync(path) {
  10. return new Promise((resolve, reject) => {
  11. readFile(path, 'utf8', (err, data) => {
  12. if (err) reject(err);
  13. else resolve(data);
  14. });
  15. });
  16. }
  17. const _contextLinesIntegration = ((options = {}) => {
  18. const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
  19. return {
  20. name: INTEGRATION_NAME,
  21. // TODO v8: Remove this
  22. setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
  23. processEvent(event) {
  24. return addSourceContext(event, contextLines);
  25. },
  26. };
  27. }) ;
  28. const contextLinesIntegration = defineIntegration(_contextLinesIntegration);
  29. /**
  30. * Add node modules / packages to the event.
  31. * @deprecated Use `contextLinesIntegration()` instead.
  32. */
  33. // eslint-disable-next-line deprecation/deprecation
  34. const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration)
  35. ;
  36. async function addSourceContext(event, contextLines) {
  37. // keep a lookup map of which files we've already enqueued to read,
  38. // so we don't enqueue the same file multiple times which would cause multiple i/o reads
  39. const enqueuedReadSourceFileTasks = {};
  40. const readSourceFileTasks = [];
  41. if (contextLines > 0 && _optionalChain([event, 'access', _2 => _2.exception, 'optionalAccess', _3 => _3.values])) {
  42. for (const exception of event.exception.values) {
  43. if (!_optionalChain([exception, 'access', _4 => _4.stacktrace, 'optionalAccess', _5 => _5.frames])) {
  44. continue;
  45. }
  46. // We want to iterate in reverse order as calling cache.get will bump the file in our LRU cache.
  47. // This ends up prioritizes source context for frames at the top of the stack instead of the bottom.
  48. for (let i = exception.stacktrace.frames.length - 1; i >= 0; i--) {
  49. const frame = exception.stacktrace.frames[i];
  50. // Call cache.get to bump the file to the top of the cache and ensure we have not already
  51. // enqueued a read operation for this filename
  52. if (frame.filename && !enqueuedReadSourceFileTasks[frame.filename] && !FILE_CONTENT_CACHE.get(frame.filename)) {
  53. readSourceFileTasks.push(_readSourceFile(frame.filename));
  54. enqueuedReadSourceFileTasks[frame.filename] = 1;
  55. }
  56. }
  57. }
  58. }
  59. // check if files to read > 0, if so, await all of them to be read before adding source contexts.
  60. // Normally, Promise.all here could be short circuited if one of the promises rejects, but we
  61. // are guarding from that by wrapping the i/o read operation in a try/catch.
  62. if (readSourceFileTasks.length > 0) {
  63. await Promise.all(readSourceFileTasks);
  64. }
  65. // Perform the same loop as above, but this time we can assume all files are in the cache
  66. // and attempt to add source context to frames.
  67. if (contextLines > 0 && _optionalChain([event, 'access', _6 => _6.exception, 'optionalAccess', _7 => _7.values])) {
  68. for (const exception of event.exception.values) {
  69. if (exception.stacktrace && exception.stacktrace.frames) {
  70. await addSourceContextToFrames(exception.stacktrace.frames, contextLines);
  71. }
  72. }
  73. }
  74. return event;
  75. }
  76. /** Adds context lines to frames */
  77. function addSourceContextToFrames(frames, contextLines) {
  78. for (const frame of frames) {
  79. // Only add context if we have a filename and it hasn't already been added
  80. if (frame.filename && frame.context_line === undefined) {
  81. const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename);
  82. if (sourceFileLines) {
  83. try {
  84. addContextToFrame(sourceFileLines, frame, contextLines);
  85. } catch (e) {
  86. // anomaly, being defensive in case
  87. // unlikely to ever happen in practice but can definitely happen in theory
  88. }
  89. }
  90. }
  91. }
  92. }
  93. // eslint-disable-next-line deprecation/deprecation
  94. /**
  95. * Reads file contents and caches them in a global LRU cache.
  96. * If reading fails, mark the file as null in the cache so we don't try again.
  97. *
  98. * @param filename filepath to read content from.
  99. */
  100. async function _readSourceFile(filename) {
  101. const cachedFile = FILE_CONTENT_CACHE.get(filename);
  102. // We have already attempted to read this file and failed, do not try again
  103. if (cachedFile === null) {
  104. return null;
  105. }
  106. // We have a cache hit, return it
  107. if (cachedFile !== undefined) {
  108. return cachedFile;
  109. }
  110. // Guard from throwing if readFile fails, this enables us to use Promise.all and
  111. // not have it short circuiting if one of the promises rejects + since context lines are added
  112. // on a best effort basis, we want to throw here anyways.
  113. // If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it
  114. let content = null;
  115. try {
  116. const rawFileContents = await readTextFileAsync(filename);
  117. content = rawFileContents.split('\n');
  118. } catch (_) {
  119. // if we fail, we will mark the file as null in the cache and short circuit next time we try to read it
  120. }
  121. FILE_CONTENT_CACHE.set(filename, content);
  122. return content;
  123. }
  124. export { ContextLines, contextLinesIntegration };
  125. //# sourceMappingURL=contextlines.js.map