Skip to content

Commit 083bb99

Browse files
authored
Merge pull request alexmojaki#402 from alexmojaki/error-boundary
Error boundary
2 parents bfe335a + ab6205d commit 083bb99

File tree

8 files changed

+192
-127
lines changed

8 files changed

+192
-127
lines changed

frontend/src/App.js

Lines changed: 92 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import {
4747
import {HintsAssistant} from "./Hints";
4848
import Toggle from 'react-toggle'
4949
import "react-toggle/style.css"
50-
import {feedbackContentStyle, FeedbackModal} from "./Feedback";
50+
import {ErrorBoundary, FeedbackMenuButton} from "./Feedback";
5151
import birdseyeIcon from "./img/birdseye_icon.png";
5252
import languageIcon from "./img/language.png";
5353
import {interrupt, runCode, terminalRef} from "./RunCode";
@@ -427,59 +427,73 @@ const CourseText = (
427427

428428
class AppComponent extends React.Component {
429429
render() {
430-
const {
431-
editorContent,
432-
assistant,
433-
specialMessages,
434-
questionWizard,
435-
pages,
436-
user,
437-
prediction,
438-
route,
439-
previousRoute,
440-
running,
441-
} = this.props;
442-
if (route === "toc") {
430+
if (this.props.route === "toc") {
443431
return <TableOfContents/>
444432
}
445-
const isQuestionWizard = route === "question";
446-
const fullIde = route === "ide";
447-
448-
const page = currentPage();
449-
const step = currentStep();
450-
451-
let showEditor, showSnoop, showPythonTutor, showBirdseye, showQuestionButton;
452-
if (fullIde || isQuestionWizard) {
453-
showEditor = true;
454-
showSnoop = true;
455-
showPythonTutor = true;
456-
showBirdseye = true;
457-
showQuestionButton = !(isQuestionWizard || previousRoute === "question");
458-
} else if (step.text.length) {
459-
showEditor = page.index >= pages.WritingPrograms.index;
460-
const snoopPageIndex = pages.UnderstandingProgramsWithSnoop.index;
461-
showSnoop = page.index > snoopPageIndex ||
462-
(page.index === snoopPageIndex && step.index >= 1);
463-
showPythonTutor = page.index >= pages.UnderstandingProgramsWithPythonTutor.index;
464-
showBirdseye = page.index >= pages.IntroducingBirdseye.index;
465-
showQuestionButton = page.index > pages.IntroducingBirdseye.index;
466-
}
467433

468-
const cantUseEditor = prediction.state === "waiting" || prediction.state === "showingResult";
469434
return <div className="book-container">
470-
<nav className="navbar navbar-expand-lg navbar-light bg-light">
435+
<NavBar user={this.props.user}/>
436+
<ErrorBoundary canGiveFeedback>
437+
<AppMain {...this.props}/>
438+
</ErrorBoundary>
439+
</div>
440+
}
441+
}
442+
443+
function NavBar({user}) {
444+
return <nav className="navbar navbar-expand-lg navbar-light bg-light">
471445
<span className="nav-item custom-popup">
472446
<MenuPopup user={user}/>
473447
</span>
474-
<span className="nav-item navbar-text">
448+
<span className="nav-item navbar-text">
475449
<HeaderLoginInfo email={user.email}/>
476450
</span>
477-
<a className="nav-item nav-link" href="#toc">
478-
<FontAwesomeIcon icon={faListOl}/> {terms.table_of_contents}
479-
</a>
480-
</nav>
451+
<a className="nav-item nav-link" href="#toc">
452+
<FontAwesomeIcon icon={faListOl}/> {terms.table_of_contents}
453+
</a>
454+
</nav>;
455+
}
456+
457+
function AppMain(
458+
{
459+
editorContent,
460+
assistant,
461+
specialMessages,
462+
questionWizard,
463+
pages,
464+
user,
465+
prediction,
466+
route,
467+
previousRoute,
468+
running,
469+
}) {
470+
const isQuestionWizard = route === "question";
471+
const fullIde = route === "ide";
472+
473+
const page = currentPage();
474+
const step = currentStep();
475+
476+
let showEditor, showSnoop, showPythonTutor, showBirdseye, showQuestionButton;
477+
if (fullIde || isQuestionWizard) {
478+
showEditor = true;
479+
showSnoop = true;
480+
showPythonTutor = true;
481+
showBirdseye = true;
482+
showQuestionButton = !(isQuestionWizard || previousRoute === "question");
483+
} else if (step.text.length) {
484+
showEditor = page.index >= pages.WritingPrograms.index;
485+
const snoopPageIndex = pages.UnderstandingProgramsWithSnoop.index;
486+
showSnoop = page.index > snoopPageIndex ||
487+
(page.index === snoopPageIndex && step.index >= 1);
488+
showPythonTutor = page.index >= pages.UnderstandingProgramsWithPythonTutor.index;
489+
showBirdseye = page.index >= pages.IntroducingBirdseye.index;
490+
showQuestionButton = page.index > pages.IntroducingBirdseye.index;
491+
}
492+
493+
const cantUseEditor = prediction.state === "waiting" || prediction.state === "showingResult";
481494

482-
{!fullIde &&
495+
return <>
496+
{!fullIde &&
483497
<div className="book-text markdown-body">
484498
{isQuestionWizard ?
485499
<QuestionWizard {...questionWizard}/>
@@ -497,47 +511,44 @@ class AppComponent extends React.Component {
497511
}
498512
</div>
499513

500-
}
514+
}
501515

502-
<EditorButtons {...{
503-
showBirdseye,
504-
showEditor,
505-
showSnoop,
506-
showPythonTutor,
507-
showQuestionButton,
508-
disabled: cantUseEditor,
509-
running,
510-
}}/>
511-
512-
<div className={`ide ide-${fullIde ? 'full' : 'half'}`}>
513-
<div className="editor-and-terminal">
514-
{showEditor &&
515-
<Editor value={editorContent} readOnly={cantUseEditor}/>
516-
}
517-
<div className="terminal" style={{height: showEditor ? undefined : "100%"}}>
518-
<Shell/>
519-
</div>
516+
<EditorButtons {...{
517+
showBirdseye,
518+
showEditor,
519+
showSnoop,
520+
showPythonTutor,
521+
showQuestionButton,
522+
disabled: cantUseEditor,
523+
running,
524+
}}/>
525+
526+
<div className={`ide ide-${fullIde ? 'full' : 'half'}`}>
527+
<div className="editor-and-terminal">
528+
{showEditor &&
529+
<Editor value={editorContent} readOnly={cantUseEditor}/>
530+
}
531+
<div className="terminal" style={{height: showEditor ? undefined : "100%"}}>
532+
<Shell/>
520533
</div>
521534
</div>
535+
</div>
522536

523-
<a className="btn btn-primary full-ide-button"
524-
href={"#" + (!fullIde ? "ide" : (specialHash(previousRoute) ? previousRoute : page.slug))}>
525-
<FontAwesomeIcon icon={fullIde ? faCompress : faExpand}/>
526-
</a>
537+
<a className="btn btn-primary full-ide-button"
538+
href={"#" + (!fullIde ? "ide" : (specialHash(previousRoute) ? previousRoute : page.slug))}>
539+
<FontAwesomeIcon icon={fullIde ? faCompress : faExpand}/>
540+
</a>
527541

528-
<>
529-
{specialMessages.map((message, index) =>
530-
<Popup
531-
key={index}
532-
open={true}
533-
onClose={() => closeSpecialMessage(index)}
534-
>
535-
<SpecialMessageModal message={message}/>
536-
</Popup>
537-
)}
538-
</>
539-
</div>
540-
}
542+
{specialMessages.map((message, index) =>
543+
<Popup
544+
key={index}
545+
open={true}
546+
onClose={() => closeSpecialMessage(index)}
547+
>
548+
<SpecialMessageModal message={message}/>
549+
</Popup>
550+
)}
551+
</>;
541552
}
542553

543554
const StepButton = ({delta, label}) =>
@@ -595,20 +606,7 @@ const MenuPopup = ({user}) =>
595606
<SettingsModal user={user}/>
596607
</Popup>
597608
</p>
598-
{process.env.REACT_APP_SENTRY_DSN && <p>
599-
<Popup
600-
trigger={
601-
<button className="btn btn-success">
602-
<FontAwesomeIcon icon={faBug}/> {terms.feedback}
603-
</button>
604-
}
605-
modal
606-
nested
607-
contentStyle={feedbackContentStyle}
608-
>
609-
{close => <FeedbackModal close={close}/>}
610-
</Popup>
611-
</p>}
609+
<FeedbackMenuButton/>
612610
{
613611
otherVisibleLanguages.map(lang =>
614612
<p key={lang.code}>

frontend/src/Feedback.js

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ import axios from "axios";
55
import * as terms from "./terms.json"
66
import * as Sentry from "@sentry/react";
77
import {uuidv4} from "sync-message";
8+
import Popup from "reactjs-popup";
9+
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
10+
import {faBug} from "@fortawesome/free-solid-svg-icons";
11+
import _ from "lodash";
812

13+
const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN;
914

10-
export const FeedbackModal = ({close}) => {
15+
16+
const FeedbackModal = ({close}) => {
1117
const email = useInput(bookState.user.email || "", {
1218
placeholder: terms.feedback_email_placeholder,
1319
type: 'text',
@@ -60,7 +66,7 @@ ${description.value.trim()}`;
6066
},
6167
{
6268
headers: {
63-
Authorization: 'DSN ' + process.env.REACT_APP_SENTRY_DSN,
69+
Authorization: 'DSN ' + SENTRY_DSN,
6470
}
6571
}
6672
);
@@ -103,10 +109,66 @@ ${description.value.trim()}`;
103109
);
104110
};
105111

106-
107-
export const feedbackContentStyle = {
112+
const feedbackContentStyle = {
108113
maxHeight: "90vh",
109114
overflow: "auto",
110115
background: "white",
111116
border: "solid 1px lightgray",
112117
}
118+
119+
export function FeedbackMenuButton() {
120+
if (!SENTRY_DSN) {
121+
return null;
122+
}
123+
return <p>
124+
<Popup
125+
trigger={
126+
<button className="btn btn-success">
127+
<FontAwesomeIcon icon={faBug}/> {terms.feedback}
128+
</button>
129+
}
130+
modal
131+
nested
132+
contentStyle={feedbackContentStyle}
133+
>
134+
{close => <FeedbackModal close={close}/>}
135+
</Popup>
136+
</p>;
137+
}
138+
139+
export function InternalError({ranCode, canGiveFeedback}) {
140+
const start = _.template(terms.internal_error_start)({
141+
maybeErrorReported: SENTRY_DSN ? terms.error_has_been_reported : '',
142+
});
143+
const suggestions = [];
144+
if (ranCode) {
145+
suggestions.push(terms.try_running_code_again);
146+
}
147+
suggestions.push(terms.refresh_and_try_again, terms.try_using_different_browser);
148+
if (SENTRY_DSN && canGiveFeedback) {
149+
suggestions.push(terms.give_feedback_from_menu);
150+
}
151+
return <div>
152+
<p>{start}</p>
153+
<ul>
154+
{suggestions.map(suggestion => <li key={suggestion}>{suggestion}</li>)}
155+
</ul>
156+
</div>;
157+
}
158+
159+
export function ErrorBoundary({children, canGiveFeedback}) {
160+
return <Sentry.ErrorBoundary fallback={
161+
({error}) => <ErrorFallback {...{error, canGiveFeedback}}/>
162+
}>
163+
{children}
164+
</Sentry.ErrorBoundary>;
165+
}
166+
167+
function ErrorFallback({error, canGiveFeedback}) {
168+
return <div style={{margin: "4em"}}>
169+
<div className="alert alert-danger" role="alert">
170+
<pre><code>{error.toString()}</code></pre>
171+
</div>
172+
<InternalError canGiveFeedback={canGiveFeedback}/>
173+
</div>;
174+
}

frontend/src/RunCode.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import {
33
bookState,
44
currentStep,
55
currentStepName,
6-
postCodeEntry,
76
loadedPromise,
87
logEvent,
98
moveStep,
9+
postCodeEntry,
1010
ranCode
1111
} from "./book/store";
1212
import _ from "lodash";
@@ -15,8 +15,7 @@ import {animateScroll} from "react-scroll";
1515
import React from "react";
1616
import * as Sentry from "@sentry/react";
1717
import {wrapAsync} from "./frontendlib/sentry";
18-
import {taskClient, runCodeTask} from "./TaskClient";
19-
import * as terms from "./terms.json";
18+
import {runCodeTask, taskClient} from "./TaskClient";
2019

2120
export const terminalRef = React.createRef();
2221

@@ -215,12 +214,9 @@ export const showCodeResult = ({birdseyeUrl, passed}) => {
215214
}
216215

217216
function showInternalErrorOutput(message) {
218-
let instructions = process.env.REACT_APP_SENTRY_DSN ?
219-
terms.report_error_instructions :
220-
terms.report_error_instructions_no_feedback;
221217
showOutputParts([
222218
{text: `\n${message.trim()}\n\n`, type: 'internal_error'},
223-
{text: instructions, type: 'internal_error_explanation'},
219+
{text: '', type: 'internal_error_explanation'},
224220
{text: '>>> ', type: 'shell_prompt'},
225221
]);
226222
}

frontend/src/english_terms.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
"error_traceback":"Error traceback:",
1919
"did_you_mean": "Did you mean...",
2020
"similar_frames_skipped": "Similar frames skipped:",
21-
"report_error_instructions": "Oops, something went wrong! The error has been reported. Here's what you can do:\n\n- Try running the code again.\n- Reload/refresh the page and try again.\n- Try using a different browser.\n- Give feedback from the top-left menu.",
22-
"report_error_instructions_no_feedback": "Oops, something went wrong! Here's what you can do:\n\n- Try running the code again.\n- Reload/refresh the page and try again.\n- Try using a different browser.",
21+
"internal_error_start": "Oops, something went wrong! ${maybeErrorReported} Here's what you can do:",
22+
"error_has_been_reported": "The error has been reported.",
23+
"try_running_code_again": "Try running the code again.",
24+
"refresh_and_try_again": "Reload/refresh the page and try again.",
25+
"try_using_different_browser": "Try using a different browser.",
26+
"give_feedback_from_menu": "Give feedback from the top-left menu.",
2327
"click_for_error_details": "Click for error details",
2428
"give_feedback": "Give feedback",
2529
"feedback_email_placeholder": "Email (optional)",

frontend/src/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import ReactDOM from 'react-dom';
33
import {App} from './App';
44
import {Provider} from "react-redux";
55
import {store} from "./store";
6+
import {ErrorBoundary} from "./Feedback";
67

78

89
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
910

1011

1112
ReactDOM.render(
12-
<Provider store={store}>
13+
<Provider store={store}>
14+
<ErrorBoundary>
1315
<App/>
14-
</Provider>,
16+
</ErrorBoundary>
17+
</Provider>,
1518
document.getElementById("root")
1619
);
1720

0 commit comments

Comments
 (0)