@sentry/node 8.9.2 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/cjs/index.js +2 -0
  2. package/cjs/index.js.map +1 -1
  3. package/cjs/integrations/anr/index.js +1 -1
  4. package/cjs/integrations/anr/index.js.map +1 -1
  5. package/cjs/integrations/context.js +4 -5
  6. package/cjs/integrations/context.js.map +1 -1
  7. package/cjs/integrations/contextlines.js +334 -69
  8. package/cjs/integrations/contextlines.js.map +1 -1
  9. package/cjs/integrations/local-variables/local-variables-async.js +10 -7
  10. package/cjs/integrations/local-variables/local-variables-async.js.map +1 -1
  11. package/cjs/integrations/local-variables/local-variables-sync.js +9 -5
  12. package/cjs/integrations/local-variables/local-variables-sync.js.map +1 -1
  13. package/cjs/otel/instrument.js +3 -2
  14. package/cjs/otel/instrument.js.map +1 -1
  15. package/cjs/proxy/parse-proxy-response.js +1 -1
  16. package/cjs/proxy/parse-proxy-response.js.map +1 -1
  17. package/cjs/sdk/index.js +1 -0
  18. package/cjs/sdk/index.js.map +1 -1
  19. package/cjs/sdk/initOtel.js +1 -1
  20. package/cjs/sdk/initOtel.js.map +1 -1
  21. package/cjs/transports/http.js +1 -1
  22. package/cjs/transports/http.js.map +1 -1
  23. package/cjs/utils/redisCache.js +3 -2
  24. package/cjs/utils/redisCache.js.map +1 -1
  25. package/esm/index.js +1 -0
  26. package/esm/index.js.map +1 -1
  27. package/esm/integrations/anr/index.js +1 -1
  28. package/esm/integrations/anr/index.js.map +1 -1
  29. package/esm/integrations/anr/worker-script.js +2 -2
  30. package/esm/integrations/context.js +4 -5
  31. package/esm/integrations/context.js.map +1 -1
  32. package/esm/integrations/contextlines.js +334 -72
  33. package/esm/integrations/contextlines.js.map +1 -1
  34. package/esm/integrations/local-variables/local-variables-async.js +10 -7
  35. package/esm/integrations/local-variables/local-variables-async.js.map +1 -1
  36. package/esm/integrations/local-variables/local-variables-sync.js +9 -5
  37. package/esm/integrations/local-variables/local-variables-sync.js.map +1 -1
  38. package/esm/integrations/local-variables/worker-script.js +2 -2
  39. package/esm/otel/instrument.js +3 -2
  40. package/esm/otel/instrument.js.map +1 -1
  41. package/esm/proxy/parse-proxy-response.js +1 -1
  42. package/esm/proxy/parse-proxy-response.js.map +1 -1
  43. package/esm/sdk/index.js +2 -1
  44. package/esm/sdk/index.js.map +1 -1
  45. package/esm/sdk/initOtel.js +1 -1
  46. package/esm/sdk/initOtel.js.map +1 -1
  47. package/esm/transports/http.js +1 -1
  48. package/esm/transports/http.js.map +1 -1
  49. package/esm/utils/redisCache.js +3 -2
  50. package/esm/utils/redisCache.js.map +1 -1
  51. package/package.json +5 -5
  52. package/types/index.d.ts +1 -0
  53. package/types/index.d.ts.map +1 -1
  54. package/types/integrations/context.d.ts.map +1 -1
  55. package/types/integrations/contextlines.d.ts +11 -6
  56. package/types/integrations/contextlines.d.ts.map +1 -1
  57. package/types/integrations/local-variables/local-variables-async.d.ts.map +1 -1
  58. package/types/integrations/local-variables/local-variables-sync.d.ts.map +1 -1
  59. package/types/otel/instrument.d.ts.map +1 -1
  60. package/types/sdk/index.d.ts.map +1 -1
  61. package/types/utils/redisCache.d.ts.map +1 -1
  62. package/types-ts3.8/index.d.ts +1 -0
  63. package/types-ts3.8/integrations/contextlines.d.ts +11 -6
@@ -5,133 +5,398 @@ var {
5
5
  Object.defineProperty(exports, '__esModule', { value: true });
6
6
 
7
7
  const node_fs = require('node:fs');
8
+ const node_readline = require('node:readline');
8
9
  const core = require('@sentry/core');
9
10
  const utils = require('@sentry/utils');
11
+ const debugBuild = require('../debug-build.js');
10
12
 
11
- const FILE_CONTENT_CACHE = new utils.LRUMap(100);
13
+ const LRU_FILE_CONTENTS_CACHE = new utils.LRUMap(10);
14
+ const LRU_FILE_CONTENTS_FS_READ_FAILED = new utils.LRUMap(20);
12
15
  const DEFAULT_LINES_OF_CONTEXT = 7;
13
16
  const INTEGRATION_NAME = 'ContextLines';
17
+ // Determines the upper bound of lineno/colno that we will attempt to read. Large colno values are likely to be
18
+ // minified code while large lineno values are likely to be bundled code.
19
+ // Exported for testing purposes.
20
+ const MAX_CONTEXTLINES_COLNO = 1000;
21
+ const MAX_CONTEXTLINES_LINENO = 10000;
14
22
 
15
- const readFileAsync = node_fs.promises.readFile;
23
+ /**
24
+ * Get or init map value
25
+ */
26
+ function emplace(map, key, contents) {
27
+ const value = map.get(key);
16
28
 
17
- /** Exported only for tests, as a type-safe variant. */
18
- const _contextLinesIntegration = ((options = {}) => {
19
- const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
29
+ if (value === undefined) {
30
+ map.set(key, contents);
31
+ return contents;
32
+ }
20
33
 
21
- return {
22
- name: INTEGRATION_NAME,
23
- processEvent(event) {
24
- return addSourceContext(event, contextLines);
25
- },
26
- };
27
- }) ;
34
+ return value;
35
+ }
28
36
 
29
37
  /**
30
- * Capture the lines before and after the frame's context.
38
+ * Determines if context lines should be skipped for a file.
39
+ * - .min.(mjs|cjs|js) files are and not useful since they dont point to the original source
40
+ * - node: prefixed modules are part of the runtime and cannot be resolved to a file
41
+ * - data: skip json, wasm and inline js https://nodejs.org/api/esm.html#data-imports
31
42
  */
32
- const contextLinesIntegration = core.defineIntegration(_contextLinesIntegration);
43
+ function shouldSkipContextLinesForFile(path) {
44
+ // Test the most common prefix and extension first. These are the ones we
45
+ // are most likely to see in user applications and are the ones we can break out of first.
46
+ if (path.startsWith('node:')) return true;
47
+ if (path.endsWith('.min.js')) return true;
48
+ if (path.endsWith('.min.cjs')) return true;
49
+ if (path.endsWith('.min.mjs')) return true;
50
+ if (path.startsWith('data:')) return true;
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * Determines if we should skip contextlines based off the max lineno and colno values.
56
+ */
57
+ function shouldSkipContextLinesForFrame(frame) {
58
+ if (frame.lineno !== undefined && frame.lineno > MAX_CONTEXTLINES_LINENO) return true;
59
+ if (frame.colno !== undefined && frame.colno > MAX_CONTEXTLINES_COLNO) return true;
60
+ return false;
61
+ }
62
+ /**
63
+ * Checks if we have all the contents that we need in the cache.
64
+ */
65
+ function rangeExistsInContentCache(file, range) {
66
+ const contents = LRU_FILE_CONTENTS_CACHE.get(file);
67
+ if (contents === undefined) return false;
68
+
69
+ for (let i = range[0]; i <= range[1]; i++) {
70
+ if (contents[i] === undefined) {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ return true;
76
+ }
77
+
78
+ /**
79
+ * Creates contiguous ranges of lines to read from a file. In the case where context lines overlap,
80
+ * the ranges are merged to create a single range.
81
+ */
82
+ function makeLineReaderRanges(lines, linecontext) {
83
+ if (!lines.length) {
84
+ return [];
85
+ }
86
+
87
+ let i = 0;
88
+ const line = lines[0];
89
+
90
+ if (typeof line !== 'number') {
91
+ return [];
92
+ }
93
+
94
+ let current = makeContextRange(line, linecontext);
95
+ const out = [];
96
+ // eslint-disable-next-line no-constant-condition
97
+ while (true) {
98
+ if (i === lines.length - 1) {
99
+ out.push(current);
100
+ break;
101
+ }
102
+
103
+ // If the next line falls into the current range, extend the current range to lineno + linecontext.
104
+ const next = lines[i + 1];
105
+ if (typeof next !== 'number') {
106
+ break;
107
+ }
108
+ if (next <= current[1]) {
109
+ current[1] = next + linecontext;
110
+ } else {
111
+ out.push(current);
112
+ current = makeContextRange(next, linecontext);
113
+ }
114
+
115
+ i++;
116
+ }
117
+
118
+ return out;
119
+ }
120
+
121
+ /**
122
+ * Extracts lines from a file and stores them in a cache.
123
+ */
124
+ function getContextLinesFromFile(path, ranges, output) {
125
+ return new Promise((resolve, _reject) => {
126
+ // It is important *not* to have any async code between createInterface and the 'line' event listener
127
+ // as it will cause the 'line' event to
128
+ // be emitted before the listener is attached.
129
+ const stream = node_fs.createReadStream(path);
130
+ const lineReaded = node_readline.createInterface({
131
+ input: stream,
132
+ });
133
+
134
+ // Init at zero and increment at the start of the loop because lines are 1 indexed.
135
+ let lineNumber = 0;
136
+ let currentRangeIndex = 0;
137
+ const range = ranges[currentRangeIndex];
138
+ if (range === undefined) {
139
+ // We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging.
140
+ resolve();
141
+ return;
142
+ }
143
+ let rangeStart = range[0];
144
+ let rangeEnd = range[1];
145
+
146
+ // We use this inside Promise.all, so we need to resolve the promise even if there is an error
147
+ // to prevent Promise.all from short circuiting the rest.
148
+ function onStreamError(e) {
149
+ // Mark file path as failed to read and prevent multiple read attempts.
150
+ LRU_FILE_CONTENTS_FS_READ_FAILED.set(path, 1);
151
+ debugBuild.DEBUG_BUILD && utils.logger.error(`Failed to read file: ${path}. Error: ${e}`);
152
+ lineReaded.close();
153
+ lineReaded.removeAllListeners();
154
+ resolve();
155
+ }
156
+
157
+ // We need to handle the error event to prevent the process from crashing in < Node 16
158
+ // https://github.com/nodejs/node/pull/31603
159
+ stream.on('error', onStreamError);
160
+ lineReaded.on('error', onStreamError);
161
+ lineReaded.on('close', resolve);
162
+
163
+ lineReaded.on('line', line => {
164
+ lineNumber++;
165
+ if (lineNumber < rangeStart) return;
33
166
 
167
+ // !Warning: This mutates the cache by storing the snipped line into the cache.
168
+ output[lineNumber] = utils.snipLine(line, 0);
169
+
170
+ if (lineNumber >= rangeEnd) {
171
+ if (currentRangeIndex === ranges.length - 1) {
172
+ // We need to close the file stream and remove listeners, else the reader will continue to run our listener;
173
+ lineReaded.close();
174
+ lineReaded.removeAllListeners();
175
+ return;
176
+ }
177
+ currentRangeIndex++;
178
+ const range = ranges[currentRangeIndex];
179
+ if (range === undefined) {
180
+ // This should never happen as it means we have a bug in the context.
181
+ lineReaded.close();
182
+ lineReaded.removeAllListeners();
183
+ return;
184
+ }
185
+ rangeStart = range[0];
186
+ rangeEnd = range[1];
187
+ }
188
+ });
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Adds surrounding (context) lines of the line that an exception occurred on to the event.
194
+ * This is done by reading the file line by line and extracting the lines. The extracted lines are stored in
195
+ * a cache to prevent multiple reads of the same file. Failures to read a file are similarly cached to prevent multiple
196
+ * failing reads from happening.
197
+ */
198
+ /* eslint-disable complexity */
34
199
  async function addSourceContext(event, contextLines) {
35
200
  // keep a lookup map of which files we've already enqueued to read,
36
201
  // so we don't enqueue the same file multiple times which would cause multiple i/o reads
37
- const enqueuedReadSourceFileTasks = {};
38
- const readSourceFileTasks = [];
202
+ const filesToLines = {};
39
203
 
40
- if (contextLines > 0 && _optionalChain([event, 'access', _2 => _2.exception, 'optionalAccess', _3 => _3.values])) {
204
+ if (contextLines > 0 && _optionalChain([event, 'access', _ => _.exception, 'optionalAccess', _2 => _2.values])) {
41
205
  for (const exception of event.exception.values) {
42
- if (!_optionalChain([exception, 'access', _4 => _4.stacktrace, 'optionalAccess', _5 => _5.frames])) {
206
+ if (!_optionalChain([exception, 'access', _3 => _3.stacktrace, 'optionalAccess', _4 => _4.frames, 'optionalAccess', _5 => _5.length])) {
43
207
  continue;
44
208
  }
45
209
 
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.
210
+ // Maps preserve insertion order, so we iterate in reverse, starting at the
211
+ // outermost frame and closer to where the exception has occurred (poor mans priority)
48
212
  for (let i = exception.stacktrace.frames.length - 1; i >= 0; i--) {
49
213
  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;
214
+ const filename = _optionalChain([frame, 'optionalAccess', _6 => _6.filename]);
215
+
216
+ if (
217
+ !frame ||
218
+ typeof filename !== 'string' ||
219
+ typeof frame.lineno !== 'number' ||
220
+ shouldSkipContextLinesForFile(filename) ||
221
+ shouldSkipContextLinesForFrame(frame)
222
+ ) {
223
+ continue;
55
224
  }
225
+
226
+ const filesToLinesOutput = filesToLines[filename];
227
+ if (!filesToLinesOutput) filesToLines[filename] = [];
228
+ // @ts-expect-error this is defined above
229
+ filesToLines[filename].push(frame.lineno);
56
230
  }
57
231
  }
58
232
  }
59
233
 
60
- // check if files to read > 0, if so, await all of them to be read before adding source contexts.
61
- // Normally, Promise.all here could be short circuited if one of the promises rejects, but we
62
- // are guarding from that by wrapping the i/o read operation in a try/catch.
63
- if (readSourceFileTasks.length > 0) {
64
- await Promise.all(readSourceFileTasks);
234
+ const files = Object.keys(filesToLines);
235
+ if (files.length == 0) {
236
+ return event;
237
+ }
238
+
239
+ const readlinePromises = [];
240
+ for (const file of files) {
241
+ // If we failed to read this before, dont try reading it again.
242
+ if (LRU_FILE_CONTENTS_FS_READ_FAILED.get(file)) {
243
+ continue;
244
+ }
245
+
246
+ const filesToLineRanges = filesToLines[file];
247
+ if (!filesToLineRanges) {
248
+ continue;
249
+ }
250
+
251
+ // Sort ranges so that they are sorted by line increasing order and match how the file is read.
252
+ filesToLineRanges.sort((a, b) => a - b);
253
+ // Check if the contents are already in the cache and if we can avoid reading the file again.
254
+ const ranges = makeLineReaderRanges(filesToLineRanges, contextLines);
255
+ if (ranges.every(r => rangeExistsInContentCache(file, r))) {
256
+ continue;
257
+ }
258
+
259
+ const cache = emplace(LRU_FILE_CONTENTS_CACHE, file, {});
260
+ readlinePromises.push(getContextLinesFromFile(file, ranges, cache));
65
261
  }
66
262
 
263
+ // The promise rejections are caught in order to prevent them from short circuiting Promise.all
264
+ await Promise.all(readlinePromises).catch(() => {
265
+ debugBuild.DEBUG_BUILD && utils.logger.log('Failed to read one or more source files and resolve context lines');
266
+ });
267
+
67
268
  // Perform the same loop as above, but this time we can assume all files are in the cache
68
269
  // and attempt to add source context to frames.
69
- if (contextLines > 0 && _optionalChain([event, 'access', _6 => _6.exception, 'optionalAccess', _7 => _7.values])) {
270
+ if (contextLines > 0 && _optionalChain([event, 'access', _7 => _7.exception, 'optionalAccess', _8 => _8.values])) {
70
271
  for (const exception of event.exception.values) {
71
- if (exception.stacktrace && exception.stacktrace.frames) {
72
- await addSourceContextToFrames(exception.stacktrace.frames, contextLines);
272
+ if (exception.stacktrace && exception.stacktrace.frames && exception.stacktrace.frames.length > 0) {
273
+ addSourceContextToFrames(exception.stacktrace.frames, contextLines, LRU_FILE_CONTENTS_CACHE);
73
274
  }
74
275
  }
75
276
  }
76
277
 
77
278
  return event;
78
279
  }
280
+ /* eslint-enable complexity */
79
281
 
80
282
  /** Adds context lines to frames */
81
- function addSourceContextToFrames(frames, contextLines) {
283
+ function addSourceContextToFrames(
284
+ frames,
285
+ contextLines,
286
+ cache,
287
+ ) {
82
288
  for (const frame of frames) {
83
289
  // Only add context if we have a filename and it hasn't already been added
84
- if (frame.filename && frame.context_line === undefined) {
85
- const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename);
86
-
87
- if (sourceFileLines) {
88
- try {
89
- utils.addContextToFrame(sourceFileLines, frame, contextLines);
90
- } catch (e) {
91
- // anomaly, being defensive in case
92
- // unlikely to ever happen in practice but can definitely happen in theory
93
- }
290
+ if (frame.filename && frame.context_line === undefined && typeof frame.lineno === 'number') {
291
+ const contents = cache.get(frame.filename);
292
+ if (contents === undefined) {
293
+ continue;
94
294
  }
295
+
296
+ addContextToFrame(frame.lineno, frame, contextLines, contents);
95
297
  }
96
298
  }
97
299
  }
98
300
 
99
301
  /**
100
- * Reads file contents and caches them in a global LRU cache.
101
- * If reading fails, mark the file as null in the cache so we don't try again.
102
- *
103
- * @param filename filepath to read content from.
302
+ * Clears the context lines from a frame, used to reset a frame to its original state
303
+ * if we fail to resolve all context lines for it.
304
+ */
305
+ function clearLineContext(frame) {
306
+ delete frame.pre_context;
307
+ delete frame.context_line;
308
+ delete frame.post_context;
309
+ }
310
+
311
+ /**
312
+ * Resolves context lines before and after the given line number and appends them to the frame;
104
313
  */
105
- async function _readSourceFile(filename) {
106
- const cachedFile = FILE_CONTENT_CACHE.get(filename);
314
+ function addContextToFrame(
315
+ lineno,
316
+ frame,
317
+ contextLines,
318
+ contents,
319
+ ) {
320
+ // When there is no line number in the frame, attaching context is nonsensical and will even break grouping.
321
+ // We already check for lineno before calling this, but since StackFrame lineno ism optional, we check it again.
322
+ if (frame.lineno === undefined || contents === undefined) {
323
+ debugBuild.DEBUG_BUILD && utils.logger.error('Cannot resolve context for frame with no lineno or file contents');
324
+ return;
325
+ }
326
+
327
+ frame.pre_context = [];
328
+ for (let i = makeRangeStart(lineno, contextLines); i < lineno; i++) {
329
+ // We always expect the start context as line numbers cannot be negative. If we dont find a line, then
330
+ // something went wrong somewhere. Clear the context and return without adding any linecontext.
331
+ const line = contents[i];
332
+ if (line === undefined) {
333
+ clearLineContext(frame);
334
+ debugBuild.DEBUG_BUILD && utils.logger.error(`Could not find line ${i} in file ${frame.filename}`);
335
+ return;
336
+ }
107
337
 
108
- // We have already attempted to read this file and failed, do not try again
109
- if (cachedFile === null) {
110
- return null;
338
+ frame.pre_context.push(line);
111
339
  }
112
340
 
113
- // We have a cache hit, return it
114
- if (cachedFile !== undefined) {
115
- return cachedFile;
341
+ // We should always have the context line. If we dont, something went wrong, so we clear the context and return
342
+ // without adding any linecontext.
343
+ if (contents[lineno] === undefined) {
344
+ clearLineContext(frame);
345
+ debugBuild.DEBUG_BUILD && utils.logger.error(`Could not find line ${lineno} in file ${frame.filename}`);
346
+ return;
116
347
  }
117
348
 
118
- // Guard from throwing if readFile fails, this enables us to use Promise.all and
119
- // not have it short circuiting if one of the promises rejects + since context lines are added
120
- // on a best effort basis, we want to throw here anyways.
121
-
122
- // If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it
123
- let content = null;
124
- try {
125
- const rawFileContents = await readFileAsync(filename, 'utf-8');
126
- content = rawFileContents.split('\n');
127
- } catch (_) {
128
- // if we fail, we will mark the file as null in the cache and short circuit next time we try to read it
349
+ frame.context_line = contents[lineno];
350
+
351
+ const end = makeRangeEnd(lineno, contextLines);
352
+ frame.post_context = [];
353
+ for (let i = lineno + 1; i <= end; i++) {
354
+ // Since we dont track when the file ends, we cant clear the context if we dont find a line as it could
355
+ // just be that we reached the end of the file.
356
+ const line = contents[i];
357
+ if (line === undefined) {
358
+ break;
359
+ }
360
+ frame.post_context.push(line);
129
361
  }
362
+ }
363
+
364
+ // Helper functions for generating line context ranges. They take a line number and the number of lines of context to
365
+ // include before and after the line and generate an inclusive range of indices.
130
366
 
131
- FILE_CONTENT_CACHE.set(filename, content);
132
- return content;
367
+ // Compute inclusive end context range
368
+ function makeRangeStart(line, linecontext) {
369
+ return Math.max(1, line - linecontext);
133
370
  }
371
+ // Compute inclusive start context range
372
+ function makeRangeEnd(line, linecontext) {
373
+ return line + linecontext;
374
+ }
375
+ // Determine start and end indices for context range (inclusive);
376
+ function makeContextRange(line, linecontext) {
377
+ return [makeRangeStart(line, linecontext), makeRangeEnd(line, linecontext)];
378
+ }
379
+
380
+ /** Exported only for tests, as a type-safe variant. */
381
+ const _contextLinesIntegration = ((options = {}) => {
382
+ const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
383
+
384
+ return {
385
+ name: INTEGRATION_NAME,
386
+ processEvent(event) {
387
+ return addSourceContext(event, contextLines);
388
+ },
389
+ };
390
+ }) ;
391
+
392
+ /**
393
+ * Capture the lines before and after the frame's context.
394
+ */
395
+ const contextLinesIntegration = core.defineIntegration(_contextLinesIntegration);
134
396
 
397
+ exports.MAX_CONTEXTLINES_COLNO = MAX_CONTEXTLINES_COLNO;
398
+ exports.MAX_CONTEXTLINES_LINENO = MAX_CONTEXTLINES_LINENO;
135
399
  exports._contextLinesIntegration = _contextLinesIntegration;
400
+ exports.addContextToFrame = addContextToFrame;
136
401
  exports.contextLinesIntegration = contextLinesIntegration;
137
402
  //# sourceMappingURL=contextlines.js.map