From 0aa6ce55c9716fdaa27e29112bf61ad8f13b197d Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Mon, 3 Feb 2025 12:37:04 -0800 Subject: [PATCH] fix: capture some errors we were logging to console We're losing visibility into user errors because we are catching and logging Errors to the console - replace a bunch of places where we do this with a new Sentry logging function that logs to the error console and then captures the error in Sentry. --- src/app/migration/create/page.tsx | 3 ++- src/app/settings/page.tsx | 5 +++-- src/app/space/[did]/page.tsx | 3 ++- src/app/space/[did]/root/[cid]/page.tsx | 3 ++- .../space/[did]/root/[cid]/shard/[shard]/page.tsx | 7 ++++--- src/components/MigrationsProvider.tsx | 3 ++- src/components/SpaceCreator.tsx | 3 ++- src/components/Uploader.tsx | 5 +++-- src/hooks.tsx | 3 ++- src/lib/migrations/nft-storage.ts | 9 +++++---- src/lib/migrations/store.ts | 3 ++- src/lib/migrations/web3-storage-psa.ts | 5 +++-- src/lib/migrations/web3-storage.ts | 7 ++----- src/sentry.ts | 12 ++++++++++++ src/share.tsx | 7 ++++--- 15 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 src/sentry.ts diff --git a/src/app/migration/create/page.tsx b/src/app/migration/create/page.tsx index b894a20..101fc63 100644 --- a/src/app/migration/create/page.tsx +++ b/src/app/migration/create/page.tsx @@ -9,6 +9,7 @@ import { DIDKey, useW3 } from '@w3ui/react' import { DidIcon } from '@/components/DidIcon' import { MigrationConfiguration, DataSourceID } from '@/lib/migrations/api' import { dataSources } from '@/app/migration/data-sources' +import { logAndCaptureError } from '@/sentry' interface WizardProps { config: Partial @@ -91,7 +92,7 @@ function AddSourceToken ({ config, onNext, onPrev }: WizardProps) { try { await ds.source.checkToken(token) } catch (err: any) { - console.error(err) + logAndCaptureError(err) return setError(`Error using token: ${err.message}`) } finally { setChecking(false) diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index b07703b..474c177 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -10,6 +10,7 @@ import { GB, TB, filesize } from '@/lib' import DefaultLoader from '@/components/Loader' import { RefcodeLink, ReferralsList, RefcodeCreator } from '../referrals/page' import { useReferrals } from '@/lib/referrals/hooks' +import { logAndCaptureError } from '@/sentry' const Plans: Record<`did:${string}`, { name: string, limit: number }> = { 'did:web:starter.web3.storage': { name: 'Starter', limit: 5 * GB }, @@ -51,13 +52,13 @@ export default function SettingsPage (): JSX.Element { } } catch (err) { // TODO: figure out why usage/report cannot be used on old spaces - console.error(err) + logAndCaptureError(err) } } } return usage }, - onError: err => console.error(err.message, err.cause) + onError: logAndCaptureError }) const product = plan?.product diff --git a/src/app/space/[did]/page.tsx b/src/app/space/[did]/page.tsx index 0612fad..864ae97 100644 --- a/src/app/space/[did]/page.tsx +++ b/src/app/space/[did]/page.tsx @@ -6,6 +6,7 @@ import useSWR from 'swr' import { useRouter, usePathname } from 'next/navigation' import { createUploadsListKey } from '@/cache' import { Breadcrumbs } from '@/components/Breadcrumbs' +import { logAndCaptureError } from '@/sentry' const pageSize = 15 @@ -39,7 +40,7 @@ export default function Page ({ params, searchParams }: PageProps): JSX.Element size: pageSize }) }, - onError: err => console.error(err.message, err.cause), + onError: logAndCaptureError, keepPreviousData: true }) diff --git a/src/app/space/[did]/root/[cid]/page.tsx b/src/app/space/[did]/root/[cid]/page.tsx index 2230121..dd8b7d8 100644 --- a/src/app/space/[did]/root/[cid]/page.tsx +++ b/src/app/space/[did]/root/[cid]/page.tsx @@ -13,6 +13,7 @@ import CopyIcon from '@/components/CopyIcon' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useRouter } from 'next/navigation' import { createUploadsListKey } from '@/cache' +import { logAndCaptureError } from '@/sentry' interface PageProps { params: { @@ -38,7 +39,7 @@ export default function ItemPage ({ params }: PageProps): JSX.Element { return await client.capability.upload.get(root) }, - onError: err => console.error(err.message, err.cause) + onError: logAndCaptureError }) const [isRemoveConfirmModalOpen, setRemoveConfirmModalOpen] = useState(false) diff --git a/src/app/space/[did]/root/[cid]/shard/[shard]/page.tsx b/src/app/space/[did]/root/[cid]/shard/[shard]/page.tsx index e864535..a72d0de 100644 --- a/src/app/space/[did]/root/[cid]/shard/[shard]/page.tsx +++ b/src/app/space/[did]/root/[cid]/shard/[shard]/page.tsx @@ -18,6 +18,7 @@ import ExpandIcon from '@/components/ExpandIcon' import { useState } from 'react' import AggregateIcon from '@/components/AggregateIcon' import PieceIcon from '@/components/PieceIcon' +import { logAndCaptureError } from '@/sentry' type ProofStyle = 'mini'|'midi'|'maxi' @@ -67,7 +68,7 @@ export default function ItemPage ({ params }: PageProps): JSX.Element { } } }, - onError: err => console.error(err.message, err.cause) + onError: logAndCaptureError }) const claimKey = `/assert/equals?content=${shard}` @@ -80,7 +81,7 @@ export default function ItemPage ({ params }: PageProps): JSX.Element { } } }, - onError: err => console.error(err.message, err.cause) + onError: logAndCaptureError }) const filecoinInfoKey = `/filecoin/info?piece=${claim.data?.equals}` @@ -99,7 +100,7 @@ export default function ItemPage ({ params }: PageProps): JSX.Element { return out.ok }, - onError: err => console.error(err.message, err.cause) + onError: logAndCaptureError }) const [proofStyle, setProofStyle] = useState('mini') diff --git a/src/components/MigrationsProvider.tsx b/src/components/MigrationsProvider.tsx index 8394c55..57416a6 100644 --- a/src/components/MigrationsProvider.tsx +++ b/src/components/MigrationsProvider.tsx @@ -6,6 +6,7 @@ import { useW3 } from '@w3ui/react' import * as Migrations from '@/lib/migrations' import { MigrationsStorage } from '@/lib/migrations/store' import { serviceConnection } from './services' +import { logAndCaptureError } from '@/sentry' const MAX_LOG_LINES = 1000 @@ -126,7 +127,7 @@ export function Provider ({ children }: ProviderProps): ReactNode { setMigrations(() => migrationsStore.load()) }, onError: async (err, upload, shard) => { - console.error(err) + logAndCaptureError(err) log(id, `failed migration ${upload.root}${shard ? ` (shard: ${shard.link})` : ''}: ${err.stack}`) const migration = migrationsStore.read(id) migration.progress = migration.progress ?? await initProgress() diff --git a/src/components/SpaceCreator.tsx b/src/components/SpaceCreator.tsx index db44fa3..7de3eb4 100644 --- a/src/components/SpaceCreator.tsx +++ b/src/components/SpaceCreator.tsx @@ -9,6 +9,7 @@ import Link from 'next/link' import { FolderPlusIcon, InformationCircleIcon } from '@heroicons/react/24/outline' import Tooltip from './Tooltip' import { H3 } from './Text' +import { logAndCaptureError } from '@/sentry' export function SpaceCreatorCreating(): JSX.Element { return ( @@ -79,7 +80,7 @@ export function SpaceCreatorForm({ resetForm() } catch (error) { /* eslint-disable-next-line no-console */ - console.error(error) + logAndCaptureError(error) throw new Error('failed to create space', { cause: error }) } } diff --git a/src/components/Uploader.tsx b/src/components/Uploader.tsx index 1e2aef6..540d7c2 100644 --- a/src/components/Uploader.tsx +++ b/src/components/Uploader.tsx @@ -16,6 +16,7 @@ import { gatewayHost } from '../components/services' import { useEffect, useState } from 'react' import { RadioGroup } from '@headlessui/react' import { H2 } from './Text' +import { logAndCaptureError } from '@/sentry' function StatusLoader ({ progressStatus }: { progressStatus: ProgressStatus }) { const { total, loaded, lengthComputable } = progressStatus @@ -66,7 +67,7 @@ export const Errored = ({ error }: { error: any }): JSX.Element => { useEffect(() => { if (error != null) { // eslint-disable-next-line no-console - console.error('Uploader Error:', error) + logAndCaptureError(new Error('Uploader Error:', { cause: error })) } }, [error]) @@ -256,7 +257,7 @@ const UploaderContents = (): JSX.Element => {
diff --git a/src/hooks.tsx b/src/hooks.tsx index 1656f54..fe427c7 100644 --- a/src/hooks.tsx +++ b/src/hooks.tsx @@ -1,5 +1,6 @@ import { Account, DID, PlanGetSuccess, PlanSetSuccess, PlanSetFailure, Result } from '@w3ui/react' import useSWR, { SWRResponse, useSWRConfig } from 'swr' +import { logAndCaptureError } from './sentry' /** * calculate the cache key for a plan's account @@ -18,7 +19,7 @@ export const usePlan = (account: Account) => { if (result.error) throw new Error('getting plan', { cause: result.error }) return result.ok }, - onError: err => console.error(err.message, err.cause) + onError: logAndCaptureError }) // @ts-ignore it's important to assign this into the existing object // to avoid calling the getters in SWRResponse when copying values over - diff --git a/src/lib/migrations/nft-storage.ts b/src/lib/migrations/nft-storage.ts index cb6a203..43446a4 100644 --- a/src/lib/migrations/nft-storage.ts +++ b/src/lib/migrations/nft-storage.ts @@ -6,6 +6,7 @@ import { CarBlockIterator } from '@ipld/car' import { LinkIndexer } from 'linkdex' import { DataSourceConfiguration, Shard, Upload } from './api' import { carCode } from './constants' +import { logAndCaptureError } from '@/sentry' export const id = 'classic.nft.storage' @@ -118,9 +119,9 @@ class Reader { } } } catch (err) { - console.error(`failed to read content claims for PSA item: ${root}`, err) + logAndCaptureError(new Error(`failed to read content claims for PSA item: ${root}`, { cause: err })) } - } + } const shards: Shard[] = [] for (const p of parts) { @@ -163,7 +164,7 @@ class Reader { const link = Link.create(carCode, await sha256.digest(bytes)) shards.push({ link, size: async () => bytes.length, bytes: async () => bytes }) } catch (err) { - console.error(`failed to download CAR for item: ${root}`, err) + logAndCaptureError(new Error(`failed to download CAR for item: ${root}`, {cause: err})) } } @@ -191,7 +192,7 @@ async function * paginator (fn: (service: Service, opts: ListOptions) => Promise // Iterate through next pages while (body && body.value.length) { // Get before timestamp with less 1ms - const before = (new Date((new Date(body.value[body.value.length-1].created)).getTime() - 1)).toISOString() + const before = (new Date((new Date(body.value[body.value.length - 1].created)).getTime() - 1)).toISOString() res = await fn(service, { ...opts, before }) body = await res.json() yield body diff --git a/src/lib/migrations/store.ts b/src/lib/migrations/store.ts index f5f86cd..0000b04 100644 --- a/src/lib/migrations/store.ts +++ b/src/lib/migrations/store.ts @@ -1,5 +1,6 @@ import * as dagJSON from '@ipld/dag-json' import { Migration, MigrationConfiguration, MigrationID } from './api' +import { logAndCaptureError } from '@/sentry' export class MigrationsStorage { load () { @@ -13,7 +14,7 @@ export class MigrationsStorage { const migration: Migration = dagJSON.parse(localStorage.getItem(`migration.${id}`) ?? '') migrations.push(migration) } catch (err) { - console.error(`failed to load migration: ${id}`, err) + logAndCaptureError(new Error(`failed to load migration: ${id}`, {cause: err})) } } return migrations diff --git a/src/lib/migrations/web3-storage-psa.ts b/src/lib/migrations/web3-storage-psa.ts index c90eea0..febe734 100644 --- a/src/lib/migrations/web3-storage-psa.ts +++ b/src/lib/migrations/web3-storage-psa.ts @@ -5,6 +5,7 @@ import { DataSourceConfiguration, Shard, Upload } from './api' import * as Gateway from './gateway' import { Result, Failure } from '@ucanto/interface' import { CARLink } from '@w3ui/react' +import { logAndCaptureError } from '@/sentry' export const id = 'psa.old.web3.storage' @@ -98,7 +99,7 @@ class Reader { } }) } catch (err) { - console.error(`determining CAR hash for root: ${root}`, err) + logAndCaptureError(new Error(`determining CAR hash for root: ${root}`, {cause: err})) } // Add a synthetic shard that is the entire DAG. @@ -108,7 +109,7 @@ class Reader { const shard = await Gateway.fetchCAR(root) shards.push(shard) } catch (err) { - console.error(`downloading CAR for root: ${root}`, err) + logAndCaptureError(new Error(`downloading CAR for root: ${root}`, {cause: err})) } } diff --git a/src/lib/migrations/web3-storage.ts b/src/lib/migrations/web3-storage.ts index 6c6486e..b949056 100644 --- a/src/lib/migrations/web3-storage.ts +++ b/src/lib/migrations/web3-storage.ts @@ -1,11 +1,8 @@ import { Web3Storage } from 'web3.storage' import * as Link from 'multiformats/link' -import { sha256 } from 'multiformats/hashes/sha2' -import { CarBlockIterator } from '@ipld/car' -import { LinkIndexer } from 'linkdex' import { DataSourceConfiguration, Shard, Upload } from './api' -import { carCode } from './constants' import { fetchCAR } from './gateway' +import { logAndCaptureError } from '@/sentry' export const id = 'old.web3.storage' @@ -81,7 +78,7 @@ class Reader { const shard = await fetchCAR(root) shards.push(shard) } catch (err) { - console.error(`failed to download CAR for item: ${root}`, err) + logAndCaptureError(new Error(`failed to download CAR for item: ${root}`, {cause: err})) } } diff --git a/src/sentry.ts b/src/sentry.ts new file mode 100644 index 0000000..1b16723 --- /dev/null +++ b/src/sentry.ts @@ -0,0 +1,12 @@ + +import * as Sentry from '@sentry/nextjs' + +/** + * Log to the error console and capture the error in Sentry. + * + * @param err the error - typed as unknown to match catch(err) + */ +export function logAndCaptureError (err: unknown) { + console.error(err) + Sentry.captureException(err) +} \ No newline at end of file diff --git a/src/share.tsx b/src/share.tsx index 4f4fb30..febcbd5 100644 --- a/src/share.tsx +++ b/src/share.tsx @@ -10,6 +10,7 @@ import Tooltip from './components/Tooltip' import { ArrowDownOnSquareStackIcon, CloudArrowDownIcon, PaperAirplaneIcon, InformationCircleIcon } from '@heroicons/react/24/outline' import * as DIDMailTo from '@web3-storage/did-mailto' import { DID } from '@ucanto/core' +import { logAndCaptureError } from './sentry' function Header(props: PropsWithChildren): JSX.Element { return ( @@ -115,7 +116,7 @@ export function ShareSpace({ spaceDID }: { spaceDID: SpaceDID }): JSX.Element { updateSharedEmails([next]) setValue('') } catch (err) { - console.error(err) + logAndCaptureError(err) } finally { // Reset to the original space if it was different if (currentSpace && currentSpace !== spaceDID) { @@ -271,14 +272,14 @@ export function ImportSpace() { } delegation = res.ok } catch (err) { - console.error(err) + logAndCaptureError(err) return } try { await client.addSpace(delegation) setProof(delegation) } catch (err) { - console.error(err) + logAndCaptureError(err) } }