diff --git a/.env.example b/.env.example index 713f186b..91009bbe 100644 --- a/.env.example +++ b/.env.example @@ -16,4 +16,10 @@ UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_TOKEN= # Resend -RESEND_API_KEY= \ No newline at end of file +RESEND_API_KEY= + +# Sentry +NEXT_PUBLIC_SENTRY_DSN= +SENTRY_ORG= +SENTRY_PROJECT= +SENTRY_AUTH_TOKEN= \ No newline at end of file diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 9371cfa4..405d4b8f 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -32,6 +32,12 @@ jobs: # Resend RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + # Sentry + NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index b7de97e3..90e4671a 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -29,6 +29,12 @@ jobs: # Resend RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + # Sentry + NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/bun.lockb b/bun.lockb index f92d8848..7002292c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.js b/eslint.config.js index 26798783..66c64aa6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -11,6 +11,7 @@ import playwright from 'eslint-plugin-playwright'; import * as regexpPlugin from 'eslint-plugin-regexp'; import pluginSecurity from 'eslint-plugin-security'; import tseslint from 'typescript-eslint'; +import globals from 'globals'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -75,6 +76,11 @@ export default tseslint.config( projectService: true, tsconfigRootDir: __dirname, }, + globals: { + ...globals.node, + ...globals.browser, + ...globals.es2024, + }, }, settings: { react: { @@ -100,6 +106,8 @@ export default tseslint.config( { checksVoidReturn: { attributes: false } }, ], + '@typescript-eslint/dot-notation': 'off', + '@typescript-eslint/no-unnecessary-condition': [ 'error', { diff --git a/next.config.js b/next.config.js index 7f9377fb..f3f66bdf 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,7 @@ import { fileURLToPath } from 'node:url'; import createJiti from 'jiti'; +import { withSentryConfig } from '@sentry/nextjs'; const jiti = createJiti(fileURLToPath(import.meta.url)); @@ -25,4 +26,13 @@ const nextConfig = { }, }; -export default nextConfig; +export default withSentryConfig(nextConfig, { + org: process.env['SENTRY_ORG'] ?? '', + project: process.env['SENTRY_PROJECT'] ?? '', + silent: !process.env['CI'], + widenClientFileUpload: true, + tunnelRoute: '/monitoring', + hideSourceMaps: true, + disableLogger: true, + automaticVercelMonitors: true, +}); diff --git a/package.json b/package.json index c59d7c30..77968ff9 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "db:up": "drizzle-kit up", - "dev": "next dev --turbo", + "dev": "next dev", "email:dev": "SKIP_ENV_VALIDATION=true email dev --port 3001 --dir src/emails/templates", "format": "pnpm format:write", "format:check": "prettier \"**/*\" --ignore-unknown --list-different", @@ -65,6 +65,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@react-email/components": "^0.0.19", + "@sentry/nextjs": "^8.8.0", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "^5.40.1", "@trpc/client": "next", @@ -131,6 +132,7 @@ "eslint-plugin-security": "^3.0.0", "eslint-plugin-tailwindcss": "^3.17.3", "eslint-plugin-testing-library": "^6.2.2", + "globals": "^15.4.0", "husky": "^9.0.11", "lint-staged": "^15.2.5", "markdownlint": "^0.34.0", diff --git a/sentry.client.config.ts b/sentry.client.config.ts new file mode 100644 index 00000000..b1d9e14e --- /dev/null +++ b/sentry.client.config.ts @@ -0,0 +1,16 @@ +import { env } from '@/env'; +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1, + debug: false, + replaysOnErrorSampleRate: 1.0, + replaysSessionSampleRate: 0.1, + integrations: [ + Sentry.replayIntegration({ + maskAllText: true, + blockAllMedia: true, + }), + ], +}); diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts new file mode 100644 index 00000000..ead9b7dd --- /dev/null +++ b/sentry.edge.config.ts @@ -0,0 +1,8 @@ +import { env } from '@/env'; +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1, + debug: false, +}); diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 00000000..ead9b7dd --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,8 @@ +import { env } from '@/env'; +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1, + debug: false, +}); diff --git a/src/app/global-error.jsx b/src/app/global-error.jsx new file mode 100644 index 00000000..fc093df1 --- /dev/null +++ b/src/app/global-error.jsx @@ -0,0 +1,19 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import Error from 'next/error'; +import { useEffect } from 'react'; + +export default function GlobalError({ error }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/src/env.ts b/src/env.ts index feb6b89c..620d6812 100644 --- a/src/env.ts +++ b/src/env.ts @@ -24,16 +24,25 @@ export const env = createEnv({ // Clerk CLERK_SECRET_KEY: z.string(), + + // Sentry + SENTRY_ORG: z.string(), + SENTRY_PROJECT: z.string(), + SENTRY_AUTH_TOKEN: z.string(), }, client: { // Clerk NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), + + // Sentry + NEXT_PUBLIC_SENTRY_DSN: z.string().url(), }, experimental__runtimeEnv: { NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env['NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY'], + NEXT_PUBLIC_SENTRY_DSN: process.env['NEXT_PUBLIC_SENTRY_DSN'], }, skipValidation: !!process.env['SKIP_ENV_VALIDATION'], diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 00000000..c0247483 --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,9 @@ +export async function register() { + if (process.env['NEXT_RUNTIME'] === 'nodejs') { + await import('../sentry.server.config'); + } + + if (process.env['NEXT_RUNTIME'] === 'edge') { + await import('../sentry.edge.config'); + } +}