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 fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
import sentry_sdk
from app.core.database import get_db from app.core.database import get_db
from app.core.security import decode_token from app.core.security import decode_token
@@ -92,6 +93,9 @@ async def get_current_active_user(
detail="password_change_required" 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 # Lightweight trial expiry check
if current_user.account_id: if current_user.account_id:
from app.models.subscription import Subscription from app.models.subscription import Subscription

View File

@@ -17,6 +17,8 @@ if settings.SENTRY_DSN:
environment="development" if settings.DEBUG else "production", environment="development" if settings.DEBUG else "production",
send_default_pii=True, send_default_pii=True,
traces_sample_rate=1.0 if settings.DEBUG else 0.2, 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 # Filter out noisy health check transactions
traces_sampler=lambda ctx: ( traces_sampler=lambda ctx: (
0.0 if ctx.get("transaction_context", {}).get("name", "").startswith("GET /health") else None 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' 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 { interface Props {
children: ReactNode children: ReactNode
fallback?: ReactNode fallback?: ReactNode
} }
interface State { export function ErrorBoundary({ children, fallback }: Props) {
hasError: boolean return (
error: Error | null <Sentry.ErrorBoundary
} fallback={({ error, resetError }) => {
if (fallback) return fallback as React.ReactElement
export class ErrorBoundary extends Component<Props, State> { return <DefaultFallback error={error as Error} resetError={resetError} />
constructor(props: Props) { }}
super(props) showDialog
this.state = { hasError: false, error: null } >
} {children}
</Sentry.ErrorBoundary>
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 default ErrorBoundary export default ErrorBoundary

View File

@@ -1,5 +1,6 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useRouteError, isRouteErrorResponse, useNavigate } from 'react-router-dom' import { useRouteError, isRouteErrorResponse, useNavigate } from 'react-router-dom'
import * as Sentry from '@sentry/react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
function isChunkLoadError(error: unknown): boolean { function isChunkLoadError(error: unknown): boolean {
@@ -19,6 +20,13 @@ export function RouteError() {
const error = useRouteError() const error = useRouteError()
const navigate = useNavigate() 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) // Auto-reload once on chunk load failures (stale deploy)
useEffect(() => { useEffect(() => {
if (isChunkLoadError(error)) { if (isChunkLoadError(error)) {

View File

@@ -10,6 +10,11 @@ Sentry.init({
maskAllText: false, maskAllText: false,
blockAllMedia: 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 // Tracing — capture 100% in dev, 20% in production
@@ -19,7 +24,13 @@ Sentry.init({
/^https:\/\/api\.resolutionflow\.com/, /^https:\/\/api\.resolutionflow\.com/,
], ],
// Session Replay — record 10% of sessions, 100% of error sessions // Session Replay — conserve free-plan quota
replaysSessionSampleRate: 0.1, // 1% of normal sessions, 100% of error sessions
replaysSessionSampleRate: import.meta.env.PROD ? 0.01 : 0.0,
replaysOnErrorSampleRate: 1.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 { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import * as Sentry from '@sentry/react'
import type { User, Token, UserCreate, UserLogin, Account, SubscriptionDetails } from '@/types' import type { User, Token, UserCreate, UserLogin, Account, SubscriptionDetails } from '@/types'
import { authApi } from '@/api/auth' import { authApi } from '@/api/auth'
import { apiClient } from '@/api/client' import { apiClient } from '@/api/client'
@@ -81,6 +82,7 @@ export const useAuthStore = create<AuthState>()(
localStorage.removeItem('access_token') localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token') localStorage.removeItem('refresh_token')
clearCachedQuota() clearCachedQuota()
Sentry.setUser(null)
set({ user: null, token: null, account: null, subscription: null, isAuthenticated: false, error: 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 throw reason
} }
// Set Sentry user context for error attribution
Sentry.setUser({ id: user.id, email: user.email })
set({ user, account, subscription, isLoading: false }) set({ user, account, subscription, isLoading: false })
} catch (error: unknown) { } catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Failed to fetch user' const message = error instanceof Error ? error.message : 'Failed to fetch user'