feat: maximize Sentry free plan coverage for frontend and backend

- ErrorBoundary: use Sentry.ErrorBoundary with crash feedback dialog
- RouteError: capture route errors in Sentry (skip chunk load errors)
- User context: set Sentry user on login (frontend + backend)
- Backend: enable profiling (profiles_sample_rate)
- Frontend: add feedback integration, lower replay rate to conserve quota
- Add temporary verification message for production validation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-09 00:03:29 -04:00
parent 2a2894496d
commit 4d2f644bac
6 changed files with 77 additions and 52 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 (
<div className="flex min-h-[400px] flex-col items-center justify-center p-8">
<div className="max-w-md text-center">
<h2 className="mb-2 text-xl font-semibold text-red-400">
Something went wrong
</h2>
<p className="mb-4 text-muted-foreground">
An unexpected error occurred. Please try refreshing the page.
</p>
<pre className="mb-4 overflow-auto rounded-xl bg-white/5 border border-border p-3 text-left text-xs text-red-400">
{error.message}
</pre>
<div className="flex justify-center gap-3">
<Button variant="secondary" onClick={resetError}>
Try Again
</Button>
<Button onClick={() => window.location.reload()}>
Refresh Page
</Button>
</div>
</div>
</div>
)
}
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
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 (
<div className="flex min-h-[400px] flex-col items-center justify-center p-8">
<div className="max-w-md text-center">
<h2 className="mb-2 text-xl font-semibold text-red-400">
Something went wrong
</h2>
<p className="mb-4 text-muted-foreground">
An unexpected error occurred. Please try refreshing the page.
</p>
{this.state.error && (
<pre className="mb-4 overflow-auto rounded-xl bg-white/5 border border-border p-3 text-left text-xs text-red-400">
{this.state.error.message}
</pre>
)}
<Button onClick={() => window.location.reload()}>
Refresh Page
</Button>
</div>
</div>
)
}
return this.props.children
}
export function ErrorBoundary({ children, fallback }: Props) {
return (
<Sentry.ErrorBoundary
fallback={({ error, resetError }) => {
if (fallback) return fallback as React.ReactElement
return <DefaultFallback error={error as Error} resetError={resetError} />
}}
showDialog
>
{children}
</Sentry.ErrorBoundary>
)
}
export default ErrorBoundary

View File

@@ -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)) {

View File

@@ -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");
}

View File

@@ -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<AuthState>()(
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<AuthState>()(
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'