Error Handling and Monitoring
Error Handling and Monitoring
Section titled “Error Handling and Monitoring”Problem and Rationale
Section titled “Problem and Rationale”RednGift requires comprehensive error tracking to:
- Detect production issues: Unhandled exceptions, API failures, render errors
- Debug efficiently: Stack traces with source maps and user context
- Monitor performance: Slow API calls, large bundle sizes, navigation timing
- Improve UX: Graceful error recovery and user-friendly error messages
Context and Constraints
Section titled “Context and Constraints”- Client-side errors: JavaScript exceptions, React errors, network failures
- Server-side errors: Next.js SSR errors, API route errors
- Performance: Track slow page loads and API response times
- Privacy: Avoid logging sensitive user data (passwords, tokens)
- Cost: Manage Sentry event volume to stay within quota
Why Sentry?
Section titled “Why Sentry?”- Full-stack: Tracks client and server errors in one platform
- Source maps: Deobfuscates minified production stack traces
- Context enrichment: Attaches user, browser, and request metadata
- Performance monitoring: Tracks transaction duration and resource loading
- Integrations: Next.js, React, Redux, Axios plugins
Design
Section titled “Design”Sentry Configuration
Section titled “Sentry Configuration”Client-side Setup
Section titled “Client-side Setup”import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0, // 100% of transactions for performance monitoring
integrations: [ new Sentry.BrowserTracing({ tracePropagationTargets: [ 'localhost', /^https:\/\/redngift\.com/, /^https:\/\/api\.redngift\.com/, ], }), new Sentry.Replay({ maskAllText: true, // Mask user-entered text for privacy blockAllMedia: true, // Don't capture images/videos }), ],
// Sample rate for session replay (10% of sessions) replaysSessionSampleRate: 0.1,
// Capture 100% of sessions with errors for replay replaysOnErrorSampleRate: 1.0,
// Filter out known unactionable errors beforeSend(event, hint) { const error = hint.originalException;
// Ignore specific errors if (error?.message?.includes('ResizeObserver loop')) { return null; }
// Scrub sensitive data from event if (event.request?.headers?.Authorization) { delete event.request.headers.Authorization; }
return event; },});Server-side Setup
Section titled “Server-side Setup”import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0,
integrations: [ new Sentry.Integrations.Http({ tracing: true }), ],});Edge Runtime Setup
Section titled “Edge Runtime Setup”import * as Sentry from '@sentry/nextjs';
Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, tracesSampleRate: 1.0,});Next.js Integration
Section titled “Next.js Integration”// next.config.js (excerpt)const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig( nextConfig, { silent: true, // Suppress upload logs org: 'rednblue', project: 'redngift-frontend', }, { widenClientFileUpload: true, // Upload more source maps transpileClientSDK: true, // Support older browsers tunnelRoute: '/monitoring', // Bypass ad-blockers hideSourceMaps: true, // Don't expose maps in prod disableLogger: true, // Remove console.logs in prod });Error Boundaries
Section titled “Error Boundaries”'use client';
import { Component, ReactNode } from 'react';import * as Sentry from '@sentry/nextjs';import ErrorView from './error-view';
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) { // Log to Sentry Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, }, }, }); }
render() { if (this.state.hasError) { return this.props.fallback || ( <ErrorView error={this.state.error} reset={() => this.setState({ hasError: false, error: null })} /> ); }
return this.props.children; }}Usage:
// Wrap app or specific sections<ErrorBoundary fallback={<ErrorPage />}> <App /></ErrorBoundary>User Context Enrichment
Section titled “User Context Enrichment”// Set user context after loginimport * as Sentry from '@sentry/nextjs';
const login = async (token: string) => { const user = await userHandlers.userInfo();
// Enrich Sentry events with user info Sentry.setUser({ id: user.id.toString(), email: user.email, username: user.name, });
// Set custom context Sentry.setContext('subscription', { plan: user.subscriptionPlan || 'free', expiresAt: user.subscriptionExpiresAt, });};
// Clear user context on logoutconst logout = async () => { Sentry.setUser(null); // ... logout logic};Error Tracking in Handlers
Section titled “Error Tracking in Handlers”import * as Sentry from '@sentry/nextjs';
const createNewGroup = async (data: NewGroupData) => { try { const res = await postRequest('groups', data); return res.data; } catch (error) { console.error('Error creating new group', error);
// Capture with additional context Sentry.captureException(error, { tags: { action: 'createGroup', domain: 'groups', }, extra: { groupData: data, endpoint: 'POST /groups', }, level: 'error', });
throw error; // Re-throw for component handling }};Lifecycle and Data Flow
Section titled “Lifecycle and Data Flow”1. Unhandled Exception
Section titled “1. Unhandled Exception”Component throws error │ ▼ErrorBoundary.componentDidCatch() │ ├─ Log to console ─────────────┐ │ │ │ ▼ │ Sentry.captureException(error) │ │ │ ▼ │ Enrich with context: │ - User info │ - Browser info │ - Component stack │ - Redux state (if configured) │ │ │ ▼ │ Send to Sentry server │ └─ Render fallback UI ─────────┐ │ ▼ Show error message to user │ ▼ Offer "Try Again" button2. API Error
Section titled “2. API Error”API call fails (e.g., 500 error) │ ▼Handler catch block │ ├─ Log error ──────────────────┐ │ │ │ ▼ │ Sentry.captureException(error) │ │ │ ▼ │ Attach tags: │ - action: 'createGroup' │ - domain: 'groups' │ │ │ ▼ │ Attach extra data: │ - Request payload │ - Endpoint URL │ │ │ ▼ │ Send to Sentry │ └─ Throw error ────────────────┐ │ ▼ Component catch block │ ▼ Show toast notification3. Performance Transaction
Section titled “3. Performance Transaction”User navigates to /group/5 │ ▼Sentry.startTransaction({ name: 'GET /group/5' }) │ ├─ Fetch group data ───────────┐ │ │ │ ▼ │ Span: 'API Call' │ Duration: 250ms │ ├─ Render components ──────────┐ │ │ │ ▼ │ Span: 'React Render' │ Duration: 50ms │ └─ Transaction complete ───────┐ │ ▼ Total duration: 300ms │ ▼ Send to Sentry PerformanceImplementation Notes
Section titled “Implementation Notes”Patterns
Section titled “Patterns”✅ DO:
- Set user context after authentication
- Capture exceptions in all async handlers
- Add tags and extra data for debugging context
- Use error boundaries for React component errors
- Filter out low-value errors with
beforeSend
❌ DON’T:
- Don’t log sensitive data (passwords, tokens, PII)
- Don’t capture every error (filter out known browser quirks)
- Don’t block user flow for error reporting
- Don’t rely solely on Sentry; also log critical errors server-side
Error Severity Levels
Section titled “Error Severity Levels”Sentry.captureException(error, { level: 'error' }); // DefaultSentry.captureException(error, { level: 'warning' }); // Non-criticalSentry.captureException(error, { level: 'info' }); // InformationalSentry.captureException(error, { level: 'fatal' }); // Critical failureManual Error Capture
Section titled “Manual Error Capture”// Capture message without exceptionSentry.captureMessage('User attempted invalid action', 'warning');
// Capture with contextSentry.withScope((scope) => { scope.setTag('feature', 'raffle'); scope.setExtra('groupId', groupId); scope.setLevel('error'); Sentry.captureMessage('Raffle generation failed');});Breadcrumbs (Event Trail)
Section titled “Breadcrumbs (Event Trail)”Sentry automatically captures breadcrumbs (navigation, console logs, network requests). Add custom breadcrumbs:
Sentry.addBreadcrumb({ category: 'user-action', message: 'User clicked "Generate Raffle" button', level: 'info', data: { groupId: 5, memberCount: 10, },});Performance Monitoring
Section titled “Performance Monitoring”import * as Sentry from '@sentry/nextjs';
// Manual transactionconst transaction = Sentry.startTransaction({ op: 'task', name: 'Process Large Dataset',});
const span = transaction.startChild({ op: 'db', description: 'Fetch user groups',});
await groupsHandlers.getUserGroups();
span.finish();transaction.finish();Source Maps Upload
Section titled “Source Maps Upload”Sentry webpack plugin automatically uploads source maps during build:
yarn build# Source maps uploaded to Sentry# Production bundle excludes maps (hideSourceMaps: true)Verify:
- Trigger error in production
- Check Sentry issue → “Source Code” tab should show original TypeScript
Error Filtering
Section titled “Error Filtering”Reduce noise by filtering out benign errors:
beforeSend(event, hint) { const error = hint.originalException;
// Ignore browser extension errors if (error?.stack?.includes('chrome-extension://')) { return null; }
// Ignore ResizeObserver loop errors (harmless) if (error?.message?.includes('ResizeObserver loop')) { return null; }
// Ignore network errors (handled by toast) if (error?.code === 'ECONNABORTED') { return null; }
return event;}Privacy Considerations
Section titled “Privacy Considerations”Scrub sensitive data:
beforeSend(event) { // Remove auth headers if (event.request?.headers?.Authorization) { delete event.request.headers.Authorization; }
// Redact email addresses if (event.user?.email) { event.user.email = event.user.email.replace(/(.{2})(.*)(@.*)/, '$1***$3'); }
// Remove sensitive form data if (event.request?.data?.password) { event.request.data.password = '[REDACTED]'; }
return event;}Testing Strategy
Section titled “Testing Strategy”Unit Tests
Section titled “Unit Tests”import * as Sentry from '@sentry/nextjs';
jest.mock('@sentry/nextjs');
it('should capture exception on error', async () => { const mockError = new Error('Test error'); jest.spyOn(httpClient, 'postRequest').mockRejectedValue(mockError);
await expect(groupsHandlers.createNewGroup(data)).rejects.toThrow();
expect(Sentry.captureException).toHaveBeenCalledWith(mockError, { tags: { action: 'createGroup', domain: 'groups' }, extra: expect.any(Object), });});Integration Tests
Section titled “Integration Tests”it('should set user context after login', async () => { const mockUser = { id: 1, email: 'test@example.com' }; jest.spyOn(userHandlers, 'userInfo').mockResolvedValue(mockUser);
await login('magic-token');
expect(Sentry.setUser).toHaveBeenCalledWith({ id: '1', email: 'test@example.com', username: mockUser.name, });});E2E Tests
Section titled “E2E Tests”- Trigger React error → verify error boundary renders
- Trigger API error → verify toast notification shows
- Check Sentry dashboard for test events (use separate test project)
Extending This Concept
Section titled “Extending This Concept”Adding Custom Integrations
Section titled “Adding Custom Integrations”Track Redux state changes:
import * as Sentry from '@sentry/nextjs';import { createReduxEnhancer } from '@sentry/react';
const sentryReduxEnhancer = createReduxEnhancer({ stateTransformer: (state) => ({ // Only send relevant slices groups: state.groups, wishlists: state.wishlists, }),});
const store = configureStore({ reducer: rootReducer, enhancers: [sentryReduxEnhancer],});Adding Performance Budgets
Section titled “Adding Performance Budgets”Alert when bundle size exceeds threshold:
module.exports = { webpack(config) { config.performance = { maxAssetSize: 500000, // 500 KB per asset maxEntrypointSize: 1000000, // 1 MB total entry hints: 'error', // Fail build if exceeded }; return config; },};Adding User Feedback Widget
Section titled “Adding User Feedback Widget”Let users report issues:
import * as Sentry from '@sentry/nextjs';
const showReportDialog = () => { Sentry.showReportDialog({ eventId: Sentry.lastEventId(), title: 'Something went wrong', subtitle: 'Our team has been notified. Please describe what happened:', user: { email: currentUser.email, name: currentUser.name, }, });};
// Show after error boundary catches error<ErrorBoundary fallback={(error) => ( <div> <h1>Oops! Something went wrong.</h1> <button onClick={showReportDialog}>Report Issue</button> </div> )}/>Adding Release Tracking
Section titled “Adding Release Tracking”Track which release introduced errors:
# In CI/CD pipelineSENTRY_RELEASE=$(sentry-cli releases propose-version)
# Create releasesentry-cli releases new $SENTRY_RELEASE
# Associate commitssentry-cli releases set-commits $SENTRY_RELEASE --auto
# Finalize releasesentry-cli releases finalize $SENTRY_RELEASENext.js Sentry plugin does this automatically if configured.
Further Reading
Section titled “Further Reading”- API Layer - Error handling in API handlers
- Authentication - User context enrichment
- State Management - Redux error states
- Real-time Communication - WebSocket error tracking