diff --git a/src/components/DateTime.tsx b/src/components/DateTime.tsx index 0e2ff5e..e0699b5 100644 --- a/src/components/DateTime.tsx +++ b/src/components/DateTime.tsx @@ -7,6 +7,7 @@ import { TimeagoFormatterContext } from "@global/LocaleContext"; import { useBooleanSetting } from "@utils/settings"; +import { criticalError } from "@utils"; import dayjs from "dayjs"; import TimeAgo from "react-timeago"; @@ -53,7 +54,7 @@ realDate.toISOString(); } catch (err) { debug("error parsing date %s", date); - console.error(err); + criticalError(err); return <>INVALID DATE; } diff --git a/src/components/wallets/SyncWallets.tsx b/src/components/wallets/SyncWallets.tsx index 90f7212..ea1c0e5 100644 --- a/src/components/wallets/SyncWallets.tsx +++ b/src/components/wallets/SyncWallets.tsx @@ -11,6 +11,8 @@ import { syncWallets, useWallets, ADDRESS_LIST_LIMIT } from "@wallets"; import { useContacts } from "@contacts"; +import { criticalError } from "@utils"; + import Debug from "debug"; const debug = Debug("kristweb:sync-wallets"); @@ -28,7 +30,7 @@ syncWallets() .then(() => debug("synced")) .catch(err => { - console.error(err); + criticalError(err); notification.error({ message: t("syncWallets.errorMessage"), description: t("syncWallets.errorDescription"), diff --git a/src/global/ForcedAuth.tsx b/src/global/ForcedAuth.tsx index b1458de..fb5c641 100644 --- a/src/global/ForcedAuth.tsx +++ b/src/global/ForcedAuth.tsx @@ -7,6 +7,7 @@ import { authMasterPassword, useMasterPassword } from "@wallets"; import { useMountEffect } from "@utils/hooks"; +import { criticalError } from "@utils"; async function forceAuth(t: TFunction, salt: string, tester: string): Promise { try { @@ -16,7 +17,7 @@ await authMasterPassword(salt, tester, password); message.warning(t("masterPassword.forcedAuthWarning")); } catch (e) { - console.error(e); + criticalError(e); } } diff --git a/src/global/LocaleContext.tsx b/src/global/LocaleContext.tsx index edd4a55..64c8fc8 100644 --- a/src/global/LocaleContext.tsx +++ b/src/global/LocaleContext.tsx @@ -13,6 +13,8 @@ import { Formatter } from "react-timeago"; import buildFormatter from "react-timeago/lib/formatters/buildFormatter"; +import { criticalError } from "@utils"; + import Debug from "debug"; const debug = Debug("kristweb:locale-context"); @@ -55,7 +57,7 @@ debug("got dayjs locale %s", dayjsLocale); dayjs.locale(dayjsLocale); }) - .catch(console.error); + .catch(criticalError); }, [lang, langCode, languages]); // Load the timeago locale if available @@ -80,7 +82,7 @@ debug("got timeago locale %s", timeagoLocale); setTimeagoFormatter({ formatter: buildFormatter(strings.default) }); }) - .catch(console.error); + .catch(criticalError); }, [lang, langCode, languages]); // Load the antd locale if available @@ -105,7 +107,7 @@ debug("got antd locale %s", antLocaleCode); setAntLocale({ locale: locale.default }); }) - .catch(console.error); + .catch(criticalError); }, [lang, langCode, languages]); return diff --git a/src/global/compat/index.ts b/src/global/compat/index.ts index b4ed6ab..baafb95 100644 --- a/src/global/compat/index.ts +++ b/src/global/compat/index.ts @@ -5,6 +5,8 @@ import { openCompatCheckModal } from "./CompatCheckModal"; +import { criticalError } from "@utils"; + import Debug from "debug"; const debug = Debug("kristweb:compat-check"); @@ -43,7 +45,7 @@ throw new Error("check returned false"); } catch (err) { debug("compatibility check %s failed", check.name); - console.error(err); + criticalError(err); failed.push(check); } } diff --git a/src/global/legacy/LegacyMigrationForm.tsx b/src/global/legacy/LegacyMigrationForm.tsx index 6063696..45601dd 100644 --- a/src/global/legacy/LegacyMigrationForm.tsx +++ b/src/global/legacy/LegacyMigrationForm.tsx @@ -17,6 +17,8 @@ import { backupVerifyPassword, backupImport } from "@pages/backup/backupImport"; import { BackupResults } from "@pages/backup/backupResults"; +import { criticalError } from "@utils"; + import Debug from "debug"; const debug = Debug("kristweb:legacy-migration-form"); @@ -81,7 +83,7 @@ setMasterPasswordError(translateError(t, err)); } else { // Any other import error - console.error(err); + criticalError(err); notification.error({ message: tStr("errorUnknown") }); } } finally { diff --git a/src/global/ws/SyncDetailedWork.tsx b/src/global/ws/SyncDetailedWork.tsx index 04947b5..0358c5c 100644 --- a/src/global/ws/SyncDetailedWork.tsx +++ b/src/global/ws/SyncDetailedWork.tsx @@ -12,6 +12,8 @@ import * as api from "@api"; import { KristWorkDetailed } from "@api/types"; +import { criticalError } from "@utils"; + import Debug from "debug"; const debug = Debug("kristweb:sync-work"); @@ -28,8 +30,7 @@ const { lastBlockID } = useSelector((s: RootState) => s.node); useEffect(() => { - // TODO: show errors to the user? - updateDetailedWork().catch(console.error); + updateDetailedWork().catch(criticalError); }, [lastBlockID]); return null; diff --git a/src/global/ws/SyncMOTD.tsx b/src/global/ws/SyncMOTD.tsx index 4aa2c99..0e7706b 100644 --- a/src/global/ws/SyncMOTD.tsx +++ b/src/global/ws/SyncMOTD.tsx @@ -18,6 +18,8 @@ } from "@wallets"; import { useAddressPrefix } from "@utils/krist"; +import { criticalError } from "@utils"; + import Debug from "debug"; const debug = Debug("kristweb:sync-motd"); @@ -65,14 +67,15 @@ // Update the MOTD when the sync node changes, and on startup useEffect(() => { if (connectionState !== "connected") return; - updateMOTD().catch(console.error); + updateMOTD().catch(criticalError); }, [syncNode, connectionState]); // When the currency's address prefix changes, or our master password appears, // recalculate the addresses if necessary useEffect(() => { if (!addressPrefix || !masterPassword) return; - recalculateWallets(masterPassword, wallets, addressPrefix).catch(console.error); + recalculateWallets(masterPassword, wallets, addressPrefix) + .catch(criticalError); }, [addressPrefix, masterPassword, wallets]); return null; diff --git a/src/index.tsx b/src/index.tsx index 0e8e360..4f78bce 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,7 @@ import "@utils/errors"; import "@utils/setup"; import { i18nLoader } from "@utils/i18n"; -import { isLocalhost } from "@utils"; +import { isLocalhost, criticalError } from "@utils"; import ReactDOM from "react-dom"; @@ -57,7 +57,7 @@ if (err?.message === "compat checks failed") return; debug("critical error in index.tsx"); - console.error(err); + criticalError(err); notification.error({ message: "Critical error", diff --git a/src/krist/api/AuthFailed.tsx b/src/krist/api/AuthFailed.tsx index e78a511..a712396 100644 --- a/src/krist/api/AuthFailed.tsx +++ b/src/krist/api/AuthFailed.tsx @@ -9,6 +9,8 @@ import { Wallet, decryptWallet, useMasterPasswordOnly } from "@wallets"; +import { criticalError } from "@utils"; + import * as api from "./"; // Used to carry around information on which address failed auth @@ -81,9 +83,9 @@ // Perform the fetch api.post("/addresses/alert", { privatekey }) .then(res => setAlert(res.alert)) - .catch(console.error) + .catch(criticalError) .finally(() => setLoading(false)); - })().catch(console.error); + })().catch(criticalError); }, [wallet, masterPassword]); return diff --git a/src/krist/api/lookup.ts b/src/krist/api/lookup.ts index 248c87f..29c21e8 100644 --- a/src/krist/api/lookup.ts +++ b/src/krist/api/lookup.ts @@ -8,6 +8,8 @@ LookupFilterOptionsBase, LookupResponseBase, getFilterOptionsQuery } from "@utils/table/table"; +import { criticalError } from "@utils"; + // ============================================================================= // Addresses // ============================================================================= @@ -35,7 +37,7 @@ return data.addresses; } catch (err) { - console.error(err); + criticalError(err); } return {}; diff --git a/src/krist/contacts/contactStorage.ts b/src/krist/contacts/contactStorage.ts index 3a486f6..182dd0f 100644 --- a/src/krist/contacts/contactStorage.ts +++ b/src/krist/contacts/contactStorage.ts @@ -9,6 +9,7 @@ import { Contact, ContactMap } from "."; import { broadcastDeleteContact } from "@global/StorageBroadcast"; +import * as Sentry from "@sentry/react"; import Debug from "debug"; const debug = Debug("kristweb:contact-storage"); @@ -38,7 +39,13 @@ return contact; } catch (e) { - console.error(e); + Sentry.withScope(scope => { + scope.setTag("contact-id", id); + scope.setTag("contact-data", data); + + Sentry.captureException(e); + console.error(e); + }); if (e.name === "SyntaxError") // Invalid JSON throw new TranslatedError("masterPassword.errorStorageCorrupt"); diff --git a/src/krist/wallets/walletStorage.ts b/src/krist/wallets/walletStorage.ts index 077aaf9..8ea7167 100644 --- a/src/krist/wallets/walletStorage.ts +++ b/src/krist/wallets/walletStorage.ts @@ -9,6 +9,7 @@ import { Wallet, WalletMap } from "."; import { broadcastDeleteWallet } from "@global/StorageBroadcast"; +import * as Sentry from "@sentry/react"; import Debug from "debug"; const debug = Debug("kristweb:wallet-storage"); @@ -44,7 +45,13 @@ return wallet; } catch (e) { - console.error(e); + Sentry.withScope(scope => { + scope.setTag("wallet-id", id); + scope.setTag("wallet-data", data); + + Sentry.captureException(e); + console.error(e); + }); if (e.name === "SyntaxError") // Invalid JSON throw new TranslatedError("masterPassword.errorStorageCorrupt"); diff --git a/src/layout/nav/Search.tsx b/src/layout/nav/Search.tsx index f7edb31..b6d3fed 100644 --- a/src/layout/nav/Search.tsx +++ b/src/layout/nav/Search.tsx @@ -19,6 +19,7 @@ import * as SearchResults from "./SearchResults"; +import * as Sentry from "@sentry/react"; import Debug from "debug"; const debug = Debug("kristweb:search"); @@ -47,8 +48,15 @@ ]); } catch (err) { // Most likely error is `rate_limit_hit`: - if (err instanceof RateLimitError) onRateLimitHit(); - else console.error(err); + if (err instanceof RateLimitError) { + onRateLimitHit(); + } else { + Sentry.withScope(scope => { + scope.setTag("search-query", query); + Sentry.captureException(err); + console.error(err); + }); + } } } diff --git a/src/pages/backup/ExportBackupModal.tsx b/src/pages/backup/ExportBackupModal.tsx index 460d9e5..4ba0ade 100644 --- a/src/pages/backup/ExportBackupModal.tsx +++ b/src/pages/backup/ExportBackupModal.tsx @@ -16,6 +16,8 @@ import dayjs from "dayjs"; import { saveAs } from "file-saver"; +import { criticalError } from "@utils"; + interface Props { visible?: boolean; setVisible: Dispatch>; @@ -44,7 +46,7 @@ backupExport() .then(setCode) - .catch(console.error); + .catch(criticalError); }, [visible, wallets]); function saveToFile() { diff --git a/src/pages/names/mgmt/NameEditModal.tsx b/src/pages/names/mgmt/NameEditModal.tsx index a38e0f6..e77a47d 100644 --- a/src/pages/names/mgmt/NameEditModal.tsx +++ b/src/pages/names/mgmt/NameEditModal.tsx @@ -77,7 +77,7 @@ // Wrap the handleError function const onError = handleEditError.bind( handleEditError, - tFns, showAuthFailed, walletAddressMap + tFns, showAuthFailed, walletAddressMap, mode ); // Actually perform the bulk name edit diff --git a/src/pages/names/mgmt/checkName.ts b/src/pages/names/mgmt/checkName.ts index 8e15108..fce8423 100644 --- a/src/pages/names/mgmt/checkName.ts +++ b/src/pages/names/mgmt/checkName.ts @@ -4,6 +4,7 @@ import { Dispatch, SetStateAction } from "react"; import * as api from "@api"; +import { criticalError } from "@utils"; interface CheckNameResponse { available: boolean; @@ -18,7 +19,7 @@ const { available } = await api.get(url); setNameAvailable(available); } catch (err) { - console.error(err); + criticalError(err); setNameAvailable(undefined); } } diff --git a/src/pages/names/mgmt/handleErrors.ts b/src/pages/names/mgmt/handleErrors.ts index d287c53..851708d 100644 --- a/src/pages/names/mgmt/handleErrors.ts +++ b/src/pages/names/mgmt/handleErrors.ts @@ -9,12 +9,16 @@ import { AuthFailedError, ShowAuthFailedFn } from "@api/AuthFailed"; import { WalletAddressMap, Wallet } from "@wallets"; +import { Mode } from "./NameEditModal"; + +import { criticalError } from "@utils"; // Convert API errors to friendlier errors export async function handleEditError( { t, tKey, tStr, tErr }: TFns, showAuthFailed: ShowAuthFailedFn, walletAddressMap: WalletAddressMap, + mode: Mode, err: Error ): Promise { const onError = (err: Error) => notification.error({ @@ -43,7 +47,7 @@ } // Pass through any other unknown errors - console.error(err); + criticalError(err, { tags: { "name-mgmt": "edit", "name-edit": mode } }); onError(err); } @@ -71,6 +75,6 @@ } // Pass through any other unknown errors - console.error(err); + criticalError(err, { tags: { "name-mgmt": "purchase" } }); onError(err); } diff --git a/src/pages/settings/translations/SettingsTranslations.tsx b/src/pages/settings/translations/SettingsTranslations.tsx index a569e1a..ef52540 100644 --- a/src/pages/settings/translations/SettingsTranslations.tsx +++ b/src/pages/settings/translations/SettingsTranslations.tsx @@ -22,6 +22,8 @@ import { SmallResult } from "@comp/results/SmallResult"; import { SettingsPageLayout } from "../SettingsPage"; +import { criticalError } from "@utils"; + const { Title } = Typography; export function SettingsTranslations(): JSX.Element { @@ -43,7 +45,7 @@ useMountEffect(() => { analyseLanguages() .then(setAnalysed) - .catch(console.error) + .catch(criticalError) .finally(() => setLoading(false)); }); diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index 28e5af5..49f09a7 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -21,6 +21,8 @@ useMasterPasswordOnly } from "@wallets"; +import { criticalError } from "@utils"; + const { Text } = Typography; const { useBreakpoint } = Grid; @@ -137,7 +139,7 @@ closeModal(); } } catch (err) { - console.error(err); + criticalError(err); notification.error({ message: t("addWallet.errorUnexpectedTitle"), description: editing diff --git a/src/pages/whatsnew/WhatsNewPage.tsx b/src/pages/whatsnew/WhatsNewPage.tsx index 17bebee..5334fdd 100644 --- a/src/pages/whatsnew/WhatsNewPage.tsx +++ b/src/pages/whatsnew/WhatsNewPage.tsx @@ -12,7 +12,7 @@ import * as api from "@api"; import { WhatsNewResponse, Commit } from "./types"; -import { getAuthorInfo } from "@utils"; +import { getAuthorInfo, criticalError } from "@utils"; import { PageLayout } from "@layout/PageLayout"; @@ -43,7 +43,7 @@ // Fetch the 'whats new' and commits from the Krist sync node api.get("whatsnew") .then(setKristData) - .catch(console.error) // TODO: show errors to the user + .catch(criticalError) // TODO: show errors to the user .finally(() => setLoading(false)); }, [syncNode]); diff --git a/src/utils/errors.ts b/src/utils/errors.ts index f48fa5d..17f4e04 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -2,6 +2,7 @@ // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import * as Sentry from "@sentry/react"; +import { CaptureContext } from "@sentry/types"; import { Integrations } from "@sentry/tracing"; declare const __GIT_VERSION__: string; @@ -17,3 +18,11 @@ // We recommend adjusting this value in production tracesSampleRate: 1.0 }); + +export function criticalError( + err: Error | string, + captureContext?: CaptureContext +): void { + Sentry.captureException(err, captureContext); + console.error(err); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 8ab095d..d6bc2b3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,7 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +export * from "./errors"; export * from "./misc/credits"; export * from "./misc/math"; export * from "./misc/promiseThrottle"; diff --git a/src/utils/serviceWorkerRegistration.ts b/src/utils/serviceWorkerRegistration.ts index d427c73..7c920c2 100644 --- a/src/utils/serviceWorkerRegistration.ts +++ b/src/utils/serviceWorkerRegistration.ts @@ -16,6 +16,7 @@ import { isLocalhost } from "./"; +import { criticalError } from "@utils"; import Debug from "debug"; const debug = Debug("kristweb:service-worker"); @@ -116,7 +117,7 @@ }; }) .catch((error) => { - console.error("Error during service worker registration:", error); + criticalError("Error during service worker registration:", error); }); } @@ -157,7 +158,7 @@ registration.unregister(); }) .catch((error) => { - console.error(error.message); + criticalError(error.message); }); } }