feat: Sentry error monitoring for React frontend #98
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
561
frontend/package-lock.json
generated
561
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
25
frontend/src/instrument.ts
Normal file
25
frontend/src/instrument.ts
Normal 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,
|
||||||
|
});
|
||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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 />,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user