Adding Azure Application Insights To Your Angular App

There’s going to be a couple of assumptions for this guide on adding App Insights to an Angular app. The biggest of all is that you are familiar with the Application Insights product itself. Maybe you’ve used in an another app before, maybe in a C# backend application or maybe a React/Vanilla Javascript app. But the biggest thing is that you know what it does and how to create an instance in the Azure Portal.

So with that out of the way. Why use Application Insights at all in an Angular App? The biggest win of all is that it can be used to track every application error thrown by your application, both handled and unhandled. And the second is that it can be used to track pageviews of users as they move through your SPA, which can then be used to track navigation paths, where users drop off in your sign up process, how your pipelines perform etc.

With all that said and done. Let’s jump right into it.

Installing Application Insights Libraries

The first thing we need to is install the following NPM package

npm install @microsoft/applicationinsights-web --save

Now this next piece of code is going to look big but it should be easy to understand. We want to create an “ApplicationInsightsService” that can handle logging exceptions, setting the logged in user, and tracking page views. The service contents will look like so :

import { Injectable } from '@angular/core';
import { ApplicationInsights, IExceptionTelemetry, DistributedTracingModes } from '@microsoft/applicationinsights-web';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class ApplicationInsightsService {
  private appInsights : ApplicationInsights;

  constructor(private router: Router) {

    this.appInsights = new ApplicationInsights({
      config: {
        instrumentationKey: "YourKeyHere"
      }
    });

    this.appInsights.loadAppInsights();
    this.loadCustomTelemetryProperties();
    this.createRouterSubscription();
  }

  setUserId(userId: string) {
    this.appInsights.setAuthenticatedUserContext(userId);
  }

  clearUserId() {
    this.appInsights.clearAuthenticatedUserContext();
  }

  logPageView(name?: string, uri?: string) {
    this.appInsights.trackPageView({ name, uri});
  }

  logException(error : Error){
    let exception : IExceptionTelemetry = {
      exception : error
    };
    this.appInsights.trackException(exception);
  }

  private loadCustomTelemetryProperties()
  {
    this.appInsights.addTelemetryInitializer(envelope => 
      {
        var item = envelope.baseData;
        item.properties = item.properties || {};
        item.properties["ApplicationPlatform"] = "WEB";
        item.properties["ApplicationName"] = "My.Application.Name";
      }
    );
  }

  private createRouterSubscription()
  {
    this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: NavigationEnd) => {
      this.logPageView(null, event.urlAfterRedirects);
    });
  }
}

So a couple of notes :

  • LoadCustomTelemetryProperties can be used to set “custom” properties inside ApplicationInsights. This is handy to name your app, or pass through specific information that isn’t normally captured by AppInsights.
  • CreateRouterSubscription allows us to listen for navigation events and then log these as pageviews. By default AppInsights only logs full page refreshes so you will need this if you are using the internal Angular router.
  • LogException can be used to log exceptions, but you need to inject this service manually for it to be of any use (We will look at that shortly).
  • The @Injectable attribute has been told that the provider is for the root, this means there will only ever be one instance of this service for your entire app (Singleton).

Error Handling

So in theory the Application Insights javascript packages should catch all unhandled errors. But in reality Angular actually catches any internal errors (for example a bug in your code), and has it’s own way of handling them. This kicks in well before AppInsights as a chance to see the error, and so you won’t actually see your exceptions being logged to AppInsights at all!

Let’s fix that. We want to create an ErrorHandler implementation that does nothing but log the exception to AppInsights, and print out to the console (If we want to). The code looks like :

import { ErrorHandler, Injector, Injectable } from '@angular/core';
import { ApplicationInsightsService } from './application-insights.service';

@Injectable()
export class ApplicationInsightsErrorHandler implements ErrorHandler{

  constructor(private injector : Injector)
  {
  }

  handleError(error: any): void {
    this.injector.get<ApplicationInsightsService>(ApplicationInsightsService).logException(error);
    console.log(error);
  }
}

Now I know the use of Injector here is a bit flaky, but there is a bit of reasoning behind it. I found that when dealing with exceptions, if they were issues with the error handler or the AppInsightsService itself, I ended up in infinite loop territory when trying to inject things in normally. The use of Injector isn’t ideal, but it works and typically broke out of loops before hitting max callstack exceptions.

In our NGModule, we then want to create a provider for our error handler :

@NgModule({ 
  declarations: [
	...
  ],
  imports: [
	...
  ],
  providers: [
    ...
    { provide : ErrorHandler, useClass : ApplicationInsightsErrorHandler},
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

We use the token of ErrorHandler so Angular knows that we want to use this particular class for handling all errors. And that’s it!

Using Distributed Tracing With CORS

If you are attempting to setup distributed tracing with CORS (That is your API is something like api.mydomain.com and your front end is mydomain.com), then there is some more mucking about to do before you get there. It’s a headache, I can assure you.

The first is that CORS is not turned on within the App Insights Angular library by default. To do so, in our ApplicationInsightsService we want to pass in an extra flag when creating our ApplicationInsights object :

this.appInsights = new ApplicationInsights({
  config: {
	instrumentationKey: "YourKeyHere", 
	enableCorsCorrelation : true
  }
});

Next you need to set up your backend app for CORS. Now this will be different depending on what language you are using. My suggestion would be to google “YourLanguage Setup CORS” and follow the steps from there, then right at the end you need to look how to set up “ExposedHeaders”. We need to expose the header “request-context” otherwise the token AppInsights uses to link up requests is not passed back and forth.

As an example, in C# .NET Core, it would look like :

app.UseCors(x =>
    x.AllowAnyOrigin()
        .AllowAnyHeader()
        .AllowAnyMethod()
        .WithExposedHeaders("request-context")
);

Only then will your application pass the distributed tracing tokens back and forth.

Obviously you will also need to wire up Application Insights for your backend application which will completely depend on which backend language you are using. But almost all “support” distributed tracing out of the box, just not using CORS.

Leave a Reply

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