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.
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 let’s 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
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/
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:
key
using the getItem
method.value
to it.value
.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:
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.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.dedupe
array in descending order based on the count
property.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:
sessionStorage
using the getItem
method, one for request data and another for response data.reduceAndCountRequests
function to obtain unique requests sorted by their occurrence count.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.
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:
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 using Date.now()
. This will be used to calculate the time taken for the request.body
, method
, responseType
, url
, and urlWithParams
are extracted from the req
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 the stringSize
to kilobytes using a method called charactersToKilobytes
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).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 })
body
, status
, and url
are extracted from the event
object, which represents the incoming HTTP response.urlWithParams
is assigned the same value as url
, 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 subtracting startTime
from endTime
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 the stringSize
to kilobytes, just like in the request section.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).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
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
WRITTEN BY
Yashlin Naidoo