local-variables-sync.js 13 KB

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