From e8b33fb7c84698d5446247ffe5bf30058d2454f2 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 17 Jul 2020 12:09:38 -0300 Subject: [PATCH] Restore state snapshot for Sentry errors (#9028) The state snapshot that was attached to Sentry errors was removed recently in #8794 because it had become too large. The snapshot has now been restored and reduced in size. A utility function has been written to reduce the state object to just the requested properties. This seemed safer than filtering out state that is known to be large or to contain identifiable information. This is not a great solution, as now knowledge about the state shape resides in this large constant, but it will suffice for now. I am hopeful that we can decorate our controllers with this metadata in the future instead, as part of the upcoming background controller refactor. A separate `getSentryState` global function has been added to get the reduced state, so that the old `getCleanAppState` function that we used to use could remain unchanged. It's still useful to get that full state copy while debugging, and in e2e tests. --- app/scripts/lib/setupSentry.js | 68 +++++++++++++++++++++++++++++++++- app/scripts/ui.js | 5 ++- ui/index.js | 37 ++++++++++++++++++ 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 91070ed67..8ec1402c4 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -8,7 +8,68 @@ const METAMASK_ENVIRONMENT = process.env.METAMASK_ENVIRONMENT const SENTRY_DSN_PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505' const SENTRY_DSN_DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' -export default function setupSentry ({ release }) { +// This describes the subset of Redux state attached to errors sent to Sentry +// These properties have some potential to be useful for debugging, and they do +// not contain any identifiable information. +export const SENTRY_STATE = { + gas: true, + history: true, + metamask: { + alertEnabledness: true, + completedOnboarding: true, + connectedStatusPopoverHasBeenShown: true, + conversionDate: true, + conversionRate: true, + currentBlockGasLimit: true, + currentCurrency: true, + currentLocale: true, + customNonceValue: true, + defaultHomeActiveTabName: true, + featureFlags: true, + firstTimeFlowType: true, + forgottenPassword: true, + incomingTxLastFetchedBlocksByNetwork: true, + ipfsGateway: true, + isAccountMenuOpen: true, + isInitialized: true, + isUnlocked: true, + metaMetricsId: true, + metaMetricsSendCount: true, + nativeCurrency: true, + network: true, + nextNonce: true, + participateInMetaMetrics: true, + preferences: true, + provider: { + nickname: true, + ticker: true, + type: true, + }, + seedPhraseBackedUp: true, + settings: { + chainId: true, + ticker: true, + nickname: true, + }, + showRestorePrompt: true, + threeBoxDisabled: true, + threeBoxLastUpdated: true, + threeBoxSynced: true, + threeBoxSyncingAllowed: true, + unapprovedDecryptMsgCount: true, + unapprovedEncryptionPublicKeyMsgCount: true, + unapprovedMsgCount: true, + unapprovedPersonalMsgCount: true, + unapprovedTypedMessagesCount: true, + useBlockie: true, + useNonceField: true, + usePhishDetect: true, + welcomeScreenSeen: true, + }, + unconnectedAccount: true, +} + +export default function setupSentry ({ release, getState }) { let sentryTarget if (METAMASK_DEBUG || process.env.IN_TEST) { @@ -37,6 +98,11 @@ export default function setupSentry ({ release }) { simplifyErrorMessages(report) // modify report urls rewriteReportUrls(report) + // append app state + if (getState) { + const appState = getState() + report.extra.appState = appState + } } catch (err) { console.warn(err) } diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 822ea73a4..3a76a5f4e 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -36,7 +36,10 @@ async function start () { // setup sentry error reporting const release = global.platform.getVersion() - setupSentry({ release }) + setupSentry({ + release, + getState: () => window.getSentryState?.() || {}, + }) // identify window type (popup, notification) const windowType = getEnvironmentType() diff --git a/ui/index.js b/ui/index.js index c5547fbfb..bc61913b2 100644 --- a/ui/index.js +++ b/ui/index.js @@ -9,6 +9,7 @@ import configureStore from './app/store/store' import txHelper from './lib/tx-helper' import { getEnvironmentType } from '../app/scripts/lib/util' import { ALERT_TYPES } from '../app/scripts/controllers/alert' +import { SENTRY_STATE } from '../app/scripts/lib/setupSentry' import { ENVIRONMENT_TYPE_POPUP } from '../app/scripts/lib/enums' import { fetchLocale, loadRelativeTimeFormatLocaleData } from './app/helpers/utils/i18n-helper' import switchDirection from './app/helpers/utils/switch-direction' @@ -138,6 +139,33 @@ async function startApp (metamaskState, backgroundConnection, opts) { return store } +/** + * Return a "masked" copy of the given object. + * + * The returned object includes only the properties present in the mask. The + * mask is an object that mirrors the structure of the given object, except + * the only values are `true` or a sub-mask. `true` implies the property + * should be included, and a sub-mask implies the property should be further + * masked according to that sub-mask. + * + * @param {Object} object - The object to mask + * @param {Object} mask - The mask to apply to the object + */ +function maskObject (object, mask) { + return Object.keys(object) + .reduce( + (state, key) => { + if (mask[key] === true) { + state[key] = object[key] + } else if (mask[key]) { + state[key] = maskObject(object[key], mask[key]) + } + return state + }, + {}, + ) +} + function setupDebuggingHelpers (store) { window.getCleanAppState = function () { const state = clone(store.getState()) @@ -145,6 +173,15 @@ function setupDebuggingHelpers (store) { state.browser = window.navigator.userAgent return state } + window.getSentryState = function () { + const fullState = store.getState() + const debugState = maskObject(fullState, SENTRY_STATE) + return { + browser: window.navigator.userAgent, + store: debugState, + version: global.platform.getVersion(), + } + } } window.logStateString = function (cb) {