Skip to content
All essays
WebMarch 27, 202620 min

Frontend Monitoring: Complete Guide to Application Observability

Master frontend monitoring and gain complete visibility into your application performance and user experience

Ü
Ümit Uz
Mobile & Full Stack Developer

Frontend monitoring provides critical insights into how your application performs in real-world conditions. Unlike backend monitoring, frontend monitoring captures the actual user experience, including device performance, network conditions, and browser compatibility. This comprehensive guide covers everything you need to implement production-grade frontend monitoring.

Why Frontend Monitoring Matters

Frontend issues directly impact user experience and business metrics. Studies show that:

  • 53% of mobile users abandon sites that take longer than 3 seconds to load
  • A 100ms increase in load time decreases conversion rates by 1%
  • Frontend errors account for 76% of all user-reported issues

Frontend monitoring helps you:

  • Detect performance regressions before users notice
  • Identify browser and device-specific issues
  • Track real-user performance metrics
  • Debug JavaScript errors in production
  • Monitor API performance from the client perspective

Core Monitoring Metrics

Track these essential metrics to understand frontend health.

Core Web Vitals

Google's Core Web Vitals measure real-world user experience:

typescript
// Measure Largest Contentful Paint (LCP)
import { getLCP } from 'web-vitals';

getLCP((metric) => {
  console.log('LCP:', metric.value);
  // Send to monitoring service
  trackMetric('lcp', metric.value);
});

// Measure First Input Delay (FID)
import { getFID } from 'web-vitals';

getFID((metric) => {
  console.log('FID:', metric.value);
  trackMetric('fid', metric.value);
});

// Measure Cumulative Layout Shift (CLS)
import { getCLS } from 'web-vitals';

getCLS((metric) => {
  console.log('CLS:', metric.value);
  trackMetric('cls', metric.value);
});

Custom Performance Metrics

Track domain-specific metrics:

typescript
// Time to Interactive
function measureTTI() {
  const startTime = performance.now();
  
  // Wait until page is interactive
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.name === 'first-contentful-paint') {
        const tti = performance.now() - startTime;
        trackMetric('tti', tti);
      }
    }
  });
  
  observer.observe({ entryTypes: ['paint'] });
}

// Component Render Time
function measureComponentRender(componentName: string) {
  const start = performance.now();
  
  return () => {
    const duration = performance.now() - start;
    trackMetric('component_render_time', duration, {
      component: componentName
    });
  };
}

// API Response Time
async function trackAPICall(url: string) {
  const start = performance.now();
  
  try {
    const response = await fetch(url);
    const duration = performance.now() - start;
    
    trackMetric('api_response_time', duration, {
      url,
      status: response.status
    });
    
    return response;
  } catch (error) {
    const duration = performance.now() - start;
    
    trackMetric('api_error', duration, {
      url,
      error: error.message
    });
    
    throw error;
  }
}

Error Monitoring

Capture and track JavaScript errors effectively.

Global Error Handler

typescript
// error-handler.ts
interface ErrorContext {
  userAgent: string;
  url: string;
  timestamp: number;
  userId?: string;
  sessionId: string;
}

class ErrorHandler {
  private sessionId: string;

  constructor() {
    this.sessionId = crypto.randomUUID();
    this.setupGlobalHandlers();
  }

  private setupGlobalHandlers() {
    // Catch unhandled errors
    window.addEventListener('error', (event) => {
      this.captureError(event.error, {
        type: 'unhandled_error',
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno
      });
    });

    // Catch unhandled promise rejections
    window.addEventListener('unhandledrejection', (event) => {
      this.captureError(event.reason, {
        type: 'unhandled_rejection'
      });
    });

    // Catch resource loading errors
    window.addEventListener('error', (event) => {
      if (event.target !== window) {
        this.captureError(new Error('Resource load failed'), {
          type: 'resource_error',
          tagName: (event.target as any).tagName,
          src: (event.target as any).src
        });
      }
    }, true);
  }

  captureError(error: Error, additionalContext?: any) {
    const context: ErrorContext = {
      userAgent: navigator.userAgent,
      url: window.location.href,
      timestamp: Date.now(),
      sessionId: this.sessionId
    };

    // Send to monitoring service
    this.sendToMonitoring({
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      },
      context,
      additionalContext
    });
  }

  private sendToMonitoring(data: any) {
    // Send to your monitoring service
    fetch('/api/errors', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    }).catch(console.error);
  }
}

export const errorHandler = new ErrorHandler();

React Error Boundary

typescript
// ErrorBoundary.tsx
import React from 'react';

interface Props {
  children: React.ReactNode;
  fallback?: React.ComponentType<{ error: Error }>;
}

interface State {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Capture error with React component stack
    errorHandler.captureError(error, {
      componentStack: errorInfo.componentStack,
      errorBoundary: true
    });
  }

  render() {
    if (this.state.hasError) {
      const FallbackComponent = this.props.fallback || DefaultErrorFallback;
      return <FallbackComponent error={this.state.error!} />;
    }

    return this.props.children;
  }
}

function DefaultErrorFallback({ error }: { error: Error }) {
  return (
    <div style={{ padding: '20px', backgroundColor: '#fee' }}>
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
      <button onClick={() => window.location.reload()}>
        Reload Page
      </button>
    </div>
  );
}

Performance Monitoring

Monitor and analyze application performance.

Performance Observer API

typescript
// performance-monitor.ts
class PerformanceMonitor {
  constructor() {
    this.setupObservers();
  }

  private setupObservers() {
    // Monitor long tasks
    if ('PerformanceObserver' in window) {
      const longTaskObserver = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.duration > 50) {
            trackMetric('long_task', entry.duration, {
              name: entry.name,
              startTime: entry.startTime
            });
          }
        }
      });

      longTaskObserver.observe({ entryTypes: ['longtask'] });

      // Monitor resource timing
      const resourceObserver = new PerformanceObserver((list) => {
        for (const entry of list.getEntries() as PerformanceResourceTiming[]) {
          trackMetric('resource_timing', entry.duration, {
            name: entry.name,
            type: entry.initiatorType,
            size: entry.transferSize
          });
        }
      });

      resourceObserver.observe({ entryTypes: ['resource'] });
    }
  }

  measureRender(componentName: string) {
    const start = performance.now();
    
    return () => {
      const duration = performance.now() - start;
      trackMetric('render_time', duration, {
        component: componentName
      });
    };
  }
}

export const performanceMonitor = new PerformanceMonitor();

User Timing API

typescript
// Measure custom operations
function measureOperation(name: string, operation: () => void) {
  performance.mark(`${name}-start`);
  
  operation();
  
  performance.mark(`${name}-end`);
  performance.measure(name, `${name}-start`, `${name}-end`);
  
  const measure = performance.getEntriesByName(name)[0];
  trackMetric('custom_operation', measure.duration, {
    name
  });
}

// Usage
measureOperation('data_processing', () => {
  // Heavy computation
  processLargeDataset(data);
});

Real User Monitoring (RUM)

Collect real user performance data from production.

RUM Implementation

typescript
// rum-collector.ts
interface RUMData {
  sessionId: string;
  userId?: string;
  pageLoad: {
    navigationTiming: PerformanceNavigationTiming;
    resourceTimings: PerformanceResourceTiming[];
  };
  webVitals: {
    lcp?: number;
    fid?: number;
    cls?: number;
  };
  customMetrics: Array<{
    name: string;
    value: number;
    timestamp: number;
  }>;
}

class RUMCollector {
  private sessionId: string;
  private data: Partial<RUMData> = {
    sessionId: crypto.randomUUID(),
    customMetrics: []
  };

  constructor() {
    this.sessionId = crypto.randomUUID();
    this.collectNavigationTiming();
    this.collectWebVitals();
  }

  private collectNavigationTiming() {
    const [navigation] = performance.getEntriesByType(
      'navigation'
    ) as PerformanceNavigationTiming[];

    this.data.pageLoad = {
      navigationTiming: navigation,
      resourceTimings: performance.getEntriesByType(
        'resource'
      ) as PerformanceResourceTiming[]
    };
  }

  private collectWebVitals() {
    import('web-vitals').then(({ getLCP, getFID, getCLS }) => {
      getLCP((metric) => {
        this.data.webVitals = { ...this.data.webVitals, lcp: metric.value };
      });

      getFID((metric) => {
        this.data.webVitals = { ...this.data.webVitals, fid: metric.value };
      });

      getCLS((metric) => {
        this.data.webVitals = { ...this.data.webVitals, cls: metric.value };
      });
    });
  }

  addCustomMetric(name: string, value: number) {
    this.data.customMetrics?.push({
      name,
      value,
      timestamp: Date.now()
    });
  }

  async send() {
    await fetch('/api/rum', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(this.data)
    });
  }
}

export const rumCollector = new RUMCollector();

Session Recording

Record and replay user sessions for debugging.

Session Replay Implementation

typescript
// session-recorder.ts
class SessionRecorder {
  private events: Array<{
    type: string;
    timestamp: number;
    data: any;
  }> = [];

  constructor() {
    if (this.shouldRecord()) {
      this.startRecording();
    }
  }

  private shouldRecord() {
    // Sample 10% of sessions
    return Math.random() < 0.1;
  }

  private startRecording() {
    // Record DOM mutations
    const mutationObserver = new MutationObserver((mutations) => {
      mutations.forEach(mutation => {
        this.events.push({
          type: 'mutation',
          timestamp: Date.now(),
          data: mutation
        });
      });
    });

    mutationObserver.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true
    });

    // Record clicks
    document.addEventListener('click', (event) => {
      this.events.push({
        type: 'click',
        timestamp: Date.now(),
        data: {
          target: (event.target as HTMLElement).tagName,
          x: event.clientX,
          y: event.clientY
        }
      });
    });

    // Record navigation
    let lastUrl = window.location.href;
    setInterval(() => {
      if (window.location.href !== lastUrl) {
        lastUrl = window.location.href;
        this.events.push({
          type: 'navigation',
          timestamp: Date.now(),
          data: { url: lastUrl }
        });
      }
    }, 1000);
  }

  async saveSession() {
    await fetch('/api/session-replay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        sessionId: crypto.randomUUID(),
        events: this.events
      })
    });
  }
}

export const sessionRecorder = new SessionRecorder();

Analytics Integration

Combine monitoring with user analytics.

Custom Events

typescript
// analytics.ts
interface AnalyticsEvent {
  name: string;
  properties?: Record<string, any>;
  userId?: string;
  timestamp: number;
}

class Analytics {
  private queue: AnalyticsEvent[] = [];

  track(name: string, properties?: Record<string, any>) {
    this.queue.push({
      name,
      properties,
      timestamp: Date.now()
    });

    // Flush queue every 30 seconds
    if (this.queue.length >= 10) {
      this.flush();
    }
  }

  async flush() {
    if (this.queue.length === 0) return;

    await fetch('/api/analytics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ events: this.queue })
    });

    this.queue = [];
  }
}

export const analytics = new Analytics();

// Usage
analytics.track('button_clicked', {
  button_id: 'submit_form',
  page: '/checkout'
});

Alerting

Set up intelligent alerts for critical issues.

Alert Rules

typescript
// alerting.ts
interface AlertRule {
  name: string;
  condition: (metrics: any) => boolean;
  severity: 'low' | 'medium' | 'high';
  notification: string[];
}

class AlertManager {
  private rules: AlertRule[] = [
    {
      name: 'High Error Rate',
      condition: (metrics) => metrics.errorRate > 0.05,
      severity: 'high',
      notification: ['slack', 'email']
    },
    {
      name: 'Slow Page Load',
      condition: (metrics) => metrics.avgPageLoadTime > 3000,
      severity: 'medium',
      notification: ['slack']
    },
    {
      name: 'Poor Core Web Vitals',
      condition: (metrics) => 
        metrics.poorLCP > 0.1 || 
        metrics.poorFID > 0.1 || 
        metrics.poorCLS > 0.25,
      severity: 'medium',
      notification: ['slack']
    }
  ];

  checkRules(metrics: any) {
    this.rules.forEach(rule => {
      if (rule.condition(metrics)) {
        this.sendAlert(rule, metrics);
      }
    });
  }

  private sendAlert(rule: AlertRule, metrics: any) {
    // Send notification based on severity
    console.error(`ALERT [${rule.severity}]: ${rule.name}`, metrics);
  }
}

export const alertManager = new AlertManager();

Best Practices

Follow these practices for effective monitoring:

  1. 1Sample Rate: Implement smart sampling to reduce costs while maintaining statistical significance
  2. 2Privacy: Anonymize PII and comply with GDPR
  3. 3Performance: Ensure monitoring code doesn't degrade performance
  4. 4Context: Capture rich context with every error
  5. 5Dashboards: Create visual dashboards for quick insights
  6. 6Alert Fatigue: Configure alerts carefully to avoid alert fatigue
  7. 7Testing: Test monitoring code in staging before production

Conclusion

Frontend monitoring is essential for delivering excellent user experiences. By implementing comprehensive monitoring covering performance, errors, and user behavior, you can proactively identify and fix issues before they impact your users.

Start with core metrics like Web Vitals, then expand to cover your specific use cases. Remember: what you can measure, you can improve.

Enjoyed this essay? Share it.

Related essays

Next essay
Micro-Frontends Architecture: Complete Implementation Guide