Error Tracking Context
Add rich context to errors for better debugging with Sentry and other tools
Error Tracking Context
When errors occur in production, context is everything. Rich error context helps you reproduce bugs, understand user impact, and fix issues faster.
Why Context Matters
Without context, you get:
Error: Cannot read property 'name' of undefined
at UserProfile.tsx:42With context, you get:
Error: Cannot read property 'name' of undefined
at UserProfile.tsx:42
User: user_123 (Premium)
Action: Viewing profile
Page: /dashboard/profile
Browser: Chrome 120 on Windows
Network: 4G
Time: 2024-01-09 14:30:25 UTC
Previous actions: Login → Dashboard → Profile
API Response: 404 from /api/users/123Difference: The second tells you exactly what happened and how to reproduce it.
Setting Up Sentry
Installation
npm install @sentry/nextjsConfiguration
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Set tracesSampleRate to 1.0 to capture 100% of transactions
tracesSampleRate: 0.1, // 10% in production
// Set sampling rate for profiling
profilesSampleRate: 0.1,
// Environment
environment: process.env.NODE_ENV,
// Release tracking
release: process.env.NEXT_PUBLIC_APP_VERSION,
// Ignore specific errors
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured',
// Add more patterns
],
// Beforehand filtering
beforeSend(event, hint) {
// Don't send user errors
const error = hint.originalException;
if (error instanceof ValidationError) {
return null;
}
// Add custom logic
return event;
},
});Adding Context
1. User Context
// lib/sentry-context.ts
import * as Sentry from '@sentry/nextjs';
export function setUserContext(user: {
id: string;
email?: string;
username?: string;
subscription?: string;
}) {
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username,
subscription: user.subscription,
});
}
export function clearUserContext() {
Sentry.setUser(null);
}
// Usage in auth flow
function onLogin(user: User) {
setUserContext({
id: user.id,
email: user.email,
username: user.username,
subscription: user.subscription?.tier,
});
}
function onLogout() {
clearUserContext();
}2. Custom Context
// Add any custom context
export function setCustomContext(name: string, data: Record<string, any>) {
Sentry.setContext(name, data);
}
// Usage examples
setCustomContext('shopping_cart', {
itemCount: cart.items.length,
totalValue: cart.total,
currency: 'USD',
});
setCustomContext('experiment', {
variantId: 'B',
testName: 'checkout-flow-v2',
});
setCustomContext('feature_flags', {
newDashboard: true,
betaFeatures: false,
});3. Tags for Filtering
// Add tags for easy filtering in Sentry
export function setTags(tags: Record<string, string>) {
Object.entries(tags).forEach(([key, value]) => {
Sentry.setTag(key, value);
});
}
// Usage
setTags({
page: 'checkout',
user_type: 'premium',
device: 'mobile',
browser: 'chrome',
});4. Breadcrumbs (Action Trail)
// Track user actions leading to the error
export function addBreadcrumb(
category: string,
message: string,
data?: Record<string, any>
) {
Sentry.addBreadcrumb({
category,
message,
level: 'info',
data,
timestamp: Date.now() / 1000,
});
}
// Usage - Track user journey
function trackAction(action: string, details?: any) {
addBreadcrumb('user_action', action, details);
}
// Examples
trackAction('button_click', { button: 'checkout' });
trackAction('form_submit', { form: 'payment' });
trackAction('api_call', { endpoint: '/api/checkout', method: 'POST' });
trackAction('navigation', { from: '/cart', to: '/checkout' });Automatic Context Collection
React Component Context
// components/SentryContextProvider.tsx
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import * as Sentry from '@sentry/nextjs';
export function SentryContextProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
useEffect(() => {
// Track page views
Sentry.addBreadcrumb({
category: 'navigation',
message: `Navigated to ${pathname}`,
level: 'info',
});
// Set page context
Sentry.setContext('page', {
path: pathname,
referrer: document.referrer,
timestamp: new Date().toISOString(),
});
}, [pathname]);
useEffect(() => {
// Browser context
Sentry.setContext('browser', {
userAgent: navigator.userAgent,
language: navigator.language,
online: navigator.onLine,
platform: navigator.platform,
});
// Screen context
Sentry.setContext('screen', {
width: window.screen.width,
height: window.screen.height,
devicePixelRatio: window.devicePixelRatio,
});
// Network context
const connection = (navigator as any).connection;
if (connection) {
Sentry.setContext('network', {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
});
}
}, []);
return <>{children}</>;
}API Call Context
// lib/api-client.ts
export async function apiCall(endpoint: string, options?: RequestInit) {
const startTime = performance.now();
// Add breadcrumb for API call
addBreadcrumb('api', `${options?.method || 'GET'} ${endpoint}`, {
endpoint,
method: options?.method,
});
try {
const response = await fetch(endpoint, options);
const duration = performance.now() - startTime;
// Add successful call context
Sentry.setContext('last_api_call', {
endpoint,
method: options?.method,
status: response.status,
duration,
timestamp: new Date().toISOString(),
});
if (!response.ok) {
throw new APIError(`API call failed: ${response.status}`, {
endpoint,
status: response.status,
duration,
});
}
return response.json();
} catch (error) {
// Add failed call context
const duration = performance.now() - startTime;
Sentry.setContext('failed_api_call', {
endpoint,
method: options?.method,
duration,
error: error.message,
});
throw error;
}
}Advanced Patterns
Performance Context
// Track performance metrics with errors
export function captureWithPerformance(error: Error) {
// Get current page metrics
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
Sentry.setContext('performance', {
// Core Web Vitals
lcp: getLCP(),
fid: getFID(),
cls: getCLS(),
// Page load
domContentLoaded: navigation?.domContentLoadedEventEnd - navigation?.fetchStart,
loadComplete: navigation?.loadEventEnd - navigation?.fetchStart,
// Resources
resourceCount: performance.getEntriesByType('resource').length,
// Memory (Chrome only)
memory: (performance as any).memory?.usedJSHeapSize,
});
Sentry.captureException(error);
}State Context
// Capture application state when error occurs
export function captureWithState(error: Error, state: any) {
Sentry.setContext('app_state', {
// Redux/Zustand state
user: state.user,
cart: state.cart,
ui: state.ui,
// Local state
localStorage: {
theme: localStorage.getItem('theme'),
language: localStorage.getItem('language'),
},
// Session state
sessionStorage: {
sessionId: sessionStorage.getItem('session_id'),
},
});
Sentry.captureException(error);
}Error Grouping
// Custom fingerprinting for better error grouping
Sentry.init({
beforeSend(event, hint) {
const error = hint.originalException;
// Group by error type + affected component
if (error instanceof APIError) {
event.fingerprint = [
'api-error',
error.endpoint,
error.statusCode.toString(),
];
}
// Group validation errors by field
if (error instanceof ValidationError) {
event.fingerprint = ['validation-error', error.field || 'unknown'];
}
return event;
},
});Context Hooks
useErrorContext Hook
// hooks/useErrorContext.ts
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
export function useErrorContext(context: Record<string, any>) {
useEffect(() => {
// Set context when component mounts
Object.entries(context).forEach(([key, value]) => {
Sentry.setContext(key, value);
});
// Clear context when component unmounts
return () => {
Object.keys(context).forEach(key => {
Sentry.setContext(key, null);
});
};
}, [context]);
}
// Usage
function CheckoutPage() {
const cart = useCart();
useErrorContext({
checkout: {
itemCount: cart.items.length,
totalValue: cart.total,
step: 'payment',
},
});
return <CheckoutForm />;
}useErrorBreadcrumbs Hook
// hooks/useErrorBreadcrumbs.ts
import { useEffect } from 'react';
export function useErrorBreadcrumbs(
category: string,
message: string,
data?: Record<string, any>
) {
useEffect(() => {
addBreadcrumb(category, message, data);
}, [category, message, data]);
}
// Usage
function ProductPage({ productId }: Props) {
useErrorBreadcrumbs('page_view', 'Viewed product page', {
productId,
timestamp: new Date().toISOString(),
});
return <Product id={productId} />;
}Filtering Sensitive Data
Remove PII
// sentry.client.config.ts
Sentry.init({
beforeSend(event) {
// Remove sensitive data from breadcrumbs
if (event.breadcrumbs) {
event.breadcrumbs = event.breadcrumbs.map(breadcrumb => {
if (breadcrumb.data) {
// Remove passwords, tokens, etc.
delete breadcrumb.data.password;
delete breadcrumb.data.token;
delete breadcrumb.data.creditCard;
}
return breadcrumb;
});
}
// Remove sensitive headers
if (event.request?.headers) {
delete event.request.headers['Authorization'];
delete event.request.headers['Cookie'];
}
return event;
},
});Data Scrubbing Rules
// Automatically scrub patterns
Sentry.init({
beforeSend(event) {
// Scrub email addresses
const emailRegex = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
if (event.message) {
event.message = event.message.replace(emailRegex, '[EMAIL]');
}
// Scrub credit card numbers
const ccRegex = /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g;
if (event.message) {
event.message = event.message.replace(ccRegex, '[CC]');
}
return event;
},
});Debugging in Development
// components/DevErrorConsole.tsx
'use client';
import { useEffect, useState } from 'react';
import * as Sentry from '@sentry/nextjs';
export function DevErrorConsole() {
const [errors, setErrors] = useState<any[]>([]);
useEffect(() => {
if (process.env.NODE_ENV !== 'development') return;
// Intercept Sentry calls in dev
const originalCaptureException = Sentry.captureException;
Sentry.captureException = function(...args) {
const [error] = args;
setErrors(prev => [...prev, {
error,
context: Sentry.getCurrentHub().getScope(),
timestamp: new Date(),
}]);
return originalCaptureException.apply(this, args);
};
}, []);
if (process.env.NODE_ENV !== 'development') return null;
return (
<div className="dev-error-console">
<h3>Errors ({errors.length})</h3>
{errors.map((err, i) => (
<div key={i} className="error-entry">
<pre>{JSON.stringify(err, null, 2)}</pre>
</div>
))}
</div>
);
}Best Practices
- Set User Context Early: As soon as user logs in
- Track User Journey: Use breadcrumbs for action trail
- Add Performance Data: Include metrics with errors
- Remove PII: Scrub sensitive data before sending
- Use Tags for Filtering: Make errors easy to find
- Custom Fingerprints: Group similar errors together
- Test in Dev: Verify context is captured correctly
Common Mistakes
❌ Too much context: Sending entire app state
✅ Relevant context only
❌ Logging sensitive data: PII, passwords, tokens
✅ Scrub before sending
❌ Generic tags: "error", "frontend"
✅ Specific tags: "checkout-step-2", "api-timeout"
❌ No breadcrumbs: Can't reproduce the issue
✅ Track user actions
Context Checklist
When an error occurs, capture:
- User ID and tier
- Current page/route
- Previous navigation (breadcrumbs)
- API calls made
- Browser and device info
- Network conditions
- Feature flags state
- Performance metrics
- User actions leading to error
- Application state (if relevant)
Rich context transforms "impossible to debug" errors into "fixed in 10 minutes" issues.