Vibe monitoring with Last9 MCP: Ask your agent to fix production issues! Setup →
Last9 Last9

Angular OpenTelemetry Setup and Troubleshooting

Learn how to set up OpenTelemetry in your Angular app and troubleshoot common issues with tracing, instrumentation, and export configuration.

May 12th, ‘25
Angular OpenTelemetry Setup and Troubleshooting
See How Last9 Works

Unified observability for all your telemetry.Open standards. Simple pricing.

Talk to us

Implementing observability in Angular applications presents unique challenges. Understanding how users experience your application and identifying performance bottlenecks requires specialized tools and approaches.

This guide covers implementing OpenTelemetry in Angular applications, with practical code examples for instrumentation, data collection, and integration with observability backends. You'll learn how to track performance metrics, monitor user interactions, and troubleshoot common implementation issues.

OpenTelemetry for Angular Applications

OpenTelemetry Angular refers to the implementation of the OpenTelemetry framework within Angular applications. OpenTelemetry is an open-source observability framework for collecting telemetry data—metrics, logs, and traces—from distributed systems.

When implemented in Angular applications, OpenTelemetry enables:

  • Tracking of HTTP requests and responses
  • Monitoring of component lifecycle events
  • Measurement of key performance metrics
  • Collection of custom business events
  • Correlation between frontend issues and backend services

This instrumentation provides visibility into your application's behavior, performance patterns, and potential issues.

💡
If you're unsure whether to use a collector or an exporter in your setup, opentelemetry-collector-vs-exporter breaks down the differences clearly.

How to Install and Configure OpenTelemetry Dependencies

Here's how to add OpenTelemetry to your Angular project, starting with the necessary package installation.

Installing Required NPM Packages

First, install the core OpenTelemetry packages for web applications:

npm install @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/context-zone @opentelemetry/instrumentation @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-xml-http-request @opentelemetry/exporter-collector

These packages provide:

When installing these packages, consider the following:

  1. Version Compatibility: Ensure all OpenTelemetry packages have compatible versions. It's recommended to use the same version number across all OpenTelemetry packages to avoid integration issues.
  2. Angular Version Compatibility: Different versions of Angular may require specific versions of OpenTelemetry packages. As a general rule:
    • Angular 12+: Compatible with OpenTelemetry 1.0+
    • Angular 9-11: Use OpenTelemetry 0.24.0 or later
    • Earlier Angular versions may require older OpenTelemetry versions
  3. Zone.js Considerations: Since Angular uses Zone.js for change detection, the @opentelemetry/context-zone package is crucial as it ensures proper context propagation within Angular's Zone.js environment.
  4. Bundle Size Impact: These packages will increase your bundle size. Consider using code splitting and lazy loading to mitigate this impact. You can also implement custom webpack configurations to optimize the bundle:
// webpack.config.js optimizations
module.exports = {
  // ... other webpack configuration
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        opentelemetry: {
          test: /[\\/]node_modules[\\/](@opentelemetry)[\\/]/,
          name: 'opentelemetry',
          priority: 10,
          reuseExistingChunk: true,
        },
      },
    },
  },
};
  1. Development vs. Production: Consider configuring OpenTelemetry differently based on your environment. For example, you might use more verbose tracing in development and a more efficient sampling approach in production.

After installation, you should verify the packages are correctly installed:

npm list @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/context-zone

With these packages installed, you're ready to create and configure your tracing service.

💡
If you're planning to scale your OpenTelemetry Collector setup, this guide offers practical strategies to handle high-volume telemetry data efficiently: scale-the-opentelemetry-collector.

How to Create an OpenTelemetry Tracing Service

Next, create a service to initialize and configure OpenTelemetry. This service handles the core setup of the tracing infrastructure:

// tracing.service.ts
import { Injectable } from '@angular/core';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { CollectorTraceExporter } from '@opentelemetry/exporter-collector';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

@Injectable({
  providedIn: 'root'
})
export class TracingService {
  constructor() {
    this.initTracing();
  }

  private initTracing(): void {
    // Define service information through resource attributes
    const resource = new Resource({
      [SemanticResourceAttributes.SERVICE_NAME]: 'angular-app',
      [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
    });

    // Configure where to send the traces
    const exporter = new CollectorTraceExporter({
      url: 'http://your-collector-endpoint/v1/traces',
    });

    // Create the trace provider with our service resource info
    const provider = new WebTracerProvider({ resource });
    
    // Configure how spans are processed and exported
    provider.addSpanProcessor(new BatchSpanProcessor(exporter));

    // Auto-instrumentation for common web operations
    registerInstrumentations({
      instrumentations: [
        new DocumentLoadInstrumentation(),
        new FetchInstrumentation({
          clearTimingResources: true,
        }),
        new XMLHttpRequestInstrumentation(),
      ],
      tracerProvider: provider,
    });

    // Register the provider to make it the active implementation
    // Using ZoneContextManager enables context propagation with Angular's Zone.js
    provider.register({
      contextManager: new ZoneContextManager(),
    });
  }
}

This service performs several crucial functions:

  1. Resource Definition:
    • Creates a Resource object that identifies your service
    • The SERVICE_NAME and SERVICE_VERSION attributes are the minimum required to properly identify your application in traces
    • You can add more attributes like deployment environment, region, or instance ID
  2. Exporter Configuration:
    • Sets up a CollectorTraceExporter that sends data to your tracing backend
    • The URL should point to your OpenTelemetry Collector endpoint
    • You can add authentication headers, custom headers, and other HTTP options
    • In production, you'll typically use HTTPS with proper authentication
  3. Tracer Provider Setup:
    • Creates a WebTracerProvider specifically designed for browser environments
    • Associates the defined resources with the provider
    • The provider is the core component that creates tracers for your application
  4. Span Processor Configuration:
    • Adds a BatchSpanProcessor to efficiently process spans
    • The batch processor collects spans and sends them in batches to reduce network overhead
    • You can configure batch size, export frequency, and queue size
    • For debugging, you can use SimpleSpanProcessor instead for immediate exports
  5. Auto-Instrumentation Registration:
    • Registers built-in instrumentations for document load, fetch, and XMLHttpRequest
    • Each instrumentation can be configured with specific options:
      • DocumentLoadInstrumentation: Traces page load performance
      • FetchInstrumentation: Traces all fetch API requests
      • XMLHttpRequestInstrumentation: Traces all XHR requests
    • These auto-instrumentations capture data without manual code changes
  6. Provider Registration:
    • Registers the provider as the global tracer provider
    • Sets up the ZoneContextManager to properly propagate context in Angular's Zone.js
    • This is critical for maintaining trace context across asynchronous operations in Angular

For a production service, consider adding:

  1. Error Handling:
try {
  // Initialization code
} catch (error) {
  console.error('Failed to initialize tracing:', error);
  // Fallback or recovery logic
}
  1. Environment-Specific Configuration:
const isDevelopment = !environment.production;
const sampler = isDevelopment 
  ? new AlwaysOnSampler()  // Sample all traces in development
  : new TraceIdRatioBasedSampler(0.1);  // Sample 10% in production

const provider = new WebTracerProvider({
  resource,
  sampler
});
  1. Performance Optimization:
// Increase batch size in production for performance
const batchSize = environment.production ? 100 : 10;
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
  maxExportBatchSize: batchSize,
  scheduledDelayMillis: environment.production ? 5000 : 1000
}));

This service is the foundation of your tracing infrastructure and should be initialized early in your application's lifecycle.

### Integrating the Tracing Service with Angular Module

Now that you have the tracing service, integrate it into your Angular application by adding it to the app module:

```typescript
// app.module.ts
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TracingService } from './services/tracing.service';

// Factory function to initialize tracing
export function initializeTracing(tracingService: TracingService) {
  return () => { /* Tracing is initialized in the constructor */ };
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    TracingService,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeTracing,
      deps: [TracingService],
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

This integration ensures that OpenTelemetry is properly initialized before your application starts. Here's a breakdown of what's happening:

  1. APP_INITIALIZER Token:
    • Angular's APP_INITIALIZER token allows you to register functions that will run during app initialization
    • These functions run before your application bootstraps and renders
    • This ensures OpenTelemetry is fully configured before any application code executes
  2. Factory Function Pattern:
    • The initializeTracing factory function returns another function
    • This returned function is executed during initialization
    • Since our TracingService initializes in its constructor, the function body is empty
    • For services that require async initialization, you would return a Promise here
  3. Dependencies Injection:
    • The deps array tells Angular what to inject into the factory function
    • By specifying TracingService, Angular will instantiate it and pass it to the factory
    • This creates a single instance of the service that's shared across the application
  4. Multi Provider:
    • The multi: true option tells Angular that this isn't the only APP_INITIALIZER
    • This allows multiple initialization functions to coexist
    • Each will be executed during the application startup phase
💡
Now, fix production Angular observability issues instantly—right from your IDE, with AI and Last9 MCP. Bring real-time production context — logs, metrics, and traces — into your local environment to auto-fix code faster.

For more complex applications, consider these advanced integration patterns:

  1. Conditional Initialization based on Environment:
export function initializeTracing(tracingService: TracingService, env: EnvironmentService) {
  return () => {
    if (env.enableTracing) {
      // Only initialize tracing if enabled in this environment
      return tracingService.initialize();
    }
    return Promise.resolve();
  };
}

// In providers array:
{
  provide: APP_INITIALIZER,
  useFactory: initializeTracing,
  deps: [TracingService, EnvironmentService],
  multi: true
}
  1. Async Initialization with Explicit Promise:
export function initializeTracing(tracingService: TracingService) {
  return () => {
    return tracingService.asyncInitialize().catch(err => {
      console.error('Failed to initialize tracing:', err);
      // Return resolved promise to continue app initialization despite tracing failure
      return Promise.resolve();
    });
  };
}
  1. Lazy Loading Module Support: For applications using lazy-loaded modules, you might need to register additional instrumentation:
// feature.module.ts
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { FeatureTracingService } from './feature-tracing.service';

@NgModule({
  providers: [FeatureTracingService]
})
export class FeatureModule {
  constructor(
    @Optional() @SkipSelf() parentModule: FeatureModule,
    featureTracingService: FeatureTracingService
  ) {
    if (parentModule) {
      throw new Error('FeatureModule is already loaded.');
    }
    // Register additional instrumentation for this module
    featureTracingService.registerInstrumentation();
  }
}

By properly integrating the tracing service with your Angular module, you ensure that OpenTelemetry is initialized at the right time in your application's lifecycle, providing consistent and reliable instrumentation.

Tracking Component Lifecycle Events with Custom Spans

To get even more insights, you can track component lifecycle events:

// monitored.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { trace, context, Span } from '@opentelemetry/api';

@Component({
  selector: 'app-monitored',
  templateUrl: './monitored.component.html'
})
export class MonitoredComponent implements OnInit, OnDestroy {
  private tracer = trace.getTracer('angular-components');
  private componentSpan: Span;

  ngOnInit(): void {
    this.componentSpan = this.tracer.startSpan('MonitoredComponent.lifecycle');
    
    // You can add attributes to your span
    this.componentSpan.setAttribute('component.route', window.location.pathname);
    
    // Track expensive operations
    this.trackExpensiveOperation();
  }

  trackExpensiveOperation(): void {
    const operationSpan = this.tracer.startSpan('expensive-calculation');
    
    try {
      // Your expensive operation here
      const result = this.calculateSomething();
      operationSpan.setAttribute('operation.result', result);
    } catch (error) {
      operationSpan.recordException(error);
    } finally {
      operationSpan.end();
    }
  }

  calculateSomething(): number {
    // Some calculation
    return 42;
  }

  ngOnDestroy(): void {
    if (this.componentSpan) {
      this.componentSpan.end();
    }
  }
}

Implementing User Interaction Tracking

User interactions are critical to understanding how people use your application. Here's how to track them:

// button.component.ts
import { Component } from '@angular/core';
import { trace } from '@opentelemetry/api';

@Component({
  selector: 'app-button',
  template: '<button (click)="handleClick()">Click me</button>'
})
export class ButtonComponent {
  private tracer = trace.getTracer('user-interactions');

  handleClick(): void {
    const clickSpan = this.tracer.startSpan('button-click');
    
    try {
      // Your click handler logic
      this.doSomething();
      clickSpan.setAttribute('interaction.success', true);
    } catch (error) {
      clickSpan.recordException(error);
      clickSpan.setAttribute('interaction.success', false);
    } finally {
      clickSpan.end();
    }
  }

  doSomething(): void {
    // Business logic here
  }
}

Intercepting and Monitoring HTTP Requests

Angular's HttpClient can be wrapped to provide automatic tracing:

// http-interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap, finalize } from 'rxjs/operators';
import { trace, context, SpanStatusCode } from '@opentelemetry/api';

@Injectable()
export class TelemetryHttpInterceptor implements HttpInterceptor {
  private tracer = trace.getTracer('http-requests');

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const span = this.tracer.startSpan(`HTTP ${req.method} ${req.url}`);
    
    // Add request details as span attributes
    span.setAttribute('http.method', req.method);
    span.setAttribute('http.url', req.url);
    
    return next.handle(req).pipe(
      tap(
        event => {
          if (event instanceof HttpResponse) {
            span.setAttribute('http.status_code', event.status);
            if (event.status >= 400) {
              span.setStatus({
                code: SpanStatusCode.ERROR,
                message: `HTTP Error ${event.status}`
              });
            }
          }
        },
        error => {
          if (error instanceof HttpErrorResponse) {
            span.setAttribute('http.status_code', error.status);
            span.setStatus({
              code: SpanStatusCode.ERROR,
              message: error.message
            });
          }
          span.recordException(error);
        }
      ),
      finalize(() => {
        span.end();
      })
    );
  }
}

Then add this interceptor to your app module:

// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TelemetryHttpInterceptor } from './interceptors/http-interceptor';

@NgModule({
  // ...
  providers: [
    // ...
    { provide: HTTP_INTERCEPTORS, useClass: TelemetryHttpInterceptor, multi: true }
  ]
})
export class AppModule { }
💡
Understanding logs in OpenTelemetry can be tricky at first—how-does-opentelemetry-logging-work breaks it down clearly with examples and use cases.

Configuring Exporters for Observability Backends

Once telemetry data is collected, it needs to be exported to an observability backend for analysis. Below are configuration examples for different backend systems:

Last9 Exporter Configuration

At Last9, we’ve built an observability platform for AI-native teams—bringing metrics, logs, and traces into one place. We support high-cardinality observability at scale and integrate with OpenTelemetry and Prometheus for correlated monitoring and alerting.

To send data from OpenTelemetry to Last9, you’ll need to configure the exporter with your specific endpoint and authentication details.

// Configure for Last9
const exporter = new CollectorTraceExporter({
  url: 'https://your-last9-endpoint/v1/traces',
  headers: {
    'Authorization': 'Bearer YOUR_LAST9_API_KEY'
  }
});

The CollectorTraceExporter class accepts various configuration options:

  • url: The Last9 endpoint where traces will be sent
  • headers: Authentication and additional HTTP headers
  • concurrencyLimit: Controls how many concurrent exports can occur (defaults to 15)
  • timeoutMillis: How long to wait before timing out requests (defaults to 15000ms)

You can also configure additional options like TLS/SSL settings if your Last9 deployment requires secure connections:

// Configure with TLS options
const exporter = new CollectorTraceExporter({
  url: 'https://your-last9-endpoint/v1/traces',
  headers: {
    'Authorization': 'Bearer YOUR_LAST9_API_KEY'
  },
  // For custom cert validation if needed
  clientKeyPem: fs.readFileSync('./client-key.pem', 'utf8'),
  clientCertPem: fs.readFileSync('./client-cert.pem', 'utf8'),
});

After setting up the exporter, add it to a span processor and then register it with your tracer provider:

// Create and register a BatchSpanProcessor with the Last9 exporter
const batchProcessor = new BatchSpanProcessor(exporter, {
  // Customize batch processing options
  maxQueueSize: 100,            // Maximum queue size (default: 100)
  maxExportBatchSize: 25,       // Maximum batch size (default: 512)
  scheduledDelayMillis: 5000,   // How often to export (default: 5000ms)
  exportTimeoutMillis: 30000    // Export timeout (default: 30000ms)
});

// Add the processor to your tracer provider
provider.addSpanProcessor(batchProcessor);

Configuration for Jaeger

Jaeger is a popular open-source distributed tracing system that works well with OpenTelemetry. Developed by Uber and now part of the Cloud Native Computing Foundation (CNCF), Jaeger provides powerful tracing visualization and analysis features.

To configure OpenTelemetry to export data to Jaeger, you have a few options:

  1. Using the Collector Exporter for Jaeger's HTTP endpoint:
// Configure for Jaeger HTTP endpoint
const exporter = new CollectorTraceExporter({
  url: 'http://localhost:14268/api/traces'
});
  1. Using the dedicated Jaeger Exporter (for more Jaeger-specific features):
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';

// Configure the Jaeger exporter
const jaegerExporter = new JaegerExporter({
  // Jaeger agent configuration
  host: 'localhost',       // Default
  port: 6832,              // Default UDP port for Jaeger agent
  
  // Or use the collector directly
  // endpoint: 'http://localhost:14268/api/traces',
  
  // Service metadata
  maxPacketSize: 65000,    // UDP packet size limit
  serviceName: 'angular-frontend'
});

// Add to your tracer provider
provider.addSpanProcessor(new BatchSpanProcessor(jaegerExporter));

When configuring for Jaeger, consider these important points:

  • Authentication: If your Jaeger instance requires authentication, add the necessary headers to the exporter configuration.
  • Sampling: Jaeger works well with sampling to reduce volume for high-traffic applications.
  • Local Development: For local development, you can run Jaeger using Docker:
docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 14250:14250 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

Once configured, you can access the Jaeger UI at http://localhost:16686 to visualize and analyze your traces.

Configuration for Zipkin

Zipkin is another well-established open-source distributed tracing system. Created by Twitter, it offers a lightweight approach to tracing with an emphasis on ease of use.

To export traces to Zipkin with OpenTelemetry, you'll need to install the dedicated Zipkin exporter package:

npm install @opentelemetry/exporter-zipkin

Then configure the exporter in your tracing service:

// Configure for Zipkin
import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';

const zipkinExporter = new ZipkinExporter({
  url: 'http://localhost:9411/api/v2/spans',  // Default Zipkin collector endpoint
  serviceName: 'angular-frontend',            // Your service name
  
  // Optional configuration
  headers: {},                                // Additional HTTP headers if needed
  timeout: 10000                              // Timeout in milliseconds
});

// Add to your tracer provider
provider.addSpanProcessor(new BatchSpanProcessor(zipkinExporter));

Zipkin configuration considerations:

  • Service Name: The serviceName parameter is particularly important as it helps identify your application in Zipkin's UI.
  • Mapping to Zipkin Format: OpenTelemetry automatically handles the conversion from OpenTelemetry's data model to Zipkin's format.
  • Running Zipkin: For local development, you can run Zipkin using Docker:
docker run -d -p 9411:9411 openzipkin/zipkin

After setup, you can view your traces in the Zipkin UI at http://localhost:9411.

For production deployments of both Jaeger and Zipkin, you'd typically:

  1. Set up the collectors behind a load balancer
  2. Configure TLS/SSL for secure communication
  3. Implement proper authentication mechanisms
  4. Consider using the OpenTelemetry Collector as a middleware to batch, process, and route your telemetry data

Diagnosing and Resolving Common OpenTelemetry Implementation Issues

Even with a solid setup, you might run into some issues. Let's tackle the most common ones:

1. No Data Being Exported

Problem: You've set up everything, but no data is showing up in your backend.

Solution:

  • Check that your exporter URL is correct
  • Look for CORS issues in the browser console
  • Verify network connectivity to your collector
  • Make sure the correct headers are set for authentication
// Add debug logging to your exporter
const exporter = new CollectorTraceExporter({
  url: 'http://your-collector-endpoint/v1/traces',
  headers: { /* Your headers */ },
  // Add this to see what's happening
  getExportRequestHeaders: () => {
    console.log('Sending traces to collector');
    return {};
  }
});

2. Zone.js Compatibility Issues

Problem: Errors related to Zone.js when initializing OpenTelemetry.

Solution: Make sure you're using the correct version of Zone.js compatible with your Angular version:

npm list zone.js @angular/core

If needed, update your Zone.js version:

npm install zone.js@0.11.x

3. Performance Impact

Problem: Adding OpenTelemetry has slowed down your application.

Solution:

  • Use sampling to reduce the volume of data:
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';

// Sample only 20% of traces
const sampler = new ParentBasedSampler({
  root: new TraceIdRatioBasedSampler(0.2)
});

const provider = new WebTracerProvider({
  resource,
  sampler
});
  • Batch your spans to reduce network overhead:
// Increase batch size and timeout
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
  maxExportBatchSize: 100,
  scheduledDelayMillis: 500
}));

4. Memory Leaks

Problem: Spans aren't being properly closed, causing memory leaks.

Solution: Always end your spans, even in error cases:

let span;
try {
  span = tracer.startSpan('operation');
  // Do work
} catch (error) {
  if (span) {
    span.recordException(error);
    span.setStatus({ code: SpanStatusCode.ERROR });
  }
  throw error;
} finally {
  if (span) {
    span.end();
  }
}
💡
To identify root spans in the OpenTelemetry Collector, this guide offers practical examples using filter and transform processors: Identify Root Spans in Otel Collector.

To maximize the benefits of OpenTelemetry in your Angular applications, follow these best practices:

To get the most out of OpenTelemetry in your Angular applications:

1. Create a Semantic Naming Convention

Establish a consistent naming pattern for your spans and attributes:

Entity Type Naming Convention Example
Component Spans ComponentName.lifecycle UserProfile.lifecycle
HTTP Requests HTTP METHOD path HTTP GET /api/users
User Interactions element-type.action button.click
Custom Business Events business-domain.event checkout.completed

2. Balance Detail and Volume

Be selective about what you trace to avoid overwhelming your system:

What to Trace What to Skip
Key user journeys Every mouse movement
Performance-critical operations Trivial operations
Error-prone areas Stable, well-tested code
Business-critical flows Background refreshes

3. Add Context to Your Spans

Make your traces more useful by adding context:

// Add user context (anonymized)
span.setAttribute('user.id_hash', hashUserId(userId));
span.setAttribute('user.type', userType);

// Add business context
span.setAttribute('order.value', orderTotal);
span.setAttribute('order.items_count', itemCount);

// Add technical context
span.setAttribute('browser.name', getBrowserName());
span.setAttribute('app.version', appVersion);

Angular Router Integration for Navigation Tracking

Tracking router navigation is essential for understanding user journeys through your application. Here's how to create a service that traces Angular router events:

// router-tracing.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { trace, context, Span } from '@opentelemetry/api';
import { filter } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class RouterTracingService {
  private tracer = trace.getTracer('angular-router');
  private activeNavigationSpan: Span | undefined;

  constructor(private router: Router) {
    this.setupRouterTracing();
  }

  private setupRouterTracing(): void {
    // Handle navigation start
    this.router.events.pipe(
      filter(event => event instanceof NavigationStart)
    ).subscribe((event: NavigationStart) => {
      this.activeNavigationSpan = this.tracer.startSpan(`route change to ${event.url}`);
      this.activeNavigationSpan.setAttribute('router.url', event.url);
      this.activeNavigationSpan.setAttribute('router.navigation_id', event.id);
    });

    // Handle successful navigation
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      if (this.activeNavigationSpan) {
        this.activeNavigationSpan.setAttribute('router.navigation_end', true);
        this.activeNavigationSpan.end();
        this.activeNavigationSpan = undefined;
      }
    });

    // Handle navigation cancellation
    this.router.events.pipe(
      filter(event => event instanceof NavigationCancel)
    ).subscribe((event: NavigationCancel) => {
      if (this.activeNavigationSpan) {
        this.activeNavigationSpan.setAttribute('router.cancelled', true);
        this.activeNavigationSpan.setAttribute('router.cancel_reason', event.reason);
        this.activeNavigationSpan.end();
        this.activeNavigationSpan = undefined;
      }
    });

    // Handle navigation errors
    this.router.events.pipe(
      filter(event => event instanceof NavigationError)
    ).subscribe((event: NavigationError) => {
      if (this.activeNavigationSpan) {
        this.activeNavigationSpan.setAttribute('router.error', true);
        this.activeNavigationSpan.recordException(event.error);
        this.activeNavigationSpan.end();
        this.activeNavigationSpan = undefined;
      }
    });
  }
}

The Angular Router is a critical part of single-page applications. Here's how to integrate OpenTelemetry with it:

// router-tracing.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { trace, context, Span } from '@opentelemetry/api';
import { filter } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class RouterTracingService {
  private tracer = trace.getTracer('angular-router');
  private activeNavigationSpan: Span | undefined;

  constructor(private router: Router) {
    this.setupRouterTracing();
  }

  private setupRouterTracing(): void {
    // Handle navigation start
    this.router.events.pipe(
      filter(event => event instanceof NavigationStart)
    ).subscribe((event: NavigationStart) => {
      this.activeNavigationSpan = this.tracer.startSpan(`route change to ${event.url}`);
      this.activeNavigationSpan.setAttribute('router.url', event.url);
      this.activeNavigationSpan.setAttribute('router.navigation_id', event.id);
    });

    // Handle successful navigation
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      if (this.activeNavigationSpan) {
        this.activeNavigationSpan.setAttribute('router.navigation_end', true);
        this.activeNavigationSpan.end();
        this.activeNavigationSpan = undefined;
      }
    });

    // Handle navigation cancellation
    this.router.events.pipe(
      filter(event => event instanceof NavigationCancel)
    ).subscribe((event: NavigationCancel) => {
      if (this.activeNavigationSpan) {
        this.activeNavigationSpan.setAttribute('router.cancelled', true);
        this.activeNavigationSpan.setAttribute('router.cancel_reason', event.reason);
        this.activeNavigationSpan.end();
        this.activeNavigationSpan = undefined;
      }
    });

    // Handle navigation errors
    this.router.events.pipe(
      filter(event => event instanceof NavigationError)
    ).subscribe((event: NavigationError) => {
      if (this.activeNavigationSpan) {
        this.activeNavigationSpan.setAttribute('router.error', true);
        this.activeNavigationSpan.recordException(event.error);
        this.activeNavigationSpan.end();
        this.activeNavigationSpan = undefined;
      }
    });
  }
}

Add this service to your app module providers and it will automatically trace all route changes.

💡
If you're exploring how the OpenTelemetry Protocol (OTLP) fits into your observability stack, this guide offers a clear breakdown of its role in transmitting traces, metrics, and logs efficiently.

Core Web Vitals and Performance Metrics Collection

Beyond tracing, OpenTelemetry can help you collect valuable performance metrics:

// performance.service.ts
import { Injectable } from '@angular/core';
import { trace } from '@opentelemetry/api';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';

@Injectable({
  providedIn: 'root'
})
export class PerformanceService {
  private tracer = trace.getTracer('performance');

  measureFirstContentfulPaint(): void {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      for (const entry of entries) {
        if (entry.name === 'first-contentful-paint') {
          const span = this.tracer.startSpan('FCP');
          span.setAttribute('performance.fcp.time_ms', entry.startTime);
          span.end();
        }
      }
      observer.disconnect();
    });
    observer.observe({ type: 'paint', buffered: true });
  }

  measureLargestContentfulPaint(): void {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries();
      // We only care about the latest LCP value
      const lcpEntry = entries[entries.length - 1];
      const span = this.tracer.startSpan('LCP');
      span.setAttribute('performance.lcp.time_ms', lcpEntry.startTime);
      span.setAttribute('performance.lcp.element', lcpEntry.element?.tagName || 'unknown');
      span.end();
      observer.disconnect();
    });
    observer.observe({ type: 'largest-contentful-paint', buffered: true });
  }

  measureCumulativeLayoutShift(): void {
    let cumulativeScore = 0;
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          cumulativeScore += entry.value;
        }
      }
    });
    observer.observe({ type: 'layout-shift', buffered: true });

    // Create a span after the page is fully loaded
    window.addEventListener('load', () => {
      // Wait a bit to capture more layout shifts
      setTimeout(() => {
        const span = this.tracer.startSpan('CLS');
        span.setAttribute('performance.cls.score', cumulativeScore);
        span.end();
      }, 5000);
    });
  }
}

Summary and Next Steps

Implementing OpenTelemetry in Angular applications provides detailed visibility into application behavior and performance. This instrumentation helps identify issues earlier, understand user interactions better, and correlate frontend experiences with backend services.

For effective implementation:

  • Start with critical user paths
  • Maintain consistent naming conventions
  • Balance instrumentation detail with performance impact
  • Gradually expand coverage as you grow more familiar with the framework

Additional Resources

FAQs

What is the difference between OpenTelemetry and traditional logging?

Traditional logging captures specific events or errors that developers explicitly code. OpenTelemetry, on the other hand, provides a more comprehensive view of your application by automatically capturing distributed traces, metrics, and logs. It gives you context around how requests flow through your system, allowing you to see the relationships between different parts of your application.

Will OpenTelemetry slow down my Angular application?

When implemented correctly, OpenTelemetry adds minimal overhead to your application. Using techniques like sampling and batching can further reduce its impact. The benefits of the insights gained typically outweigh the small performance cost.

How does OpenTelemetry work with existing monitoring tools?

OpenTelemetry is designed to be vendor-agnostic. It provides a standardized way to collect telemetry data that can then be exported to virtually any observability backend. This means you can use OpenTelemetry with your existing monitoring stack, whether that's Last9, Prometheus, Jaeger, or other tools.

Do I need to instrument every part of my application?

No, you don't need to instrument everything. Start with the most critical paths in your application, such as user authentication, checkout processes, or performance-critical features. You can always add more instrumentation later as needed.

How can I filter sensitive data from my traces?

OpenTelemetry allows you to implement processors that can modify spans before they're exported. You can use these to filter out sensitive information:

import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';

// Create a custom processor to filter sensitive data
class SensitiveDataProcessor implements SpanProcessor {
  onStart(span, parentContext) {}
  
  onEnd(span) {
    // Remove sensitive attributes
    if (span.attributes['http.url'] && span.attributes['http.url'].includes('/auth')) {
      span.attributes['http.url'] = '[REDACTED]';
    }
    
    // Remove PII
    if (span.attributes['user.email']) {
      span.attributes['user.email'] = '[REDACTED]';
    }
  }
  
  shutdown() {
    return Promise.resolve();
  }
}

// Add the processor to your provider
provider.addSpanProcessor(new SensitiveDataProcessor());

Authors
Prathamesh Sonpatki

Prathamesh Sonpatki

Prathamesh works as an evangelist at Last9, runs SRE stories - where SRE and DevOps folks share their stories, and maintains o11y.wiki - a glossary of all terms related to observability.

X

Contents

Do More with Less

Unlock high cardinality monitoring for your teams.