Let's face it, users aren't very good at providing feedback. "It doesn't work." Ok, but WHAT doesn't work? We developers probably spend as much time trying to collect enough information from QA or users on how something isn't working as we do trying to fix it.
I was recently working on an Angular application that has a lot of tabs and HTTP calls. It was often difficult to find the exact moment during the user's browsing timeline of when an error modal was displayed and what the server response was. You might be thinking there's Application Insights or Sentry or [insert favorite logging tool here] and you'd be correct, those things do exist, but they might only log an error given that you have a 500 message, for example.
HAR Introduction
If you're not familiar with a HAR file, it's simply a JSON file that contains information about a set of requests and responses in the browser. You can easily import and export these files from your browser's dev tools.
Open your DevTools in your favorite browser. The steps shown here are for Chrome. Firefox and Edge are slightly different but provide similar functionality.
- Click the network tab and refresh the page.
- This button allows you to import a HAR file.
- This button exports a HAR file.
One of the cool parts about this process is that you can pass this file on to another developer. Perhaps you're creating your own bug report, or maybe the information needs to be reviewed by a senior resource. That person down the line can easily import the HAR file and get a snapshot of the network events that took place.
Angular
I have a completed demo on StackBlitz that I'll be referencing and you can view the full code on GitHub .
(If you don't see the embedded StackBlitz, your browser is probably blocking the cookies)
I built a method to capture HTTP events through the use of an HTTP interceptor and save to a HAR file from Angular. Version 1 is successful at creating the files and can be imported into Chrome or other tools. This first round requires the users to click a button when an error modal is displayed. I would like to carry this through to something more automated like triggering an email to go to a CSR or developer.
ActivityWatcher
The ActivityWatcher
class is where most of the magic happens. It's also not very interesting as far as code goes, so I'm not going to go line by line. However, I do want to highlight the methods and what they do.
First, we create a new log by calling startNewActivity
with the current page URL. For my purposes, I only wanted to record the current page load. If you need to track multiple pages, you could probably do that.
For each request and response we use the HTTP interceptor to hand off the ActivityWatcher
.
When we make a request we create a new log entry recording any headers, the querystring and if provided, any post data by calling addRequest
.
Once we get a response back we can call addResponse
to find the entry created above and update the response body. This also calculates the amount of time a request took to complete.
One thing I'd like to come back to is a way to get the mime type of the events. I'm assuming that we're dealing with JSON, but obviously that's not 100% ideal.
In case of a failure, addError
provides a way to set any errors we got as the response body.
The bread winner of this class is getHar
. This method returns a data URL that we can use in a link tag to download the HAR file to the client. As I mentioned previously, I think this would be better sent as an email directly, or saved to a secure blob storage or something. What's cool about this process is that it's utilizing JavaScript's built-in Blob class to house the stringified version of our JSON log to turn that into a file.
Let's look at how to start a new activity in the code.
AppComponent
The AppComponent
in the demo is pretty simple. Obviously, a real application would look different. In the project I wrote this in, there was an Angular global error handler. That handler would log to Application Insights on Azure, and present a modal to the user. On that modal, I added a button to click that called the HAR generation and downloaded it to the client's machine. Here in the demo app, I'm doing all of that in AppComponent
including some sample API calls.
Even if you end up with a more complex scenario, you will likely want the constructor setup this way.
constructor(private readonly activityWatcher: ActivityWatcher, router: Router, private readonly httpClient: HttpClient) {
router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
activityWatcher.startNewActivity((event as NavigationStart).url);
}
});
}
We're going to listen for a NavigationStart
event and grab that URL. This is likely going to be something like myapp.com/students
.
We've got a button on the screen to make some get
calls with the HTTP client. I just found some random free test API endpoints I could call, plus one I'm faking as a "local" API call. None of that's particularly interesting, but we need something in the log, or it would be a pretty boring demo.
The most important method in AppComponent
is below.
generateHar() {
const blobLink = <HTMLElement>document.getElementById('blobLink');
const blobUrl = this.activityWatcher.getHar();
blobLink.setAttribute('href', blobUrl);
blobLink.setAttribute('download', `${new Date().toISOString()}.har`);
blobLink.click();
}
On the HTML side of the AppComponent
there's a hidden link as <a id="blobLink" style="display: none"></a>
. Once we get the data URL from the ActivityWatcher
, we can set the link's href value and simulate a click. The link ends up looking like this after the button is clicked:
<a _ngcontent-oso-c16="" id="blobLink" style="display: none;"
href="blob:http://localhost:4200/c1376fd4-3c58-4e01-8515-49339103971c"
download="2021-09-02T22:28:16.567Z.har"></a>
Neat!
Now for the interceptor.
CustomInterceptor
The HttpInterceptor
is sort of the glue that brings all of this together. You could easily integrate this into an existing interceptor, if you have one. I have it set up so that you can easily ignore certain URLs, for instance the login call. After all, we don't want to save the user's credentials. If the URL isn't in the exclusion list, then pass the request and a start time to the ActivityWatcher
and then follow whatever normal execution path you have. If the request is successful, then we'll call into the ActivityWatcher
again with the response and the start time. If there's an error, we can log that too.
The start time is purely for an execution time calculation. You could do without it, as long as you change the watcher to record 0.
Chrome was kind of picky about some of the properties. If a request body is empty, for instance, it won't import the file. Fiddler Everywhere was less picky about it in my testing.
Conclusion
There are some pitfalls with this process that you should be aware of and there's room for improvement.
This doesn't record console errors/warnings. You might find that Angular or one of your modules produces an error that's not related to HTTP calls. You can still log that to Application Insights or Sentry.io of course, but it might be interesting to package those logs alongside of the HAR component.
The biggest issue that I see is HIPAA/PII/etc. There is a danger that you're going to be logging sensitive information depending on your particular application, and you will have to weigh if that's worth the extra debugging information or not. You could filter out URLs, as I mentioned previously, or perhaps write some kind of scrubber. Maybe you don't care about the in and out data unless there's an error, and you could more safely collect these details.
Even with those pitfalls in mind, there were numerous occasions when having a QA person provide these HAR files to a developer was very helpful. We could see what the initial URL was and what the server's response was to data requests. That made the round and round of trying to extract crucial triage information from the client or QA much shorter.
I hope this helps you in your programming journeys, or at least provided some interesting information.
Comments
You can also comment directly on GitHub.