Introduction: Deployment Is Not the End, It Is the Beginning
A perfect CI/CD pipeline is useless if we do not know what happens after deployment. Real users interact with our application in unpredictable ways, on different devices, with varying connections. Monitoring and alerting close the DevOps loop: they allow us to detect problems before users report them, and to respond quickly when something goes wrong.
In this final article of the DevOps for Frontend Developers series, we will explore the three categories of frontend monitoring: error tracking, performance monitoring, and user behavior analysis. We will configure Sentry for error tracking in Angular, implement Real User Monitoring with web-vitals, and define effective alerting strategies.
What You Will Learn in This Article
- The three categories of frontend monitoring (errors, performance, user behavior)
- How to integrate Sentry in an Angular application
- How to implement Real User Monitoring (RUM)
- How to configure synthetic monitoring (uptime checks)
- How to monitor performance with the web-vitals library
- How to create a custom Error Handler in Angular
- How to upload source maps for production debugging
- Effective alerting strategies
- How to set up monitoring dashboards
- Comparison of the most popular monitoring tools
1. The Three Categories of Frontend Monitoring
Frontend monitoring is divided into three complementary areas, each with specific objectives and tools:
Monitoring Categories
| Category | What It Monitors | Typical Tools | Priority |
|---|---|---|---|
| Error Tracking | JavaScript exceptions, HTTP errors, application crashes | Sentry, Bugsnag, Rollbar | Critical |
| Performance Monitoring | Core Web Vitals, load times, network metrics | web-vitals, SpeedCurve, Datadog RUM | High |
| User Behavior | Navigation, interactions, conversion funnels | Google Analytics, Mixpanel, Amplitude | Medium |
2. Error Tracking with Sentry
Sentry is the most widely used error tracking platform in the frontend world. It automatically captures JavaScript exceptions, enriches them with browser context, and groups them by type, greatly facilitating debugging.
Installation and Basic Configuration
# Install the Sentry package for Angular
npm install @sentry/angular @sentry/browser
# Or with the automatic wizard
npx @sentry/wizard@latest -i angular
// main.ts - Sentry initialization
import * as Sentry from '@sentry/angular';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
// Initialize Sentry BEFORE bootstrapping the application
Sentry.init({
dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
environment: 'production',
release: 'my-app@1.0.0',
// Performance tracing
tracesSampleRate: 0.2, // 20% of transactions
// Session replay for errored sessions
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
// Filter irrelevant errors
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error exception captured',
/Loading chunk [\d]+ failed/,
],
// Additional information for each event
beforeSend(event) {
// Remove sensitive data
if (event.request?.cookies) {
delete event.request.cookies;
}
return event;
},
});
bootstrapApplication(AppComponent, appConfig);
Angular Configuration with Providers
// app.config.ts - Register Sentry providers
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { provideRouter, Router } from '@angular/router';
import * as Sentry from '@sentry/angular';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
// Custom Error Handler that sends to Sentry
{
provide: ErrorHandler,
useValue: Sentry.createErrorHandler({
showDialog: false, // Don't show Sentry dialog to users
logErrors: true, // Also log to console
}),
},
// Angular navigation tracking
{
provide: Sentry.TraceService,
deps: [Router],
},
],
};
Watch the Sample Rate
The tracesSampleRate determines how many transactions are sent to Sentry.
A value of 1.0 (100%) is useful in development but too expensive in production
with high traffic. Start with 0.1 or 0.2 and increase if necessary.
For replaysOnErrorSampleRate, keep 1.0 to always capture the
session when an error occurs.
3. Custom Error Handler in Angular
For more granular control over errors, we can create a custom Error Handler that combines Sentry with additional logic:
// custom-error-handler.ts
import { ErrorHandler, Injectable, inject } from '@angular/core';
import * as Sentry from '@sentry/angular';
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
handleError(error: unknown): void {
// Classify the error
const errorInfo = this.classifyError(error);
// Send to Sentry with additional context
Sentry.withScope((scope) => {
scope.setTag('error.type', errorInfo.type);
scope.setTag('error.severity', errorInfo.severity);
scope.setLevel(errorInfo.severity as Sentry.SeverityLevel);
if (errorInfo.type === 'http') {
scope.setContext('http', {
status: (error as any)?.status,
url: (error as any)?.url,
});
}
Sentry.captureException(error);
});
// Log to console in development
console.error('[CustomErrorHandler]', error);
}
private classifyError(error: unknown): { type: string; severity: string } {
if (error instanceof TypeError) {
return { type: 'type-error', severity: 'error' };
}
if ((error as any)?.status !== undefined) {
const status = (error as any).status;
if (status >= 500) return { type: 'http', severity: 'error' };
if (status >= 400) return { type: 'http', severity: 'warning' };
}
if (error instanceof Error && error.message?.includes('ChunkLoadError')) {
return { type: 'chunk-load', severity: 'warning' };
}
return { type: 'unknown', severity: 'error' };
}
}
4. Real User Monitoring (RUM)
Real User Monitoring measures performance as perceived by real users, not in a
simulated environment. Google's web-vitals library is the standard for collecting
Core Web Vitals directly from the user's browser.
# Install web-vitals
npm install web-vitals
// web-vitals.service.ts
import { Injectable } from '@angular/core';
import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals';
import type { Metric } from 'web-vitals';
@Injectable({ providedIn: 'root' })
export class WebVitalsService {
initializeTracking(): void {
// Core Web Vitals
onCLS(this.reportMetric.bind(this));
onINP(this.reportMetric.bind(this));
onLCP(this.reportMetric.bind(this));
// Supplementary metrics
onFCP(this.reportMetric.bind(this));
onTTFB(this.reportMetric.bind(this));
}
private reportMetric(metric: Metric): void {
const data = {
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
};
// Send to an analytics endpoint
this.sendToAnalytics(data);
// Send to Sentry as breadcrumb
this.sendToSentry(data);
// Console log for debugging
console.log(`[Web Vital] ${metric.name}: ${metric.value} (${metric.rating})`);
}
private sendToAnalytics(data: Record<string, unknown>): void {
// Send to Google Analytics
if (typeof gtag !== 'undefined') {
gtag('event', data['name'] as string, {
value: Math.round(data['value'] as number),
event_category: 'Web Vitals',
event_label: data['rating'] as string,
non_interaction: true,
});
}
}
private sendToSentry(data: Record<string, unknown>): void {
import('@sentry/angular').then((Sentry) => {
Sentry.addBreadcrumb({
category: 'web-vital',
message: `${data['name']}: ${data['value']} (${data['rating']})`,
level: data['rating'] === 'poor' ? 'warning' : 'info',
data: data,
});
});
}
}
Initialize Tracking in the Root Component
// app.component.ts
import { Component, OnInit, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { WebVitalsService } from './services/web-vitals.service';
@Component({
selector: 'app-root',
template: '<router-outlet />',
})
export class AppComponent implements OnInit {
private webVitals = inject(WebVitalsService);
private platformId = inject(PLATFORM_ID);
ngOnInit(): void {
// Web Vitals only works in the browser, not in SSR
if (isPlatformBrowser(this.platformId)) {
this.webVitals.initializeTracking();
}
}
}
5. Synthetic Monitoring (Uptime Checks)
Synthetic monitoring runs automated checks at regular intervals to verify that the site is reachable and functioning. Unlike RUM, it does not depend on user traffic: it detects problems even at 3 AM when nobody is visiting the site.
# GitHub Actions for uptime monitoring
name: Uptime Check
on:
schedule:
- cron: '*/15 * * * *' # Every 15 minutes
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check site availability
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://example.com)
RESPONSE_TIME=$(curl -s -o /dev/null -w "%{time_total}" https://example.com)
echo "Status: $STATUS"
echo "Response time: ${RESPONSE_TIME}s"
if [ "$STATUS" != "200" ]; then
echo "ALERT: Site returned status $STATUS"
exit 1
fi
# Check response time
THRESHOLD="3.0"
if (( $(echo "$RESPONSE_TIME > $THRESHOLD" | bc -l) )); then
echo "WARNING: Response time ${RESPONSE_TIME}s exceeds threshold ${THRESHOLD}s"
fi
Synthetic Monitoring Services
- UptimeRobot: Free plan with 50 monitors, checks every 5 minutes
- Pingdom: Advanced monitoring with integrated RUM analysis
- Checkly: Browser checks with Playwright, ideal for complex flows
- Better Uptime: Incident management with public status pages
- Google Cloud Monitoring: Uptime checks integrated with the GCP ecosystem
6. Source Map Upload for Production Debugging
In production, JavaScript code is minified and unreadable. Source maps allow Sentry to display the original stack trace with variable names and line numbers from the source code. It is essential to upload them to Sentry but not serve them publicly.
# Upload source maps in the CI/CD pipeline
- name: Build with source maps
run: npx ng build --configuration=production --source-map
- name: Upload source maps to Sentry
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: my-organization
SENTRY_PROJECT: my-angular-app
with:
environment: production
version: ${{ github.sha }}
sourcemaps: './dist/my-app/browser'
url_prefix: '~/'
- name: Remove source maps before deploy
run: find dist/my-app/browser -name "*.map" -delete
Never Serve Source Maps in Production
Source maps contain the complete source code of the application. Serving them publicly would allow anyone to read the business logic, identify vulnerabilities, and understand the internal architecture. Upload them to Sentry and then delete them from the deployment directory.
7. Alerting Strategies
A monitoring system without alerting is like a fire alarm without a siren. But too many alerts lead to "alert fatigue," where the team ignores notifications. The key is to define intelligent thresholds:
Recommended Alert Levels
| Level | Trigger | Channel | Response |
|---|---|---|---|
| Critical (P0) | Site completely down, errors > 50% | SMS + Slack + PagerDuty | Immediate (within 15 min) |
| High (P1) | Critical functionality broken, error rate > 10% | Slack + Email | Within 1 hour |
| Medium (P2) | Degraded performance, non-critical errors > 5% | Slack | Within 4 hours |
| Low (P3) | Warnings, metrics trending negative | Weekly report | Next sprint |
Sentry Alert Configuration
Sentry Alert Configuration:
Alert 1: Error Spike
Condition: When event count > 100 in 1 hour
Action: Notify Slack #alerts + email team lead
Frequency: Max 1 notification every 30 minutes
Alert 2: New Error in Production
Condition: First occurrence of a new issue
Action: Notify Slack #errors
Frequency: Every new issue
Alert 3: Regression
Condition: Previously resolved issue reappears
Action: Notify Slack #alerts + assign to last resolver
Frequency: Every regression
Alert 4: Performance Budget
Condition: LCP p95 > 4 seconds
Action: Notify Slack #performance
Frequency: Max 1 notification every 2 hours
8. Monitoring Dashboards
An effective dashboard provides an immediate overview of application health. Here are the essential metrics to include:
Essential Dashboard
+----------------------------------------------------------+
| Application Health |
+----------------------------------------------------------+
| |
| Uptime: 99.97% Error Rate: 0.3% Avg LCP: 1.8s |
| |
+----------------------------------------------------------+
| Core Web Vitals (p75) |
| +--------+ +--------+ +--------+ |
| | LCP | | INP | | CLS | |
| | 1.8s | | 120ms | | 0.05 | |
| | (good) | | (good) | | (good) | |
| +--------+ +--------+ +--------+ |
+----------------------------------------------------------+
| Errors Last 7 Days | Top Errors |
| +-----------------------------+ | 1. TypeError: null |
| | Mon Tue Wed Thu Fri Sat Sun | | 2. ChunkLoadError |
| | 5 3 8 2 12 1 0 | | 3. HttpError 500 |
| +-----------------------------+ | 4. NetworkError |
+----------------------------------------------------------+
9. Monitoring Tools Comparison
Monitoring Tools Compared
| Tool | Type | Free Plan | Strength | Ideal For |
|---|---|---|---|---|
| Sentry | Error tracking + Performance | 5K events/month | Error grouping, source maps | All projects |
| Datadog RUM | Full-stack monitoring | Limited | Frontend-backend correlation | Enterprise |
| LogRocket | Session replay + Error tracking | 1K sessions/month | Video session replay | UX debugging |
| New Relic Browser | RUM + Error tracking | 100 GB/month | Customizable dashboards | Operations teams |
| Bugsnag | Error tracking | 7.5K events/month | Release stability tracking | Mobile + Web |
10. Complete Implementation: Checklist
Here is a complete checklist for implementing monitoring in an Angular project:
// monitoring-setup.ts - Centralized configuration
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class MonitoringService {
private platformId = inject(PLATFORM_ID);
initialize(): void {
if (!isPlatformBrowser(this.platformId)) return;
// 1. Error Tracking (Sentry already configured in main.ts)
// 2. Web Vitals
this.setupWebVitals();
// 3. Custom error boundary for unhandled errors
this.setupGlobalErrorHandlers();
// 4. Network monitoring
this.setupNetworkMonitoring();
}
private async setupWebVitals(): Promise<void> {
const { onCLS, onINP, onLCP, onFCP, onTTFB } = await import('web-vitals');
const report = (metric: any) => {
console.log(`[Vital] ${metric.name}: ${metric.value}`);
};
onCLS(report);
onINP(report);
onLCP(report);
onFCP(report);
onTTFB(report);
}
private setupGlobalErrorHandlers(): void {
// Catch unhandled promises
window.addEventListener('unhandledrejection', (event) => {
console.error('[Unhandled Rejection]', event.reason);
});
// Catch resource loading errors
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLScriptElement) {
console.error('[Script Load Error]', event);
}
}, true);
}
private setupNetworkMonitoring(): void {
// Monitor connection changes
const connection = (navigator as any).connection;
if (connection) {
connection.addEventListener('change', () => {
console.log('[Network]', {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
});
});
}
}
}
Monitoring Checklist for Go-Live
- Sentry configured and working with custom error handler
- Source maps uploaded to Sentry (and removed from deploy)
- Web Vitals tracking active with Google Analytics reporting
- Uptime monitoring configured (checks every 5 minutes)
- Alerts configured for critical errors (Slack, email)
- Dashboard created with essential metrics
- Sentry token rotation plan configured
- Incident response procedure documented
- SSR compatibility verified (isPlatformBrowser)
- Sample rate optimized for traffic volume
Conclusion
With this article we conclude the DevOps for Frontend Developers series. We have completed the cycle: from creating CI/CD pipelines with GitHub Actions, to containerization with Docker, to automatic deployment with Firebase, to secret management, to performance monitoring with Lighthouse CI, and finally to production monitoring with Sentry and web-vitals.
Production monitoring closes the DevOps loop: data collected from real users feeds development decisions, bugfix priorities, and performance optimizations. It is not an optional component, it is the foundation for building reliable applications and for continuously improving the user experience.
The key to success in frontend DevOps is progressive iteration: do not implement everything at once, but start with the basics (CI/CD + error tracking) and progressively add advanced components (performance budgets, synthetic monitoring, structured alerting) as the project and team mature.
Related deep dive: 5 - Performance Budgets e Lighthouse CI in Pipeline.
Frequently asked questions
What is the purpose of frontend monitoring?
Frontend monitoring allows developers to detect problems before users report them, and to respond quickly when something goes wrong.
How does Sentry automatically capture JavaScript exceptions?
Sentry automatically captures JavaScript exceptions and enriches them with browser context, greatly facilitating debugging.
What is the difference between Real User Monitoring (RUM) and Synthetic Monitoring (Uptime Checks)?
RUM measures performance as perceived by real users, while Uptime Checks run automated checks at regular intervals to verify that the site is reachable and functioning.
Why should source maps not be served publicly?
Source maps contain the complete source code of the application, and serving them publicly would allow anyone to read the business logic, identify vulnerabilities, and understand the internal architecture.
Commenti
Caricamento commenti...
Sign in to comment