Angular HTTP Interceptors: creating a custom http diagnostics report

What is an Angular Interceptor?

Angular interceptors are a powerful mechanism that allows you to intercept HTTP requests and responses. They are a part of Angular’s HTTP client module and can be used to perform various tasks, such as adding headers to requests, handling errors, or, in our case, logging network calls.

Creating the HTTP Interceptor

Let’s start by creating a new interceptor that will log network calls to the session storage. Open your terminal and run the following command to generate a new interceptor:


ng generate interceptor logger

This command will generate a new interceptor file named logger.interceptor.ts in the src/app folder.

The implementation will look sort of like this , which just includes all the boilerplate angular adds when you generate the interceptor via the CLI

We’re going to modify this slightly to give us access to both request and response events and this will make it easier for us to add our logic for storing both our request and response data into session storage.

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpResponse
} from '@angular/common/http';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggerInterceptor implements HttpInterceptor {
  constructor() {
    // TODO dependency injection for session storage service

  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap({
        next: (event: HttpEvent<any>) => {
          if (event instanceof HttpResponse) {
            // TODO session storage implementation

          }
        },
        error: (error) => {

        }
      }))
    }

}

Now that we have our foundation for our interceptor lets pivot slightly and add a service for storing network logs in session storage and processing those logs to generate diagnostics and insights into our network activity

The session storage implementation

I cover the full implementation in a separate blog so please check this article here if you want to find out more about the session storage implementation and some of the other moving parts of this service.
https://runninghill.co.za/creating-a-generic-session-storage-implementation-in-angular/

Your session storage service needs to look like this

import { Injectable } from '@angular/core';
export const REQUEST_ARRAY_KEY = "reqObjectArray"
export const RESPONSE_ARRAY_KEY = "resObjectArray"
export interface RequestArray {
  size: string
  method: string
  responseType: string
  url: string
  urlWithParams: string
  count: number
}

@Injectable({
  providedIn: 'root'
})
export class SessionStorageService {

  constructor() { }

  getItem(key: string) {
    try {
      const result = JSON.parse(sessionStorage.getItem(key) || '{}');
      return result;

    } catch (error: any) {
      console.log(error);
    }
  }

  setItem(key: string, value: string | any) {
    try {
      return sessionStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.log(error);
    }
  }

  removeItem(key: string) {
    try {
      return sessionStorage.removeItem(key);
    } catch (error) {
      console.log(error);

    }
  }

  clearEntireSession() {
    sessionStorage.clear();
  }

  setArrayItem(key: string, value: any) {
    let array = this.getItem(key);
    if (array && array.length > 0) {
      array.push(value);
    }
    else {
      array = [value];
    }
    this.setItem(key, array);
  }

  reduceAndCountRequests(requestList: RequestArray[]) {
    let dedupe: RequestArray[] = [];
    dedupe = this.removeDuplicates(requestList, "urlWithParams")
    dedupe.forEach(element => {
      const propValues = this.findOccurrences(requestList, "urlWithParams", element.urlWithParams)
      element.count = propValues
    });
    dedupe.sort((a, b) => b.count - a.count);
    return dedupe;
  }

  findOccurrences(arr: any[], prop: string, value: string) {
    const matches = arr.filter(obj => obj[prop] === value);
    return matches.length;
  }

  removeDuplicates = (arr: RequestArray[], prop: keyof RequestArray): RequestArray[] => {
    const seen: { [key: string]: boolean } = {};
    return arr.reduce((acc: RequestArray[], obj: RequestArray) => {
      if (!seen[obj[prop].toString()]) {
        seen[obj[prop].toString()] = true;
        acc.push(obj);
      }
      return acc;
    }, []);
  }

  retrieveRequestObjectFromStorage() {
    const resData = this.getItem(RESPONSE_ARRAY_KEY)
    const reqData: RequestArray[] = this.getItem(REQUEST_ARRAY_KEY)
    const requestData = this.reduceAndCountRequests(reqData)
    const responseData = this.reduceAndCountRequests(resData)
    return { requestData, responseData }
  }

	}

Lets break down some of the code implemented here so we understand what we’re trying to achieve. As mentioned I’ve covered the implementation of this as a generic service in a separate blog so I’m only going to break down the additional logic I added for processing the network logs.

setArrayItem(key: string, value: any)

This function is responsible for storing an item in an array within sessionStorage. It takes a key and a value as parameters. Here’s what it does:

  • It retrieves the current array stored under the given key using the getItem method.
  • If the array already exists and has items, it appends the new value to it.
  • If the array doesn’t exist or is empty, it creates a new array with the value.
  • Finally, it stores the updated array back in sessionStorage using the setItem method.

reduceAndCountRequests(requestList: RequestArray[])

This function operates on an array of RequestArray objects, which represents network requests. Here’s what it does:

  • It starts by creating an empty array dedupe and assigns it the result of calling the removeDuplicates function on requestList. This removes any duplicate entries from the input array based on the urlWithParams property.
  • It then iterates over the dedupe array using forEach, and for each unique request object, it counts how many times it appears in the original requestList. The count is stored in the count property of each request object.
  • After counting occurrences, it sorts the dedupe array in descending order based on the count property.
  • Finally, it returns the sorted dedupe array, which now contains unique requests with counts indicating how many times each request occurred.

This function is useful for analyzing and ranking the most frequently occurring network requests.

findOccurrences(arr: any[], prop: string, value: string)

This utility function searches for the number of occurrences of a specific value within an array of objects based on a specified prop (property). It iterates through the array and filters objects where the specified property matches the given value, then returns the count of matches.

removeDuplicates

This is a reusable utility function that removes duplicates from an array of objects based on a specified property. It uses a JavaScript reduce operation and an object seen to keep track of whether a specific value has been encountered before. It effectively deduplicates the input array based on the specified property.

retrieveRequestObjectFromStorage

This function retrieves and processes data stored in sessionStorage. Here’s what it does:

  • It retrieves two sets of data from sessionStorage using the getItem method, one for request data and another for response data.
  • It processes both sets of data using the reduceAndCountRequests function to obtain unique requests sorted by their occurrence count.
  • Finally, it returns an object containing the processed request and response data.

In summary, these functions provide a way to manage and analyze network request data stored in sessionStorage. They help with tasks like counting request occurrences, removing duplicates, and preparing the data for analysis or presentation.

Implementing the session storage logic into the interceptor

After adding our session storage implementation to the interceptor , our code will look like this

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpResponse
} from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { SessionStorageService, REQUEST_ARRAY_KEY, RESPONSE_ARRAY_KEY } from './session-storage.service';

@Injectable()
export class LoggerInterceptor implements HttpInterceptor {
  constructor(private sessionStorageService: SessionStorageService) {

  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const startTime = Date.now();
    // Modify the request here if needed
    const { body, method, responseType, url, urlWithParams } = req;
    const stringSize = JSON.stringify(body).length;
    const size = this.charactersToKilobytes(stringSize) + ' kb'
    this.sessionStorageService.setArrayItem(REQUEST_ARRAY_KEY, { method, responseType, url, urlWithParams, size, count: 1 });

    return next.handle(req).pipe(
      tap({
        next: (event: HttpEvent<any>) => {
          if (event instanceof HttpResponse) {
            const { body, status, url } = event;
            const urlWithParams = url
            const endTime = Date.now();
            const timeTaken = (endTime - startTime) + ' ms'
            const stringSize = JSON.stringify(body).length; // rough estimate in characters
            const size = this.charactersToKilobytes(stringSize) + ' kb'

            this.sessionStorageService.setArrayItem(RESPONSE_ARRAY_KEY, { urlWithParams, status, timeTaken, size, count: 1 })

          }
        },
        error: (error) => {
          // Handle or log errors globally here
          console.error('Error occurred:', error);
        }
      }))
  }

  charactersToKilobytes(numCharacters: number) {
    const bytes = numCharacters * 2; // UTF-16 encoding
    return bytes / 1024;
  }

}

Let’s break down what this code does:

Capturing Request Information

const startTime = Date.now();
// Modify the request here if needed
const { body, method, responseType, url, urlWithParams } = req;
const stringSize = JSON.stringify(body).length;
const size = this.charactersToKilobytes(stringSize) + ' kb'
this.sessionStorageService.setArrayItem(REQUEST_ARRAY_KEY, { method, responseType, url, urlWithParams, size, count: 1 });

  1. startTime is a timestamp captured when the request is initiated using Date.now(). This will be used to calculate the time taken for the request.
  2. The request properties such as body, method, responseType, url, and urlWithParams are extracted from the req object, which represents the outgoing HTTP request.
  3. stringSize calculates the size of the request body by converting it to a JSON string and measuring its length in characters.
  4. size converts the stringSize to kilobytes using a method called charactersToKilobytes
  5. Finally, this.sessionStorageService.setArrayItem is used to store this request information in sessionStorage under the REQUEST_ARRAY_KEY. It includes properties such as the request method, response type, URL, URL with parameters, size, and an initial count of 1 (indicating this is the first occurrence of the request).

Capturing Response Information

const { body, status, url } = event;
const urlWithParams = url
const endTime = Date.now();
const timeTaken = (endTime - startTime) + ' ms'
const stringSize = JSON.stringify(body).length; // rough estimate in characters
const size = this.charactersToKilobytes(stringSize) + ' kb'

this.sessionStorageService.setArrayItem(RESPONSE_ARRAY_KEY, { urlWithParams, status, timeTaken, size, count: 1 })

  1. The response properties such as body, status, and url are extracted from the event object, which represents the incoming HTTP response.
  2. urlWithParams is assigned the same value as url, which appears to be creating a copy of the URL.
  3. endTime is another timestamp captured when the response is received, allowing for the calculation of the time taken for the request.
  4. timeTaken calculates the time taken by subtracting startTime from endTime and appending ‘ ms’ to represent the time in milliseconds.
  5. stringSize calculates the size of the response body in a similar manner to the request.
  6. size converts the stringSize to kilobytes, just like in the request section.
  7. Finally, this.sessionStorageService.setArrayItem is used again, but this time, it stores the response information in sessionStorage under the RESPONSE_ARRAY_KEY. It includes properties such as the URL with parameters, response status, time taken, size, and an initial count of 1 (indicating this is the first occurrence of the response).

Seeing our implementation in action

The last step before we can test out our implementation is to register our interceptor

Register the interceptor in your app.module.ts file by providing it in the HTTP_INTERCEPTORS multi-provider token. Import the necessary modules and add the interceptor to the providers array in the NgModule decorator:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { LoggerInterceptor } from './logger.interceptor';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [ {
    provide: HTTP_INTERCEPTORS,
    useClass: LoggerInterceptor,
    multi: true,
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }

Below , I wrote a simple application which makes network requests in a loop, you can observe both our session storage logs and the result of the utility functions we wrote to process the request information and generate diagnostics.

If you would like to see this code and run it locally for yourself please find the repo here
https://github.com/Yashlin-Naidoo/BaseUi.Angular/tree/feature/interceptor

Conclusion

In summary, this Angular HTTP interceptor, along with the associated session storage service, offers a robust solution for logging and analyzing network activity in your Angular application. You can use this approach to gain insights into your application’s performance, troubleshoot issues, and optimize your HTTP requests and responses

2 Comments

  1. This is a great post! I’m currently working on a project that uses AngularJS and I’m finding the same issues you mention. I’m going to try the solution you mention and see if it works.

  2. Pingback:Downloading Objects as JSON files in Angular – Runninghill Software Development

Leave a Comment

Your email address will not be published. Required fields are marked *