TopErrorBoundary.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import React from "react";
  2. import * as Sentry from "@sentry/browser";
  3. import { t } from "../i18n";
  4. interface TopErrorBoundaryState {
  5. hasError: boolean;
  6. sentryEventId: string;
  7. localStorage: string;
  8. }
  9. export class TopErrorBoundary extends React.Component<
  10. any,
  11. TopErrorBoundaryState
  12. > {
  13. state: TopErrorBoundaryState = {
  14. hasError: false,
  15. sentryEventId: "",
  16. localStorage: "",
  17. };
  18. render() {
  19. return this.state.hasError ? this.errorSplash() : this.props.children;
  20. }
  21. componentDidCatch(error: Error, errorInfo: any) {
  22. const _localStorage: any = {};
  23. for (const [key, value] of Object.entries({ ...localStorage })) {
  24. try {
  25. _localStorage[key] = JSON.parse(value);
  26. } catch (error: any) {
  27. _localStorage[key] = value;
  28. }
  29. }
  30. Sentry.withScope((scope) => {
  31. scope.setExtras(errorInfo);
  32. const eventId = Sentry.captureException(error);
  33. this.setState((state) => ({
  34. hasError: true,
  35. sentryEventId: eventId,
  36. localStorage: JSON.stringify(_localStorage),
  37. }));
  38. });
  39. }
  40. private selectTextArea(event: React.MouseEvent<HTMLTextAreaElement>) {
  41. if (event.target !== document.activeElement) {
  42. event.preventDefault();
  43. (event.target as HTMLTextAreaElement).select();
  44. }
  45. }
  46. private async createGithubIssue() {
  47. let body = "";
  48. try {
  49. const templateStrFn = (
  50. await import(
  51. /* webpackChunkName: "bug-issue-template" */ "../bug-issue-template"
  52. )
  53. ).default;
  54. body = encodeURIComponent(templateStrFn(this.state.sentryEventId));
  55. } catch (error: any) {
  56. console.error(error);
  57. }
  58. window.open(
  59. `https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
  60. );
  61. }
  62. private errorSplash() {
  63. return (
  64. <div className="ErrorSplash excalidraw">
  65. <div className="ErrorSplash-messageContainer">
  66. <div className="ErrorSplash-paragraph bigger align-center">
  67. {t("errorSplash.headingMain_pre")}
  68. <button onClick={() => window.location.reload()}>
  69. {t("errorSplash.headingMain_button")}
  70. </button>
  71. </div>
  72. <div className="ErrorSplash-paragraph align-center">
  73. {t("errorSplash.clearCanvasMessage")}
  74. <button
  75. onClick={() => {
  76. try {
  77. localStorage.clear();
  78. window.location.reload();
  79. } catch (error: any) {
  80. console.error(error);
  81. }
  82. }}
  83. >
  84. {t("errorSplash.clearCanvasMessage_button")}
  85. </button>
  86. <br />
  87. <div className="smaller">
  88. <span role="img" aria-label="warning">
  89. ⚠️
  90. </span>
  91. {t("errorSplash.clearCanvasCaveat")}
  92. <span role="img" aria-hidden="true">
  93. ⚠️
  94. </span>
  95. </div>
  96. </div>
  97. <div>
  98. <div className="ErrorSplash-paragraph">
  99. {t("errorSplash.trackedToSentry_pre")}
  100. {this.state.sentryEventId}
  101. {t("errorSplash.trackedToSentry_post")}
  102. </div>
  103. <div className="ErrorSplash-paragraph">
  104. {t("errorSplash.openIssueMessage_pre")}
  105. <button onClick={() => this.createGithubIssue()}>
  106. {t("errorSplash.openIssueMessage_button")}
  107. </button>
  108. {t("errorSplash.openIssueMessage_post")}
  109. </div>
  110. <div className="ErrorSplash-paragraph">
  111. <div className="ErrorSplash-details">
  112. <label>{t("errorSplash.sceneContent")}</label>
  113. <textarea
  114. rows={5}
  115. onPointerDown={this.selectTextArea}
  116. readOnly={true}
  117. value={this.state.localStorage}
  118. />
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. </div>
  124. );
  125. }
  126. }