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 thegetItem
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 thesetItem
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 theremoveDuplicates
function onrequestList
. This removes any duplicate entries from the input array based on theurlWithParams
property. - It then iterates over the
dedupe
array usingforEach
, and for each unique request object, it counts how many times it appears in the originalrequestList
. The count is stored in thecount
property of each request object. - After counting occurrences, it sorts the
dedupe
array in descending order based on thecount
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 thegetItem
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 });
startTime
is a timestamp captured when the request is initiated usingDate.now()
. This will be used to calculate the time taken for the request.- The request properties such as
body
,method
,responseType
,url
, andurlWithParams
are extracted from thereq
object, which represents the outgoing HTTP request. stringSize
calculates the size of the request body by converting it to a JSON string and measuring its length in characters.size
converts thestringSize
to kilobytes using a method calledcharactersToKilobytes
- Finally,
this.sessionStorageService.setArrayItem
is used to store this request information insessionStorage
under theREQUEST_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 })
- The response properties such as
body
,status
, andurl
are extracted from theevent
object, which represents the incoming HTTP response. urlWithParams
is assigned the same value asurl
, which appears to be creating a copy of the URL.endTime
is another timestamp captured when the response is received, allowing for the calculation of the time taken for the request.timeTaken
calculates the time taken by subtractingstartTime
fromendTime
and appending ‘ ms’ to represent the time in milliseconds.stringSize
calculates the size of the response body in a similar manner to the request.size
converts thestringSize
to kilobytes, just like in the request section.- Finally,
this.sessionStorageService.setArrayItem
is used again, but this time, it stores the response information insessionStorage
under theRESPONSE_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