Published on

Comprehensive Frontend Monitoring Guide: From Sentry to Performance Insights

Frontend monitoring is no longer a nice-to-haveβ€”it's essential for delivering reliable user experiences. When users encounter errors or performance issues, you need to know about them before they abandon your application. Today, we'll explore comprehensive frontend monitoring strategies using Sentry and other powerful tools.

Why Frontend Monitoring Matters

Unlike backend systems where you control the environment, frontend applications run in unpredictable conditions:

  • Diverse browsers with different JavaScript engines
  • Network conditions ranging from 5G to spotty mobile
  • Device capabilities from high-end laptops to budget phones
  • User behaviors that create unexpected edge cases

The result? Issues that are impossible to reproduce in development but plague your users in production.

🎯 The Complete Monitoring Strategy

Effective frontend monitoring covers four critical areas:

Monitoring TypePurposeKey Metrics
Error TrackingCatch JavaScript errors and crashesError rate, affected users, stack traces
Performance MonitoringTrack loading speed and responsivenessCore Web Vitals, bundle size, network timing
User ExperienceMonitor real user interactionsClick tracking, form abandonment, user flows
Business MetricsTrack feature usage and conversionsFeature adoption, conversion funnels, A/B test results

πŸ›‘οΈ Error Tracking with Sentry

What is Sentry?

Sentry is a powerful application monitoring platform that helps developers identify, triage, and resolve errors in real-time. Originally built for error tracking, Sentry has evolved into a comprehensive observability platform that covers:

Core Features:

  • Error Tracking: Capture and aggregate JavaScript errors with detailed stack traces
  • Performance Monitoring: Track application performance and identify bottlenecks
  • Release Tracking: Monitor error rates across different deployments
  • User Context: Understand which users are affected by issues
  • Alerting: Get notified when critical issues occur

Why Choose Sentry?

  • Developer-First: Built by developers, for developers with excellent DX
  • Open Source: Core functionality is open source with enterprise features
  • Easy Integration: Works seamlessly with React, Vue, Angular, and vanilla JS
  • Generous Free Tier: 5,000 errors/month and 10,000 performance transactions
  • Rich Ecosystem: Integrations with Slack, Jira, GitHub, and more

Sentry vs. Alternatives:

  • vs. LogRocket: Sentry focuses on errors/performance, LogRocket on session replay
  • vs. Bugsnag: Similar error tracking, but Sentry has better performance monitoring
  • vs. Rollbar: Comparable features, but Sentry has a more modern interface and better React integration

Setting Up Sentry

Sentry is the gold standard for error tracking. Let's set it up properly:

Basic Sentry Setup

npm install @sentry/react @sentry/tracing
// src/sentry.js
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'

Sentry.init({
  dsn: process.env.REACT_APP_SENTRY_DSN,
  integrations: [
    new BrowserTracing({
      // Capture interactions like clicks, navigation
      tracePropagationTargets: ['localhost', /^https:\/\/yourapi\.domain\.com\/api/],
    }),
  ],
  // Performance monitoring
  tracesSampleRate: 1.0, // Adjust for production
  // Release tracking
  release: process.env.REACT_APP_VERSION,
  environment: process.env.NODE_ENV,
})

React Integration

// src/App.js
import * as Sentry from '@sentry/react'

const App = () => {
  return (
    <Sentry.ErrorBoundary fallback={ErrorFallback} showDialog>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/profile" element={<Profile />} />
        </Routes>
      </Router>
    </Sentry.ErrorBoundary>
  )
}

// Custom error fallback component
const ErrorFallback = ({ error, resetError }) => (
  <div className="error-boundary">
    <h2>Something went wrong</h2>
    <p>We've been notified and are working to fix this issue.</p>
    <button onClick={resetError}>Try Again</button>
    <details>
      <summary>Error Details</summary>
      <pre>{error.message}</pre>
    </details>
  </div>
)

export default Sentry.withSentryConfig(App)

Advanced Error Context

// Add user context
Sentry.setUser({
  id: user.id,
  email: user.email,
  username: user.username,
})

// Add custom tags for filtering
Sentry.setTag('feature', 'checkout')
Sentry.setTag('user_type', 'premium')

// Add breadcrumbs for debugging
Sentry.addBreadcrumb({
  message: 'User clicked checkout button',
  level: 'info',
  category: 'ui.click',
})

// Custom error with context
try {
  await processPayment(paymentData)
} catch (error) {
  Sentry.withScope((scope) => {
    scope.setContext('payment', {
      amount: paymentData.amount,
      currency: paymentData.currency,
      method: paymentData.method,
    })
    scope.setLevel('error')
    Sentry.captureException(error)
  })
}

⚑ Performance Monitoring

Core Web Vitals Tracking

// src/performance.js
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

const sendToAnalytics = (metric) => {
  // Send to your analytics service
  gtag('event', metric.name, {
    value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
    event_category: 'Web Vitals',
    event_label: metric.id,
    non_interaction: true,
  })

  // Also send to Sentry
  Sentry.addBreadcrumb({
    message: `${metric.name}: ${metric.value}`,
    level: 'info',
    category: 'performance',
  })
}

// Track all Core Web Vitals
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)

Custom Performance Monitoring

// Performance marks and measures
class PerformanceTracker {
  static startTiming(name) {
    performance.mark(`${name}-start`)
  }

  static endTiming(name) {
    performance.mark(`${name}-end`)
    performance.measure(name, `${name}-start`, `${name}-end`)

    const measure = performance.getEntriesByName(name)[0]

    // Send to monitoring service
    this.sendMetric({
      name,
      value: measure.duration,
      type: 'timing',
    })
  }

  static sendMetric(metric) {
    // Send to your analytics
    if (window.gtag) {
      gtag('event', 'custom_timing', {
        value: Math.round(metric.value),
        event_category: 'Performance',
        event_label: metric.name,
      })
    }

    // Send to Sentry
    Sentry.addBreadcrumb({
      message: `Custom timing: ${metric.name} took ${metric.value}ms`,
      level: 'info',
      category: 'performance',
    })
  }
}

// Usage in components
const DataComponent = () => {
  useEffect(() => {
    const fetchData = async () => {
      PerformanceTracker.startTiming('data-fetch')
      try {
        const data = await api.getData()
        setData(data)
      } finally {
        PerformanceTracker.endTiming('data-fetch')
      }
    }

    fetchData()
  }, [])

  return <div>{/* component JSX */}</div>
}

πŸ“Š User Experience Monitoring

Click and Interaction Tracking

// src/analytics.js
class UserAnalytics {
  static trackClick(element, properties = {}) {
    const eventData = {
      event: 'click',
      element: element.tagName.toLowerCase(),
      text: element.textContent?.slice(0, 100),
      href: element.href,
      classes: element.className,
      timestamp: Date.now(),
      ...properties,
    }

    // Send to multiple services
    this.sendToSentry(eventData)
    this.sendToGoogleAnalytics(eventData)
  }

  static trackFormInteraction(formName, field, action) {
    const eventData = {
      event: 'form_interaction',
      form: formName,
      field,
      action, // focus, blur, change, submit
      timestamp: Date.now(),
    }

    this.sendToSentry(eventData)
  }

  static sendToSentry(data) {
    Sentry.addBreadcrumb({
      message: `User interaction: ${data.event}`,
      level: 'info',
      category: 'user',
      data,
    })
  }
}

// Auto-track clicks
document.addEventListener('click', (event) => {
  UserAnalytics.trackClick(event.target, {
    pageUrl: window.location.href,
    userAgent: navigator.userAgent,
  })
})

Form Abandonment Tracking

// src/hooks/useFormTracking.js
import { useEffect, useRef } from 'react'

export const useFormTracking = (formName, formData) => {
  const startTime = useRef(Date.now())
  const hasInteracted = useRef(false)

  useEffect(() => {
    const handleBeforeUnload = () => {
      if (hasInteracted.current && Object.keys(formData).length > 0) {
        // Track form abandonment
        UserAnalytics.trackFormInteraction(formName, null, 'abandon')
      }
    }

    window.addEventListener('beforeunload', handleBeforeUnload)
    return () => window.removeEventListener('beforeunload', handleBeforeUnload)
  }, [formName, formData])

  const trackFieldChange = (fieldName) => {
    hasInteracted.current = true
    UserAnalytics.trackFormInteraction(formName, fieldName, 'change')
  }

  const trackSubmit = () => {
    const duration = Date.now() - startTime.current
    UserAnalytics.trackFormInteraction(formName, null, 'submit')

    // Track form completion time
    PerformanceTracker.sendMetric({
      name: `form_completion_${formName}`,
      value: duration,
      type: 'timing',
    })
  }

  return { trackFieldChange, trackSubmit }
}

// Usage in form component
const ContactForm = () => {
  const [formData, setFormData] = useState({})
  const { trackFieldChange, trackSubmit } = useFormTracking('contact', formData)

  const handleSubmit = (e) => {
    e.preventDefault()
    trackSubmit()
    // Submit form...
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        onChange={(e) => {
          setFormData({ ...formData, email: e.target.value })
          trackFieldChange('email')
        }}
      />
    </form>
  )
}

🌐 Network and API Monitoring

API Call Tracking

// src/api/monitor.js
class APIMonitor {
  static wrapFetch() {
    const originalFetch = window.fetch

    window.fetch = async (...args) => {
      const startTime = performance.now()
      const url = args[0]

      try {
        const response = await originalFetch(...args)
        const duration = performance.now() - startTime

        this.trackAPICall({
          url,
          method: args[1]?.method || 'GET',
          status: response.status,
          duration,
          success: response.ok,
        })

        return response
      } catch (error) {
        const duration = performance.now() - startTime

        this.trackAPICall({
          url,
          method: args[1]?.method || 'GET',
          duration,
          success: false,
          error: error.message,
        })

        throw error
      }
    }
  }

  static trackAPICall(data) {
    // Send to Sentry
    Sentry.addBreadcrumb({
      message: `API call to ${data.url}`,
      level: data.success ? 'info' : 'error',
      category: 'http',
      data,
    })

    // Track slow API calls
    if (data.duration > 2000) {
      Sentry.captureMessage(`Slow API call: ${data.url} took ${data.duration}ms`, 'warning')
    }

    // Send to analytics
    gtag('event', 'api_call', {
      event_category: 'API',
      event_label: data.url,
      value: Math.round(data.duration),
      custom_map: {
        status: data.status,
        success: data.success,
      },
    })
  }
}

// Initialize API monitoring
APIMonitor.wrapFetch()

πŸ” Advanced Monitoring Techniques

Memory Leak Detection

// src/monitoring/memory.js
class MemoryMonitor {
  static startMonitoring() {
    if (!('memory' in performance)) return

    setInterval(() => {
      const memory = performance.memory
      const memoryData = {
        usedJSHeapSize: memory.usedJSHeapSize,
        totalJSHeapSize: memory.totalJSHeapSize,
        jsHeapSizeLimit: memory.jsHeapSizeLimit,
      }

      // Alert if memory usage is high
      const usagePercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100

      if (usagePercent > 80) {
        Sentry.captureMessage(`High memory usage: ${usagePercent.toFixed(2)}%`, 'warning')
      }

      // Send memory metrics
      this.sendMemoryMetrics(memoryData)
    }, 30000) // Check every 30 seconds
  }

  static sendMemoryMetrics(data) {
    Sentry.addBreadcrumb({
      message: 'Memory usage check',
      level: 'info',
      category: 'performance',
      data,
    })
  }
}

Feature Flag Monitoring

// src/monitoring/features.js
class FeatureMonitor {
  static trackFeatureUsage(featureName, enabled, userId) {
    const eventData = {
      feature: featureName,
      enabled,
      userId,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      url: window.location.href,
    }

    // Track in Sentry
    Sentry.setTag('feature_flag', `${featureName}:${enabled}`)
    Sentry.addBreadcrumb({
      message: `Feature ${featureName} ${enabled ? 'enabled' : 'disabled'}`,
      level: 'info',
      category: 'feature',
      data: eventData,
    })

    // Send to analytics
    gtag('event', 'feature_flag', {
      event_category: 'Features',
      event_label: featureName,
      value: enabled ? 1 : 0,
    })
  }

  static trackFeatureError(featureName, error) {
    Sentry.withScope((scope) => {
      scope.setTag('feature', featureName)
      scope.setLevel('error')
      Sentry.captureException(error)
    })
  }
}

πŸ› οΈ Monitoring Tools Ecosystem

Essential Tools Comparison

ToolBest ForPricingKey Features
SentryError tracking & performanceFree tier + paidError tracking, performance, releases
LogRocketSession replay & debuggingPaidSession replay, error tracking, performance
DatadogFull-stack monitoringPaidRUM, logs, traces, infrastructure
New RelicPerformance monitoringFree tier + paidBrowser monitoring, distributed tracing
HotjarUser behavior analyticsFree tier + paidHeatmaps, session recordings, surveys

Monitoring Stack Setup

// src/monitoring/index.js
class MonitoringStack {
  static init() {
    // Initialize error tracking
    this.initSentry()

    // Initialize performance monitoring
    this.initPerformanceTracking()

    // Initialize user analytics
    this.initUserTracking()

    // Initialize API monitoring
    this.initAPIMonitoring()
  }

  static initSentry() {
    // Sentry configuration (shown earlier)
  }

  static initPerformanceTracking() {
    // Core Web Vitals tracking
    import('./performance').then((module) => {
      module.initWebVitals()
    })
  }

  static initUserTracking() {
    // User interaction tracking
    if (process.env.NODE_ENV === 'production') {
      this.initGoogleAnalytics()
      this.initHotjar()
    }
  }

  static initAPIMonitoring() {
    APIMonitor.wrapFetch()
    MemoryMonitor.startMonitoring()
  }
}

// Initialize monitoring when app starts
MonitoringStack.init()

πŸ“ˆ Monitoring Dashboard and Alerts

Creating Actionable Alerts

// src/monitoring/alerts.js
class AlertManager {
  static checkHealthMetrics() {
    const healthChecks = [this.checkErrorRate(), this.checkPerformance(), this.checkAPIHealth()]

    Promise.all(healthChecks).then((results) => {
      results.forEach((result) => {
        if (result.status === 'critical') {
          this.sendCriticalAlert(result)
        }
      })
    })
  }

  static checkErrorRate() {
    // Check error rate over last 5 minutes
    const errorRate = this.calculateErrorRate()

    if (errorRate > 5) {
      // 5% error rate threshold
      return {
        status: 'critical',
        type: 'error_rate',
        value: errorRate,
        message: `Error rate is ${errorRate}% - above 5% threshold`,
      }
    }

    return { status: 'ok', type: 'error_rate' }
  }

  static sendCriticalAlert(alert) {
    // Send to Slack, email, etc.
    Sentry.captureMessage(`Critical alert: ${alert.message}`, 'error')
  }
}

Key Metrics to Track

Error Metrics

  • Error rate by page/feature
  • Unique errors vs. recurring errors
  • Error impact (how many users affected)
  • Time to resolution

Performance Metrics

  • Core Web Vitals trends
  • Bundle size changes
  • API response times
  • Memory usage patterns

User Experience Metrics

  • Feature adoption rates
  • Form completion rates
  • User journey drop-offs
  • Session duration

πŸš€ Best Practices and Implementation Tips

1. Start Small, Scale Up

// Phase 1: Basic error tracking
const basicMonitoring = {
  errorTracking: true,
  basicPerformance: true,
  userTracking: false,
  advancedAnalytics: false,
}

// Phase 2: Add performance monitoring
const intermediateMonitoring = {
  ...basicMonitoring,
  coreWebVitals: true,
  apiMonitoring: true,
  customMetrics: true,
}

// Phase 3: Full observability
const advancedMonitoring = {
  ...intermediateMonitoring,
  userTracking: true,
  sessionReplay: true,
  featureFlags: true,
  businessMetrics: true,
}

2. Privacy-First Monitoring

// src/monitoring/privacy.js
class PrivacyMonitor {
  static sanitizeData(data) {
    // Remove sensitive information
    const sensitiveFields = ['password', 'ssn', 'creditCard', 'email']

    return Object.keys(data).reduce((clean, key) => {
      if (sensitiveFields.some((field) => key.toLowerCase().includes(field))) {
        clean[key] = '[REDACTED]'
      } else {
        clean[key] = data[key]
      }
      return clean
    }, {})
  }

  static respectUserConsent() {
    // Check user consent before tracking
    const hasConsent = localStorage.getItem('analytics-consent') === 'true'
    return hasConsent
  }
}

3. Performance Budget Monitoring

// src/monitoring/budget.js
class PerformanceBudget {
  static budgets = {
    bundleSize: 250000, // 250KB
    imageSize: 100000, // 100KB per image
    totalImages: 20, // Max 20 images per page
    apiCalls: 10, // Max 10 API calls per page load
  }

  static checkBudgets() {
    const results = {
      bundleSize: this.checkBundleSize(),
      imageCount: this.checkImageCount(),
      apiCalls: this.checkAPICalls(),
    }

    Object.entries(results).forEach(([metric, result]) => {
      if (result.exceeded) {
        Sentry.captureMessage(
          `Performance budget exceeded: ${metric} is ${result.actual} (limit: ${result.limit})`,
          'warning'
        )
      }
    })
  }
}

πŸ“š Monitoring Checklist

πŸ”§ Setup Checklist

  • Error tracking configured with proper source maps
  • Performance monitoring for Core Web Vitals
  • User consent management for privacy compliance
  • Environment-specific configuration (dev/staging/prod)
  • Alert thresholds defined for critical metrics
  • Team notifications set up for critical issues

πŸ“Š Metrics Checklist

  • Error rates by page and feature
  • Performance metrics (LCP, FID, CLS)
  • API response times and error rates
  • User flow completion rates
  • Feature adoption metrics
  • Business impact measurements

πŸ”„ Maintenance Checklist

  • Regular review of alert thresholds
  • Source map updates with each deployment
  • Monitoring tool updates and security patches
  • Data retention policy compliance
  • Performance budget reviews
  • Team training on monitoring tools

Conclusion

Effective frontend monitoring is about more than catching errorsβ€”it's about understanding your users' real experience and continuously improving it. Start with basic error tracking using Sentry, then gradually add performance monitoring, user analytics, and business metrics.

Remember the monitoring pyramid:

  1. Foundation: Error tracking and basic performance
  2. Growth: User experience and API monitoring
  3. Optimization: Advanced analytics and business metrics

With proper monitoring in place, you'll transform from reactive bug fixing to proactive user experience optimization. Your users will thank you, and your team will ship with confidence.

Happy monitoring! πŸš€