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;
+ }
+}