diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py
index 4cdd8a94..f17858de 100644
--- a/backend/app/api/deps.py
+++ b/backend/app/api/deps.py
@@ -4,6 +4,7 @@ from fastapi import Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
+import sentry_sdk
from app.core.database import get_db
from app.core.security import decode_token
@@ -92,6 +93,9 @@ async def get_current_active_user(
detail="password_change_required"
)
+ # Set Sentry user context for error attribution
+ sentry_sdk.set_user({"id": str(current_user.id), "email": current_user.email})
+
# Lightweight trial expiry check
if current_user.account_id:
from app.models.subscription import Subscription
diff --git a/backend/app/main.py b/backend/app/main.py
index ccf3feb8..61e6a4e7 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -17,6 +17,8 @@ if 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,
+ # Profiling — included in free plan
+ profiles_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
diff --git a/frontend/src/components/common/ErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary.tsx
index e2f4a371..7d9b327d 100644
--- a/frontend/src/components/common/ErrorBoundary.tsx
+++ b/frontend/src/components/common/ErrorBoundary.tsx
@@ -1,60 +1,55 @@
-import { Component, type ReactNode } from 'react'
+import * as Sentry from '@sentry/react'
+import { type ReactNode } from 'react'
import { Button } from '@/components/ui/Button'
+interface FallbackProps {
+ error: Error
+ resetError: () => void
+}
+
+function DefaultFallback({ error, resetError }: FallbackProps) {
+ return (
+
+
+
+ Something went wrong
+
+
+ An unexpected error occurred. Please try refreshing the page.
+
+
+ {error.message}
+
+
+
+
+
+
+
+ )
+}
+
interface Props {
children: ReactNode
fallback?: ReactNode
}
-interface State {
- hasError: boolean
- error: Error | null
-}
-
-export class ErrorBoundary extends Component {
- constructor(props: Props) {
- super(props)
- this.state = { hasError: false, error: null }
- }
-
- static getDerivedStateFromError(error: Error): State {
- return { hasError: true, error }
- }
-
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
- console.error('ErrorBoundary caught an error:', error, errorInfo)
- }
-
- render() {
- if (this.state.hasError) {
- if (this.props.fallback) {
- return this.props.fallback
- }
-
- return (
-
-
-
- Something went wrong
-
-
- An unexpected error occurred. Please try refreshing the page.
-
- {this.state.error && (
-
- {this.state.error.message}
-
- )}
-
-
-
- )
- }
-
- return this.props.children
- }
+export function ErrorBoundary({ children, fallback }: Props) {
+ return (
+ {
+ if (fallback) return fallback as React.ReactElement
+ return
+ }}
+ showDialog
+ >
+ {children}
+
+ )
}
export default ErrorBoundary
diff --git a/frontend/src/components/common/RouteError.tsx b/frontend/src/components/common/RouteError.tsx
index 01931aea..45ba1d71 100644
--- a/frontend/src/components/common/RouteError.tsx
+++ b/frontend/src/components/common/RouteError.tsx
@@ -1,5 +1,6 @@
import { useEffect } from 'react'
import { useRouteError, isRouteErrorResponse, useNavigate } from 'react-router-dom'
+import * as Sentry from '@sentry/react'
import { Button } from '@/components/ui/Button'
function isChunkLoadError(error: unknown): boolean {
@@ -19,6 +20,13 @@ export function RouteError() {
const error = useRouteError()
const navigate = useNavigate()
+ // Report route errors to Sentry (skip chunk load errors — those are deploy artifacts)
+ useEffect(() => {
+ if (error && !isChunkLoadError(error)) {
+ Sentry.captureException(error)
+ }
+ }, [error])
+
// Auto-reload once on chunk load failures (stale deploy)
useEffect(() => {
if (isChunkLoadError(error)) {
diff --git a/frontend/src/instrument.ts b/frontend/src/instrument.ts
index 0e484d23..7fed0cc9 100644
--- a/frontend/src/instrument.ts
+++ b/frontend/src/instrument.ts
@@ -10,6 +10,11 @@ Sentry.init({
maskAllText: false,
blockAllMedia: false,
}),
+ // Crash feedback dialog — prompts users after unhandled errors
+ Sentry.feedbackIntegration({
+ autoInject: false,
+ colorScheme: "dark",
+ }),
],
// Tracing — capture 100% in dev, 20% in production
@@ -19,7 +24,13 @@ Sentry.init({
/^https:\/\/api\.resolutionflow\.com/,
],
- // Session Replay — record 10% of sessions, 100% of error sessions
- replaysSessionSampleRate: 0.1,
+ // Session Replay — conserve free-plan quota
+ // 1% of normal sessions, 100% of error sessions
+ replaysSessionSampleRate: import.meta.env.PROD ? 0.01 : 0.0,
replaysOnErrorSampleRate: 1.0,
});
+
+// TODO: Remove after verifying Sentry is receiving frontend events
+if (import.meta.env.PROD) {
+ Sentry.captureMessage("ResolutionFlow frontend Sentry verification", "info");
+}
diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts
index a883f6d8..8ada3ccc 100644
--- a/frontend/src/store/authStore.ts
+++ b/frontend/src/store/authStore.ts
@@ -1,5 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
+import * as Sentry from '@sentry/react'
import type { User, Token, UserCreate, UserLogin, Account, SubscriptionDetails } from '@/types'
import { authApi } from '@/api/auth'
import { apiClient } from '@/api/client'
@@ -81,6 +82,7 @@ export const useAuthStore = create()(
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
clearCachedQuota()
+ Sentry.setUser(null)
set({ user: null, token: null, account: null, subscription: null, isAuthenticated: false, error: null })
}
},
@@ -104,6 +106,9 @@ export const useAuthStore = create()(
throw reason
}
+ // Set Sentry user context for error attribution
+ Sentry.setUser({ id: user.id, email: user.email })
+
set({ user, account, subscription, isLoading: false })
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Failed to fetch user'