diff --git a/.eslintrc.json b/.eslintrc.json index 818e88f2c..bd2345b99 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,7 +32,8 @@ "expect": true, "it": true, "jest": true, - "test": true + "test": true, + "__DEV__": true }, "rules": { "array-bracket-spacing": [2, "never"], diff --git a/app/mattermost_managed/mattermost-managed.android.js b/app/mattermost_managed/mattermost-managed.android.js index 2e3f78adf..7aa71819c 100644 --- a/app/mattermost_managed/mattermost-managed.android.js +++ b/app/mattermost_managed/mattermost-managed.android.js @@ -56,7 +56,7 @@ export default { } }, isTrustedDevice: () => { - if (__DEV__) { //eslint-disable-line no-undef + if (__DEV__) { return true; } diff --git a/app/mattermost_managed/mattermost-managed.ios.js b/app/mattermost_managed/mattermost-managed.ios.js index 01c134993..1e0bb973b 100644 --- a/app/mattermost_managed/mattermost-managed.ios.js +++ b/app/mattermost_managed/mattermost-managed.ios.js @@ -55,7 +55,7 @@ export default { } }, isTrustedDevice: () => { - if (__DEV__) { //eslint-disable-line no-undef + if (__DEV__) { return true; } diff --git a/app/utils/error_handling.js b/app/utils/error_handling.js index b0429c097..aa9d8d191 100644 --- a/app/utils/error_handling.js +++ b/app/utils/error_handling.js @@ -16,27 +16,35 @@ import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {purgeOfflineStore} from 'app/actions/views/root'; import { captureException, + captureJSException, initializeSentry, - LOGGER_JAVASCRIPT, LOGGER_NATIVE, } from 'app/utils/sentry'; import {app, store} from 'app/mattermost'; const errorHandler = (e, isFatal) => { - console.warn('Handling Javascript error ', e); // eslint-disable-line no-console + if (__DEV__ && !e && !isFatal) { + // react-native-exception-handler redirects console.error to call this, and React calls + // console.error without an exception when prop type validation fails, so this ends up + // being called with no arguments when the error handler is enabled in dev mode. + return; + } + + console.warn('Handling Javascript error', e, isFatal); // eslint-disable-line no-console + captureJSException(e, isFatal, store); + const {dispatch} = store; - captureException(e, LOGGER_JAVASCRIPT, store); - - const translations = app.getTranslations(); dispatch(closeWebSocket()); if (Client4.getUrl()) { dispatch(logError(e)); } - if (isFatal) { + if (isFatal && e instanceof Error) { + const translations = app.getTranslations(); + Alert.alert( translations['mobile.error_handler.title'], translations['mobile.error_handler.description'], diff --git a/app/utils/sentry/index.js b/app/utils/sentry/index.js index af184abfc..80bfbaa26 100644 --- a/app/utils/sentry/index.js +++ b/app/utils/sentry/index.js @@ -17,6 +17,9 @@ export const LOGGER_JAVASCRIPT_WARNING = 'javascript_warning'; export const LOGGER_NATIVE = 'native'; export const LOGGER_REDUX = 'redux'; +export const BREADCRUMB_UNCAUGHT_APP_ERROR = 'uncaught-app-error'; +export const BREADCRUMB_UNCAUGHT_NON_ERROR = 'uncaught-non-error'; + export function initializeSentry() { if (!Config.SentryEnabled) { // Still allow Sentry to configure itself in case other code tries to call it @@ -44,12 +47,28 @@ function getDsn() { return ''; } -export function captureException(error, logger, store) { - if (error && logger && store) { - capture(() => { - Sentry.captureException(error, {logger}); - }, store); +export function captureJSException(error, isFatal, store) { + if (!error || !store) { + console.warn('captureJSException called with missing arguments', error, store); // eslint-disable-line no-console + return; } + + if (error instanceof Error) { + captureException(error, LOGGER_JAVASCRIPT, store); + } else { + captureNonErrorAsBreadcrumb(error, isFatal); + } +} + +export function captureException(error, logger, store) { + if (!error || !logger || !store) { + console.warn('captureException called with missing arguments', error, logger, store); // eslint-disable-line no-console + return; + } + + capture(() => { + Sentry.captureException(error, {logger}); + }, store); } export function captureExceptionWithoutState(err, logger) { @@ -74,6 +93,54 @@ export function captureMessage(message, logger, store) { } } +export function captureNonErrorAsBreadcrumb(obj, isFatal) { + if (!obj || typeof obj !== 'object') { + console.warning('Invalid object passed to captureNonErrorAsBreadcrumb', obj); // eslint-disable-line no-console + return; + } + + const isAppError = Boolean(obj.server_error_id); + + const breadcrumb = { + category: isAppError ? BREADCRUMB_UNCAUGHT_APP_ERROR : BREADCRUMB_UNCAUGHT_NON_ERROR, + data: { + isFatal: String(isFatal), + }, + level: 'warn', + }; + + if (obj.message) { + breadcrumb.message = obj.message; + } else if (obj.intl && obj.intl.defaultMessage) { + breadcrumb.message = breadcrumb.intl.defaultMessage; + } else { + breadcrumb.message = 'no message provided'; + } + + if (obj.server_error_id) { + breadcrumb.data.server_error_id = obj.server_error_id; + } + + if (obj.status_code) { + breadcrumb.data.status_code = obj.status_code; + } + + if (obj.url) { + const index = obj.url.indexOf('/'); + + if (index !== -1) { + breadcrumb.data.url = obj.url.substring(index); + } + } + + try { + Sentry.captureBreadcrumb(breadcrumb); + } catch (e) { + // Do nothing since this is only here to make sure we don't crash when handling an exception + console.warn('Failed to capture breadcrumb of non-error', e); // eslint-disable-line no-console + } +} + // Wrapper function to any calls to Sentry so that we can gather any necessary extra data // before sending. function capture(captureFunc, store) { diff --git a/index.js b/index.js index acb85c8b6..5746e3135 100644 --- a/index.js +++ b/index.js @@ -16,7 +16,6 @@ if (Platform.OS === 'android') { /* /!* eslint-disable no-console *!/ -/!* eslint-disable no-undef *!/ if (__DEV__) { const modules = require.getModules(); const moduleIds = Object.keys(modules);