local-variables-sync.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import { _optionalChain } from '@sentry/utils';
  2. import { defineIntegration, convertIntegrationFnToClass, getClient } from '@sentry/core';
  3. import { LRUMap, logger } from '@sentry/utils';
  4. import { NODE_VERSION } from '../../nodeVersion.js';
  5. import { createRateLimiter, hashFromStack, hashFrames, functionNamesMatch } from './common.js';
  6. /* eslint-disable max-lines */
  7. /** Creates a container for callbacks to be called sequentially */
  8. function createCallbackList(complete) {
  9. // A collection of callbacks to be executed last to first
  10. let callbacks = [];
  11. let completedCalled = false;
  12. function checkedComplete(result) {
  13. callbacks = [];
  14. if (completedCalled) {
  15. return;
  16. }
  17. completedCalled = true;
  18. complete(result);
  19. }
  20. // complete should be called last
  21. callbacks.push(checkedComplete);
  22. function add(fn) {
  23. callbacks.push(fn);
  24. }
  25. function next(result) {
  26. const popped = callbacks.pop() || checkedComplete;
  27. try {
  28. popped(result);
  29. } catch (_) {
  30. // If there is an error, we still want to call the complete callback
  31. checkedComplete(result);
  32. }
  33. }
  34. return { add, next };
  35. }
  36. /**
  37. * Promise API is available as `Experimental` and in Node 19 only.
  38. *
  39. * Callback-based API is `Stable` since v14 and `Experimental` since v8.
  40. * Because of that, we are creating our own `AsyncSession` class.
  41. *
  42. * https://nodejs.org/docs/latest-v19.x/api/inspector.html#promises-api
  43. * https://nodejs.org/docs/latest-v14.x/api/inspector.html
  44. */
  45. class AsyncSession {
  46. /** Throws if inspector API is not available */
  47. constructor() {
  48. /*
  49. TODO: We really should get rid of this require statement below for a couple of reasons:
  50. 1. It makes the integration unusable in the SvelteKit SDK, as it's not possible to use `require`
  51. in SvelteKit server code (at least not by default).
  52. 2. Throwing in a constructor is bad practice
  53. More context for a future attempt to fix this:
  54. We already tried replacing it with import but didn't get it to work because of async problems.
  55. We still called import in the constructor but assigned to a promise which we "awaited" in
  56. `configureAndConnect`. However, this broke the Node integration tests as no local variables
  57. were reported any more. We probably missed a place where we need to await the promise, too.
  58. */
  59. // Node can be built without inspector support so this can throw
  60. // eslint-disable-next-line @typescript-eslint/no-var-requires
  61. const { Session } = require('inspector');
  62. this._session = new Session();
  63. }
  64. /** @inheritdoc */
  65. configureAndConnect(onPause, captureAll) {
  66. this._session.connect();
  67. this._session.on('Debugger.paused', event => {
  68. onPause(event, () => {
  69. // After the pause work is complete, resume execution or the exception context memory is leaked
  70. this._session.post('Debugger.resume');
  71. });
  72. });
  73. this._session.post('Debugger.enable');
  74. this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' });
  75. }
  76. setPauseOnExceptions(captureAll) {
  77. this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' });
  78. }
  79. /** @inheritdoc */
  80. getLocalVariables(objectId, complete) {
  81. this._getProperties(objectId, props => {
  82. const { add, next } = createCallbackList(complete);
  83. for (const prop of props) {
  84. if (_optionalChain([prop, 'optionalAccess', _2 => _2.value, 'optionalAccess', _3 => _3.objectId]) && _optionalChain([prop, 'optionalAccess', _4 => _4.value, 'access', _5 => _5.className]) === 'Array') {
  85. const id = prop.value.objectId;
  86. add(vars => this._unrollArray(id, prop.name, vars, next));
  87. } else if (_optionalChain([prop, 'optionalAccess', _6 => _6.value, 'optionalAccess', _7 => _7.objectId]) && _optionalChain([prop, 'optionalAccess', _8 => _8.value, 'optionalAccess', _9 => _9.className]) === 'Object') {
  88. const id = prop.value.objectId;
  89. add(vars => this._unrollObject(id, prop.name, vars, next));
  90. } else if (_optionalChain([prop, 'optionalAccess', _10 => _10.value, 'optionalAccess', _11 => _11.value]) || _optionalChain([prop, 'optionalAccess', _12 => _12.value, 'optionalAccess', _13 => _13.description])) {
  91. add(vars => this._unrollOther(prop, vars, next));
  92. }
  93. }
  94. next({});
  95. });
  96. }
  97. /**
  98. * Gets all the PropertyDescriptors of an object
  99. */
  100. _getProperties(objectId, next) {
  101. this._session.post(
  102. 'Runtime.getProperties',
  103. {
  104. objectId,
  105. ownProperties: true,
  106. },
  107. (err, params) => {
  108. if (err) {
  109. next([]);
  110. } else {
  111. next(params.result);
  112. }
  113. },
  114. );
  115. }
  116. /**
  117. * Unrolls an array property
  118. */
  119. _unrollArray(objectId, name, vars, next) {
  120. this._getProperties(objectId, props => {
  121. vars[name] = props
  122. .filter(v => v.name !== 'length' && !isNaN(parseInt(v.name, 10)))
  123. .sort((a, b) => parseInt(a.name, 10) - parseInt(b.name, 10))
  124. .map(v => _optionalChain([v, 'optionalAccess', _14 => _14.value, 'optionalAccess', _15 => _15.value]));
  125. next(vars);
  126. });
  127. }
  128. /**
  129. * Unrolls an object property
  130. */
  131. _unrollObject(objectId, name, vars, next) {
  132. this._getProperties(objectId, props => {
  133. vars[name] = props
  134. .map(v => [v.name, _optionalChain([v, 'optionalAccess', _16 => _16.value, 'optionalAccess', _17 => _17.value])])
  135. .reduce((obj, [key, val]) => {
  136. obj[key] = val;
  137. return obj;
  138. }, {} );
  139. next(vars);
  140. });
  141. }
  142. /**
  143. * Unrolls other properties
  144. */
  145. _unrollOther(prop, vars, next) {
  146. if (_optionalChain([prop, 'optionalAccess', _18 => _18.value, 'optionalAccess', _19 => _19.value])) {
  147. vars[prop.name] = prop.value.value;
  148. } else if (_optionalChain([prop, 'optionalAccess', _20 => _20.value, 'optionalAccess', _21 => _21.description]) && _optionalChain([prop, 'optionalAccess', _22 => _22.value, 'optionalAccess', _23 => _23.type]) !== 'function') {
  149. vars[prop.name] = `<${prop.value.description}>`;
  150. }
  151. next(vars);
  152. }
  153. }
  154. /**
  155. * When using Vercel pkg, the inspector module is not available.
  156. * https://github.com/getsentry/sentry-javascript/issues/6769
  157. */
  158. function tryNewAsyncSession() {
  159. try {
  160. return new AsyncSession();
  161. } catch (e) {
  162. return undefined;
  163. }
  164. }
  165. const INTEGRATION_NAME = 'LocalVariables';
  166. /**
  167. * Adds local variables to exception frames
  168. */
  169. const _localVariablesSyncIntegration = ((
  170. options = {},
  171. session = tryNewAsyncSession(),
  172. ) => {
  173. const cachedFrames = new LRUMap(20);
  174. let rateLimiter;
  175. let shouldProcessEvent = false;
  176. function handlePaused(
  177. stackParser,
  178. { params: { reason, data, callFrames } },
  179. complete,
  180. ) {
  181. if (reason !== 'exception' && reason !== 'promiseRejection') {
  182. complete();
  183. return;
  184. }
  185. _optionalChain([rateLimiter, 'optionalCall', _24 => _24()]);
  186. // data.description contains the original error.stack
  187. const exceptionHash = hashFromStack(stackParser, _optionalChain([data, 'optionalAccess', _25 => _25.description]));
  188. if (exceptionHash == undefined) {
  189. complete();
  190. return;
  191. }
  192. const { add, next } = createCallbackList(frames => {
  193. cachedFrames.set(exceptionHash, frames);
  194. complete();
  195. });
  196. // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack
  197. // For this reason we only attempt to get local variables for the first 5 frames
  198. for (let i = 0; i < Math.min(callFrames.length, 5); i++) {
  199. const { scopeChain, functionName, this: obj } = callFrames[i];
  200. const localScope = scopeChain.find(scope => scope.type === 'local');
  201. // obj.className is undefined in ESM modules
  202. const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`;
  203. if (_optionalChain([localScope, 'optionalAccess', _26 => _26.object, 'access', _27 => _27.objectId]) === undefined) {
  204. add(frames => {
  205. frames[i] = { function: fn };
  206. next(frames);
  207. });
  208. } else {
  209. const id = localScope.object.objectId;
  210. add(frames =>
  211. _optionalChain([session, 'optionalAccess', _28 => _28.getLocalVariables, 'call', _29 => _29(id, vars => {
  212. frames[i] = { function: fn, vars };
  213. next(frames);
  214. })]),
  215. );
  216. }
  217. }
  218. next([]);
  219. }
  220. function addLocalVariablesToException(exception) {
  221. const hash = hashFrames(_optionalChain([exception, 'optionalAccess', _30 => _30.stacktrace, 'optionalAccess', _31 => _31.frames]));
  222. if (hash === undefined) {
  223. return;
  224. }
  225. // Check if we have local variables for an exception that matches the hash
  226. // remove is identical to get but also removes the entry from the cache
  227. const cachedFrame = cachedFrames.remove(hash);
  228. if (cachedFrame === undefined) {
  229. return;
  230. }
  231. const frameCount = _optionalChain([exception, 'access', _32 => _32.stacktrace, 'optionalAccess', _33 => _33.frames, 'optionalAccess', _34 => _34.length]) || 0;
  232. for (let i = 0; i < frameCount; i++) {
  233. // Sentry frames are in reverse order
  234. const frameIndex = frameCount - i - 1;
  235. // Drop out if we run out of frames to match up
  236. if (!_optionalChain([exception, 'optionalAccess', _35 => _35.stacktrace, 'optionalAccess', _36 => _36.frames, 'optionalAccess', _37 => _37[frameIndex]]) || !cachedFrame[i]) {
  237. break;
  238. }
  239. if (
  240. // We need to have vars to add
  241. cachedFrame[i].vars === undefined ||
  242. // We're not interested in frames that are not in_app because the vars are not relevant
  243. exception.stacktrace.frames[frameIndex].in_app === false ||
  244. // The function names need to match
  245. !functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrame[i].function)
  246. ) {
  247. continue;
  248. }
  249. exception.stacktrace.frames[frameIndex].vars = cachedFrame[i].vars;
  250. }
  251. }
  252. function addLocalVariablesToEvent(event) {
  253. for (const exception of _optionalChain([event, 'optionalAccess', _38 => _38.exception, 'optionalAccess', _39 => _39.values]) || []) {
  254. addLocalVariablesToException(exception);
  255. }
  256. return event;
  257. }
  258. return {
  259. name: INTEGRATION_NAME,
  260. setupOnce() {
  261. const client = getClient();
  262. const clientOptions = _optionalChain([client, 'optionalAccess', _40 => _40.getOptions, 'call', _41 => _41()]);
  263. if (session && _optionalChain([clientOptions, 'optionalAccess', _42 => _42.includeLocalVariables])) {
  264. // Only setup this integration if the Node version is >= v18
  265. // https://github.com/getsentry/sentry-javascript/issues/7697
  266. const unsupportedNodeVersion = NODE_VERSION.major < 18;
  267. if (unsupportedNodeVersion) {
  268. logger.log('The `LocalVariables` integration is only supported on Node >= v18.');
  269. return;
  270. }
  271. const captureAll = options.captureAllExceptions !== false;
  272. session.configureAndConnect(
  273. (ev, complete) =>
  274. handlePaused(clientOptions.stackParser, ev , complete),
  275. captureAll,
  276. );
  277. if (captureAll) {
  278. const max = options.maxExceptionsPerSecond || 50;
  279. rateLimiter = createRateLimiter(
  280. max,
  281. () => {
  282. logger.log('Local variables rate-limit lifted.');
  283. _optionalChain([session, 'optionalAccess', _43 => _43.setPauseOnExceptions, 'call', _44 => _44(true)]);
  284. },
  285. seconds => {
  286. logger.log(
  287. `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`,
  288. );
  289. _optionalChain([session, 'optionalAccess', _45 => _45.setPauseOnExceptions, 'call', _46 => _46(false)]);
  290. },
  291. );
  292. }
  293. shouldProcessEvent = true;
  294. }
  295. },
  296. processEvent(event) {
  297. if (shouldProcessEvent) {
  298. return addLocalVariablesToEvent(event);
  299. }
  300. return event;
  301. },
  302. // These are entirely for testing
  303. _getCachedFramesCount() {
  304. return cachedFrames.size;
  305. },
  306. _getFirstCachedFrame() {
  307. return cachedFrames.values()[0];
  308. },
  309. };
  310. }) ;
  311. const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration);
  312. /**
  313. * Adds local variables to exception frames.
  314. * @deprecated Use `localVariablesSyncIntegration()` instead.
  315. */
  316. // eslint-disable-next-line deprecation/deprecation
  317. const LocalVariablesSync = convertIntegrationFnToClass(
  318. INTEGRATION_NAME,
  319. localVariablesSyncIntegration,
  320. )
  321. ;
  322. // eslint-disable-next-line deprecation/deprecation
  323. export { LocalVariablesSync, createCallbackList, localVariablesSyncIntegration };
  324. //# sourceMappingURL=local-variables-sync.js.map