Skip to content

Error Handling and Monitoring

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
  • 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
  • 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
sentry.client.config.ts
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;
},
});
sentry.server.config.ts
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 }),
],
});
sentry.edge.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
});
// 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
}
);
src/components/error-boundary/error-boundary.tsx
'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>
// Set user context after login
import * 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 logout
const logout = async () => {
Sentry.setUser(null);
// ... logout logic
};
src/services/handlers/groups/groupsHandlers.ts
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
}
};
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" button
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 notification
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 Performance

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
Sentry.captureException(error, { level: 'error' }); // Default
Sentry.captureException(error, { level: 'warning' }); // Non-critical
Sentry.captureException(error, { level: 'info' }); // Informational
Sentry.captureException(error, { level: 'fatal' }); // Critical failure
// Capture message without exception
Sentry.captureMessage('User attempted invalid action', 'warning');
// Capture with context
Sentry.withScope((scope) => {
scope.setTag('feature', 'raffle');
scope.setExtra('groupId', groupId);
scope.setLevel('error');
Sentry.captureMessage('Raffle generation failed');
});

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,
},
});
import * as Sentry from '@sentry/nextjs';
// Manual transaction
const 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();

Sentry webpack plugin automatically uploads source maps during build:

Terminal window
yarn build
# Source maps uploaded to Sentry
# Production bundle excludes maps (hideSourceMaps: true)

Verify:

  1. Trigger error in production
  2. Check Sentry issue → “Source Code” tab should show original TypeScript

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;
}

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;
}
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),
});
});
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,
});
});
  • Trigger React error → verify error boundary renders
  • Trigger API error → verify toast notification shows
  • Check Sentry dashboard for test events (use separate test project)

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],
});

Alert when bundle size exceeds threshold:

next.config.js
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;
},
};

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>
)}
/>

Track which release introduced errors:

Terminal window
# In CI/CD pipeline
SENTRY_RELEASE=$(sentry-cli releases propose-version)
# Create release
sentry-cli releases new $SENTRY_RELEASE
# Associate commits
sentry-cli releases set-commits $SENTRY_RELEASE --auto
# Finalize release
sentry-cli releases finalize $SENTRY_RELEASE

Next.js Sentry plugin does this automatically if configured.