Skip to main content

6 - Monitoring and Alerting for Web Applications

How to implement monitoring, error tracking and alerting for web applications: Sentry, uptime monitoring, log management and dashboards.

What do you think about this article?

At a Glance

How to implement monitoring, error tracking and alerting for web applications: Sentry, uptime monitoring, log management and dashboards.

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.

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.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects