- 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 Type | Purpose | Key Metrics |
---|---|---|
Error Tracking | Catch JavaScript errors and crashes | Error rate, affected users, stack traces |
Performance Monitoring | Track loading speed and responsiveness | Core Web Vitals, bundle size, network timing |
User Experience | Monitor real user interactions | Click tracking, form abandonment, user flows |
Business Metrics | Track feature usage and conversions | Feature 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
Tool | Best For | Pricing | Key Features |
---|---|---|---|
Sentry | Error tracking & performance | Free tier + paid | Error tracking, performance, releases |
LogRocket | Session replay & debugging | Paid | Session replay, error tracking, performance |
Datadog | Full-stack monitoring | Paid | RUM, logs, traces, infrastructure |
New Relic | Performance monitoring | Free tier + paid | Browser monitoring, distributed tracing |
Hotjar | User behavior analytics | Free tier + paid | Heatmaps, 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:
- Foundation: Error tracking and basic performance
- Growth: User experience and API monitoring
- 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! π