diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts index 1211a12b..76baa8d4 100644 --- a/sentry.edge.config.ts +++ b/sentry.edge.config.ts @@ -11,9 +11,21 @@ if (process.env.NODE_ENV === "production") { dsn: "https://d4d2dfa789477bdaeea1a7e468f54d08@o4509168932880384.ingest.de.sentry.io/4509168934125648", // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, + tracesSampleRate: 0.2, // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, + + beforeSend(event, hint) { + const error = hint.originalException; + if ( + error instanceof Error && + "code" in error && + (error as Error & { code: string }).code === "FORBIDDEN" + ) { + return null; + } + return event; + }, }); } diff --git a/sentry.server.config.ts b/sentry.server.config.ts index a16a9aac..d7cf4c57 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -10,9 +10,21 @@ if (process.env.NODE_ENV === "production") { dsn: "https://d4d2dfa789477bdaeea1a7e468f54d08@o4509168932880384.ingest.de.sentry.io/4509168934125648", // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, + tracesSampleRate: 0.2, // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, + + beforeSend(event, hint) { + const error = hint.originalException; + if ( + error instanceof Error && + "code" in error && + (error as Error & { code: string }).code === "FORBIDDEN" + ) { + return null; + } + return event; + }, }); } diff --git a/src/app/(main)/pools/[address]/error.tsx b/src/app/(main)/pools/[address]/error.tsx new file mode 100644 index 00000000..89d7d778 --- /dev/null +++ b/src/app/(main)/pools/[address]/error.tsx @@ -0,0 +1,64 @@ +"use client"; // Error components must be Client Components + +import { RotateCw, TriangleAlert } from "lucide-react"; +import Link from "next/link"; +import { useEffect } from "react"; +import { ContentContainer } from "~/components/layout/content-container"; +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; +import { Button } from "~/components/ui/button"; + +interface ErrorPageProps { + error: Error & { digest?: string }; + reset: () => void; +} + +export default function PoolError({ error, reset }: ErrorPageProps) { + useEffect(() => { + // Log the error to an error reporting service + console.error("Pool error:", error); + }, [error]); + + return ( + +
+
+ +
+ +

+ Unable to Load Pool +

+ +

+ {error.message || + "An unexpected error occurred while loading this pool."} +

+ + {error.digest && ( + + Error Details + + {error.digest} + + + )} + +
+ + + + +
+
+
+ ); +} diff --git a/src/app/(main)/pools/[address]/page.tsx b/src/app/(main)/pools/[address]/page.tsx index ce61fddc..4cb0abc9 100644 --- a/src/app/(main)/pools/[address]/page.tsx +++ b/src/app/(main)/pools/[address]/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { getAddress } from "viem"; +import { getAddress, isAddress } from "viem"; import { getContractIndex, getSwapPool, @@ -25,6 +25,9 @@ type Props = { export async function generateMetadata(props: Props): Promise { const params = await props.params; + if (!isAddress(params.address)) { + return { title: "Invalid Pool Address" }; + } const pool_address = getAddress(params.address); const poolDetails = await getSwapPool(publicClient, pool_address); diff --git a/src/app/(main)/pools/[address]/pool-client-page.tsx b/src/app/(main)/pools/[address]/pool-client-page.tsx index 30ab7093..eb536f96 100644 --- a/src/app/(main)/pools/[address]/pool-client-page.tsx +++ b/src/app/(main)/pools/[address]/pool-client-page.tsx @@ -4,7 +4,7 @@ import { TagIcon } from "lucide-react"; import Image from "next/image"; import { useParams } from "next/navigation"; import { toast } from "sonner"; -import { getAddress } from "viem"; +import { getAddress, isAddress } from "viem"; import Address from "~/components/address"; import { EditableImageOverlay } from "~/components/editable-image-overlay"; import { ContentContainer } from "~/components/layout/content-container"; @@ -20,9 +20,12 @@ import { PoolTabs } from "./pool-tabs"; export function PoolClientPage() { const { address } = useParams<{ address: string }>(); - const pool_address = getAddress(address); + const isValid = isAddress(address); + const pool_address = isValid ? getAddress(address) : ("0x0000000000000000000000000000000000000000" as `0x${string}`); const { data: pool } = useSwapPool(pool_address); - const { data: metadata } = trpc.pool.get.useQuery(pool_address); + const { data: metadata } = trpc.pool.get.useQuery(pool_address, { + enabled: isValid, + }); const isOwner = useIsContractOwner(pool_address); const auth = useAuth(); @@ -47,6 +50,15 @@ export function PoolClientPage() { } }; + if (!isValid) { + return ( + +
+

Invalid pool address

+
+
+ ); + } return ( {/* Modern Hero Section */} diff --git a/src/app/(main)/vouchers/[address]/error.tsx b/src/app/(main)/vouchers/[address]/error.tsx new file mode 100644 index 00000000..7813da49 --- /dev/null +++ b/src/app/(main)/vouchers/[address]/error.tsx @@ -0,0 +1,64 @@ +"use client"; // Error components must be Client Components + +import { RotateCw, TriangleAlert } from "lucide-react"; +import Link from "next/link"; +import { useEffect } from "react"; +import { ContentContainer } from "~/components/layout/content-container"; +import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; +import { Button } from "~/components/ui/button"; + +interface ErrorPageProps { + error: Error & { digest?: string }; + reset: () => void; +} + +export default function VoucherError({ error, reset }: ErrorPageProps) { + useEffect(() => { + // Log the error to an error reporting service + console.error("Voucher error:", error); + }, [error]); + + return ( + +
+
+ +
+ +

+ Unable to Load Voucher +

+ +

+ {error.message || + "An unexpected error occurred while loading this voucher."} +

+ + {error.digest && ( + + Error Details + + {error.digest} + + + )} + +
+ + + + +
+
+
+ ); +} diff --git a/src/components/file-uploader/use-file-upload.tsx b/src/components/file-uploader/use-file-upload.tsx index 88f9ebee..6fc34c60 100644 --- a/src/components/file-uploader/use-file-upload.tsx +++ b/src/components/file-uploader/use-file-upload.tsx @@ -11,6 +11,7 @@ const useFileUpload = () => { const xhr = new XMLHttpRequest(); xhr.open("POST", "https://storage.sarafu.network/v1/upload"); + xhr.timeout = 30_000; xhr.upload.onprogress = (event) => { if (event.lengthComputable) { @@ -20,9 +21,17 @@ const useFileUpload = () => { }; return new Promise((resolve, reject) => { - xhr.onerror = (e) => { - console.error(e); - reject(new Error("File upload failed")); + xhr.ontimeout = () => { + toast.error("File upload timed out. Please try again."); + reject(new Error("File upload timed out")); + }; + xhr.onerror = () => { + console.error("File upload network error", { + status: xhr.status, + statusText: xhr.statusText, + }); + toast.error("File upload failed. Please check your connection."); + reject(new Error(`File upload failed: network error`)); }; xhr.onload = () => { if (xhr.status === 200) { @@ -37,11 +46,13 @@ const useFileUpload = () => { resolve(response.payload.s3); } else { toast.error("File upload failed"); - reject(new Error("File upload failed")); + reject(new Error("File upload failed: server returned ok=false")); } } else { - toast.error("File upload failed"); - reject(new Error("File upload failed")); + const errorMsg = `File upload failed (HTTP ${xhr.status})`; + console.error(errorMsg, xhr.responseText?.slice(0, 200)); + toast.error(errorMsg); + reject(new Error(errorMsg)); } }; diff --git a/src/components/file-uploader/webcam-capture.tsx b/src/components/file-uploader/webcam-capture.tsx index 05101972..6534a516 100644 --- a/src/components/file-uploader/webcam-capture.tsx +++ b/src/components/file-uploader/webcam-capture.tsx @@ -1,6 +1,7 @@ import { CameraIcon, RotateCcwIcon, XIcon } from "lucide-react"; import { forwardRef, useState } from "react"; import Webcam from "react-webcam"; +import { toast } from "sonner"; import { Button } from "../ui/button"; const WebcamCapture = forwardRef< @@ -18,6 +19,14 @@ const WebcamCapture = forwardRef< })); }; + const handleCameraError = (error: string | DOMException) => { + console.error("Camera error:", error); + toast.error( + "Camera access failed. Please check permissions or use file upload instead." + ); + props.onCancel(); + }; + return (
); diff --git a/src/contracts/writer.ts b/src/contracts/writer.ts index 6cac25aa..b18ea03f 100644 --- a/src/contracts/writer.ts +++ b/src/contracts/writer.ts @@ -1,3 +1,4 @@ +import * as Sentry from "@sentry/nextjs"; import { createWalletClient } from "viem"; import { nonceManager, privateKeyToAccount } from "viem/accounts"; import { celo } from "viem/chains"; @@ -81,6 +82,14 @@ export async function withWriterLock(fn: () => Promise): Promise { const lockId = await acquireWriterLock(); try { return await fn(); + } catch (error) { + Sentry.addBreadcrumb({ + category: "writer", + message: "Writer lock transaction failed", + data: { lockId }, + level: "error", + }); + throw error; } finally { await releaseWriterLock(lockId); } diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts index 564d0c48..28e0759a 100644 --- a/src/instrumentation-client.ts +++ b/src/instrumentation-client.ts @@ -10,10 +10,22 @@ if (process.env.NODE_ENV === "production") { dsn: "https://d4d2dfa789477bdaeea1a7e468f54d08@o4509168932880384.ingest.de.sentry.io/4509168934125648", // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, + tracesSampleRate: 0.2, // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, + + ignoreErrors: [ + /router state header.*could not be parsed/i, + /Failed to execute 'removeChild' on 'Node'/, + /Failed to execute 'insertBefore' on 'Node'/, + /Failed to execute 'appendChild' on 'Node'/, + /setPhotoOptions failed/, + /The associated Track is in an invalid state/, + /Proposal expired/, + /Connection interrupted/, + /Error invoking postEvent/, + ], }); } diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 8aff09f0..2186648b 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -10,4 +10,16 @@ export async function register() { } } -export const onRequestError = Sentry.captureRequestError; +export const onRequestError = ( + ...args: Parameters +) => { + const error = args[0]; + if ( + error instanceof Error && + "code" in error && + (error as Error & { code: string }).code === "FORBIDDEN" + ) { + return; + } + Sentry.captureRequestError(...args); +}; diff --git a/src/server/api/models/token.ts b/src/server/api/models/token.ts index eabcc644..848363b4 100644 --- a/src/server/api/models/token.ts +++ b/src/server/api/models/token.ts @@ -6,7 +6,7 @@ import { type PublicClient, type Transport, } from "viem"; -import { type CeloChain, publicClient } from "~/config/viem.config.server"; +import { type CeloChain } from "~/config/viem.config.server"; export async function getTokenDetails( client: PublicClient, @@ -30,7 +30,7 @@ export async function getTokenDetails( try { const [name, symbol, decimals] = await Promise.all([ - publicClient.readContract({ + client.readContract({ address, abi: erc20Abi, functionName: "name", @@ -55,8 +55,11 @@ export async function getTokenDetails( await redis.set(cacheKey, tokenDetails, { ex: 60 * 60 * 24 }); // 24 hours return tokenDetails; } catch (error) { - console.error(error); - throw new Error("Failed to fetch token details from contract"); + console.error(`Failed to fetch token details for ${address}:`, error); + throw new Error( + `Failed to fetch token details for ${address}: ${error instanceof Error ? error.message : "Unknown error"}`, + { cause: error } + ); } }