feat: Sentry error monitoring for React frontend #98

Merged
chihlasm merged 2 commits from feat/sentry-frontend into main 2026-03-08 00:29:58 +00:00
10 changed files with 563 additions and 75 deletions

View File

@@ -118,6 +118,9 @@ class Settings(BaseSettings):
"""Check if any AI provider is configured.""" """Check if any AI provider is configured."""
return self.ANTHROPIC_API_KEY is not None or self.GOOGLE_AI_API_KEY is not None return self.ANTHROPIC_API_KEY is not None or self.GOOGLE_AI_API_KEY is not None
# Monitoring
SENTRY_DSN: Optional[str] = None
# Deployment auto-seed test data on PR environments # Deployment auto-seed test data on PR environments
SEED_ON_DEPLOY: bool = False SEED_ON_DEPLOY: bool = False

View File

@@ -8,6 +8,21 @@ from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
from app.core.config import settings from app.core.config import settings
# Initialize Sentry before any other app code
import sentry_sdk
if settings.SENTRY_DSN:
sentry_sdk.init(
dsn=settings.SENTRY_DSN,
environment="development" if settings.DEBUG else "production",
send_default_pii=True,
traces_sample_rate=1.0 if settings.DEBUG else 0.2,
# Filter out noisy health check transactions
traces_sampler=lambda ctx: (
0.0 if ctx.get("transaction_context", {}).get("name", "").startswith("GET /health") else None
),
)
from app.core.database import init_db, async_session_maker from app.core.database import init_db, async_session_maker
from app.core.logging_config import setup_logging from app.core.logging_config import setup_logging
from app.core.middleware import RequestLoggingMiddleware, ErrorLoggingMiddleware from app.core.middleware import RequestLoggingMiddleware, ErrorLoggingMiddleware

View File

@@ -39,6 +39,9 @@ google-genai>=1.0.0
pgvector>=0.3.6 pgvector>=0.3.6
voyageai>=0.3.0 voyageai>=0.3.0
# Monitoring
sentry-sdk[fastapi]>=2.54.0
# Utilities # Utilities
python-dotenv==1.0.1 python-dotenv==1.0.1
croniter>=2.0.0 croniter>=2.0.0

View File

@@ -1,2 +1,5 @@
# API URL - defaults to http://localhost:8000 if not set # API URL - defaults to http://localhost:8000 if not set
VITE_API_URL=http://localhost:8000 VITE_API_URL=http://localhost:8000
# Sentry error monitoring (optional in dev, required in production)
VITE_SENTRY_DSN=

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,8 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@sentry/react": "^10.42.0",
"@sentry/vite-plugin": "^5.1.1",
"@stripe/stripe-js": "^8.7.0", "@stripe/stripe-js": "^8.7.0",
"@xyflow/react": "^12.10.0", "@xyflow/react": "^12.10.0",
"axios": "^1.13.4", "axios": "^1.13.4",

View File

@@ -0,0 +1,25 @@
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
// Tracing — capture 100% in dev, 20% in production
tracesSampleRate: import.meta.env.PROD ? 0.2 : 1.0,
tracePropagationTargets: [
"localhost",
/^https:\/\/api\.resolutionflow\.com/,
],
// Session Replay — record 10% of sessions, 100% of error sessions
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});

View File

@@ -1,11 +1,18 @@
import "./instrument"; // Sentry must init before any other imports
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { reactErrorHandler } from '@sentry/react'
import { HelmetProvider } from 'react-helmet-async' import { HelmetProvider } from 'react-helmet-async'
import { Toaster } from 'sonner' import { Toaster } from 'sonner'
import './index.css' import './index.css'
import App from './App' import App from './App'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!, {
onUncaughtError: reactErrorHandler(),
onCaughtError: reactErrorHandler(),
onRecoverableError: reactErrorHandler(),
}).render(
<StrictMode> <StrictMode>
<HelmetProvider> <HelmetProvider>
{/* Toast notification system - theme syncs automatically via CSS custom properties */} {/* Toast notification system - theme syncs automatically via CSS custom properties */}

View File

@@ -1,9 +1,12 @@
import { createBrowserRouter } from 'react-router-dom' import { createBrowserRouter } from 'react-router-dom'
import * as Sentry from '@sentry/react'
import { lazy, Suspense } from 'react' import { lazy, Suspense } from 'react'
import { AppLayout, ProtectedRoute } from '@/components/layout' import { AppLayout, ProtectedRoute } from '@/components/layout'
import { RouteError } from '@/components/common/RouteError' import { RouteError } from '@/components/common/RouteError'
import { ErrorBoundary } from '@/components/common/ErrorBoundary' import { ErrorBoundary } from '@/components/common/ErrorBoundary'
import { PageLoader } from '@/components/common/PageLoader' import { PageLoader } from '@/components/common/PageLoader'
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowserRouter)
import { import {
LoginPage, LoginPage,
RegisterPage, RegisterPage,
@@ -73,7 +76,7 @@ function page(Component: React.LazyExoticComponent<React.ComponentType>) {
) )
} }
export const router = createBrowserRouter([ export const router = sentryCreateBrowserRouter([
{ {
path: '/login', path: '/login',
element: <LoginPage />, element: <LoginPage />,

View File

@@ -1,11 +1,20 @@
/// <reference types="vitest/config" /> /// <reference types="vitest/config" />
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { sentryVitePlugin } from '@sentry/vite-plugin'
import path from 'path' import path from 'path'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
react(),
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: !process.env.SENTRY_AUTH_TOKEN, // Don't error in local dev
}),
],
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
watch: { watch: {
@@ -24,6 +33,7 @@ export default defineConfig({
include: ['src/**/*.{test,spec}.{ts,tsx}'], include: ['src/**/*.{test,spec}.{ts,tsx}'],
}, },
build: { build: {
sourcemap: 'hidden', // Generate source maps but don't expose them publicly
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: { manualChunks: {