diff --git a/examples/static_react_task/webapp/src/app.jsx b/examples/static_react_task/webapp/src/app.jsx index 5615f11fc..06f67ea8e 100644 --- a/examples/static_react_task/webapp/src/app.jsx +++ b/examples/static_react_task/webapp/src/app.jsx @@ -9,11 +9,12 @@ import React from "react"; import ReactDOM from "react-dom"; import { BaseFrontend, LoadingScreen } from "./components/core_components.jsx"; -import { useMephistoTask } from "mephisto-task"; +import { useMephistoTask, ErrorBoundary } from "mephisto-task"; /* ================= Application Components ================= */ function MainApp() { + const { blockedReason, blockedExplanation, @@ -21,6 +22,7 @@ function MainApp() { isLoading, initialTaskData, handleSubmit, + handleFatalError, isOnboarding, } = useMephistoTask(); @@ -50,14 +52,15 @@ function MainApp() { ); } - return (
+ +
); } diff --git a/examples/static_react_task/webapp/src/components/core_components.jsx b/examples/static_react_task/webapp/src/components/core_components.jsx index 60701372a..2354e5141 100644 --- a/examples/static_react_task/webapp/src/components/core_components.jsx +++ b/examples/static_react_task/webapp/src/components/core_components.jsx @@ -49,6 +49,10 @@ function SimpleFrontend({ taskData, isOnboarding, onSubmit }) { if (isOnboarding) { return ; } + + // test case for Type 1 error +// throw new Error('Test SimpleFrontend component error!'); + return (
diff --git a/mephisto/core/supervisor.py b/mephisto/core/supervisor.py index 0e05efd17..63d8c171e 100644 --- a/mephisto/core/supervisor.py +++ b/mephisto/core/supervisor.py @@ -22,6 +22,7 @@ PACKET_TYPE_SUBMIT_ONBOARDING, PACKET_TYPE_REQUEST_ACTION, PACKET_TYPE_UPDATE_AGENT_STATUS, + PACKET_TYPE_ERROR_LOG, ) from mephisto.data_model.worker import Worker from mephisto.data_model.qualification import worker_is_qualified @@ -661,6 +662,13 @@ def _get_init_data(self, packet, channel_info: ChannelInfo): packet.receiver_id = agent_id agent_info.agent.pending_observations.append(packet) + @staticmethod + def _log_frontend_error(packet): + error_msg = packet.data['final_data'].get("errorMsg") + error_stack = packet.data['final_data'].get("error") + logger.info(f"[FRONT_END_ERROR]: {error_msg}") + logger.info(f"[FRONT_END_ERROR_Trace]: {error_stack}") + def _on_message(self, packet: Packet, channel_info: ChannelInfo): """Handle incoming messages from the channel""" # TODO(#102) this method currently assumes that the packet's sender_id will @@ -668,6 +676,8 @@ def _on_message(self, packet: Packet, channel_info: ChannelInfo): # is a valid assumption, but will not be on recovery from catastrophic failure. if packet.type == PACKET_TYPE_AGENT_ACTION: self._on_act(packet, channel_info) + elif packet.type == PACKET_TYPE_ERROR_LOG: + self._log_frontend_error(packet) elif packet.type == PACKET_TYPE_NEW_AGENT: self._register_agent(packet, channel_info) elif packet.type == PACKET_TYPE_SUBMIT_ONBOARDING: diff --git a/mephisto/data_model/packet.py b/mephisto/data_model/packet.py index e2d92e771..743deeeb8 100644 --- a/mephisto/data_model/packet.py +++ b/mephisto/data_model/packet.py @@ -18,6 +18,7 @@ PACKET_TYPE_ALIVE = "alive" PACKET_TYPE_PROVIDER_DETAILS = "provider_details" PACKET_TYPE_SUBMIT_ONBOARDING = "submit_onboarding" +PACKET_TYPE_ERROR_LOG = "log_error" class Packet: diff --git a/mephisto/providers/mock/wrap_crowd_source.js b/mephisto/providers/mock/wrap_crowd_source.js index 7f2caddb5..489e27b8f 100644 --- a/mephisto/providers/mock/wrap_crowd_source.js +++ b/mephisto/providers/mock/wrap_crowd_source.js @@ -15,6 +15,7 @@ for both to be able to register them with the backend database. Returning None for the assignment_id means that the task is being previewed by the given worker. \------------------------------------------*/ +auto_submit = false // MOCK IMPLEMENTATION function getWorkerName() { @@ -53,3 +54,22 @@ function handleSubmitToProvider(task_data) { alert("The task has been submitted! Data: " + JSON.stringify(task_data)) return true; } + +// Adding event listener instead of using window.onerror prevents the error to be caught twice +window.addEventListener('error', function (event) { + + if (event.error.hasBeenCaught !== undefined){ + return false + } + event.error.hasBeenCaught = true + if (!auto_submit) { + if (confirm("Do you want to report the error?")) { + prompt('send the following error to the email address: '+ + '[email address]', JSON.stringify(event.error.message)) + } + } + else { + console.log("sending to email address: ####") + } + return true; +}) diff --git a/mephisto/server/architects/router/deploy/server.js b/mephisto/server/architects/router/deploy/server.js index bbc0b1ce4..74634c330 100644 --- a/mephisto/server/architects/router/deploy/server.js +++ b/mephisto/server/architects/router/deploy/server.js @@ -81,6 +81,7 @@ const PACKET_TYPE_ALIVE = 'alive' const PACKET_TYPE_PROVIDER_DETAILS = 'provider_details' const PACKET_TYPE_SUBMIT_ONBOARDING = 'submit_onboarding' const PACKET_TYPE_HEARTBEAT = 'heartbeat' +const PACKET_TYPE_ERROR_LOG = 'log_error' // State for agents tracked by the server class LocalAgentState { @@ -301,6 +302,8 @@ wss.on('connection', function(socket) { update_wanted_acts(packet.sender_id, false); send_status_for_agent(packet.sender_id); } + } else if (packet['packet_type'] == PACKET_TYPE_ERROR_LOG) { + handle_forward(packet); } else if (packet['packet_type'] == PACKET_TYPE_ALIVE) { debug_log('Agent alive: ', packet); handle_alive(socket, packet); @@ -483,6 +486,18 @@ app.post('/submit_task', upload.any(), function(req, res) { } }); +app.post('/log_error', function(req, res) { + const { USED_AGENT_ID: agent_id, ...provider_data } = req.body + let log_packet = { + packet_type: PACKET_TYPE_ERROR_LOG, + sender_id: agent_id, + receiver_id: SYSTEM_SOCKET_ID, + data: provider_data, + }; + _send_message(mephisto_socket, log_packet); + res.json({status: 'Error log sent!'}) +}); + // Quick status check for this server app.get('/is_alive', function(req, res) { res.json({status: 'Alive!'}); diff --git a/packages/mephisto-task/src/index.js b/packages/mephisto-task/src/index.js index b7b7d6977..973fbdb14 100644 --- a/packages/mephisto-task/src/index.js +++ b/packages/mephisto-task/src/index.js @@ -12,8 +12,10 @@ import { isMobile, getInitTaskData, postCompleteTask, + postErrorLog, postCompleteOnboarding, getBlockedExplanation, + ErrorBoundary, } from "./utils"; export * from "./MephistoContext"; @@ -68,6 +70,12 @@ const useMephistoTask = function () { [state.agentId] ); + const handleFatalError = React.useCallback( + (data) => { + postErrorLog(state.agentId, data) + }, + [state.agentId]); + function handleIncomingTaskConfig(taskConfig) { if (taskConfig.block_mobile && isMobile()) { setState({ blockedReason: "no_mobile" }); @@ -113,7 +121,8 @@ const useMephistoTask = function () { blockedExplanation: state.blockedReason && getBlockedExplanation(state.blockedReason), handleSubmit, + handleFatalError, }; }; -export { useMephistoTask }; +export { useMephistoTask , ErrorBoundary}; diff --git a/packages/mephisto-task/src/utils.js b/packages/mephisto-task/src/utils.js index 08e9e08d5..941147e64 100644 --- a/packages/mephisto-task/src/utils.js +++ b/packages/mephisto-task/src/utils.js @@ -3,10 +3,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ - +import React from "react"; import Bowser from "bowser"; const axios = require("axios"); + /* The following global methods are to be specified in wrap_crowd_source.js They are sideloaded and exposed as global import during the build process: @@ -132,6 +133,16 @@ export function postCompleteTask(agent_id, complete_data) { }); } +export function postErrorLog(agent_id, complete_data) { + return postData("/log_error", { + USED_AGENT_ID: agent_id, + final_data: complete_data, + }) + .then(function (data) { + console.log("Error log sent to server"); + }); +} + export function getBlockedExplanation(reason) { const explanations = { no_mobile: @@ -150,3 +161,38 @@ export function getBlockedExplanation(reason) { return `Sorry, you are not able to work on this task. (code: ${reason})`; } } + +export class ErrorBoundary extends React.Component { + + state = { error: null, errorInfo: null }; + + componentDidCatch(error, errorInfo) { + // Catch errors in any components below and re-render with error message + this.setState({ + error: error, + errorInfo: errorInfo + }) + // alert Mephisto worker of a component error + alert("Error from the frontend occurred: " + error) + // pass the error and errorInfo to the backend through /submit_task endpoint + this.props.handleError({error:error.message, errorInfo:errorInfo}) + } + + render() { + if (this.state.errorInfo) { + // Error path + return ( +
+

Something went wrong.

+
+ {this.state.error && this.state.error.toString()} +
+ {this.state.errorInfo.componentStack} +
+
+ ); + } + // Normally, just render children + return this.props.children; + } +}