From 1a45f21abbaf73fb2aa57ac0cc0a22c24af6b35f Mon Sep 17 00:00:00 2001 From: MananTank Date: Mon, 21 Oct 2024 16:42:19 +0000 Subject: [PATCH] Handle page crash when rendering NFTs with unexpected title, description values (#5095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem solved Short description of the bug fixed or feature added --- ## PR-Codex overview This PR introduces error handling components to display messages for unexpected values in the NFT-related pages, enhancing user experience by providing feedback when data does not meet expectations. ### Detailed summary - Added `react-error-boundary` to handle errors. - Created `UnexpectedValueErrorMessage` component for displaying error messages. - Integrated error handling in `TokenIdPage`, `NFTName`, and `NFTDescription` components. - Updated `NFTGetAllTable` to use `UnexpectedValueErrorMessage` for invalid data. - Wrapped NFT cell rendering in `ErrorBoundary` to catch errors. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- apps/dashboard/package.json | 1 + .../unexpect-value-error-message.tsx | 44 +++++++ .../nfts/[tokenId]/token-id.tsx | 49 +++++++- .../nfts/components/table.tsx | 110 +++++++++++++----- pnpm-lock.yaml | 27 +++-- 5 files changed, 193 insertions(+), 38 deletions(-) create mode 100644 apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 2e750a82979..49f84f86cca 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -85,6 +85,7 @@ "react-day-picker": "^8.10.1", "react-dom": "18.3.1", "react-dropzone": "^14.2.9", + "react-error-boundary": "^4.1.2", "react-hook-form": "7.52.0", "react-intersection-observer": "^9.10.3", "react-markdown": "^9.0.1", diff --git a/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx b/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx new file mode 100644 index 00000000000..1e3816e7700 --- /dev/null +++ b/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx @@ -0,0 +1,44 @@ +import { CopyTextButton } from "@/components/ui/CopyTextButton"; +import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow"; +import { useMemo } from "react"; + +export function UnexpectedValueErrorMessage(props: { + value: unknown; + title: string; + description: string; + className?: string; +}) { + const stringifiedValue = useMemo(() => { + try { + return JSON.stringify(props.value, null, 2); + } catch { + return undefined; + } + }, [props.value]); + + return ( +
+

{props.title}

+

{props.description}

+ {stringifiedValue && ( +
+

Value Received

+ + + {stringifiedValue} + + + + +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx index 347fc967375..c11b32962d9 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx @@ -1,5 +1,6 @@ "use client"; +import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message"; import { WalletAddress } from "@/components/blocks/wallet-address"; import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { Spinner } from "@/components/ui/Spinner/Spinner"; @@ -53,6 +54,8 @@ interface TokenIdPageProps { isErc721: boolean; } +// TODO: verify the entire nft object with zod schema and display an error message + export const TokenIdPage: React.FC = ({ contract, tokenId, @@ -132,15 +135,15 @@ export const TokenIdPage: React.FC = ({ height={isMobile ? "100%" : "300px"} /> + - - {nft.metadata.name} + + {nft.metadata?.description && ( - - {nft.metadata.description} - + )} + = ({ ); }; + +function NFTName(props: { + value: unknown; +}) { + if (typeof props.value === "string") { + return ( +

{props.value}

+ ); + } + + return ( + + ); +} + +function NFTDescription(props: { + value: unknown; +}) { + if (typeof props.value === "string") { + return

{props.value}

; + } + + return ( + + ); +} diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx index c156f12dd97..c03c50a0a3c 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx @@ -1,7 +1,10 @@ "use client"; +import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message"; import { WalletAddress } from "@/components/blocks/wallet-address"; +import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { useDashboardRouter } from "@/lib/DashboardRouter"; +import { cn } from "@/lib/utils"; import { Flex, IconButton, @@ -16,6 +19,7 @@ import { Thead, Tr, } from "@chakra-ui/react"; +import * as Sentry from "@sentry/nextjs"; import { MediaCell } from "components/contract-pages/table/table-columns/cells/media-cell"; import { useChainSlug } from "hooks/chains/chainSlug"; import { @@ -26,6 +30,7 @@ import { ChevronRightIcon, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; +import { ErrorBoundary, type FallbackProps } from "react-error-boundary"; import { type CellProps, type Column, @@ -72,24 +77,51 @@ export const NFTGetAllTable: React.FC = ({ { Header: "Name", accessor: (row) => row.metadata.name, - Cell: (cell: CellProps) => ( - - {cell.value} - - ), + Cell: (cell: CellProps) => { + if (typeof cell.value !== "string") { + return ( + + ); + } + + return ( +

+ {cell.value} +

+ ); + }, }, { Header: "Description", accessor: (row) => row.metadata.description, - Cell: (cell: CellProps) => ( - - {cell.value || "No description"} - - ), + Cell: (cell: CellProps) => { + if (typeof cell.value !== "string") { + return ( + + ); + } + + return ( +

+ {cell.value || "No description"} +

+ ); + }, }, ]; if (isErc721) { @@ -280,18 +312,22 @@ export const NFTGetAllTable: React.FC = ({ // biome-ignore lint/suspicious/noArrayIndexKey: FIXME key={rowIndex} > - {row.cells.map((cell, cellIndex) => ( - - {cell.render("Cell")} - - ))} + {row.cells.map((cell, cellIndex) => { + return ( + + + {cell.render("Cell")} + + + ); + })} {!failedToLoad && } @@ -374,3 +410,25 @@ export const NFTGetAllTable: React.FC = ({
); }; + +function NFTCellErrorBoundary(errorProps: FallbackProps) { + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + Sentry.withScope((scope) => { + scope.setTag("component-crashed", "true"); + scope.setTag("component-crashed-boundary", "NFTCellErrorBoundary"); + scope.setLevel("fatal"); + Sentry.captureException(errorProps.error); + }); + }, [errorProps.error]); + + return ( + + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8afc0b8f286..e4d9e8faf48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,9 @@ importers: react-dropzone: specifier: ^14.2.9 version: 14.2.9(react@18.3.1) + react-error-boundary: + specifier: ^4.1.2 + version: 4.1.2(react@18.3.1) react-hook-form: specifier: 7.52.0 version: 7.52.0(react@18.3.1) @@ -12836,6 +12839,11 @@ packages: react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0 + react-error-boundary@4.1.2: + resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==} + peerDependencies: + react: '>=16.13.1' + react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -19041,7 +19049,7 @@ snapshots: '@emotion/babel-plugin@11.12.0': dependencies: '@babel/helper-module-imports': 7.24.7 - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.1 @@ -20210,7 +20218,7 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 @@ -20223,7 +20231,7 @@ snapshots: '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -25685,7 +25693,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 cosmiconfig: 7.1.0 resolve: 1.22.8 @@ -26946,7 +26954,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 csstype: 3.1.3 dom-serializer@0.2.2: @@ -32443,6 +32451,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-is: 18.1.0 + react-error-boundary@4.1.2(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.7 + react: 18.3.1 + react-fast-compare@3.2.2: {} react-focus-lock@2.12.1(@types/react@18.3.11)(react@18.3.1): @@ -32761,7 +32774,7 @@ snapshots: react-select@5.8.0(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@emotion/cache': 11.13.1 '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) '@floating-ui/dom': 1.6.5 @@ -32808,7 +32821,7 @@ snapshots: react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1