From f806c52396cfad73227c296bf24fa6bdeb4d26bf Mon Sep 17 00:00:00 2001 From: gaboesquivel Date: Sat, 7 Mar 2026 22:13:14 +0000 Subject: [PATCH 1/4] refactor(error): consolidate error package from sentry and utils - Rename packages/sentry to packages/error - Move packages/utils/src/error into packages/error/src/utils - Add no-DSN fallback: log errors when reporting backend not initialized - Rename initSentry to initErrorReporting, tech-agnostic API - Update env vars: ERROR_REPORTING_DSN, ERROR_REPORTING_ENVIRONMENT - Add Next.js error reporting setup (error-reporting.*.ts, sentry shims) - Update Fastify to use initErrorReporting from @repo/error/node - Update all imports: @repo/sentry -> @repo/error, @repo/utils/error -> @repo/error - Update docs, cursor rules, and skills for @repo/error - Recommend GlitchTip in docs; Sentry also works (same DSN format) --- .cursor/skills/next-v16/error-handling.md | 12 +- .cursor/skills/wagmi-v3/SKILL.md | 2 +- README.md | 4 +- .../docs/architecture/error-handling.mdx | 58 ++--- .../docs/architecture/esm-strategy.mdx | 4 +- .../content/docs/architecture/logging.mdx | 6 +- apps/docu/content/docs/deployment/vercel.mdx | 4 +- .../content/docs/development/dev-tooling.mdx | 4 +- .../docs/development/package-conventions.mdx | 2 +- .../content/docs/development/packages.mdx | 18 +- apps/fastify/package.json | 2 +- .../scripts/create-verification-table.ts | 2 +- apps/fastify/server.ts | 14 +- apps/fastify/src/lib/env.ts | 4 +- apps/fastify/src/plugins/auth.ts | 2 +- apps/fastify/src/plugins/error-handler.ts | 2 +- .../src/routes/auth/session/refresh.ts | 2 +- apps/next/README.md | 4 +- apps/next/app/(dashboard)/(news)/page.tsx | 2 +- apps/next/app/(dashboard)/markets/page.tsx | 2 +- apps/next/error-reporting.client.ts | 7 + apps/next/error-reporting.server.ts | 7 + apps/next/instrumentation-client.ts | 5 + apps/next/instrumentation.ts | 12 +- apps/next/lib/env.ts | 8 + apps/next/next.config.mjs | 10 +- apps/next/package.json | 2 + apps/next/sentry.edge.config.ts | 7 + apps/next/sentry.server.config.ts | 1 + biome.json | 2 +- packages/error/README.md | 74 ++++++ packages/{sentry => error}/eslint.config.mjs | 0 packages/{sentry => error}/package.json | 11 +- .../src/__tests__/browser.test.ts | 18 +- .../src/__tests__/capture.test.ts | 6 +- .../src/__tests__/sentry.test.ts | 32 +-- .../{sentry => error}/src/browser/capture.ts | 6 +- .../src/node => error/src/browser}/index.ts | 2 +- .../{sentry => error}/src/browser/sentry.ts | 18 +- packages/error/src/core/capture-impl.ts | 80 +++++++ packages/{sentry => error}/src/core/index.ts | 0 .../{sentry => error}/src/nextjs/capture.ts | 8 +- packages/error/src/nextjs/index.ts | 6 + .../{sentry => error}/src/nextjs/sentry.ts | 21 +- .../{sentry => error}/src/node/capture.ts | 6 +- .../src/browser => error/src/node}/index.ts | 2 +- packages/{sentry => error}/src/node/sentry.ts | 20 +- .../src/react/error-boundary.tsx | 2 +- packages/{sentry => error}/src/react/index.ts | 0 packages/error/src/types.ts | 67 ++++++ .../src/error => error/src/utils}/index.ts | 2 +- .../src/error => error/src/utils}/types.ts | 0 packages/{sentry => error}/tsconfig.json | 0 packages/{sentry => error}/tsup.config.ts | 2 + packages/{sentry => error}/vitest.config.ts | 1 - packages/react/README.md | 2 +- packages/sentry/README.md | 104 --------- packages/sentry/src/core/capture-impl.ts | 77 ------- packages/sentry/src/nextjs/index.ts | 6 - packages/sentry/src/types.ts | 70 ------ packages/utils/README.md | 1 - packages/utils/package.json | 7 - packages/utils/src/error/README.md | 46 ---- packages/utils/src/index.ts | 2 - packages/utils/tsup.config.ts | 1 - pnpm-lock.yaml | 217 ++++++++++++------ turbo.json | 16 +- 67 files changed, 599 insertions(+), 545 deletions(-) create mode 100644 apps/next/error-reporting.client.ts create mode 100644 apps/next/error-reporting.server.ts create mode 100644 apps/next/instrumentation-client.ts create mode 100644 apps/next/sentry.edge.config.ts create mode 100644 apps/next/sentry.server.config.ts create mode 100644 packages/error/README.md rename packages/{sentry => error}/eslint.config.mjs (100%) rename packages/{sentry => error}/package.json (88%) rename packages/{sentry => error}/src/__tests__/browser.test.ts (83%) rename packages/{sentry => error}/src/__tests__/capture.test.ts (96%) rename packages/{sentry => error}/src/__tests__/sentry.test.ts (78%) rename packages/{sentry => error}/src/browser/capture.ts (80%) rename packages/{sentry/src/node => error/src/browser}/index.ts (59%) rename packages/{sentry => error}/src/browser/sentry.ts (64%) create mode 100644 packages/error/src/core/capture-impl.ts rename packages/{sentry => error}/src/core/index.ts (100%) rename packages/{sentry => error}/src/nextjs/capture.ts (83%) create mode 100644 packages/error/src/nextjs/index.ts rename packages/{sentry => error}/src/nextjs/sentry.ts (57%) rename packages/{sentry => error}/src/node/capture.ts (84%) rename packages/{sentry/src/browser => error/src/node}/index.ts (59%) rename packages/{sentry => error}/src/node/sentry.ts (53%) rename packages/{sentry => error}/src/react/error-boundary.tsx (95%) rename packages/{sentry => error}/src/react/index.ts (100%) create mode 100644 packages/error/src/types.ts rename packages/{utils/src/error => error/src/utils}/index.ts (97%) rename packages/{utils/src/error => error/src/utils}/types.ts (100%) rename packages/{sentry => error}/tsconfig.json (100%) rename packages/{sentry => error}/tsup.config.ts (84%) rename packages/{sentry => error}/vitest.config.ts (86%) delete mode 100644 packages/sentry/README.md delete mode 100644 packages/sentry/src/core/capture-impl.ts delete mode 100644 packages/sentry/src/nextjs/index.ts delete mode 100644 packages/sentry/src/types.ts delete mode 100644 packages/utils/src/error/README.md diff --git a/.cursor/skills/next-v16/error-handling.md b/.cursor/skills/next-v16/error-handling.md index 0be93cd6..d20d6b03 100644 --- a/.cursor/skills/next-v16/error-handling.md +++ b/.cursor/skills/next-v16/error-handling.md @@ -1,12 +1,12 @@ # Error Handling -Handle errors in Next.js with **Sentry reporting** (`@repo/sentry/*`) and **structured logging** (`@repo/utils/logger/server` or `@repo/utils/logger/client`). +Handle errors in Next.js with **Sentry reporting** (`@repo/error/*`) and **structured logging** (`@repo/utils/logger/server` or `@repo/utils/logger/client`). Reference: https://nextjs.org/docs/app/getting-started/error-handling ## Observability (Required) -- Report unexpected errors with `captureError` from `@repo/sentry/nextjs` +- Report unexpected errors with `captureError` from `@repo/error/nextjs` - Log operational context with `logger` from `@repo/utils/logger/server` or `@repo/utils/logger/client` (never `console.*`) ## Error Boundaries @@ -18,7 +18,7 @@ Catches errors in a route segment and its children: ```tsx 'use client' -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' import { logger } from '@repo/utils/logger/client' import { useEffect } from 'react' @@ -58,7 +58,7 @@ Catches errors in the root layout. Same capture pattern as `error.tsx`, but it * ```tsx 'use client' -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' import { logger } from '@repo/utils/logger/client' import { useEffect } from 'react' @@ -97,7 +97,7 @@ Reference: https://nextjs.org/docs/app/api-reference/functions/redirect#behavior 'use server' import { redirect, unstable_rethrow } from 'next/navigation' -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' import { logger } from '@repo/utils/logger/server' export async function action() { @@ -119,7 +119,7 @@ Capture, log, then return a safe result. If navigation APIs may be involved, cal ```tsx import { unstable_rethrow } from 'next/navigation' -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' import { logger } from '@repo/utils/logger/server' async function action() { diff --git a/.cursor/skills/wagmi-v3/SKILL.md b/.cursor/skills/wagmi-v3/SKILL.md index 7f9dd38d..7a088798 100644 --- a/.cursor/skills/wagmi-v3/SKILL.md +++ b/.cursor/skills/wagmi-v3/SKILL.md @@ -543,7 +543,7 @@ Comprehensive error handling with user-friendly messages: ```tsx import { useWriteContract } from 'wagmi' import { BaseError } from 'viem' -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' import type { WriteContractParameters } from 'wagmi/actions' function useContractWriteWithErrorHandling() { diff --git a/README.md b/README.md index 0cb9e282..07494135 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Fastify • OpenAPI • Next.js • Expo — one stack, multiple platforms. - ⚙️ **Preconfigured dev tools** — Biome, Git workflows, hooks, and security checks - 🛡️ **Security & quality** — Automated checks in CI (e.g. Gitleaks, OSV) - ⛓️ **Multichain** — EVM, Solana; shared validation and chain-specific tooling -- 📐 **Conventions** — Cursor rules per domain, @repo/sentry, Pino logging, shared TS and style +- 📐 **Conventions** — Cursor rules per domain, @repo/error, Pino logging, shared TS and style - 🧑‍💻 **TypeScript-first** — End-to-end types from database to frontend ## Technology stack @@ -39,7 +39,7 @@ Fastify • OpenAPI • Next.js • Expo — one stack, multiple platforms. - **[@repo/react](packages/react/README.md)** — React Query hooks for `@repo/core` API functions - **[@repo/ui](packages/ui/README.md)** — Shared UI component library (Shadcn/ui, Tailwind) - **[@repo/utils](packages/utils/README.md)** — Shared utilities (async, data, debug, error, logger, web3) -- **[@repo/sentry](packages/sentry/README.md)** — Common `captureError` interface for error reporting +- **[@repo/error](packages/error/README.md)** — Error reporting and utilities (`captureError`, `getErrorMessage`) - **[@repo/email](packages/email/README.md)** — Email template library built with React Email - **[@repo/notif](packages/notif/README.md)** — Notification service (email, activity) with type-safe schemas diff --git a/apps/docu/content/docs/architecture/error-handling.mdx b/apps/docu/content/docs/architecture/error-handling.mdx index fdeb91a5..254590fd 100644 --- a/apps/docu/content/docs/architecture/error-handling.mdx +++ b/apps/docu/content/docs/architecture/error-handling.mdx @@ -1,15 +1,15 @@ --- title: "Error Handling" -description: "Error handling with Sentry integration and app-level error catalogs." +description: "Error handling with error reporting and app-level error catalogs." --- -Complete guide for error handling across the monorepo: **`@repo/sentry`** for reporting to Sentry, **`@repo/utils/error`** for message extraction, and **app-level catalogs** for safe API responses. +Complete guide for error handling across the monorepo: **`@repo/error`** for error reporting and message extraction, and **app-level catalogs** for safe API responses. ## Overview -- **`@repo/sentry`** – `captureError(options)` sends errors to Sentry (void; does not return a catalog). Use platform subpaths: `node`, `nextjs`, `browser`, `react`. -- **`@repo/utils/error`** – `getErrorMessage(error)` and related utilities for type-safe message extraction. -- **Sentry initialization** – Not in `@repo/sentry`. Initialize per your platform’s [Sentry docs](https://docs.sentry.io/platforms/) (Node, Next.js, Browser). +- **`@repo/error`** – `captureError(options)` sends errors to the reporting backend (void; does not return a catalog). Use platform subpaths: `node`, `nextjs`, `browser`, `react`. +- **`@repo/error`** – `getErrorMessage(error)` and related utilities for type-safe message extraction (base export or `@repo/error/nextjs`). +- **Initialization** – Use `initErrorReporting` from `@repo/error/node` or `@repo/error/nextjs`. [GlitchTip](https://glitchtip.com/sdkdocs/javascript/) is recommended; Sentry also works (same DSN format). - **Error catalogs and safe responses** – Implemented in each app (e.g. Fastify’s `apps/fastify/src/lib/catalogs/` with `getError`, `mapHttpStatusToErrorCode`). Apps build the safe `{ code, message }` response themselves. **Key principles:** @@ -23,10 +23,10 @@ Complete guide for error handling across the monorepo: **`@repo/sentry`** for re ```typescript // For Next.js apps -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' // For Node.js/Fastify apps -// import { captureError } from '@repo/sentry/node' +// import { captureError } from '@repo/error/node' captureError({ error, @@ -41,14 +41,14 @@ captureError({ ### Extract Error Message ```typescript -import { getErrorMessage } from '@repo/utils/error' +import { getErrorMessage } from '@repo/error/nextjs' const message = getErrorMessage(error) // Type-safe ``` ### Initialize Sentry -Sentry is not initialized by `@repo/sentry`. Follow your platform’s setup: +Sentry is not initialized by `@repo/error`. Follow your platform’s setup: - **Next.js**: [Sentry Next.js guide](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/) – `withSentryConfig`, `instrumentation.ts`, etc. - **Node/Fastify**: [Sentry Node guide](https://docs.sentry.io/platforms/javascript/guides/node/) @@ -96,7 +96,7 @@ reply.status(500).send(catalogError) ### App-level error catalogs -Error catalogs and safe responses are implemented **per app**, not in `@repo/sentry`. For example, the Fastify app defines catalogs in `apps/fastify/src/lib/catalogs/` (e.g. `server.ts`, `client.ts`, `common.ts`) and exposes `getError(code)`, `mapHttpStatusToErrorCode(statusCode)`. The error handler calls `captureError` from `@repo/sentry/node` then builds the response using the app’s `getError`. +Error catalogs and safe responses are implemented **per app**, not in `@repo/error`. For example, the Fastify app defines catalogs in `apps/fastify/src/lib/catalogs/` (e.g. `server.ts`, `client.ts`, `common.ts`) and exposes `getError(code)`, `mapHttpStatusToErrorCode(statusCode)`. The error handler calls `captureError` from `@repo/error/node` then builds the response using the app’s `getError`. **Adding new error codes:** Add them to your app’s catalog (e.g. `apps/fastify/src/lib/catalogs/`), then use `getError(yourCode)` when building responses. Use `UPPER_SNAKE_CASE` (e.g. `NETWORK_ERROR`, `USER_NOT_FOUND`). @@ -121,11 +121,11 @@ Error catalogs and safe responses are implemented **per app**, not in `@repo/sen #### Global Error Handler Plugin -Use `captureError` from `@repo/sentry/node` (void); build the response from the app’s catalog (`getError`, `mapHttpStatusToErrorCode` from `apps/fastify/src/lib/catalogs/`): +Use `captureError` from `@repo/error/node` (void); build the response from the app’s catalog (`getError`, `mapHttpStatusToErrorCode` from `apps/fastify/src/lib/catalogs/`): ```typescript // apps/fastify/src/plugins/error-handler.ts -import { captureError } from '@repo/sentry/node' +import { captureError } from '@repo/error/node' import { getError, mapHttpStatusToErrorCode } from '../lib/catalogs/mapper.js' // ... route extraction, redaction ... @@ -158,8 +158,8 @@ fastify.setErrorHandler((error, request, reply) => { // apps/next/app/layout.tsx 'use client' -import { AppErrorBoundary } from '@repo/sentry/react' -import { captureError } from '@repo/sentry/nextjs' +import { AppErrorBoundary } from '@repo/error/react' +import { captureError } from '@repo/error/nextjs' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( @@ -170,13 +170,13 @@ export default function RootLayout({ children }: { children: React.ReactNode }) } ``` -**Custom implementation:** Use `captureError` from `@repo/sentry/nextjs` in `onError`; it returns void. Show user-facing message via your fallback (e.g. `getErrorMessage(error)` from `@repo/utils/error`). +**Custom implementation:** Use `captureError` from `@repo/error/nextjs` in `onError`; it returns void. Show user-facing message via your fallback (e.g. `getErrorMessage(error)` from `@repo/error/nextjs`). #### Event Handlers ```typescript -import { captureError } from '@repo/sentry/nextjs' -import { getErrorMessage } from '@repo/utils/error' +import { captureError } from '@repo/error/nextjs' +import { getErrorMessage } from '@repo/error/nextjs' import { toast } from 'sonner' async function handleSubmit() { @@ -200,7 +200,7 @@ async function handleSubmit() { ```typescript // app/error.tsx 'use client' -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' import { useEffect } from 'react' export default function ErrorPage({ @@ -237,8 +237,8 @@ export default function ErrorPage({ ```typescript 'use server' -import { captureError } from '@repo/sentry/nextjs' -import { getErrorMessage } from '@repo/utils/error' +import { captureError } from '@repo/error/nextjs' +import { getErrorMessage } from '@repo/error/nextjs' export async function serverAction() { try { @@ -255,7 +255,7 @@ export async function serverAction() { ```typescript 'use server' -import { captureError } from '@repo/sentry/nextjs' +import { captureError } from '@repo/error/nextjs' export async function serverAction() { try { @@ -267,16 +267,16 @@ export async function serverAction() { } ``` -**Initialization:** Sentry is not initialized by `@repo/sentry`. Use [Sentry’s Next.js setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/) in `next.config`, `instrumentation.ts`, etc. +**Initialization:** Sentry is not initialized by `@repo/error`. Use [Sentry’s Next.js setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/) in `next.config`, `instrumentation.ts`, etc. ## API Reference -### `captureError(options): void` (`@repo/sentry/*`) +### `captureError(options): void` (`@repo/error/*`) Sends the error to Sentry asynchronously. Does not return a catalog; each app builds its own response. ```typescript -import { captureError } from '@repo/sentry/nextjs' // or /node, /browser +import { captureError } from '@repo/error/nextjs' // or /node, /browser captureError({ error: unknown, // Converted to Error if needed; full stack sent to Sentry @@ -292,25 +292,25 @@ captureError({ See [packages/sentry/README.md](https://github.com/blockmatic/basilic/blob/main/packages/sentry/README.md) for full options. **Serverless:** Consider `Sentry.flush()` before function exit if the process terminates quickly. -### `getErrorMessage(error)` (`@repo/utils/error`) +### `getErrorMessage(error)` (`@repo/error` or `@repo/error/nextjs`) ```typescript -import { getErrorMessage } from '@repo/utils/error' +import { getErrorMessage } from '@repo/error/nextjs' const message = getErrorMessage(error) // Type-safe extraction from unknown ``` ### App-level: `getError`, `mapHttpStatusToErrorCode` -These live in each app (e.g. Fastify’s `apps/fastify/src/lib/catalogs/mapper.ts`). `mapHttpStatusToErrorCode(statusCode)` maps HTTP status to a catalog code; `getError(code)` returns `{ code, message }` for the response. Not part of `@repo/sentry`. +These live in each app (e.g. Fastify’s `apps/fastify/src/lib/catalogs/mapper.ts`). `mapHttpStatusToErrorCode(statusCode)` maps HTTP status to a catalog code; `getError(code)` returns `{ code, message }` for the response. Not part of `@repo/error`. ## Best Practices ### Common Patterns ```typescript -import { getErrorMessage } from '@repo/utils/error' -import { captureError } from '@repo/sentry/nextjs' +import { getErrorMessage } from '@repo/error/nextjs' +import { captureError } from '@repo/error/nextjs' // Extract message const message = getErrorMessage(error) diff --git a/apps/docu/content/docs/architecture/esm-strategy.mdx b/apps/docu/content/docs/architecture/esm-strategy.mdx index 5cb7541f..5eb02e30 100644 --- a/apps/docu/content/docs/architecture/esm-strategy.mdx +++ b/apps/docu/content/docs/architecture/esm-strategy.mdx @@ -41,7 +41,7 @@ Packages expose TypeScript source for workspace consumption. Two patterns exist: } ``` -**Pattern B** – `source` condition for bundlers, `node`/`import` for Node (packages like `@repo/utils`, `@repo/sentry`, `@repo/email`, `@repo/notif`): +**Pattern B** – `source` condition for bundlers, `node`/`import` for Node (packages like `@repo/utils`, `@repo/error`, `@repo/email`, `@repo/notif`): ```json { @@ -113,7 +113,7 @@ Next.js apps import TypeScript source directly using **package interpolation**: ```js // next.config.mjs export default { - transpilePackages: ['@repo/ui', '@repo/core', '@repo/react', '@repo/sentry', '@repo/utils'], + transpilePackages: ['@repo/ui', '@repo/core', '@repo/react', '@repo/error', '@repo/utils'], serverExternalPackages: ['import-in-the-middle', 'require-in-the-middle'], webpack: config => { // Resolve "source" condition so Next.js uses TypeScript source from workspace packages diff --git a/apps/docu/content/docs/architecture/logging.mdx b/apps/docu/content/docs/architecture/logging.mdx index f5a3e13b..442e4fdb 100644 --- a/apps/docu/content/docs/architecture/logging.mdx +++ b/apps/docu/content/docs/architecture/logging.mdx @@ -215,10 +215,10 @@ try { } ``` -**5. Integration with `@repo/sentry`** - When using `captureError` in Fastify, pass `request.log` to use Fastify's native logger. The catalog and response are built in the app (e.g. from `apps/fastify/src/lib/catalogs/`): +**5. Integration with `@repo/error`** - When using `captureError` in Fastify, pass `request.log` to use Fastify's native logger. The catalog and response are built in the app (e.g. from `apps/fastify/src/lib/catalogs/`): ```typescript -import { captureError } from '@repo/sentry/node' +import { captureError } from '@repo/error/node' fastify.setErrorHandler((error, request, reply) => { captureError({ @@ -251,4 +251,4 @@ fastify.setErrorHandler((error, request, reply) => { ## Related Documentation - [Installation](/docs/development) - Environment variable configuration -- [Error Handling](/docs/architecture/error-handling) - Error logging with `@repo/sentry` +- [Error Handling](/docs/architecture/error-handling) - Error logging with `@repo/error` diff --git a/apps/docu/content/docs/deployment/vercel.mdx b/apps/docu/content/docs/deployment/vercel.mdx index e10c95ce..ece7364a 100644 --- a/apps/docu/content/docs/deployment/vercel.mdx +++ b/apps/docu/content/docs/deployment/vercel.mdx @@ -41,13 +41,13 @@ OLLAMA_BASE_URL=https://ollama.yourdomain.com RESEND_API_KEY=re_... PORT=3001 NODE_ENV=production -SENTRY_DSN=https://... +ERROR_REPORTING_DSN=https://... ``` **For Web (`apps/next`):** ```bash NEXT_PUBLIC_API_URL=https://api.yourdomain.com -NEXT_PUBLIC_SENTRY_DSN=https://... +NEXT_PUBLIC_ERROR_REPORTING_DSN=https://... ``` **For Docs (`apps/docu`):** diff --git a/apps/docu/content/docs/development/dev-tooling.mdx b/apps/docu/content/docs/development/dev-tooling.mdx index 02781193..3371f32b 100644 --- a/apps/docu/content/docs/development/dev-tooling.mdx +++ b/apps/docu/content/docs/development/dev-tooling.mdx @@ -130,7 +130,7 @@ Tasks are configured in `turbo.json` with: - **`@repo/core`** - Watches OpenAPI spec and regenerates API client - **`@repo/react`** - Watches OpenAPI spec and regenerates React hooks, watches TypeScript for rebuilds -- **`@repo/sentry`** - Watches TypeScript for rebuilds +- **`@repo/error`** - Watches TypeScript for rebuilds - **`@repo/utils`** - Watches TypeScript for rebuilds - **`@repo/fastify`** - Starts Fastify API server with OpenAPI generation watcher - **`@repo/next`** - Starts Next.js development server @@ -156,7 +156,7 @@ You can run apps directly, but build dependencies first: ```bash # Build required packages -pnpm build --filter=@repo/core --filter=@repo/react --filter=@repo/sentry --filter=@repo/utils +pnpm build --filter=@repo/core --filter=@repo/react --filter=@repo/error --filter=@repo/utils # Then run the app cd apps/next diff --git a/apps/docu/content/docs/development/package-conventions.mdx b/apps/docu/content/docs/development/package-conventions.mdx index f0081b98..ec3000fd 100644 --- a/apps/docu/content/docs/development/package-conventions.mdx +++ b/apps/docu/content/docs/development/package-conventions.mdx @@ -151,7 +151,7 @@ For `@repo/utils`, prefer subpath imports even though a root barrel exists: ```ts import { delay } from '@repo/utils/async' import { logger } from '@repo/utils/logger/server' -import { getErrorMessage } from '@repo/utils/error' +import { getErrorMessage } from '@repo/error' ``` ## Dependency management strategy diff --git a/apps/docu/content/docs/development/packages.mdx b/apps/docu/content/docs/development/packages.mdx index 218ed4b5..595d1826 100644 --- a/apps/docu/content/docs/development/packages.mdx +++ b/apps/docu/content/docs/development/packages.mdx @@ -18,7 +18,7 @@ Only import from **exported entrypoints** (package root or documented subpaths). - `@repo/react` - **Subpath-only packages** (no root export): - `@repo/ui/*` - - `@repo/sentry/*` + - `@repo/error/*` - `@repo/email/*` - `@repo/notif/*` - **Mixed**: @@ -40,7 +40,7 @@ Examples: | `@repo/react` | React Query hooks + React utilities | `@repo/react` | | `@repo/utils` | Cross-runtime utilities | `@repo/utils/*` (prefer subpaths) | | `@repo/ui` | Shared shadcn/ui components | `@repo/ui/components/*`, `@repo/ui/lib/*`, `@repo/ui/radix` | -| `@repo/sentry` | Error reporting interface | `@repo/sentry/node`, `/nextjs`, `/browser`, `/react` | +| `@repo/error` | Error reporting interface | `@repo/error/node`, `/nextjs`, `/browser`, `/react` | | `@repo/email` | Email templates + renderer | `@repo/email/emails/*`, `@repo/email/render` | | `@repo/notif` | Notification env/types (no root export) | `@repo/notif/node`, `@repo/notif/types/*` | @@ -139,7 +139,7 @@ export function HealthStatus() { Prefer subpath imports: - `@repo/utils/async` — async helpers (`delay`, `fetchWithTimeout`, …) -- `@repo/utils/error` — error normalization (`getErrorMessage`, `tryCatch`, …) +- `@repo/error` — error normalization (`getErrorMessage`, `tryCatch`, …) - `@repo/utils/web3` — chain metadata + helpers - `@repo/utils/logger/server`, `@repo/utils/logger/client` — Pino (server) and console (client) - `@repo/utils/debug` — client-only debug hooks (`useDevtools`, `useNuqsDebug`, `useVconsole`) @@ -168,17 +168,17 @@ import { cn } from '@repo/ui/lib/utils' ## Error reporting -### `@repo/sentry` — capture interface +### `@repo/error` — capture interface Subpath-only exports (choose the platform path): -- `@repo/sentry/node` -- `@repo/sentry/nextjs` -- `@repo/sentry/browser` -- `@repo/sentry/react` +- `@repo/error/node` +- `@repo/error/nextjs` +- `@repo/error/browser` +- `@repo/error/react` ```ts -import { captureError } from '@repo/sentry/node' // or /nextjs, /browser +import { captureError } from '@repo/error/node' // or /nextjs, /browser captureError({ error, label: 'API Call', tags: { app: 'api' } }) ``` diff --git a/apps/fastify/package.json b/apps/fastify/package.json index 413de26a..18ff38c0 100644 --- a/apps/fastify/package.json +++ b/apps/fastify/package.json @@ -56,7 +56,7 @@ "@fastify/type-provider-typebox": "^6.1.0", "@openrouter/ai-sdk-provider": "^2.2.3", "@repo/email": "workspace:*", - "@repo/sentry": "workspace:*", + "@repo/error": "workspace:*", "@repo/utils": "workspace:*", "@scalar/fastify-api-reference": "^1.44.18", "@sentry/node": "^10.38.0", diff --git a/apps/fastify/scripts/create-verification-table.ts b/apps/fastify/scripts/create-verification-table.ts index fb0343b1..6e5f0602 100644 --- a/apps/fastify/scripts/create-verification-table.ts +++ b/apps/fastify/scripts/create-verification-table.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import 'dotenv/config' -import { captureError } from '@repo/sentry/node' +import { captureError } from '@repo/error/node' import { logger } from '@repo/utils/logger/server' import { Pool } from 'pg' import { env } from '../src/lib/env.js' diff --git a/apps/fastify/server.ts b/apps/fastify/server.ts index 1f6376c5..4cef0249 100644 --- a/apps/fastify/server.ts +++ b/apps/fastify/server.ts @@ -1,6 +1,6 @@ import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox' +import { initErrorReporting } from '@repo/error/node' import { logger } from '@repo/utils/logger/server' -import * as Sentry from '@sentry/node' import Fastify from 'fastify' import app from './src/app.js' import { waitForDatabase } from './src/db/health.js' @@ -13,14 +13,10 @@ if (env.NODE_ENV === 'production' && env.ALLOW_TEST) { process.exit(1) } -// Initialize Sentry before other app code (conventional Node setup) -if (env.SENTRY_DSN) - Sentry.init({ - dsn: env.SENTRY_DSN, - environment: env.SENTRY_ENVIRONMENT ?? env.NODE_ENV, - tracesSampleRate: env.NODE_ENV === 'production' ? 0.1 : 1.0, - ignoreErrors: ['Non-Error promise rejection'], - }) +initErrorReporting({ + dsn: env.ERROR_REPORTING_DSN, + environment: env.ERROR_REPORTING_ENVIRONMENT ?? env.NODE_ENV, +}) const isTestOrCi = env.NODE_ENV === 'test' || env.CI const fastify = Fastify({ diff --git a/apps/fastify/src/lib/env.ts b/apps/fastify/src/lib/env.ts index 78dead32..284bd8eb 100644 --- a/apps/fastify/src/lib/env.ts +++ b/apps/fastify/src/lib/env.ts @@ -52,8 +52,8 @@ export const env = createEnv({ BODY_LIMIT: z.coerce.number().int().positive().default(1048576), REQUEST_TIMEOUT: z.coerce.number().int().positive().default(30000), LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error', 'silent']).default('info'), - SENTRY_DSN: z.string().min(1).optional(), - SENTRY_ENVIRONMENT: z.string().min(1).optional(), + ERROR_REPORTING_DSN: z.string().min(1).optional(), + ERROR_REPORTING_ENVIRONMENT: z.string().min(1).optional(), OLLAMA_BASE_URL: z.string().url().optional().default('http://localhost:11434'), AI_PROVIDER: z.enum(['ollama', 'openrouter']).optional(), OPEN_ROUTER_API_KEY: z.string().min(1).optional(), diff --git a/apps/fastify/src/plugins/auth.ts b/apps/fastify/src/plugins/auth.ts index ad259a00..981630b9 100644 --- a/apps/fastify/src/plugins/auth.ts +++ b/apps/fastify/src/plugins/auth.ts @@ -1,4 +1,4 @@ -import { captureError } from '@repo/sentry/node' +import { captureError } from '@repo/error/node' import { eq } from 'drizzle-orm' import type { FastifyPluginAsync } from 'fastify' import fp from 'fastify-plugin' diff --git a/apps/fastify/src/plugins/error-handler.ts b/apps/fastify/src/plugins/error-handler.ts index 4a9f63b0..673e0baa 100644 --- a/apps/fastify/src/plugins/error-handler.ts +++ b/apps/fastify/src/plugins/error-handler.ts @@ -1,4 +1,4 @@ -import { captureError } from '@repo/sentry/node' +import { captureError } from '@repo/error/node' import type { FastifyError, FastifyInstance } from 'fastify' import fp from 'fastify-plugin' import { getError, mapHttpStatusToErrorCode } from '../lib/catalogs/mapper.js' diff --git a/apps/fastify/src/routes/auth/session/refresh.ts b/apps/fastify/src/routes/auth/session/refresh.ts index d36ecbc5..e22fcf16 100644 --- a/apps/fastify/src/routes/auth/session/refresh.ts +++ b/apps/fastify/src/routes/auth/session/refresh.ts @@ -1,5 +1,5 @@ import type { TypeBoxTypeProvider } from '@fastify/type-provider-typebox' -import { captureError } from '@repo/sentry/node' +import { captureError } from '@repo/error/node' import { Type } from '@sinclair/typebox' import { eq } from 'drizzle-orm' import type { FastifyPluginAsync } from 'fastify' diff --git a/apps/next/README.md b/apps/next/README.md index 4f27c666..c73abc65 100644 --- a/apps/next/README.md +++ b/apps/next/README.md @@ -54,14 +54,14 @@ This starts all development servers including: ```bash # Build required packages first -pnpm build --filter=@repo/core --filter=@repo/react --filter=@repo/sentry --filter=@repo/utils +pnpm build --filter=@repo/core --filter=@repo/react --filter=@repo/error --filter=@repo/utils # Then run from this directory cd apps/next pnpm dev ``` -**Note**: When running directly, you must rebuild dependencies (`@repo/core`, `@repo/react`, `@repo/sentry`, `@repo/utils`) whenever they change. Using `pnpm dev` from the root handles this automatically with watch mode. +**Note**: When running directly, you must rebuild dependencies (`@repo/core`, `@repo/react`, `@repo/error`, `@repo/utils`) whenever they change. Using `pnpm dev` from the root handles this automatically with watch mode. The application will be available at `http://localhost:3000` (or the next available port). diff --git a/apps/next/app/(dashboard)/(news)/page.tsx b/apps/next/app/(dashboard)/(news)/page.tsx index 849b7c8e..8ef971c9 100644 --- a/apps/next/app/(dashboard)/(news)/page.tsx +++ b/apps/next/app/(dashboard)/(news)/page.tsx @@ -1,4 +1,4 @@ -import { getErrorMessage } from '@repo/utils/error' +import { getErrorMessage } from '@repo/error/nextjs' import { getAuthStatus } from 'lib/auth/auth-utils' import { redirect } from 'next/navigation' import { env } from '@/lib/env' diff --git a/apps/next/app/(dashboard)/markets/page.tsx b/apps/next/app/(dashboard)/markets/page.tsx index 5d45fdc3..797304d7 100644 --- a/apps/next/app/(dashboard)/markets/page.tsx +++ b/apps/next/app/(dashboard)/markets/page.tsx @@ -1,4 +1,4 @@ -import { getErrorMessage } from '@repo/utils/error' +import { getErrorMessage } from '@repo/error/nextjs' import { getAuthStatus } from 'lib/auth/auth-utils' import { redirect } from 'next/navigation' import type { CoinMarket } from './markets-table' diff --git a/apps/next/error-reporting.client.ts b/apps/next/error-reporting.client.ts new file mode 100644 index 00000000..d95d3a2b --- /dev/null +++ b/apps/next/error-reporting.client.ts @@ -0,0 +1,7 @@ +import { initErrorReporting } from '@repo/error/nextjs' +import { env } from './lib/env.js' + +initErrorReporting({ + dsn: env.NEXT_PUBLIC_ERROR_REPORTING_DSN, + environment: env.NEXT_PUBLIC_NODE_ENV, +}) diff --git a/apps/next/error-reporting.server.ts b/apps/next/error-reporting.server.ts new file mode 100644 index 00000000..385ac684 --- /dev/null +++ b/apps/next/error-reporting.server.ts @@ -0,0 +1,7 @@ +import { initErrorReporting } from '@repo/error/nextjs' +import { env } from './lib/env.js' + +initErrorReporting({ + dsn: env.ERROR_REPORTING_DSN, + environment: env.ERROR_REPORTING_ENVIRONMENT ?? env.NODE_ENV, +}) diff --git a/apps/next/instrumentation-client.ts b/apps/next/instrumentation-client.ts new file mode 100644 index 00000000..e35e48dd --- /dev/null +++ b/apps/next/instrumentation-client.ts @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/nextjs' + +import './error-reporting.client.js' + +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart diff --git a/apps/next/instrumentation.ts b/apps/next/instrumentation.ts index 8203abdd..26bafc99 100644 --- a/apps/next/instrumentation.ts +++ b/apps/next/instrumentation.ts @@ -1,9 +1,9 @@ +import * as Sentry from '@sentry/nextjs' + export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - // Node.js runtime initialization - } + if (process.env.NEXT_RUNTIME === 'nodejs') await import('./sentry.server.config.js') - if (process.env.NEXT_RUNTIME === 'edge') { - // Edge runtime initialization if needed - } + if (process.env.NEXT_RUNTIME === 'edge') await import('./sentry.edge.config.js') } + +export const onRequestError = Sentry.captureRequestError diff --git a/apps/next/lib/env.ts b/apps/next/lib/env.ts index ae3ad8f0..7af95a9d 100644 --- a/apps/next/lib/env.ts +++ b/apps/next/lib/env.ts @@ -7,11 +7,15 @@ export const env = createEnv({ ALLOW_TEST: z.enum(['true', 'false']).optional(), AUTH_COOKIE_NAME: z.string().default('api.session'), NEWSAPI_KEY: z.string().optional(), + ERROR_REPORTING_DSN: z.string().min(1).optional(), + ERROR_REPORTING_ENVIRONMENT: z.string().min(1).optional(), }, client: { NEXT_PUBLIC_NODE_ENV: z.enum(['development', 'test', 'production']).default('production'), NEXT_PUBLIC_API_URL: z.string().min(1), NEXT_PUBLIC_AUTH_COOKIE_NAME: z.string().default('api.session'), + NEXT_PUBLIC_ERROR_REPORTING_DSN: z.string().min(1).optional(), + NEXT_PUBLIC_ERROR_REPORTING_ENVIRONMENT: z.string().min(1).optional(), // Logging configuration NEXT_PUBLIC_LOG_ENABLED: z.coerce.boolean().optional(), NEXT_PUBLIC_LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error', 'silent']).optional(), @@ -25,6 +29,10 @@ export const env = createEnv({ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_AUTH_COOKIE_NAME: process.env.NEXT_PUBLIC_AUTH_COOKIE_NAME ?? process.env.AUTH_COOKIE_NAME, + NEXT_PUBLIC_ERROR_REPORTING_DSN: process.env.NEXT_PUBLIC_ERROR_REPORTING_DSN, + NEXT_PUBLIC_ERROR_REPORTING_ENVIRONMENT: process.env.NEXT_PUBLIC_ERROR_REPORTING_ENVIRONMENT, + ERROR_REPORTING_DSN: process.env.ERROR_REPORTING_DSN, + ERROR_REPORTING_ENVIRONMENT: process.env.ERROR_REPORTING_ENVIRONMENT, NEXT_PUBLIC_LOG_ENABLED: process.env.NEXT_PUBLIC_LOG_ENABLED, NEXT_PUBLIC_LOG_LEVEL: process.env.NEXT_PUBLIC_LOG_LEVEL, }, diff --git a/apps/next/next.config.mjs b/apps/next/next.config.mjs index 27504738..3ed11e50 100644 --- a/apps/next/next.config.mjs +++ b/apps/next/next.config.mjs @@ -1,5 +1,7 @@ /** @type {import('next').NextConfig} */ +import { withSentryConfig } from '@sentry/nextjs' + // Must match basilic-fastify Vercel deployment URL pattern. Fork/deploy: change API_PROJECT_NAME // and TEAM_SLUG to your Fastify project and Vercel team slug. // - Production: basilic-fastify.vercel.app (or basilic-fastify-gaboesquivel.vercel.app) @@ -69,7 +71,7 @@ const nextConfig = { '@repo/ui', '@repo/core', '@repo/react', - '@repo/sentry', + '@repo/error', '@repo/utils', 'ai', 'eventsource-parser', @@ -110,4 +112,8 @@ const nextConfig = { }, } -export default nextConfig +export default withSentryConfig(nextConfig, { + org: process.env.SENTRY_ORG ?? 'placeholder', + project: process.env.SENTRY_PROJECT ?? 'placeholder', + silent: !process.env.CI, +}) diff --git a/apps/next/package.json b/apps/next/package.json index 9aeeb6d6..3677e242 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -36,6 +36,7 @@ "@json-render/react": "^0.7.0", "@lukemorales/query-key-factory": "^1.3.4", "@repo/core": "workspace:*", + "@repo/error": "workspace:*", "@repo/react": "workspace:*", "@repo/ui": "workspace:*", "@repo/utils": "workspace:*", @@ -54,6 +55,7 @@ "jose": "^6.1.3", "lucide-react": "^0.564.0", "next": "16.1.6", + "@sentry/nextjs": "^10.38.0", "next-themes": "^0.4.6", "sonner": "^2.0.7", "nuqs": "^2.8.8", diff --git a/apps/next/sentry.edge.config.ts b/apps/next/sentry.edge.config.ts new file mode 100644 index 00000000..385ac684 --- /dev/null +++ b/apps/next/sentry.edge.config.ts @@ -0,0 +1,7 @@ +import { initErrorReporting } from '@repo/error/nextjs' +import { env } from './lib/env.js' + +initErrorReporting({ + dsn: env.ERROR_REPORTING_DSN, + environment: env.ERROR_REPORTING_ENVIRONMENT ?? env.NODE_ENV, +}) diff --git a/apps/next/sentry.server.config.ts b/apps/next/sentry.server.config.ts new file mode 100644 index 00000000..030edef3 --- /dev/null +++ b/apps/next/sentry.server.config.ts @@ -0,0 +1 @@ +import './error-reporting.server.js' diff --git a/biome.json b/biome.json index a7d01d90..06aa42c8 100644 --- a/biome.json +++ b/biome.json @@ -101,7 +101,7 @@ "options": { "paths": { "lodash-es": "Use per-function imports: lodash-es/ (e.g., lodash-es/isEmpty).", - "@repo/sentry": "Use subpath imports: @repo/sentry/nextjs, @repo/sentry/node, etc.", + "@repo/error": "Use subpath imports: @repo/error/nextjs, @repo/error/node, etc.", "@repo/utils": "Use subpath imports: @repo/utils/logger/server, @repo/utils/logger/client, @repo/utils/async, etc.", "@repo/ui": "Use subpath imports: @repo/ui/components/*, @repo/ui/radix, etc." } diff --git a/packages/error/README.md b/packages/error/README.md new file mode 100644 index 00000000..2ece7446 --- /dev/null +++ b/packages/error/README.md @@ -0,0 +1,74 @@ +# @repo/error + +Error reporting and utilities for the monorepo. Use **`captureError`** for consistent, type-safe error reporting. Use **`getErrorMessage`**, **`tryCatch`**, etc. for message extraction and Result handling. Error reporting is **initialized at the application level**; packages only import and call `captureError`. + +## Exports + +| Path | Use for | +|------|--------| +| `@repo/error` | Error utils: `getErrorMessage`, `tryCatch`, `toErrorWithMessage`, `isErrorWithMessage`, `ErrorWithMessage`, `Result` | +| `@repo/error/node` | Node.js / Fastify: `captureError`, `initErrorReporting` | +| `@repo/error/nextjs` | Next.js (client + server): `captureError`, `initErrorReporting`, `getErrorMessage` | +| `@repo/error/browser` | Browser-only (TanStack Start, Vue, Svelte, etc.) | +| `@repo/error/react` | React Error Boundary component | + +**Import rule:** Use the platform path that matches your app. For error utils only, use `@repo/error`. For capture + init, use `@repo/error/node`, `@repo/error/nextjs`, or `@repo/error/browser`. + +## Error reporting backend + +**GlitchTip** (open source) is recommended. Sentry (open source or cloud) also works—same DSN format and SDK. Set `ERROR_REPORTING_DSN` (server) and `NEXT_PUBLIC_ERROR_REPORTING_DSN` (client) to enable. + +## Quick start + +**Capture an error** (non-blocking, async): + +```typescript +import { captureError } from '@repo/error/node' // or /nextjs, /browser + +captureError({ + error, + label: 'API Call', + code: 'NETWORK_ERROR', + data: { endpoint: '/api/data' }, + tags: { app: 'web' }, +}) +``` + +**Extract error message:** + +```typescript +import { getErrorMessage } from '@repo/error/nextjs' // or @repo/error + +const message = getErrorMessage(error) +``` + +## Initialize error reporting + +Use `initErrorReporting` from the platform path. **Do NOT call it from `instrumentation.ts`**—use dedicated config files to avoid OpenTelemetry conflicts (see Next.js and OpenTelemetry below). + +**Node/Fastify:** + +```typescript +import { initErrorReporting } from '@repo/error/node' + +initErrorReporting({ + dsn: env.ERROR_REPORTING_DSN, + environment: env.ERROR_REPORTING_ENVIRONMENT ?? env.NODE_ENV, +}) +``` + +**Next.js:** Use `error-reporting.server.ts`, `error-reporting.client.ts`, `sentry.edge.config.ts` with thin `sentry.server.config.ts` and `instrumentation-client.ts` shims. See [GlitchTip Next.js docs](https://glitchtip.com/sdkdocs/javascript-nextjs/). + +## Next.js and OpenTelemetry + +Do **not** call `initErrorReporting` inside `instrumentation.ts` `register()`. It can cause Next.js span attributes (e.g. `next.route`) to be lost. Use dedicated config files instead; `instrumentation.ts` should only import them and export `onRequestError`. + +## No-DSN fallback + +When no DSN is configured, errors are logged via the provided or default logger instead of being dropped. Set `ERROR_REPORTING_DSN` to enable full reporting. + +## Scripts + +- `pnpm --filter @repo/error build` - Build package +- `pnpm --filter @repo/error checktypes` - Type-check +- `pnpm --filter @repo/error test` - Run tests diff --git a/packages/sentry/eslint.config.mjs b/packages/error/eslint.config.mjs similarity index 100% rename from packages/sentry/eslint.config.mjs rename to packages/error/eslint.config.mjs diff --git a/packages/sentry/package.json b/packages/error/package.json similarity index 88% rename from packages/sentry/package.json rename to packages/error/package.json index bc6b20cc..ae1f2e73 100644 --- a/packages/sentry/package.json +++ b/packages/error/package.json @@ -1,11 +1,18 @@ { - "name": "@repo/sentry", + "name": "@repo/error", "version": "0.0.1", "type": "module", "private": true, - "description": "Sentry integration and error reporting utilities", + "description": "Error reporting and utilities", "license": "PROPRIETARY", "exports": { + ".": { + "types": "./src/utils/index.ts", + "source": "./src/utils/index.ts", + "import": "./dist/index.js", + "node": "./dist/index.js", + "default": "./dist/index.js" + }, "./node": { "types": "./src/node/index.ts", "source": "./src/node/index.ts", diff --git a/packages/sentry/src/__tests__/browser.test.ts b/packages/error/src/__tests__/browser.test.ts similarity index 83% rename from packages/sentry/src/__tests__/browser.test.ts rename to packages/error/src/__tests__/browser.test.ts index ec68ccc2..7957d1c8 100644 --- a/packages/sentry/src/__tests__/browser.test.ts +++ b/packages/error/src/__tests__/browser.test.ts @@ -1,6 +1,6 @@ import { logger } from '@repo/utils/logger/client' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { initSentry } from '../browser/sentry.js' +import { initErrorReporting } from '../browser/sentry.js' // Mock logger vi.mock('@repo/utils/logger/client', () => ({ @@ -20,15 +20,15 @@ vi.mock('@sentry/browser', () => ({ getClient: mockGetClient, })) -describe('browser sentry', () => { +describe('browser error reporting', () => { beforeEach(() => { vi.clearAllMocks() mockInit.mockClear() }) - describe('initSentry', () => { - it('should initialize Sentry with DSN', () => { - initSentry({ + describe('initErrorReporting', () => { + it('should initialize with DSN', () => { + initErrorReporting({ dsn: 'https://test@test.ingest.sentry.io/test', environment: 'test', }) @@ -49,7 +49,7 @@ describe('browser sentry', () => { }) it('should use production tracesSampleRate in production', () => { - initSentry({ + initErrorReporting({ dsn: 'https://test@test.ingest.sentry.io/test', environment: 'production', }) @@ -62,17 +62,17 @@ describe('browser sentry', () => { }) it('should warn and not initialize if DSN is missing', () => { - initSentry({}) + initErrorReporting({}) expect(logger.warn).toHaveBeenCalledWith( - 'Sentry DSN not configured - error reporting disabled', + 'Error reporting DSN not configured - error reporting disabled', ) expect(mockInit).not.toHaveBeenCalled() }) it('should accept custom beforeSend hook', () => { const beforeSend = vi.fn() - initSentry({ + initErrorReporting({ dsn: 'https://test@test.ingest.sentry.io/test', beforeSend, }) diff --git a/packages/sentry/src/__tests__/capture.test.ts b/packages/error/src/__tests__/capture.test.ts similarity index 96% rename from packages/sentry/src/__tests__/capture.test.ts rename to packages/error/src/__tests__/capture.test.ts index 3b17ab57..ce615107 100644 --- a/packages/sentry/src/__tests__/capture.test.ts +++ b/packages/error/src/__tests__/capture.test.ts @@ -4,23 +4,26 @@ import { captureError as captureErrorNextjs } from '../nextjs/capture.js' import { captureError as captureErrorNode } from '../node/capture.js' // Mock logger - use vi.hoisted to define mocks before hoisted mock factories -const { mockLoggerChild, mockLoggerWarn } = vi.hoisted(() => ({ +const { mockLoggerChild, mockLoggerError, mockLoggerWarn } = vi.hoisted(() => ({ mockLoggerChild: vi.fn(() => ({ error: vi.fn(), warn: vi.fn(), })), + mockLoggerError: vi.fn(), mockLoggerWarn: vi.fn(), })) vi.mock('@repo/utils/logger/server', () => ({ logger: { child: mockLoggerChild, + error: mockLoggerError, warn: mockLoggerWarn, }, })) vi.mock('@repo/utils/logger/client', () => ({ logger: { child: mockLoggerChild, + error: mockLoggerError, warn: mockLoggerWarn, }, })) @@ -179,6 +182,7 @@ describe('capture', () => { it('should use custom logger when provided', async () => { const customLogger = { + error: vi.fn(), warn: vi.fn(), } mockGetClient.mockReturnValue(null) diff --git a/packages/sentry/src/__tests__/sentry.test.ts b/packages/error/src/__tests__/sentry.test.ts similarity index 78% rename from packages/sentry/src/__tests__/sentry.test.ts rename to packages/error/src/__tests__/sentry.test.ts index 3a77269d..5c5e3a0b 100644 --- a/packages/sentry/src/__tests__/sentry.test.ts +++ b/packages/error/src/__tests__/sentry.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { initSentry as initSentryBrowser } from '../browser/sentry.js' -import { initSentry as initSentryNextjs } from '../nextjs/sentry.js' -import { initSentry as initSentryNode } from '../node/sentry.js' +import { initErrorReporting as initErrorReportingBrowser } from '../browser/sentry.js' +import { initErrorReporting as initErrorReportingNextjs } from '../nextjs/sentry.js' +import { initErrorReporting as initErrorReportingNode } from '../node/sentry.js' // Mock logger - use vi.hoisted to define mocks before hoisted mock factories const { mockLoggerWarn } = vi.hoisted(() => ({ @@ -40,19 +40,19 @@ vi.mock('@sentry/browser', () => ({ getClient: mockGetClient, })) -describe('sentry', () => { +describe('error reporting', () => { beforeEach(() => { vi.clearAllMocks() mockInit.mockClear() }) describe.each([ - ['Node.js', initSentryNode], - ['Next.js', initSentryNextjs], - ['Browser', initSentryBrowser], - ])('initSentry (%s)', (name, initSentry) => { - it('should initialize Sentry with default config', () => { - initSentry({ + ['Node.js', initErrorReportingNode], + ['Next.js', initErrorReportingNextjs], + ['Browser', initErrorReportingBrowser], + ])('initErrorReporting (%s)', (name, initErrorReporting) => { + it('should initialize with default config', () => { + initErrorReporting({ dsn: 'https://test@sentry.io/123', environment: 'test', }) @@ -69,7 +69,7 @@ describe('sentry', () => { it('should support optional custom beforeSend', () => { const customBeforeSend = vi.fn(event => event) - initSentry({ + initErrorReporting({ dsn: 'https://test@sentry.io/123', beforeSend: customBeforeSend, }) @@ -82,16 +82,16 @@ describe('sentry', () => { }) it('should warn if DSN is not provided', () => { - initSentry({ dsn: undefined }) + initErrorReporting({ dsn: undefined }) expect(mockLoggerWarn).toHaveBeenCalledWith( - 'Sentry DSN not configured - error reporting disabled', + 'Error reporting DSN not configured - error reporting disabled', ) expect(mockInit).not.toHaveBeenCalled() }) it('should use production tracesSampleRate for production environment', () => { - initSentry({ + initErrorReporting({ dsn: 'https://test@sentry.io/123', environment: 'production', }) @@ -104,7 +104,7 @@ describe('sentry', () => { }) it('should use development tracesSampleRate for non-production', () => { - initSentry({ + initErrorReporting({ dsn: 'https://test@sentry.io/123', environment: 'development', }) @@ -117,7 +117,7 @@ describe('sentry', () => { }) it('should include ignoreErrors array', () => { - initSentry({ + initErrorReporting({ dsn: 'https://test@sentry.io/123', }) diff --git a/packages/sentry/src/browser/capture.ts b/packages/error/src/browser/capture.ts similarity index 80% rename from packages/sentry/src/browser/capture.ts rename to packages/error/src/browser/capture.ts index f9839b8e..de895ea2 100644 --- a/packages/sentry/src/browser/capture.ts +++ b/packages/error/src/browser/capture.ts @@ -3,14 +3,14 @@ import * as Sentry from '@sentry/browser' import { createCaptureError } from '../core/capture-impl.js' /** - * Captures errors to Sentry for browser environments. + * Captures errors to the reporting backend for browser environments. * * Use this export for browser-only frameworks like TanStack Start, Vue, Svelte, etc. - * For Next.js applications, use `@repo/sentry/nextjs` instead (works for both client and server). + * For Next.js applications, use `@repo/error/nextjs` instead (works for both client and server). * * @example * ```ts - * import { captureError } from '@repo/sentry/browser' + * import { captureError } from '@repo/error/browser' * * try { * // Some operation diff --git a/packages/sentry/src/node/index.ts b/packages/error/src/browser/index.ts similarity index 59% rename from packages/sentry/src/node/index.ts rename to packages/error/src/browser/index.ts index 56097d8c..e29477b4 100644 --- a/packages/sentry/src/node/index.ts +++ b/packages/error/src/browser/index.ts @@ -1,5 +1,5 @@ // Re-export core functionality (types) export * from '../core/index.js' -// Single public interface: captureError. Init Sentry per Sentry Node docs. export { captureError } from './capture.js' +export { type InitErrorReportingOptions, initErrorReporting } from './sentry.js' diff --git a/packages/sentry/src/browser/sentry.ts b/packages/error/src/browser/sentry.ts similarity index 64% rename from packages/sentry/src/browser/sentry.ts rename to packages/error/src/browser/sentry.ts index 068d0397..3eeb2def 100644 --- a/packages/sentry/src/browser/sentry.ts +++ b/packages/error/src/browser/sentry.ts @@ -3,10 +3,10 @@ import type { ErrorEvent, EventHint } from '@sentry/browser' import * as Sentry from '@sentry/browser' /** - * Options for initializing Sentry + * Options for initializing error reporting */ -export interface InitSentryOptions { - /** Sentry DSN (optional - if not provided, Sentry is disabled) */ +export interface InitErrorReportingOptions { + /** DSN (optional - if not provided, error reporting is disabled) */ dsn?: string /** Environment name (e.g., 'production', 'staging', 'development') */ environment?: string @@ -17,19 +17,17 @@ export interface InitSentryOptions { } /** - * Initializes Sentry for error tracking - * Uses @sentry/browser for browser applications (TanStack Start, Vue, Svelte, etc.) - * Uses Sentry's built-in PII scrubbing by default + * Initializes error reporting for browser applications + * Uses @sentry/browser internally (GlitchTip and Sentry use same DSN format) * - * @param options - Sentry initialization options + * @param options - Error reporting initialization options */ -export function initSentry(options: InitSentryOptions): void { +export function initErrorReporting(options: InitErrorReportingOptions): void { if (!options.dsn) { - logger.warn('Sentry DSN not configured - error reporting disabled') + logger.warn('Error reporting DSN not configured - error reporting disabled') return } - // Check if Sentry is already initialized to prevent double initialization if (Sentry.getClient()) return Sentry.init({ diff --git a/packages/error/src/core/capture-impl.ts b/packages/error/src/core/capture-impl.ts new file mode 100644 index 00000000..fd83cdab --- /dev/null +++ b/packages/error/src/core/capture-impl.ts @@ -0,0 +1,80 @@ +import type { Logger } from '@repo/utils/logger/server' +import type { CaptureErrorOptions } from '../types.js' +import { toErrorWithMessage } from '../utils/index.js' + +// Module-scoped flag for warning suppression (shows once per app runtime) +let reportingWarningShown = false + +/** + * Minimal interface for reporting backend - compatible with @sentry/node and @sentry/nextjs + */ +interface ReportingAdapter { + getClient: () => object | null + captureException: ( + exception: Error, + hint?: { + tags?: Record + level?: 'error' | 'warning' | 'info' + contexts?: Record> + }, + ) => void +} + +/** + * Creates a captureError function bound to a specific reporting backend + * + * @internal This is an internal implementation detail, not part of public API + */ +export function createCaptureError(reporting: ReportingAdapter, defaultLogger: Logger) { + return function captureError(options: CaptureErrorOptions): void { + const errorWithMessage = toErrorWithMessage(options.error) + + if (options.report === false) return + + Promise.resolve().then(() => { + const reportingClient = reporting.getClient() + + if (!reportingClient) { + if (!reportingWarningShown) { + const log = options.logger ?? defaultLogger + log.warn( + 'Error reporting not initialized - errors will be logged only. Set ERROR_REPORTING_DSN to enable.', + ) + reportingWarningShown = true + } + const log = options.logger ?? defaultLogger + log.error( + { + err: errorWithMessage, + label: options.label, + code: options.code, + ...options.data, + }, + 'Error captured (no DSN)', + ) + return + } + + const tags: Record = { + component: options.label, + ...(options.code ? { errorCode: options.code } : {}), + ...options.tags, + } + + reporting.captureException( + options.error instanceof Error ? options.error : new Error(errorWithMessage.message), + { + tags, + level: options.level ?? 'error', + contexts: { + error: { + label: options.label, + ...(options.code ? { code: options.code } : {}), + ...options.data, + }, + }, + }, + ) + }) + } +} diff --git a/packages/sentry/src/core/index.ts b/packages/error/src/core/index.ts similarity index 100% rename from packages/sentry/src/core/index.ts rename to packages/error/src/core/index.ts diff --git a/packages/sentry/src/nextjs/capture.ts b/packages/error/src/nextjs/capture.ts similarity index 83% rename from packages/sentry/src/nextjs/capture.ts rename to packages/error/src/nextjs/capture.ts index 3cf248cc..b20cde0e 100644 --- a/packages/sentry/src/nextjs/capture.ts +++ b/packages/error/src/nextjs/capture.ts @@ -3,15 +3,15 @@ import * as Sentry from '@sentry/nextjs' import { createCaptureError } from '../core/capture-impl.js' /** - * Captures errors to Sentry for Next.js applications. + * Captures errors to the reporting backend for Next.js applications. * * Use this export for Next.js applications. Works for both client-side and server-side code. - * For pure Node.js/Fastify apps, use `@repo/sentry/node` instead. - * For browser-only frameworks, use `@repo/sentry/browser` instead. + * For pure Node.js/Fastify apps, use `@repo/error/node` instead. + * For browser-only frameworks, use `@repo/error/browser` instead. * * @example * ```ts - * import { captureError } from '@repo/sentry/nextjs' + * import { captureError } from '@repo/error/nextjs' * * // In API route * export async function GET(request: Request) { diff --git a/packages/error/src/nextjs/index.ts b/packages/error/src/nextjs/index.ts new file mode 100644 index 00000000..a32db61b --- /dev/null +++ b/packages/error/src/nextjs/index.ts @@ -0,0 +1,6 @@ +// Re-export core functionality (types) and utils +export * from '../core/index.js' +export { getErrorMessage } from '../utils/index.js' + +export { captureError } from './capture.js' +export { type InitErrorReportingOptions, initErrorReporting } from './sentry.js' diff --git a/packages/sentry/src/nextjs/sentry.ts b/packages/error/src/nextjs/sentry.ts similarity index 57% rename from packages/sentry/src/nextjs/sentry.ts rename to packages/error/src/nextjs/sentry.ts index 4dcd18f1..692bd99c 100644 --- a/packages/sentry/src/nextjs/sentry.ts +++ b/packages/error/src/nextjs/sentry.ts @@ -3,10 +3,10 @@ import type { ErrorEvent, EventHint } from '@sentry/nextjs' import * as Sentry from '@sentry/nextjs' /** - * Options for initializing Sentry + * Options for initializing error reporting */ -export interface InitSentryOptions { - /** Sentry DSN (optional - if not provided, Sentry is disabled) */ +export interface InitErrorReportingOptions { + /** DSN for error reporting backend (optional - if not provided, errors are logged only) */ dsn?: string /** Environment name (e.g., 'production', 'staging', 'development') */ environment?: string @@ -17,19 +17,18 @@ export interface InitSentryOptions { } /** - * Initializes Sentry for error tracking - * Uses @sentry/nextjs for Next.js applications - * Uses Sentry's built-in PII scrubbing by default + * Initializes error reporting for Next.js applications. + * Uses a compatible SDK (GlitchTip, Sentry, etc.) - same DSN format. + * Do NOT call from instrumentation.ts - use dedicated config files instead (see package README). * - * @param options - Sentry initialization options + * @param options - Error reporting initialization options */ -export function initSentry(options: InitSentryOptions): void { +export function initErrorReporting(options: InitErrorReportingOptions): void { if (!options.dsn) { - logger.warn('Sentry DSN not configured - error reporting disabled') + logger.warn('Error reporting DSN not configured - error reporting disabled') return } - // Check if Sentry is already initialized to prevent double initialization if (Sentry.getClient()) return Sentry.init({ @@ -37,8 +36,6 @@ export function initSentry(options: InitSentryOptions): void { environment: options.environment ?? 'development', release: options.release, tracesSampleRate: options.environment === 'production' ? 0.1 : 1.0, - // Optional: custom beforeSend for domain-specific scrubbing - // Sentry's built-in scrubbing handles most cases automatically beforeSend: options.beforeSend, ignoreErrors: [ 'ResizeObserver loop', diff --git a/packages/sentry/src/node/capture.ts b/packages/error/src/node/capture.ts similarity index 84% rename from packages/sentry/src/node/capture.ts rename to packages/error/src/node/capture.ts index 5a5a4aeb..76605e12 100644 --- a/packages/sentry/src/node/capture.ts +++ b/packages/error/src/node/capture.ts @@ -3,14 +3,14 @@ import * as Sentry from '@sentry/node' import { createCaptureError } from '../core/capture-impl.js' /** - * Captures errors to Sentry for Node.js/Fastify environments. + * Captures errors to the reporting backend for Node.js/Fastify environments. * * Use this export for Node.js applications and Fastify servers. - * For Next.js applications, use `@repo/sentry/nextjs` instead. + * For Next.js applications, use `@repo/error/nextjs` instead. * * @example * ```ts - * import { captureError } from '@repo/sentry/node' + * import { captureError } from '@repo/error/node' * * fastify.setErrorHandler((error, request, reply) => { * captureError({ diff --git a/packages/sentry/src/browser/index.ts b/packages/error/src/node/index.ts similarity index 59% rename from packages/sentry/src/browser/index.ts rename to packages/error/src/node/index.ts index e00cec91..e29477b4 100644 --- a/packages/sentry/src/browser/index.ts +++ b/packages/error/src/node/index.ts @@ -1,5 +1,5 @@ // Re-export core functionality (types) export * from '../core/index.js' -// Single public interface: captureError. Init Sentry per Sentry browser docs. export { captureError } from './capture.js' +export { type InitErrorReportingOptions, initErrorReporting } from './sentry.js' diff --git a/packages/sentry/src/node/sentry.ts b/packages/error/src/node/sentry.ts similarity index 53% rename from packages/sentry/src/node/sentry.ts rename to packages/error/src/node/sentry.ts index a7fb0dbc..e77c29b9 100644 --- a/packages/sentry/src/node/sentry.ts +++ b/packages/error/src/node/sentry.ts @@ -3,10 +3,10 @@ import type { ErrorEvent, EventHint } from '@sentry/node' import * as Sentry from '@sentry/node' /** - * Options for initializing Sentry + * Options for initializing error reporting */ -export interface InitSentryOptions { - /** Sentry DSN (optional - if not provided, Sentry is disabled) */ +export interface InitErrorReportingOptions { + /** DSN for error reporting backend (optional - if not provided, errors are logged only) */ dsn?: string /** Environment name (e.g., 'production', 'staging', 'development') */ environment?: string @@ -17,15 +17,14 @@ export interface InitSentryOptions { } /** - * Initializes Sentry for error tracking - * Uses @sentry/node for Node.js/Fastify applications - * Uses Sentry's built-in PII scrubbing by default + * Initializes error reporting for Node.js/Fastify applications. + * Uses a compatible SDK (GlitchTip, Sentry, etc.) - same DSN format. * - * @param options - Sentry initialization options + * @param options - Error reporting initialization options */ -export function initSentry(options: InitSentryOptions): void { +export function initErrorReporting(options: InitErrorReportingOptions): void { if (!options.dsn) { - logger.warn('Sentry DSN not configured - error reporting disabled') + logger.warn('Error reporting DSN not configured - error reporting disabled') return } @@ -34,9 +33,6 @@ export function initSentry(options: InitSentryOptions): void { environment: options.environment ?? 'development', release: options.release, tracesSampleRate: options.environment === 'production' ? 0.1 : 1.0, - // Optional: custom beforeSend for domain-specific scrubbing - // Sentry's built-in scrubbing handles most cases automatically - // Network-related errors (e.g., ECONNREFUSED, ETIMEDOUT) should be filtered via beforeSend beforeSend: options.beforeSend, ignoreErrors: ['Non-Error promise rejection'], }) diff --git a/packages/sentry/src/react/error-boundary.tsx b/packages/error/src/react/error-boundary.tsx similarity index 95% rename from packages/sentry/src/react/error-boundary.tsx rename to packages/error/src/react/error-boundary.tsx index db4b8d40..163d3ec8 100644 --- a/packages/sentry/src/react/error-boundary.tsx +++ b/packages/error/src/react/error-boundary.tsx @@ -8,7 +8,7 @@ export interface AppErrorBoundaryProps { children: ReactNode /** Application name for tagging (required) */ app: string - /** Error capture function - import from @repo/sentry/node, @repo/sentry/nextjs, or @repo/sentry/browser */ + /** Error capture function - import from @repo/error/node, @repo/error/nextjs, or @repo/error/browser */ captureError: (options: CaptureErrorOptions) => void /** Optional fallback component */ fallback?: (props: { error: Error; resetErrorBoundary: () => void }) => ReactNode diff --git a/packages/sentry/src/react/index.ts b/packages/error/src/react/index.ts similarity index 100% rename from packages/sentry/src/react/index.ts rename to packages/error/src/react/index.ts diff --git a/packages/error/src/types.ts b/packages/error/src/types.ts new file mode 100644 index 00000000..91e6fba8 --- /dev/null +++ b/packages/error/src/types.ts @@ -0,0 +1,67 @@ +import type { Logger } from '@repo/utils/logger/server' + +/** + * Options for capturing an error to the reporting backend. + * + * Provides a developer-friendly abstraction with automatic error conversion, + * consistent labeling, and optional logging integration. + * + * @example + * ```ts + * captureError({ + * code: 'NETWORK_ERROR', + * error: new Error('Failed to fetch'), + * label: 'API Call', + * data: { endpoint: '/api/data' }, + * tags: { app: 'web' }, + * }) + * ``` + */ +export interface CaptureErrorOptions { + /** + * Real error object (sent to reporting backend with full stack trace). + * Accepts any error type (Error, string, object, etc.) - automatically converted to Error. + */ + error: unknown + + /** + * Component/feature label for error reporting. + * Maps to tags and context for filtering and grouping. + */ + label: string + + /** + * Optional error code (used as tag for filtering). + */ + code?: string + + /** + * Additional tags for filtering in the reporting backend. + */ + tags?: Record + + /** + * Additional context (sent to reporting backend only, not exposed to users). + */ + data?: Record + + /** + * Error level for reporting. + * @default 'error' + */ + level?: 'error' | 'warning' | 'info' + + /** + * Whether to report to the backend. + * Allows disabling reporting entirely without conditional logic. + * @default true + */ + report?: boolean + + /** + * Optional logger instance (for non-Fastify contexts). + * Used when reporting backend is not initialized (errors are logged instead). + * In Fastify, pass `request.log` for request context. + */ + logger?: Logger +} diff --git a/packages/utils/src/error/index.ts b/packages/error/src/utils/index.ts similarity index 97% rename from packages/utils/src/error/index.ts rename to packages/error/src/utils/index.ts index b02677ee..f65f2ec2 100644 --- a/packages/utils/src/error/index.ts +++ b/packages/error/src/utils/index.ts @@ -71,4 +71,4 @@ export function getErrorMessage(error: unknown): string { } // Re-export types -export type { ErrorWithMessage } from './types.js' +export type { ErrorWithMessage, Result } from './types.js' diff --git a/packages/utils/src/error/types.ts b/packages/error/src/utils/types.ts similarity index 100% rename from packages/utils/src/error/types.ts rename to packages/error/src/utils/types.ts diff --git a/packages/sentry/tsconfig.json b/packages/error/tsconfig.json similarity index 100% rename from packages/sentry/tsconfig.json rename to packages/error/tsconfig.json diff --git a/packages/sentry/tsup.config.ts b/packages/error/tsup.config.ts similarity index 84% rename from packages/sentry/tsup.config.ts rename to packages/error/tsup.config.ts index 0e78fb53..1d43438f 100644 --- a/packages/sentry/tsup.config.ts +++ b/packages/error/tsup.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: { + index: 'src/utils/index.ts', core: 'src/core/index.ts', node: 'src/node/index.ts', nextjs: 'src/nextjs/index.ts', @@ -14,5 +15,6 @@ export default defineConfig({ clean: true, outDir: 'dist', noExternal: ['@repo/utils'], + // Utils are internal; logger from @repo/utils external: ['@sentry/node', '@sentry/nextjs', '@sentry/browser', 'react', 'pino'], }) diff --git a/packages/sentry/vitest.config.ts b/packages/error/vitest.config.ts similarity index 86% rename from packages/sentry/vitest.config.ts rename to packages/error/vitest.config.ts index 912557db..b4ded787 100644 --- a/packages/sentry/vitest.config.ts +++ b/packages/error/vitest.config.ts @@ -12,7 +12,6 @@ export default defineConfig({ }, resolve: { alias: { - '@repo/utils/error': resolve(configDir, '../utils/src/error/index.ts'), '@repo/utils': resolve(configDir, '../utils/src'), }, }, diff --git a/packages/react/README.md b/packages/react/README.md index 0dfb8e83..d366abc9 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -29,7 +29,7 @@ Add `@repo/react` and `@repo/core` to `transpilePackages` in your `next.config.m // next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = { - transpilePackages: ['@repo/ui', '@repo/core', '@repo/react', '@repo/sentry', '@repo/utils'], + transpilePackages: ['@repo/ui', '@repo/core', '@repo/react', '@repo/error', '@repo/utils'], webpack: config => { // Resolve .js imports to .ts files for transpiled packages config.resolve.extensionAlias = { diff --git a/packages/sentry/README.md b/packages/sentry/README.md deleted file mode 100644 index 8347a43a..00000000 --- a/packages/sentry/README.md +++ /dev/null @@ -1,104 +0,0 @@ -# @repo/sentry - -Common interface for error reporting across the monorepo. Apps and packages use **`captureError`** for a consistent, type-safe way to send errors to Sentry. Sentry is **registered at the application level** (e.g. Fastify plugin, Next.js instrumentation); individual packages only import and call `captureError`. - -## Exports - -| Path | Use for | -|------|--------| -| `@repo/sentry/node` | Node.js / Fastify | -| `@repo/sentry/nextjs` | Next.js (client + server) | -| `@repo/sentry/browser` | Browser-only (TanStack Start, Vue, Svelte, etc.) | -| `@repo/sentry/react` | React Error Boundary component | - -**Import rule:** Use the platform path that matches your app. The `CaptureErrorOptions` type is re-exported from each platform path. **Initialization** is not part of this package—init Sentry per your platform’s [Sentry docs](https://docs.sentry.io/platforms/) (Node, Browser, Next.js). **Error normalization** (e.g. `toErrorWithMessage`, `getErrorMessage`) lives in `@repo/utils/error` - -## Quick start - -**Capture an error** (non-blocking, async): - -```typescript -import { captureError } from '@repo/sentry/node' // or /nextjs, /browser - -captureError({ - error, - label: 'API Call', - code: 'NETWORK_ERROR', // optional, for Sentry tags - data: { endpoint: '/api/data' }, - tags: { app: 'web' }, -}) -``` - -## API - -### `captureError(options): void` - -Sends the error to Sentry asynchronously. Does not return errors or handle app catalogs; each app owns response handling and error catalogs. - -| Option | Type | Description | -|--------|------|-------------| -| `error` | `unknown` | Converted to `Error` if needed; full stack sent to Sentry | -| `label` | `string` | Component/feature label → Sentry `tags.component` and `contexts.error.label` | -| `code?` | `string` | Optional tag → `tags.errorCode`, `contexts.error.code` | -| `data?` | `Record` | Extra context → `contexts.error` | -| `tags?` | `Record` | Extra tags | -| `level?` | `'error' \| 'warning' \| 'info'` | Default `'error'` | -| `report?` | `boolean` | If `false`, skip Sentry (default `true`) | -| `logger?` | `Logger` | Optional; only used to warn when Sentry is not initialized. In Fastify, pass `request.log` for request context. | - -## Framework integration - -**Fastify** — Init Sentry with `@sentry/node` at app bootstrap (see [Sentry Node](https://docs.sentry.io/platforms/javascript/guides/node/)). Use `captureError` in your error handler: - -```typescript -import { captureError } from '@repo/sentry/node' - -fastify.setErrorHandler((error, request, reply) => { - captureError({ - error, - label: `${request.method} ${request.url}`, - logger: request.log, - tags: { app: 'api' }, - }) - // App handles response (catalog, status, etc.) - const catalogError = getError(mapHttpStatusToErrorCode(error.statusCode)) ?? getError('UNEXPECTED_ERROR') - reply.status(error.statusCode ?? 500).send(catalogError) -}) -``` - -**React Error Boundary** — Pass platform-specific `captureError`: - -```typescript -import { AppErrorBoundary } from '@repo/sentry/react' -import { captureError } from '@repo/sentry/nextjs' - - - - -``` - -**Next.js** — Use [Sentry’s standard setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/): `withSentryConfig` in `next.config`, plus `sentry.server.config.ts`, `instrumentation-client.ts`, `sentry.edge.config.ts`, and `instrumentation.ts` with `register()` and `onRequestError`. In your app code, use the common interface: - -```typescript -import { captureError } from '@repo/sentry/nextjs' - -// Use anywhere (client or server) -captureError({ error, label: 'Checkout', tags: { app: 'web' } }) -``` - -## Security and performance - -- Sentry’s built-in PII scrubbing is used; add `beforeSend` for domain-specific filtering. -- Capture is async (`Promise.resolve().then(...)`), non-blocking. In serverless, consider `Sentry.flush()` before returning if the process exits quickly. - -## Scripts - -- `pnpm --filter @repo/sentry build` - Build package -- `pnpm --filter @repo/sentry checktypes` - Type-check -- `pnpm --filter @repo/sentry test` - Run tests - -## See also - -- [Error Handling Guide](/docs/architecture/error-handling) -- [Logging Guide](/docs/architecture/logging) -- [Security Guide](/docs/security) diff --git a/packages/sentry/src/core/capture-impl.ts b/packages/sentry/src/core/capture-impl.ts deleted file mode 100644 index fbcca59d..00000000 --- a/packages/sentry/src/core/capture-impl.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Error normalization: use @repo/utils/error only (no local duplication) -import { toErrorWithMessage } from '@repo/utils/error' -import type { Logger } from '@repo/utils/logger/server' -import type { CaptureErrorOptions } from '../types.js' - -// Module-scoped flag for warning suppression (shows once per app runtime) -let sentryWarningShown = false - -/** - * Minimal interface for Sentry modules - compatible with both @sentry/node and @sentry/nextjs - */ -interface SentryAdapter { - getClient: () => object | null - captureException: ( - exception: Error, - hint?: { - tags?: Record - level?: 'error' | 'warning' | 'info' - contexts?: Record> - }, - ) => void -} - -/** - * Creates a captureError function bound to a specific Sentry implementation - * This factory allows sharing the capture logic between Node.js and Next.js - * - * @internal This is an internal implementation detail, not part of public API - */ -export function createCaptureError(Sentry: SentryAdapter, defaultLogger: Logger) { - return function captureError(options: CaptureErrorOptions): void { - const errorWithMessage = toErrorWithMessage(options.error) - - // Don't report if explicitly disabled - if (options.report !== false) - // Capture to Sentry asynchronously (non-blocking) - // Note: Uses Promise.resolve().then() for runtime compatibility across Node.js, Edge, and browsers. - // Limitation: In serverless environments, errors may be dropped if function terminates before promise executes. - // For critical paths, apps should call Sentry.flush() explicitly before function termination. - Promise.resolve().then(() => { - const sentryClient = Sentry.getClient() - - if (!sentryClient) { - if (!sentryWarningShown) { - const log = options.logger ?? defaultLogger - log.warn('Sentry not initialized - error reporting disabled. Set SENTRY_DSN to enable.') - sentryWarningShown = true - } - return - } - - // Build tags object - const tags: Record = { - component: options.label, - ...(options.code ? { errorCode: options.code } : {}), - ...options.tags, - } - - // Capture REAL error in Sentry (async, non-blocking) - // Sentry's built-in scrubbing handles PII automatically - Sentry.captureException( - options.error instanceof Error ? options.error : new Error(errorWithMessage.message), - { - tags, - level: options.level ?? 'error', - contexts: { - error: { - label: options.label, - ...(options.code ? { code: options.code } : {}), - ...options.data, - }, - }, - }, - ) - }) - } -} diff --git a/packages/sentry/src/nextjs/index.ts b/packages/sentry/src/nextjs/index.ts deleted file mode 100644 index 3ad2e684..00000000 --- a/packages/sentry/src/nextjs/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Re-export core functionality (types, utils) -export * from '../core/index.js' - -// Next.js: use Sentry's standard setup (sentry.server.config.ts, instrumentation-client.ts, instrumentation.ts). -// This package only provides the common captureError interface. -export { captureError } from './capture.js' diff --git a/packages/sentry/src/types.ts b/packages/sentry/src/types.ts deleted file mode 100644 index c159436f..00000000 --- a/packages/sentry/src/types.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { Logger } from '@repo/utils/logger/server' - -/** - * Options for capturing an error to Sentry. - * - * Provides a developer-friendly abstraction over Sentry's native `captureException` API - * with automatic error conversion, consistent labeling, and optional logging integration. - * - * @example - * ```ts - * captureError({ - * code: 'NETWORK_ERROR', - * error: new Error('Failed to fetch'), - * label: 'API Call', - * data: { endpoint: '/api/data' }, - * tags: { app: 'web' }, - * }) - * ``` - */ -export interface CaptureErrorOptions { - /** - * Real error object (sent to Sentry with full stack trace). - * Accepts any error type (Error, string, object, etc.) - automatically converted to Error. - */ - error: unknown - - /** - * Component/feature label for Sentry. - * Automatically maps to both `tags.component` and `contexts.error.label` in Sentry. - */ - label: string - - /** - * Optional error code (used as tag only, for filtering in Sentry). - * Automatically maps to both `tags.errorCode` and `contexts.error.code` in Sentry. - */ - code?: string - - /** - * Tags for filtering in Sentry. - * Additional tags beyond the automatic `component` and `errorCode` tags. - */ - tags?: Record - - /** - * Additional context (sent to Sentry only, not exposed to users). - * Automatically merged into `contexts.error` for convenience. - */ - data?: Record - - /** - * Error level for Sentry. - * @default 'error' - */ - level?: 'error' | 'warning' | 'info' - - /** - * Whether to report to Sentry. - * Allows disabling Sentry reporting entirely without conditional logic. - * @default true - */ - report?: boolean - - /** - * Optional logger instance (for non-Fastify contexts only, Fastify handles logging). - * Only used for warnings when Sentry is not initialized (e.g., missing DSN). - * If omitted, defaults to `@repo/utils/logger`. In Fastify, pass `request.log` for request context. - */ - logger?: Logger -} diff --git a/packages/utils/README.md b/packages/utils/README.md index 0855da2c..735d3b9e 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -5,7 +5,6 @@ Shared utility library for the Basilic monorepo. Prefer subpath imports; details - [async](src/async/README.md) — `@repo/utils/async` - [data](src/data/README.md) — `@repo/utils/data` - [debug](src/debug/README.md) — `@repo/utils/debug` -- [error](src/error/README.md) — `@repo/utils/error` - [logger](src/logger/README.md) — `@repo/utils/logger/server`, `@repo/utils/logger/client` - [web3](src/web3/README.md) — `@repo/utils/web3` diff --git a/packages/utils/package.json b/packages/utils/package.json index ca6f48a6..da76c287 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -48,13 +48,6 @@ "node": "./dist/debug/index.js", "default": "./dist/debug/index.js" }, - "./error": { - "types": "./src/error/index.ts", - "source": "./src/error/index.ts", - "import": "./dist/error/index.js", - "node": "./dist/error/index.js", - "default": "./dist/error/index.js" - }, "./data": { "types": "./src/data/index.ts", "source": "./src/data/index.ts", diff --git a/packages/utils/src/error/README.md b/packages/utils/src/error/README.md deleted file mode 100644 index 4eeadcd4..00000000 --- a/packages/utils/src/error/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Error utils (`@repo/utils/error`) - -Type-safe error normalization and Result-style async handling. No peer dependencies. - -## API - -### `getErrorMessage(error)` - -Extracts a string message from any thrown value. - -```ts -import { getErrorMessage } from '@repo/utils/error' - -try { - await risky() -} catch (e) { - console.error(getErrorMessage(e)) -} -``` - -### `toErrorWithMessage(maybeError)` - -Normalizes unknown to `{ message: string }` (Error, string, or fallback). - -### `isErrorWithMessage(error)` - -Type guard: `error is ErrorWithMessage`. - -### `tryCatch(promiseOrFn)` - -Wraps a promise or async function; returns `{ data, error }` (one undefined). Errors are normalized. - -```ts -import { tryCatch } from '@repo/utils/error' - -const { error, data } = await tryCatch(fetchUser(id)) -if (error) return console.error(error.message) -// data is defined here -``` - -### Types - -- **ErrorWithMessage** — `{ message: string }` -- **Result** — `{ data: T; error?: undefined } | { data?: undefined; error: E }` - -For Sentry reporting use `@repo/sentry` (e.g. `captureError`); use this package only for message extraction and Result handling. diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 79c91630..06a9d790 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,6 +1,4 @@ // Async utilities export * from './async/index.js' -// Error utilities -export * from './error/index.js' // Chain utilities export * from './web3/index.js' diff --git a/packages/utils/tsup.config.ts b/packages/utils/tsup.config.ts index 3cd64196..3f321197 100644 --- a/packages/utils/tsup.config.ts +++ b/packages/utils/tsup.config.ts @@ -8,7 +8,6 @@ export default defineConfig({ 'src/logger/client.ts', 'src/logger/server.ts', 'src/debug/index.ts', - 'src/error/index.ts', 'src/data/index.ts', ], format: ['esm'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d290ffa7..9a477374 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,9 +167,9 @@ importers: '@repo/email': specifier: workspace:* version: link:../../packages/email - '@repo/sentry': + '@repo/error': specifier: workspace:* - version: link:../../packages/sentry + version: link:../../packages/error '@repo/utils': specifier: workspace:* version: link:../../packages/utils @@ -324,6 +324,9 @@ importers: '@repo/core': specifier: workspace:* version: link:../../packages/core + '@repo/error': + specifier: workspace:* + version: link:../../packages/error '@repo/react': specifier: workspace:* version: link:../../packages/react @@ -333,6 +336,9 @@ importers: '@repo/utils': specifier: workspace:* version: link:../../packages/utils + '@sentry/nextjs': + specifier: ^10.38.0 + version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.105.2) '@solana/wallet-adapter-base': specifier: ^0.9.27 version: 0.9.27(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -386,7 +392,7 @@ importers: version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) nuqs: specifier: ^2.8.8 - version: 2.8.8(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 2.8.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.4 version: 19.2.4 @@ -537,6 +543,52 @@ importers: specifier: ^19.2.4 version: 19.2.4 + packages/error: + dependencies: + '@repo/utils': + specifier: workspace:* + version: link:../utils + pino: + specifier: '*' + version: 10.3.1 + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../../tools/eslint + '@repo/typescript-config': + specifier: workspace:* + version: link:../../tools/typescript + '@sentry/browser': + specifier: ^10.38.0 + version: 10.39.0 + '@sentry/nextjs': + specifier: ^10.38.0 + version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.105.2(esbuild@0.27.3)) + '@sentry/node': + specifier: ^10.38.0 + version: 10.39.0 + '@types/node': + specifier: ^25.2.3 + version: 25.2.3 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260213.1 + version: 7.0.0-dev.20260213.1 + eslint: + specifier: ^9.0.0 + version: 9.39.2(jiti@2.6.1) + react: + specifier: ^19.2.4 + version: 19.2.4 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + vitest: + specifier: ^4.0.18 + version: 4.0.18(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + packages/notif: dependencies: '@repo/email': @@ -650,52 +702,6 @@ importers: specifier: ^4.0.18 version: 4.0.18(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.0)(@types/node@25.3.5)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - packages/sentry: - dependencies: - '@repo/utils': - specifier: workspace:* - version: link:../utils - pino: - specifier: '*' - version: 10.3.1 - devDependencies: - '@repo/eslint-config': - specifier: workspace:* - version: link:../../tools/eslint - '@repo/typescript-config': - specifier: workspace:* - version: link:../../tools/typescript - '@sentry/browser': - specifier: ^10.38.0 - version: 10.39.0 - '@sentry/nextjs': - specifier: ^10.38.0 - version: 10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.105.2(esbuild@0.27.3)) - '@sentry/node': - specifier: ^10.38.0 - version: 10.39.0 - '@types/node': - specifier: ^25.2.3 - version: 25.2.3 - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@typescript/native-preview': - specifier: 7.0.0-dev.20260213.1 - version: 7.0.0-dev.20260213.1 - eslint: - specifier: ^9.0.0 - version: 9.39.2(jiti@2.6.1) - react: - specifier: ^19.2.4 - version: 19.2.4 - tsup: - specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) - vitest: - specifier: ^4.0.18 - version: 4.0.18(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - packages/ui: dependencies: '@hookform/resolvers': @@ -930,7 +936,7 @@ importers: version: 9.39.2(jiti@2.6.1) nuqs: specifier: ^2.8.8 - version: 2.8.8(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 2.8.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) react: specifier: ^19.2.4 version: 19.2.4 @@ -17205,6 +17211,31 @@ snapshots: '@sentry/core@10.39.0': {} + '@sentry/nextjs@10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.105.2)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + '@rollup/plugin-commonjs': 28.0.1(rollup@4.59.0) + '@sentry-internal/browser-utils': 10.39.0 + '@sentry/bundler-plugin-core': 4.9.1 + '@sentry/core': 10.39.0 + '@sentry/node': 10.39.0 + '@sentry/opentelemetry': 10.39.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) + '@sentry/react': 10.39.0(react@19.2.4) + '@sentry/vercel-edge': 10.39.0 + '@sentry/webpack-plugin': 4.9.1(webpack@5.105.2) + next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + rollup: 4.59.0 + stacktrace-parser: 0.1.11 + transitivePeerDependencies: + - '@opentelemetry/context-async-hooks' + - '@opentelemetry/core' + - '@opentelemetry/sdk-trace-base' + - encoding + - react + - supports-color + - webpack + '@sentry/nextjs@10.39.0(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.105.2(esbuild@0.27.3))': dependencies: '@opentelemetry/api': 1.9.0 @@ -17283,7 +17314,7 @@ snapshots: '@sentry/node-core': 10.39.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) '@sentry/opentelemetry': 10.39.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) import-in-the-middle: 2.0.6 - minimatch: 10.2.3 + minimatch: 9.0.7 transitivePeerDependencies: - supports-color @@ -17318,6 +17349,16 @@ snapshots: - encoding - supports-color + '@sentry/webpack-plugin@4.9.1(webpack@5.105.2)': + dependencies: + '@sentry/bundler-plugin-core': 4.9.1 + unplugin: 1.0.1 + uuid: 9.0.1 + webpack: 5.105.2 + transitivePeerDependencies: + - encoding + - supports-color + '@shikijs/core@3.22.0': dependencies: '@shikijs/types': 3.22.0 @@ -19597,8 +19638,8 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.3 '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -19616,8 +19657,8 @@ snapshots: dependencies: '@mapbox/node-pre-gyp': 2.0.3 '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -20511,6 +20552,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -20519,6 +20564,10 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-walk@8.3.4: dependencies: acorn: 8.15.0 @@ -22195,7 +22244,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.15.0 + acorn: 8.16.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -23127,7 +23176,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 10.2.3 + minimatch: 9.0.7 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -23525,8 +23574,8 @@ snapshots: import-in-the-middle@2.0.6: dependencies: - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 @@ -24856,8 +24905,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 @@ -25065,7 +25114,7 @@ snapshots: mlly@1.8.0: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.3 @@ -25206,7 +25255,7 @@ snapshots: nullthrows@1.1.1: {} - nuqs@2.8.8(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + nuqs@2.8.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.4 @@ -27261,6 +27310,14 @@ snapshots: optionalDependencies: esbuild: 0.27.3 + terser-webpack-plugin@5.3.17(webpack@5.105.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.0 + webpack: 5.105.2 + terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -27680,7 +27737,7 @@ snapshots: unplugin@1.0.1: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 chokidar: 3.6.0 webpack-sources: 3.3.4 webpack-virtual-modules: 0.5.0 @@ -28133,6 +28190,38 @@ snapshots: webpack-virtual-modules@0.5.0: {} + webpack@5.105.2: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.0 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.17(webpack@5.105.2) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.105.2(esbuild@0.27.3): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/turbo.json b/turbo.json index 2008ac59..87c2c974 100644 --- a/turbo.json +++ b/turbo.json @@ -9,7 +9,7 @@ "env": ["NODE_ENV"] }, "@repo/fastify#generate:openapi": { - "dependsOn": ["@repo/utils#build", "@repo/sentry#build"], + "dependsOn": ["@repo/utils#build", "@repo/error#build"], "inputs": ["$TURBO_DEFAULT$", "src/**/*.ts"], "outputs": ["openapi/openapi.json"], "env": ["NODE_ENV", "OLLAMA_BASE_URL", "OPEN_ROUTER_API_KEY", "AI_PROVIDER"] @@ -68,7 +68,12 @@ "NEXT_RUNTIME", "DEBUG_TEST", "APP_URL", - "EMAIL_ASSETS_URL" + "EMAIL_ASSETS_URL", + "ERROR_REPORTING_DSN", + "ERROR_REPORTING_ENVIRONMENT", + "NEXT_PUBLIC_ERROR_REPORTING_DSN", + "SENTRY_ORG", + "SENTRY_PROJECT" ] }, "lint:eslint:fix": { @@ -101,7 +106,12 @@ "NEXT_RUNTIME", "DEBUG_TEST", "APP_URL", - "EMAIL_ASSETS_URL" + "EMAIL_ASSETS_URL", + "ERROR_REPORTING_DSN", + "ERROR_REPORTING_ENVIRONMENT", + "NEXT_PUBLIC_ERROR_REPORTING_DSN", + "SENTRY_ORG", + "SENTRY_PROJECT" ] }, "checktypes": { "env": ["NODE_ENV"] }, From 2d8a069e338245530e1e3990e5edb890ea260cea Mon Sep 17 00:00:00 2001 From: gaboesquivel Date: Sat, 7 Mar 2026 22:27:43 +0000 Subject: [PATCH 2/4] feat(error): add Next.js global-error boundary and harden capture --- .../docs/architecture/error-handling.mdx | 2 +- .../content/docs/development/packages.mdx | 6 +- apps/next/app/global-error.tsx | 53 ++++++++++++ packages/error/src/core/capture-impl.ts | 82 ++++++++++--------- packages/error/vitest.config.ts | 10 +-- 5 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 apps/next/app/global-error.tsx diff --git a/apps/docu/content/docs/architecture/error-handling.mdx b/apps/docu/content/docs/architecture/error-handling.mdx index 254590fd..ed969f35 100644 --- a/apps/docu/content/docs/architecture/error-handling.mdx +++ b/apps/docu/content/docs/architecture/error-handling.mdx @@ -290,7 +290,7 @@ captureError({ }) ``` -See [packages/sentry/README.md](https://github.com/blockmatic/basilic/blob/main/packages/sentry/README.md) for full options. **Serverless:** Consider `Sentry.flush()` before function exit if the process terminates quickly. +See [@repo/error](https://github.com/blockmatic/basilic/blob/main/packages/error/README.md) for full options. **Serverless:** Consider `Sentry.flush()` before function exit if the process terminates quickly. ### `getErrorMessage(error)` (`@repo/error` or `@repo/error/nextjs`) diff --git a/apps/docu/content/docs/development/packages.mdx b/apps/docu/content/docs/development/packages.mdx index 595d1826..0fd84f87 100644 --- a/apps/docu/content/docs/development/packages.mdx +++ b/apps/docu/content/docs/development/packages.mdx @@ -139,7 +139,6 @@ export function HealthStatus() { Prefer subpath imports: - `@repo/utils/async` — async helpers (`delay`, `fetchWithTimeout`, …) -- `@repo/error` — error normalization (`getErrorMessage`, `tryCatch`, …) - `@repo/utils/web3` — chain metadata + helpers - `@repo/utils/logger/server`, `@repo/utils/logger/client` — Pino (server) and console (client) - `@repo/utils/debug` — client-only debug hooks (`useDevtools`, `useNuqsDebug`, `useVconsole`) @@ -168,10 +167,11 @@ import { cn } from '@repo/ui/lib/utils' ## Error reporting -### `@repo/error` — capture interface +### `@repo/error` — capture interface + error utilities Subpath-only exports (choose the platform path): +- `@repo/error` — error utilities (`getErrorMessage`, `tryCatch`, `toErrorWithMessage`, …) - `@repo/error/node` - `@repo/error/nextjs` - `@repo/error/browser` @@ -179,8 +179,10 @@ Subpath-only exports (choose the platform path): ```ts import { captureError } from '@repo/error/node' // or /nextjs, /browser +import { getErrorMessage, tryCatch } from '@repo/error' // or @repo/error/nextjs captureError({ error, label: 'API Call', tags: { app: 'api' } }) +const message = getErrorMessage(error) ``` ## Email diff --git a/apps/next/app/global-error.tsx b/apps/next/app/global-error.tsx new file mode 100644 index 00000000..2340f419 --- /dev/null +++ b/apps/next/app/global-error.tsx @@ -0,0 +1,53 @@ +'use client' + +import { captureError } from '@repo/error/nextjs' +import { logger } from '@repo/utils/logger/client' +import { useEffect } from 'react' + +import '@repo/ui/styles/globals.css' + +import { env } from 'lib/env' + +// eslint-disable-next-line import/no-default-export -- Next.js requires default export for global-error.tsx +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + useEffect(() => { + captureError({ + code: 'UNEXPECTED_ERROR', + data: { digest: error.digest }, + error, + label: 'Next.js global-error.tsx', + tags: { runtime: 'nextjs' }, + }) + logger.error({ digest: error.digest, error }, 'Unhandled error in root layout') + }, [error]) + + return ( + + +
+
+

Something went wrong

+

An unexpected error occurred

+ {env.NEXT_PUBLIC_NODE_ENV === 'development' && error?.message && ( +
+                {error.message}
+              
+ )} + +
+
+ + + ) +} diff --git a/packages/error/src/core/capture-impl.ts b/packages/error/src/core/capture-impl.ts index fd83cdab..d667fe37 100644 --- a/packages/error/src/core/capture-impl.ts +++ b/packages/error/src/core/capture-impl.ts @@ -31,50 +31,58 @@ export function createCaptureError(reporting: ReportingAdapter, defaultLogger: L if (options.report === false) return - Promise.resolve().then(() => { - const reportingClient = reporting.getClient() + Promise.resolve() + .then(() => { + const reportingClient = reporting.getClient() - if (!reportingClient) { - if (!reportingWarningShown) { + if (!reportingClient) { + if (!reportingWarningShown) { + const log = options.logger ?? defaultLogger + log.warn( + 'Error reporting not initialized - errors will be logged only. Set ERROR_REPORTING_DSN to enable.', + ) + reportingWarningShown = true + } const log = options.logger ?? defaultLogger - log.warn( - 'Error reporting not initialized - errors will be logged only. Set ERROR_REPORTING_DSN to enable.', + log.error( + { + err: errorWithMessage, + label: options.label, + code: options.code, + ...options.data, + }, + 'Error captured (no DSN)', ) - reportingWarningShown = true + return } - const log = options.logger ?? defaultLogger - log.error( - { - err: errorWithMessage, - label: options.label, - code: options.code, - ...options.data, - }, - 'Error captured (no DSN)', - ) - return - } - const tags: Record = { - component: options.label, - ...(options.code ? { errorCode: options.code } : {}), - ...options.tags, - } + const tags: Record = { + component: options.label, + ...(options.code ? { errorCode: options.code } : {}), + ...options.tags, + } - reporting.captureException( - options.error instanceof Error ? options.error : new Error(errorWithMessage.message), - { - tags, - level: options.level ?? 'error', - contexts: { - error: { - label: options.label, - ...(options.code ? { code: options.code } : {}), - ...options.data, + reporting.captureException( + options.error instanceof Error ? options.error : new Error(errorWithMessage.message), + { + tags, + level: options.level ?? 'error', + contexts: { + error: { + label: options.label, + ...(options.code ? { code: options.code } : {}), + ...options.data, + }, }, }, - }, - ) - }) + ) + }) + .catch(err => { + const log = options.logger ?? defaultLogger + log.error( + { err, label: options.label, code: options.code, ...options.data }, + 'Error reporting failed (captureException threw)', + ) + }) } } diff --git a/packages/error/vitest.config.ts b/packages/error/vitest.config.ts index b4ded787..ef9e4ef4 100644 --- a/packages/error/vitest.config.ts +++ b/packages/error/vitest.config.ts @@ -9,15 +9,15 @@ export default defineConfig({ include: ['src/**/*.{test,spec}.ts'], globals: true, environment: 'node', + server: { + deps: { + inline: [/@repo\/utils/], + }, + }, }, resolve: { alias: { '@repo/utils': resolve(configDir, '../utils/src'), }, }, - server: { - deps: { - inline: [/@repo\/utils/], - }, - }, }) From e057256e982ad2d0067a563f34df074a52a0d5eb Mon Sep 17 00:00:00 2001 From: gaboesquivel Date: Sat, 7 Mar 2026 23:04:58 +0000 Subject: [PATCH 3/4] fix(error): align error handling docs, env, and logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - next-v16: replace ambiguous 'Sentry reporting' with @repo/error package refs - global-error: log only sanitized metadata (digest), not raw error object - error-reporting.client: use NEXT_PUBLIC_ERROR_REPORTING_ENVIRONMENT ?? NODE_ENV - error-boundary: remove @repo/error/node from captureError JSDoc (client-only) - types: import Logger from @repo/utils/logger/types (platform-neutral) - wagmi skill: update 'Log to Sentry' comment to 'Report error via @repo/error/nextjs' - packages.mdx: move @repo/error to Mixed (root + subpath exports) - error-handler: replace Sentry comments with error-reporting abstraction refs - README: dev command sentry → error - error-handling.mdx: rename 'Initialize Sentry' to 'Error Reporting Initialization' - capture-impl: wrap log.error in try/catch, fallback to console on logger throw - utils: add @repo/utils/logger/types export --- .cursor/skills/next-v16/error-handling.md | 2 +- .cursor/skills/wagmi-v3/SKILL.md | 2 +- README.md | 2 +- .../docs/architecture/error-handling.mdx | 2 +- .../docu/content/docs/development/packages.mdx | 6 +++--- apps/fastify/src/plugins/error-handler.ts | 6 +++--- apps/next/app/global-error.tsx | 2 +- apps/next/error-reporting.client.ts | 2 +- packages/error/src/core/capture-impl.ts | 18 ++++++++++++++---- packages/error/src/react/error-boundary.tsx | 4 ++-- packages/error/src/types.ts | 2 +- packages/utils/package.json | 7 +++++++ packages/utils/tsup.config.ts | 1 + 13 files changed, 37 insertions(+), 19 deletions(-) diff --git a/.cursor/skills/next-v16/error-handling.md b/.cursor/skills/next-v16/error-handling.md index d20d6b03..92b90544 100644 --- a/.cursor/skills/next-v16/error-handling.md +++ b/.cursor/skills/next-v16/error-handling.md @@ -1,6 +1,6 @@ # Error Handling -Handle errors in Next.js with **Sentry reporting** (`@repo/error/*`) and **structured logging** (`@repo/utils/logger/server` or `@repo/utils/logger/client`). +Handle errors in Next.js with the **`@repo/error` package** (Sentry-backed) and **structured logging** (`@repo/utils/logger/server` or `@repo/utils/logger/client`). Reference: https://nextjs.org/docs/app/getting-started/error-handling diff --git a/.cursor/skills/wagmi-v3/SKILL.md b/.cursor/skills/wagmi-v3/SKILL.md index 7a088798..a24defc2 100644 --- a/.cursor/skills/wagmi-v3/SKILL.md +++ b/.cursor/skills/wagmi-v3/SKILL.md @@ -567,7 +567,7 @@ function useContractWriteWithErrorHandling() { userMessage = 'Gas estimation failed. Please try again.' } - // Log to Sentry + // Report error via @repo/error/nextjs captureError({ code: 'TRANSACTION_ERROR', error, diff --git a/README.md b/README.md index 07494135..c54e39da 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Run with `pnpm