From d406055e8e52ee815d880f057475591d00a4c17b Mon Sep 17 00:00:00 2001 From: William Luke Date: Thu, 9 Apr 2026 10:32:45 +0300 Subject: [PATCH 1/5] fix: filter noisy Sentry errors, fix bugs, and add debugging context Filter expected errors from Sentry (FORBIDDEN TRPCErrors, browser extension DOM errors, camera/WalletConnect noise), fix optional chaining bugs in voucher stats and address validation in pool pages, add error boundaries for voucher/pool detail pages, improve file upload error messages with timeouts and status codes, add webcam error handling, and add Sentry breadcrumbs to writer lock failures. Co-Authored-By: Claude Opus 4.6 --- sentry.edge.config.ts | 14 +++- sentry.server.config.ts | 14 +++- src/app/(main)/pools/[address]/error.tsx | 64 +++++++++++++++++++ src/app/(main)/pools/[address]/page.tsx | 5 +- .../pools/[address]/pool-client-page.tsx | 11 +++- src/app/(main)/vouchers/[address]/error.tsx | 64 +++++++++++++++++++ .../file-uploader/use-file-upload.tsx | 23 +++++-- .../file-uploader/webcam-capture.tsx | 10 +++ .../voucher/voucher-statistics-grid.tsx | 18 +++--- src/contracts/writer.ts | 9 +++ src/instrumentation-client.ts | 14 +++- src/instrumentation.ts | 14 +++- src/server/api/models/token.ts | 11 ++-- 13 files changed, 246 insertions(+), 25 deletions(-) create mode 100644 src/app/(main)/pools/[address]/error.tsx create mode 100644 src/app/(main)/vouchers/[address]/error.tsx 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 19175195..b98a191f 100644 --- a/src/app/(main)/pools/[address]/pool-client-page.tsx +++ b/src/app/(main)/pools/[address]/pool-client-page.tsx @@ -3,7 +3,7 @@ import { TagIcon } from "lucide-react"; import Image from "next/image"; import { useParams } from "next/navigation"; -import { getAddress } from "viem"; +import { getAddress, isAddress } from "viem"; import Address from "~/components/address"; import { ContentContainer } from "~/components/layout/content-container"; import { useSwapPool } from "~/components/pools/hooks"; @@ -16,6 +16,15 @@ import { PoolTabs } from "./pool-tabs"; export function PoolClientPage() { const { address } = useParams<{ address: string }>(); + if (!isAddress(address)) { + return ( + +
+

Invalid pool address

+
+
+ ); + } const pool_address = getAddress(address); const { data: pool } = useSwapPool(pool_address); const { data: metadata } = trpc.pool.get.useQuery(pool_address); 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 } + ); } } From a6bc3039ab17ab45489154bab5829063461d405c Mon Sep 17 00:00:00 2001 From: William Luke Date: Thu, 9 Apr 2026 10:35:44 +0300 Subject: [PATCH 2/5] fix: move hooks before early return in pool client page Fixes React hooks rules violation by calling all hooks unconditionally before the address validation early return. Co-Authored-By: Claude Opus 4.6 --- .../(main)/pools/[address]/pool-client-page.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/pools/[address]/pool-client-page.tsx b/src/app/(main)/pools/[address]/pool-client-page.tsx index b98a191f..83d93583 100644 --- a/src/app/(main)/pools/[address]/pool-client-page.tsx +++ b/src/app/(main)/pools/[address]/pool-client-page.tsx @@ -16,7 +16,16 @@ import { PoolTabs } from "./pool-tabs"; export function PoolClientPage() { const { address } = useParams<{ address: string }>(); - if (!isAddress(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, { + enabled: isValid, + }); + + const isOwner = useIsContractOwner(pool_address); + + if (!isValid) { return (
@@ -25,11 +34,6 @@ export function PoolClientPage() { ); } - const pool_address = getAddress(address); - const { data: pool } = useSwapPool(pool_address); - const { data: metadata } = trpc.pool.get.useQuery(pool_address); - - const isOwner = useIsContractOwner(pool_address); return ( {/* Modern Hero Section */} From 53fd0ead8d728cd2ccfe1fa4946bca84811aabc4 Mon Sep 17 00:00:00 2001 From: William Luke Date: Thu, 9 Apr 2026 12:24:22 +0300 Subject: [PATCH 3/5] fix: increase claude-review max-turns from 10 to 20 The CI review bot was hitting the 10-turn limit on larger PRs, causing the action to fail. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/claude-code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 7320a21e..2ac37cac 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -47,4 +47,4 @@ jobs: - Check for unnecessary re-renders in client components Be constructive and specific. Reference the relevant file and line when possible. - claude_args: "--max-turns 10" + claude_args: "--max-turns 20" From c5e1c613068bfcf729a4e37961c7881ba19218c2 Mon Sep 17 00:00:00 2001 From: William Luke Date: Thu, 9 Apr 2026 12:35:56 +0300 Subject: [PATCH 4/5] fix: update claude-code-review workflow with progress tracking Updated to use actions/checkout@v6, enabled track_progress, and revised the review prompt with structured focus areas. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/claude-code-review.yml | 76 +++++++++++++++--------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 2ac37cac..d46efa44 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,20 +1,22 @@ -name: Claude Code Review +name: PR Review with Progress Tracking + +# This example demonstrates how to use the track_progress feature to get +# visual progress tracking for PR reviews, similar to v0.x agent mode. on: pull_request: - types: [opened, synchronize] + types: [opened, synchronize, ready_for_review, reopened] jobs: - claude-review: + review-with-tracking: runs-on: ubuntu-latest permissions: contents: read pull-requests: write - issues: read id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 @@ -23,28 +25,44 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + # Enable progress tracking + track_progress: true + + # Your custom review instructions prompt: | - Review this pull request with focus on this codebase's specific patterns: - - ## Code Quality - - tRPC routers: correct procedure type (publicProcedure vs authenticatedProcedure vs staffProcedure), proper Zod input validation, class-based model usage - - Kysely queries: safe query construction, correct usage of dual databases (graphDB for app data, federatedDB for blockchain data via FDW) - - TypeScript strict mode compliance (noUncheckedIndexedAccess — check for unchecked indexed access) - - Coding conventions: `~/` path alias, inline type imports, kebab-case files, const object enums (not TS enum) - - ## Architecture - - Server/Client Component split: unnecessary "use client" directives, data fetching in Server Components via `caller`, props passed to `*Client` components - - Caching: proper use of cacheQuery with TTL and tag-based invalidation, cache key correctness - - ## Security - - SIWE authentication: proper session checks, RBAC permission enforcement via hasPermission() - - Blockchain interactions: address validation with Viem, safe smart contract calls, no hardcoded private keys or sensitive data - - Input validation: Zod schemas on all tRPC inputs, no unsanitized user input in queries - - ## Performance - - Avoid N+1 queries in Kysely models, prefer batch fetches - - Proper use of React Suspense boundaries and streaming - - Check for unnecessary re-renders in client components - - Be constructive and specific. Reference the relevant file and line when possible. - claude_args: "--max-turns 20" + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Perform a comprehensive code review with the following focus areas: + + 1. **Code Quality** + - Clean code principles and best practices + - Proper error handling and edge cases + - Code readability and maintainability + + 2. **Security** + - Check for potential security vulnerabilities + - Validate input sanitization + - Review authentication/authorization logic + + 3. **Performance** + - Identify potential performance bottlenecks + - Review database queries for efficiency + - Check for memory leaks or resource issues + + 4. **Testing** + - Verify adequate test coverage + - Review test quality and edge cases + - Check for missing test scenarios + + 5. **Documentation** + - Ensure code is properly documented + - Verify README updates for new features + - Check API documentation accuracy + + Provide detailed feedback using inline comments for specific issues. + Use top-level comments for general observations or praise. + + # Tools for comprehensive PR review + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" From 3ca34bb3c02b8388d134df56bac8a8a2175d34ad Mon Sep 17 00:00:00 2001 From: William Luke Date: Thu, 9 Apr 2026 16:43:13 +0300 Subject: [PATCH 5/5] revert: restore claude-code-review workflow to original Co-Authored-By: Claude Opus 4.6 --- .github/workflows/claude-code-review.yml | 76 +++++++++--------------- 1 file changed, 29 insertions(+), 47 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index d46efa44..7320a21e 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -1,22 +1,20 @@ -name: PR Review with Progress Tracking - -# This example demonstrates how to use the track_progress feature to get -# visual progress tracking for PR reviews, similar to v0.x agent mode. +name: Claude Code Review on: pull_request: - types: [opened, synchronize, ready_for_review, reopened] + types: [opened, synchronize] jobs: - review-with-tracking: + claude-review: runs-on: ubuntu-latest permissions: contents: read pull-requests: write + issues: read id-token: write steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: fetch-depth: 1 @@ -25,44 +23,28 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - # Enable progress tracking - track_progress: true - - # Your custom review instructions prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} - - Perform a comprehensive code review with the following focus areas: - - 1. **Code Quality** - - Clean code principles and best practices - - Proper error handling and edge cases - - Code readability and maintainability - - 2. **Security** - - Check for potential security vulnerabilities - - Validate input sanitization - - Review authentication/authorization logic - - 3. **Performance** - - Identify potential performance bottlenecks - - Review database queries for efficiency - - Check for memory leaks or resource issues - - 4. **Testing** - - Verify adequate test coverage - - Review test quality and edge cases - - Check for missing test scenarios - - 5. **Documentation** - - Ensure code is properly documented - - Verify README updates for new features - - Check API documentation accuracy - - Provide detailed feedback using inline comments for specific issues. - Use top-level comments for general observations or praise. - - # Tools for comprehensive PR review - claude_args: | - --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" + Review this pull request with focus on this codebase's specific patterns: + + ## Code Quality + - tRPC routers: correct procedure type (publicProcedure vs authenticatedProcedure vs staffProcedure), proper Zod input validation, class-based model usage + - Kysely queries: safe query construction, correct usage of dual databases (graphDB for app data, federatedDB for blockchain data via FDW) + - TypeScript strict mode compliance (noUncheckedIndexedAccess — check for unchecked indexed access) + - Coding conventions: `~/` path alias, inline type imports, kebab-case files, const object enums (not TS enum) + + ## Architecture + - Server/Client Component split: unnecessary "use client" directives, data fetching in Server Components via `caller`, props passed to `*Client` components + - Caching: proper use of cacheQuery with TTL and tag-based invalidation, cache key correctness + + ## Security + - SIWE authentication: proper session checks, RBAC permission enforcement via hasPermission() + - Blockchain interactions: address validation with Viem, safe smart contract calls, no hardcoded private keys or sensitive data + - Input validation: Zod schemas on all tRPC inputs, no unsanitized user input in queries + + ## Performance + - Avoid N+1 queries in Kysely models, prefer batch fetches + - Proper use of React Suspense boundaries and streaming + - Check for unnecessary re-renders in client components + + Be constructive and specific. Reference the relevant file and line when possible. + claude_args: "--max-turns 10"