Front-end Engineering Lab

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:42

With 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/123

Difference: The second tells you exactly what happened and how to reproduce it.

Setting Up Sentry

Installation

npm install @sentry/nextjs

Configuration

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

  1. Set User Context Early: As soon as user logs in
  2. Track User Journey: Use breadcrumbs for action trail
  3. Add Performance Data: Include metrics with errors
  4. Remove PII: Scrub sensitive data before sending
  5. Use Tags for Filtering: Make errors easy to find
  6. Custom Fingerprints: Group similar errors together
  7. 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.

On this page