errorboundary.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. Object.defineProperty(exports, '__esModule', { value: true });
  2. const browser = require('@sentry/browser');
  3. const utils = require('@sentry/utils');
  4. const hoistNonReactStatics = require('hoist-non-react-statics');
  5. const React = require('react');
  6. const debugBuild = require('./debug-build.js');
  7. const _interopDefault = e => e && e.__esModule ? e.default : e;
  8. function _interopNamespace(e) {
  9. if (e && e.__esModule) return e;
  10. const n = Object.create(null);
  11. if (e) {
  12. for (const k in e) {
  13. n[k] = e[k];
  14. }
  15. }
  16. n.default = e;
  17. return n;
  18. }
  19. const hoistNonReactStatics__default = /*#__PURE__*/_interopDefault(hoistNonReactStatics);
  20. const React__namespace = /*#__PURE__*/_interopNamespace(React);
  21. const _jsxFileName = "/home/runner/work/sentry-javascript/sentry-javascript/packages/react/src/errorboundary.tsx";
  22. function isAtLeastReact17(version) {
  23. const major = version.match(/^([^.]+)/);
  24. return major !== null && parseInt(major[0]) >= 17;
  25. }
  26. const UNKNOWN_COMPONENT = 'unknown';
  27. const INITIAL_STATE = {
  28. componentStack: null,
  29. error: null,
  30. eventId: null,
  31. };
  32. function setCause(error, cause) {
  33. const seenErrors = new WeakMap();
  34. function recurse(error, cause) {
  35. // If we've already seen the error, there is a recursive loop somewhere in the error's
  36. // cause chain. Let's just bail out then to prevent a stack overflow.
  37. if (seenErrors.has(error)) {
  38. return;
  39. }
  40. if (error.cause) {
  41. seenErrors.set(error, true);
  42. return recurse(error.cause, cause);
  43. }
  44. error.cause = cause;
  45. }
  46. recurse(error, cause);
  47. }
  48. /**
  49. * A ErrorBoundary component that logs errors to Sentry. Requires React >= 16.
  50. * NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the
  51. * Sentry React SDK ErrorBoundary caught an error invoking your application code. This
  52. * is expected behavior and NOT indicative of a bug with the Sentry React SDK.
  53. */
  54. class ErrorBoundary extends React__namespace.Component {
  55. constructor(props) {
  56. super(props);ErrorBoundary.prototype.__init.call(this);
  57. this.state = INITIAL_STATE;
  58. this._openFallbackReportDialog = true;
  59. const client = browser.getClient();
  60. if (client && client.on && props.showDialog) {
  61. this._openFallbackReportDialog = false;
  62. client.on('afterSendEvent', event => {
  63. if (!event.type && event.event_id === this._lastEventId) {
  64. // eslint-disable-next-line deprecation/deprecation
  65. browser.showReportDialog({ ...props.dialogOptions, eventId: this._lastEventId });
  66. }
  67. });
  68. }
  69. }
  70. componentDidCatch(error, { componentStack }) {
  71. const { beforeCapture, onError, showDialog, dialogOptions } = this.props;
  72. browser.withScope(scope => {
  73. // If on React version >= 17, create stack trace from componentStack param and links
  74. // to to the original error using `error.cause` otherwise relies on error param for stacktrace.
  75. // Linking errors requires the `LinkedErrors` integration be enabled.
  76. // See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#native-component-stacks
  77. //
  78. // Although `componentDidCatch` is typed to accept an `Error` object, it can also be invoked
  79. // with non-error objects. This is why we need to check if the error is an error-like object.
  80. // See: https://github.com/getsentry/sentry-javascript/issues/6167
  81. if (isAtLeastReact17(React__namespace.version) && utils.isError(error)) {
  82. const errorBoundaryError = new Error(error.message);
  83. errorBoundaryError.name = `React ErrorBoundary ${error.name}`;
  84. errorBoundaryError.stack = componentStack;
  85. // Using the `LinkedErrors` integration to link the errors together.
  86. setCause(error, errorBoundaryError);
  87. }
  88. if (beforeCapture) {
  89. beforeCapture(scope, error, componentStack);
  90. }
  91. const eventId = browser.captureException(error, {
  92. captureContext: {
  93. contexts: { react: { componentStack } },
  94. },
  95. mechanism: { handled: false },
  96. });
  97. if (onError) {
  98. onError(error, componentStack, eventId);
  99. }
  100. if (showDialog) {
  101. this._lastEventId = eventId;
  102. if (this._openFallbackReportDialog) {
  103. browser.showReportDialog({ ...dialogOptions, eventId });
  104. }
  105. }
  106. // componentDidCatch is used over getDerivedStateFromError
  107. // so that componentStack is accessible through state.
  108. this.setState({ error, componentStack, eventId });
  109. });
  110. }
  111. componentDidMount() {
  112. const { onMount } = this.props;
  113. if (onMount) {
  114. onMount();
  115. }
  116. }
  117. componentWillUnmount() {
  118. const { error, componentStack, eventId } = this.state;
  119. const { onUnmount } = this.props;
  120. if (onUnmount) {
  121. onUnmount(error, componentStack, eventId);
  122. }
  123. }
  124. __init() {this.resetErrorBoundary = () => {
  125. const { onReset } = this.props;
  126. const { error, componentStack, eventId } = this.state;
  127. if (onReset) {
  128. onReset(error, componentStack, eventId);
  129. }
  130. this.setState(INITIAL_STATE);
  131. };}
  132. render() {
  133. const { fallback, children } = this.props;
  134. const state = this.state;
  135. if (state.error) {
  136. let element = undefined;
  137. if (typeof fallback === 'function') {
  138. element = fallback({
  139. error: state.error,
  140. componentStack: state.componentStack,
  141. resetError: this.resetErrorBoundary,
  142. eventId: state.eventId,
  143. });
  144. } else {
  145. element = fallback;
  146. }
  147. if (React__namespace.isValidElement(element)) {
  148. return element;
  149. }
  150. if (fallback) {
  151. debugBuild.DEBUG_BUILD && utils.logger.warn('fallback did not produce a valid ReactElement');
  152. }
  153. // Fail gracefully if no fallback provided or is not valid
  154. return null;
  155. }
  156. if (typeof children === 'function') {
  157. return (children )();
  158. }
  159. return children;
  160. }
  161. }
  162. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  163. function withErrorBoundary(
  164. WrappedComponent,
  165. errorBoundaryOptions,
  166. ) {
  167. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  168. const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT;
  169. const Wrapped = (props) => (
  170. React__namespace.createElement(ErrorBoundary, { ...errorBoundaryOptions, __self: this, __source: {fileName: _jsxFileName, lineNumber: 238}}
  171. , React__namespace.createElement(WrappedComponent, { ...props, __self: this, __source: {fileName: _jsxFileName, lineNumber: 239}} )
  172. )
  173. );
  174. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  175. Wrapped.displayName = `errorBoundary(${componentDisplayName})`;
  176. // Copy over static methods from Wrapped component to Profiler HOC
  177. // See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
  178. hoistNonReactStatics__default(Wrapped, WrappedComponent);
  179. return Wrapped;
  180. }
  181. exports.ErrorBoundary = ErrorBoundary;
  182. exports.UNKNOWN_COMPONENT = UNKNOWN_COMPONENT;
  183. exports.isAtLeastReact17 = isAtLeastReact17;
  184. exports.withErrorBoundary = withErrorBoundary;
  185. //# sourceMappingURL=errorboundary.js.map