Angular  

Integrate New Relic with Angular for SPA Monitoring, API Tracing, and Error Tracking

New Relic Browser Monitoring provides comprehensive observability for Angular Single Page Applications (SPAs). This article demonstrates how to integrate New Relic into an Angular application using the @newrelic/browser-agent NPM package, providing a more maintainable approach than traditional script-based implementations.

Prerequisites

Before starting, ensure you have:

  • Angular Application (Angular 15+ recommended)

  • New Relic Account with Browser Monitoring enabled

  • New Relic Credentials:

    • Account ID

    • License Key (Browser monitoring license key)

    • Application ID

    • Agent ID

    • Trust Key

You can find these credentials in your New Relic account:

  • Go to Account Settings → API keys → Browser monitoring

Why Use NPM Package Instead of Script Tag?

Advantages:

  • TypeScript support with full type safety

  • Environment-based configuration management

  • Better integration with Angular's build system

  • Custom service layer for easier usage

  • Framework-specific features (SPA route tracking, HTTP interception)

  • Version control through package. json

Installation

Install the New Relic Browser Agent Package

npm install @newrelic/browse

Configuration

Add New Relic Configuration to Environment Files

export const environment = {
  newRelic: {
    enabled: true,
    accountID: 'YOUR_ACCOUNT_ID',
    trustKey: 'YOUR_TRUST_KEY',
    agentID: 'YOUR_AGENT_ID',
    licenseKey: 'YOUR_LICENSE_KEY',
    applicationID: 'YOUR_APPLICATION_ID'
  }
};

Create separate configurations for different environments (development, staging, production) with appropriate credentials.

Implementation

Step 1: Initialize New Relic in main.ts

The New Relic agent should be initialized after Angular bootstraps.

import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
import {NewRelicHttpInterceptor} from './app/global/services/newrelic-handler/newrelic-http.interceptor';

// ... other imports and providers

if (environment.production) {
  enableProdMode();
}

// Bootstrap Angular first
bootstrapApplication(AppComponent, {
  providers : [
    // ... your other providers
    provideHttpClient(withInterceptorsFromDi()),

    // NEW RELIC: Register HTTP Interceptor
    {
      provide : HTTP_INTERCEPTORS,
      useClass : NewRelicHttpInterceptor,
      multi : true
    }
  ]
})
    .then(() => {
      if (environment.newRelic?.enabled) {
        setTimeout(() => {
          try {
            // Capture native console functions before agent patches
            const nativeWarn = console.warn.bind(console);
            const nativeError = console.error.bind(console);

            // Initialize New Relic Browser Agent
            new BrowserAgent( {
              init : {
                distributed_tracing : { enabled : true },
                privacy : { cookies_enabled : true },
                ajax : { deny_list : [], enabled : true },
                session_trace : { enabled : true },
                session_replay : {
                  enabled : true,
                  sampling_rate : 10,
                  error_sampling_rate : 100
                },
                jserrors : {
                  enabled : true,
                  harvestConsoleErrors : false  // Don't capture console.error
                } as any,
                logging : {
                  enabled : true,
                  harvestConsoleErrors : false,
                  harvestConsoleWarns : false,
                  harvestConsoleInfo : false
                } as any,
                metrics : { enabled : true },
                page_action : { enabled : true }
              },
              info : {
                beacon : 'bam.nr-data.net',
                errorBeacon : 'bam.nr-data.net',
                licenseKey : environment.newRelic.licenseKey,
                applicationID : environment.newRelic.applicationID,
                sa : 1
              },
              loader_config : {
                accountID : environment.newRelic.accountID,
                trustKey : environment.newRelic.trustKey,
                agentID : environment.newRelic.agentID,
                licenseKey : environment.newRelic.licenseKey,
                applicationID : environment.newRelic.applicationID
              }
            });

            // Restore native console functions to prevent console logs from
            // being sent to New Relic
            console.warn = nativeWarn;
            console.error = nativeError;

            // Optional: Customize New Relic logging behavior
            const nr : any = (window as any).newrelic;
            if (nr?.log) {
              const originalLog = nr.log.bind(nr);
              nr.log = function(message: string, attributes ?: any) {
                const enhancedAttributes = { ... attributes };
                return originalLog(message, enhancedAttributes);
              };
            }
          } catch (error) {
            console.error('New Relic initialization failed:', error);
          }
        }, 100);
      }
    })
    .catch(err => console.log(err));
  • Initialize after Angular bootstrap to ensure proper timing

  • Capture and restore console functions to prevent console logs from being sent to New Relic

  • Wrap initialization in try-catch for error handling

Step 2: Create New Relic Service Wrapper

Create a service to wrap New Relic functionality: src/app/global/services/newrelic-handler/newrelic.service.ts

import { Injectable } from '@angular/core';
@Injectable({providedIn: 'root'})
export class NewRelicService {
  private isInitialized = false;

  constructor() {
    // Check if New Relic is already initialized (from main.ts)
    if ((window as any).newrelic) {
      this.isInitialized = true;
    }
  }

  /**
   * Report custom error to New Relic
   * @param error - Error object
   * @param customAttributes - Additional attributes to track
   */
  noticeError(error: Error, customAttributes?: Record<string, any>): void {
    if (!this.isReady()) return;

    try {
      const attributes = {
        timestamp: new Date().toISOString(),
        errorName: error.name,
        errorMessage: error.message,
        errorStack: (error as any).originalStack || error.stack,
        userAgent: navigator.userAgent,
        url: window.location.href,
        ...customAttributes
      };
      const nr = (window as any).newrelic;

      if (nr.log) {
        Object.entries(attributes).forEach(([key, value]) => {
          if (value !== undefined && value !== null && typeof value !== 'object') {
            try {
              nr.setCustomAttribute(key, value);
            } catch {}
          }
        });
        nr.log(error.message || 'Error occurred', {
          level: 'ERROR',
          ...attributes
        });
      }
    } catch (e) {
      console.error('New Relic error reporting failed:', e);
    }
  }

  /**
   * Track custom user action/event
   * @param name - Name of the action
   * @param attributes - Custom attributes for the action
   */
  addPageAction(name: string, attributes?: Record<string, any>): void {
    if (!this.isReady()) return;

    try {
      (window as any).newrelic.addPageAction(name, {
        ...attributes,
        timestamp: new Date().toISOString(),
        url: window.location.href,
        userAgent: navigator.userAgent
      });
    } catch (e) {
      console.error('New Relic page action failed:', e);
    }
  }

  /**
   * Set custom attribute for the current session
   * @param name - Attribute name
   * @param value - Attribute value
   */
  setCustomAttribute(name: string, value: string | number | boolean): void {
    if (!this.isReady()) return;

    try {
      (window as any).newrelic.setCustomAttribute(name, value);
    } catch (e) {
      console.error('New Relic custom attribute failed:', e);
    }
  }

  /**
   * Set user ID for tracking
   * @param userId - Unique user identifier
   */
  setUserId(userId: string): void {
    this.setCustomAttribute('userId', userId);
    this.setCustomAttribute('enduser.id', userId);
  }

  /**
   * Set user information
   * @param userInfo - User information object
   */
  setUserInfo(userInfo: { userId: string; email?: string; name?: string; role?: string }): void {
    if (userInfo.userId) this.setUserId(userInfo.userId);
    if (userInfo.email) this.setCustomAttribute('userEmail', userInfo.email);
    if (userInfo.name) this.setCustomAttribute('userName', userInfo.name);
    if (userInfo.role) this.setCustomAttribute('userRole', userInfo.role);
  }

  /**
   * Track page view manually (useful for SPA)
   * @param pageName - Name of the page/route
   */
  setPageViewName(pageName: string): void {
    if (!this.isReady()) return;

    try {
      (window as any).newrelic.setPageViewName(pageName);
    } catch (e) {
      console.error('New Relic page view name failed:', e);
    }
  }

  /**
   * Add release version for tracking
   * @param version - Application version
   */
  setApplicationVersion(version: string): void {
    this.setCustomAttribute('applicationVersion', version);
    this.setCustomAttribute('release', version);
  }

  /**
   * Check if New Relic is initialized and ready with full API
   */
  isReady(): boolean {
    const nr = (window as any).newrelic;
    const isReady =
      !!nr &&
      typeof nr.addPageAction === 'function' &&
      typeof nr.noticeError === 'function';

    if (isReady && !this.isInitialized) {
      this.isInitialized = true;
    }
    return isReady;
  }

  /**
   * Track custom metric
   * @param metricName - Name of the metric
   * @param value - Metric value
   * @param unit - Unit of measurement (default: 'ms')
   */
  trackMetric(metricName: string, value: number, unit: string = 'ms'): void {
    this.addPageAction('CustomMetric', { metricName, value, unit });
  }
}
  • Provides a clean TypeScript interface to New Relic

  • Includes readiness checks before making API calls

  • Handles errors gracefully

  • Adds useful metadata automatically

Step 3: Create Router Tracking Service

For SPAs, tracking route changes is crucial. So, create: src/app/global/services/newrelic-handler/newrelic-router-tracker.service.ts

import { Injectable } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationError, NavigationCancel, Event } from '@angular/router';
import { NewRelicService } from './newrelic.service';
import { filter } from 'rxjs/operators';

@Injectable({  providedIn: 'root'})
export class NewRelicRouterTrackerService {
  private navigationStartTime: number = 0;
  private currentUrl: string = '';
  private previousUrl: string = '';

  constructor(
    private router: Router,
    private newRelicService: NewRelicService
  ) {}

  /**
   * Start tracking Angular router navigation events
   */
  startTracking(): void {
    if (this.newRelicService.isReady()) {
      this.initializeTracking();
    } else {
      // Use polling with exponential backoff to wait for New Relic
      this.waitForNewRelic();
    }
  }

  /**
   * Wait for New Relic to be ready before initializing tracking
   * Uses polling with exponential backoff
   */
  private waitForNewRelic(attempt: number = 1, maxAttempts: number = 10): void {
    // Start with 100ms, increase with each attempt (100, 200, 400, 800, etc.)
    const delay = Math.min(100 * Math.pow(2, attempt - 1), 3000);

    console.log(`Waiting for New Relic to be ready (attempt ${attempt}/${maxAttempts})...`);

    setTimeout(() => {
      if (this.newRelicService.isReady()) {
        console.log('✓ New Relic is now ready, starting router tracking');
        this.initializeTracking();
      } else if (attempt < maxAttempts) {
        this.waitForNewRelic(attempt + 1, maxAttempts);
      } else {
        console.warn(`New Relic not ready after ${maxAttempts} attempts, router tracking disabled`);
      }
    }, delay);
  }

  /**
   * Initialize the actual tracking once New Relic is ready
   */
  private initializeTracking(): void {
    // Track navigation start
    this.router.events.pipe(
      filter((event: Event): event is NavigationStart => event instanceof NavigationStart)
    ).subscribe((event: NavigationStart) => {
      this.navigationStartTime = performance.now();
      this.previousUrl = this.currentUrl;
      this.currentUrl = event.url;

      this.newRelicService.addPageAction('RouteChangeStart', {
        url: event.url,
        previousUrl: this.previousUrl,
        navigationTrigger: event.navigationTrigger,
        restoredState: event.restoredState ? 'yes' : 'no'
      });
    });

    // Track navigation end (success)
    this.router.events.pipe(
      filter((event: Event): event is NavigationEnd => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      const duration = performance.now() - this.navigationStartTime;

      this.newRelicService.addPageAction('RouteChangeComplete', {
        url: event.urlAfterRedirects,
        previousUrl: this.previousUrl,
        duration: Math.round(duration),
        status: 'success'
      });

      // Set page view name for better tracking in New Relic
      const pageName = this.extractPageName(event.urlAfterRedirects);
      this.newRelicService.setPageViewName(pageName);

      // Track as successful route change metric
      this.newRelicService.trackMetric('RouteChangeDuration', duration, 'ms');
    });

    // Track navigation errors
    this.router.events.pipe(
      filter((event: Event): event is NavigationError => event instanceof NavigationError)
    ).subscribe((event: NavigationError) => {
      const duration = performance.now() - this.navigationStartTime;

      this.newRelicService.addPageAction('RouteChangeError', {
        url: event.url,
        previousUrl: this.previousUrl,
        duration: Math.round(duration),
        status: 'error',
        errorMessage: event.error?.message || 'Unknown navigation error'
      });

      // Report as error to New Relic
      const error = new Error(`Navigation Error: ${event.error?.message || 'Unknown error'}`);
      this.newRelicService.noticeError(error, {
        errorType: 'NavigationError',
        url: event.url,
        previousUrl: this.previousUrl
      });
    });

    // Track navigation cancel
    this.router.events.pipe(
      filter((event: Event): event is NavigationCancel => event instanceof NavigationCancel)
    ).subscribe((event: NavigationCancel) => {
      const duration = performance.now() - this.navigationStartTime;

      this.newRelicService.addPageAction('RouteChangeCancel', {
        url: event.url,
        previousUrl: this.previousUrl,
        duration: Math.round(duration),
        status: 'cancelled',
        reason: event.reason
      });
    });

    console.log('✓ New Relic Router Tracking started');
  }

  /**
   * Extract a clean page name from URL
   * @param url - Full URL path
   */
  private extractPageName(url: string): string {
    // Remove query parameters and fragments
    let cleanUrl = url.split('?')[0].split('#')[0];

    // Remove leading slash
    if (cleanUrl.startsWith('/')) {
      cleanUrl = cleanUrl.substring(1);
    }

    // If empty, it's the home page
    if (!cleanUrl) {
      return 'Home';
    }

    // Replace slashes with dots and capitalize
    const pageName = cleanUrl
      .split('/')
      .map(part => part.charAt(0).toUpperCase() + part.slice(1))
      .join('.');

    return pageName;
  }

  /**
   * Track specific route manually
   * @param routeName - Name of the route
   * @param metadata - Additional metadata
   */
  trackRouteManually(routeName: string, metadata?: Record<string, any>): void {
    this.newRelicService.addPageAction('ManualRouteTrack', {
      routeName,
      url: window.location.href,
      ...metadata
    });
  }
}
  • Handles timing issues with exponential backoff polling

  • Tracks all router events (start, end, error, cancel)

  • Measures navigation duration

  • Extracts clean page names for better dashboard organization

Step 4: Create HTTP Interceptor

Track API calls automatically: src/app/global/services/newrelic-handler/newrelic-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 } from 'rxjs/operators';
import { NewRelicService } from './newrelic.service';

@Injectable()
export class NewRelicHttpInterceptor implements HttpInterceptor {
  constructor(private newRelicService: NewRelicService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!this.newRelicService.isReady()) {
      return next.handle(req);
    }

    const startTime = performance.now();
    const requestDetails = {
      url: req.url,
      method: req.method,
      urlWithParams: req.urlWithParams
    };

    return next.handle(req).pipe(
      tap(
        event => {
          if (event instanceof HttpResponse) {
            const duration = Math.round(performance.now() - startTime);

            this.newRelicService.addPageAction('APICallSuccess', {
              ...requestDetails,
              statusCode: event.status,
              statusText: event.statusText,
              duration,
              responseType: event.type,
              contentType: event.headers.get('content-type') || 'unknown'
            });

            this.newRelicService.trackMetric(`API_${req.method}_Duration`, duration);
          }
        },
        error => {
          if (error instanceof HttpErrorResponse) {
            const duration = Math.round(performance.now() - startTime);

            this.newRelicService.addPageAction('APICallFailure', {
              ...requestDetails,
              statusCode: error.status,
              statusText: error.statusText,
              errorMessage: error.message,
              errorName: error.name,
              duration
            });

            const errorObj = new Error(`API Error`);

            this.newRelicService.noticeError(errorObj, {
              errorType: 'HttpError',
              httpMethod: req.method,
              apiUrl: req.url,
              apiEndpoint: new URL(req.url).pathname,
              statusCode: error.status,
              statusText: error.statusText,
              errorMessage: error.message,
              serverErrorMessage: error.error?.error || error.error?.message || error.error,
              duration,
              pageUrl: window.location.href,
              pagePath: window.location.pathname,
              timestamp: new Date().toISOString()
            });
          }
        }
      )
    );
  }
}
  • Automatically tracks all HTTP requests

  • Measures API call duration

  • Captures success and failure scenarios

  • Provides detailed error information

Step 5: Initialize Router Tracking in AppComponent

Start router tracking in your root component: src/app/app.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { NewRelicRouterTrackerService } from './global/services/newrelic-handler/newrelic-router-tracker.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(
    private router: Router,
    private newRelicRouterTracker: NewRelicRouterTrackerService
  ) {}

  ngOnInit() {
    // ... other initialization code

    // Start New Relic router tracking
    this.startNewRelicRouterTracking();
  }

  /**
   * Start New Relic Router Tracking
   */
  private startNewRelicRouterTracking(): void {
    try {
      // Start router tracking for SPA navigation
      this.newRelicRouterTracker.startTracking();
    } catch (error) {
      // Silently fail if New Relic is not available
      console.debug('New Relic router tracking not started:', error);
    }
  }
}

Advanced Features

User Identification

Track user sessions with user information:

import { NewRelicService } from './global/services/newrelic-handler/newrelic.service';

constructor(private newRelicService: NewRelicService) {}

onUserLogin(user: any) {
  this.newRelicService.setUserInfo({
    userId: user.id,
    email: user.email,
    name: user.name,
    role: user.role
  });
}

Custom Events

Track custom business events:

// Track a button click
this.newRelicService.addPageAction('ButtonClick', {
  buttonName: 'SubmitForm',
  formType: 'Contact',
  timestamp: new Date().toISOString()
});

// Track a purchase
this.newRelicService.addPageAction('Purchase', {
  productId: '123',
  amount: 99.99,
  currency: 'USD'
});

Application Version Tracking

Track application versions for release management:

this.newRelicService.setApplicationVersion('1.2.3');

Performance Considerations

  • New Relic data is batched and sent asynchronously

  • The agent has minimal performance impact

  • Use custom attributes sparingly to avoid payload size issues

Conclusion

This implementation provides a robust, production-ready New Relic integration for Angular applications. Which include:

  • Type-Safe Integration: Full TypeScript support

  • SPA Support: Automatic route tracking

  • API Monitoring: Automatic HTTP request tracking

  • Error Tracking: Comprehensive error reporting

  • Custom Events: Flexible event tracking

  • User Tracking: Session and user identification

  • Maintainable: Clean service-based architecture

More Articles from my Account