onuncaughtexception.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { defineIntegration, convertIntegrationFnToClass, getClient, captureException } from '@sentry/core';
  2. import { logger } from '@sentry/utils';
  3. import { DEBUG_BUILD } from '../debug-build.js';
  4. import { logAndExitProcess } from './utils/errorhandling.js';
  5. const INTEGRATION_NAME = 'OnUncaughtException';
  6. const _onUncaughtExceptionIntegration = ((options = {}) => {
  7. const _options = {
  8. exitEvenIfOtherHandlersAreRegistered: true,
  9. ...options,
  10. };
  11. return {
  12. name: INTEGRATION_NAME,
  13. // TODO v8: Remove this
  14. setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
  15. setup(client) {
  16. global.process.on('uncaughtException', makeErrorHandler(client, _options));
  17. },
  18. };
  19. }) ;
  20. const onUncaughtExceptionIntegration = defineIntegration(_onUncaughtExceptionIntegration);
  21. /**
  22. * Global Exception handler.
  23. * @deprecated Use `onUncaughtExceptionIntegration()` instead.
  24. */
  25. // eslint-disable-next-line deprecation/deprecation
  26. const OnUncaughtException = convertIntegrationFnToClass(
  27. INTEGRATION_NAME,
  28. onUncaughtExceptionIntegration,
  29. )
  30. ;
  31. // eslint-disable-next-line deprecation/deprecation
  32. /** Exported only for tests */
  33. function makeErrorHandler(client, options) {
  34. const timeout = 2000;
  35. let caughtFirstError = false;
  36. let caughtSecondError = false;
  37. let calledFatalError = false;
  38. let firstError;
  39. const clientOptions = client.getOptions();
  40. return Object.assign(
  41. (error) => {
  42. let onFatalError = logAndExitProcess;
  43. if (options.onFatalError) {
  44. onFatalError = options.onFatalError;
  45. } else if (clientOptions.onFatalError) {
  46. onFatalError = clientOptions.onFatalError ;
  47. }
  48. // Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not
  49. // want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust
  50. // exit behaviour of the SDK accordingly:
  51. // - If other listeners are attached, do not exit.
  52. // - If the only listener attached is ours, exit.
  53. const userProvidedListenersCount = (
  54. global.process.listeners('uncaughtException')
  55. ).reduce((acc, listener) => {
  56. if (
  57. // There are 3 listeners we ignore:
  58. listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself
  59. (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing
  60. (listener )._errorHandler // the handler we register in this integration
  61. ) {
  62. return acc;
  63. } else {
  64. return acc + 1;
  65. }
  66. }, 0);
  67. const processWouldExit = userProvidedListenersCount === 0;
  68. const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;
  69. if (!caughtFirstError) {
  70. // this is the first uncaught error and the ultimate reason for shutting down
  71. // we want to do absolutely everything possible to ensure it gets captured
  72. // also we want to make sure we don't go recursion crazy if more errors happen after this one
  73. firstError = error;
  74. caughtFirstError = true;
  75. if (getClient() === client) {
  76. captureException(error, {
  77. originalException: error,
  78. captureContext: {
  79. level: 'fatal',
  80. },
  81. mechanism: {
  82. handled: false,
  83. type: 'onuncaughtexception',
  84. },
  85. });
  86. }
  87. if (!calledFatalError && shouldApplyFatalHandlingLogic) {
  88. calledFatalError = true;
  89. onFatalError(error);
  90. }
  91. } else {
  92. if (shouldApplyFatalHandlingLogic) {
  93. if (calledFatalError) {
  94. // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down
  95. DEBUG_BUILD &&
  96. logger.warn(
  97. 'uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown',
  98. );
  99. logAndExitProcess(error);
  100. } else if (!caughtSecondError) {
  101. // two cases for how we can hit this branch:
  102. // - capturing of first error blew up and we just caught the exception from that
  103. // - quit trying to capture, proceed with shutdown
  104. // - a second independent error happened while waiting for first error to capture
  105. // - want to avoid causing premature shutdown before first error capture finishes
  106. // it's hard to immediately tell case 1 from case 2 without doing some fancy/questionable domain stuff
  107. // so let's instead just delay a bit before we proceed with our action here
  108. // in case 1, we just wait a bit unnecessarily but ultimately do the same thing
  109. // in case 2, the delay hopefully made us wait long enough for the capture to finish
  110. // two potential nonideal outcomes:
  111. // nonideal case 1: capturing fails fast, we sit around for a few seconds unnecessarily before proceeding correctly by calling onFatalError
  112. // nonideal case 2: case 2 happens, 1st error is captured but slowly, timeout completes before capture and we treat second error as the sendErr of (nonexistent) failure from trying to capture first error
  113. // note that after hitting this branch, we might catch more errors where (caughtSecondError && !calledFatalError)
  114. // we ignore them - they don't matter to us, we're just waiting for the second error timeout to finish
  115. caughtSecondError = true;
  116. setTimeout(() => {
  117. if (!calledFatalError) {
  118. // it was probably case 1, let's treat err as the sendErr and call onFatalError
  119. calledFatalError = true;
  120. onFatalError(firstError, error);
  121. }
  122. }, timeout); // capturing could take at least sendTimeout to fail, plus an arbitrary second for how long it takes to collect surrounding source etc
  123. }
  124. }
  125. }
  126. },
  127. { _errorHandler: true },
  128. );
  129. }
  130. export { OnUncaughtException, makeErrorHandler, onUncaughtExceptionIntegration };
  131. //# sourceMappingURL=onuncaughtexception.js.map