Virtually all web applications use some form of user analytics to determine which aspects of an application are popular and which are causing issues for users. Probably the most well known is Google Analytics but there are other similar services that offer additional options and features. One such service is Segment which can act as a funnel into other analytics engines such as Google Analytics, Mixpanel, or Salesforce.

In this post I show how you can add the Segment analytics.js library to your ASP.NET Core application, to provide analytics for your application.

I'm only looking at how to add client-side analytics to a server-side rendered ASP.NET Core application i.e. an MVC application using Razor. If you want to add analytics to an SPA app that uses Angular for example, see the Segment documentation.

Client-side vs. Server-side tracking

Segment supports two types of tracking: client-side and server-side. The difference should be fairly obvious:

  • Client-side tracking uses JavaScript to make calls to the Segment API, to track page views, sign-ins, page clicks etc.
  • Server-side tracking happens on the server. That means you can send data that's only available on the server, or that you wouldn't want to send to a client.

Whether you want server-side tracking, client-side tracking, or both, depends on your requirements. Segment has a good breakdown of the pros and cons of both approaches on their docs.

In this post I'm going to add client-side tracking using Segment to an ASP.NET Core application.

Fetching an API key

I'll assume you already have a Segment account - if not, head to https://app.segment.com/signup and signup.

Signup page

Once you have configured your account, you'll need to obtain an API key for your app. If you haven't already, create a new source by clicking Add Source on the Home screen. Select the JavaScript source, and enter all the required fields.

Connect source

Once the source is configured, view the API keys for the source, and make a note of the Write key. This is the API key you will provide when calling the Segment API.

write key

With the Segment side complete, we can move on to your application. Even though we're doing client-side tracking here, we need to do some work on the server.

Configuring the server-side components

Given I said I'm only looking at client-side tracking, you might be surprised to know you need any server-side components. However, if you're rendering your pages server side using Razor, you need a way of passing the API keys, and the user's ID to the JavaScript code. The easiest way is to write the values directly in the JavaScript rendered in your layout.

Adding the configuration

First thing's first, you need somewhere to store the API key. The simplest place would be to dump it in appsettings.json, but you shouldn't put values like API keys in there. The Segment key isn't really that sensitive (we'll be exposing it in JavaScript anyway) but out of principle, it just shouldn't be there.

Never store API keys in appsettings.json - store them in User Secrets, environment variables, or a password vault like Azure Key Vault.

Store the API key in the User Secrets JSON file for now, using a suitably descriptive key name:

{
  "Segment": {
    "ApiKey": "56f7fggjsGGyishfuknvyfGFDfg3643"
  }
}

Assuming you're using the default web host builder (or similar) this value will be added to your IConfiguration object. Create a strongly-typed settings object for good measure:

public class SegmentSettings
{
    public string ApiKey { get; set; }
}

And bind it to your configuration in Startup.ConfigureServices:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<SegmentSettings>(Configuration.GetSection("Segment"));
    }
}

Now you've got the Segment API key available in your application, you can look at rendering the analytics.js JavaScript code.

Rendering the analytics code in Razor

The Segment JavaScript API is exposed as the analytics.js library. This library lets you send all sorts of analytics to Segment from a client, but at it's simplest you just need to do three things:

  1. Load the analytics.js library
  2. Initialise the library with your API key
  3. Call page() to track a page view.

You can read about this and all the other options available in the quickstart guide in Segment's documentation. I'm going to create a partial view called _SegmentPartial.cshtml, for rendering the JavaScript snippet. You can add this partial to your application by adding the following to your _Layout.cshtml.

@await Html.PartialAsync("_SegmentPartial");

The Razor partial itself consists almost entirely of the JavaScript snippet provided by Segment:

@inject IOptions<SegmentSettings> Settings
@{
    var apiKey = Settings.Value.ApiKey
}
<script type="text/javascript">
  !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t,e){var n=document.createElement("script");n.type="text/javascript";n.async=!0;n.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var o=document.getElementsByTagName("script")[0];o.parentNode.insertBefore(n,o);analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.1.0";
  analytics.load("@apiKey");
  analytics.page();
  }}();
</script>

There's a couple of things to note here. We're injecting the API key using the strongly-typed SegmentSettings options object directly into the view, and then writing the key out using @apiKey. This will HTML encode the output, but given we know the apiKey is alphanumeric, this shouldn't be an issue.

This is a special case as we know the key is not coming form user input and contains a known set of safe values, but it's bad practice really. Generally speaking you should use one of the techniques discussed in the docs to inject values into JavaScript code.

If you reload your website, you should see the JavaScript snippet rendered to the page, and if you look in the debugger of your Segment Source you should see a tracking event for the page:

The Segment Debugger after tracking a page

Associating page data with a user

You've now got basic page analytics, but what if you want to send more information. The analytics.js library lets you track a variety of different properties and events, but often one of the most important is tracking individual users. This is extremely powerful as it lets you track a user's flow through your application, and where they hit stumbling blocks for example.

User tracking and privacy is obviously a hot-topic at the moment, but I'm going to just avoid that for now. You should always take into consideration your user's expectation of privacy, especially with the recent GDPR legislation.

To associate multiple analytics.page() and analytics.track() calls with a specific user, you must first call analytics.identify() in your page. You should add this call just after the analytics.load() call and just before analytics.page() in our JavaScript snippet.

In order to track a user, you need a unique identifier. If a user is browsing anonymously, then Segment will assign an anonymous ID automatically; you don't need to do anything. However, if a user has logged in to your app, you can associate your Segment data with them by providing a unique ID.

In this example, I'm going to assume you're using a default ASP.NET Core Identity setup, so that when a user logs in to your app, a ClaimsPrincipal is set which contains two claims:

  • ClaimTypes.NameIdentifier: the unique identifier for the user
  • ClaimTypes.Name : the name of the user (often an email address)

For privacy/security reasons, you may not want to expose the unique id of your users to a third-party API (and the client browser). You can work around this by creating an additional unique GUID for each user, and adding an additional Claim to the ClaimsPrincipal on login. That's beyond the scope of this post, so I'll just use the two main claims for now.

The following Razor uses the User property on the page to check if the current user is authenticated. If they are, it extracts the id and email of the principal, and creates an anonymous "traits" object, with the details about the user we're going to send to Segment. Finally, after loading the snippet and assigning the API key, we call analytics.identify(), passing in the user id, and the serialized traits object.

@inject IOptions<SegmentSettings> Settings
@using System.Security.Claims
@using System.Text.Encodings.Web
@using System.Security.Claims
@{
    var apiKey = Settings.Value.ApiKey
    var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
    if (isAuthenticated)
    {
        var id = User.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value;
        var name = User.Claims.First(x => x.Type == ClaimTypes.Name).Value;
        var traits = new {username = name, email = name};
    }
}
<script type="text/javascript">
  !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t,e){var n=document.createElement("script");n.type="text/javascript";n.async=!0;n.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var o=document.getElementsByTagName("script")[0];o.parentNode.insertBefore(n,o);analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.1.0";
  analytics.load("@apiKey");
  @if (isAuthenticated)
  {
     @:analytics.identify('@id', @Json.Serialize(traits));
  }
  analytics.page();
  }}();
</script>

Now if you login to your application, you should see an additional identify call in the Segment Debugger, containing the id and the additional traits. Actions taken by that user will be associated together, so you can easily follow the steps a user took before they ran into an issue for, example.

There's rather more logic in this partial than I like to see in a view so I suggest encapsulating this logic somewhere else, perhaps by converting it to a ViewComponent.

There's many more things you can do to provide analytics for your application, but I'll leave you to check out the excellent Segment documentation if you want to do more.

Summary

In this post I showed how you can use a Segment's analytics.js library to add client-side analytics to your ASP.NET Core application. Adding analytics is as simple as including a JavaScript snippet and providing an API key. I also showed how you can associate page actions with users by reading Claims from the ClaimsPrincipal and calling analytics.identify().