Application Insight for C# WPF

Introduction

Some years ago I wrote a post about using Application Insight in a WPF application to capture telemetry data. Today there is still no official support for WPF (or WinForms or CommandLine) in application Insights and my post remains relevant, however over time the Application Insight libraries (and my understanding of code) has changed and my post does not reflect these changes. As such I have decided to write an updated post to show how to implement an Application Insight client for simple inclusion and use in a project.

The code that I am going to be walking through is part of the VaraniumSharp.Initiator library, you can either check out the code on GitHub or just follow along here. If you only want to use the client and you're not interested in the implementation details grab a copy of the VaraniumSharp.Initiator nuget and jump to the AppInsightClient Usage section.

AppInsightClient

Design Overview

For my previous attempt I developed the Application Insight Helper class to be inject-able so that it would play well with Dependency Injection, however actual use of this code has led me to believe that the client should rather be a static class. This is similar to how logging frameworks work and allow for greater usage flexibility. The AppInsightClient class guards against attempts to track telemetry data when it has not been initialized, as such it can be left in place during testing without requiring any configuration and without the risk of posting test data to the telemetry server.

The Code

The actual implementation of the client can be found in the AppInsightClient.cs file.
AppInsightClient.cs file

This file contains all of the details that is required to set up and use the Application Insight Telemetry client to post data to Application Insight.

Application Insight Nuget Package

VaraniumSharp.Initiator makes use of Paket to manage its Nuget dependencies. A reference to the Microsoft.ApplicationInsights nuget package has been added in the paket.dependencies and paket.references files
Application.Insight nuget package
The package that we use for this implementation is the Microsoft.ApplicationInsights package which contains all the Core functionality for interacting with Application Insights without any of the specialization (like automatic data capture) that is available to other application type like web apps.

Constructor
/// <summary>
/// Static Constructor
/// </summary>
static AppInsightClient()
{
    LogInstance = Log.Logger.ForContext("Module", nameof(AppInsightClient));
    TrackTelemetry = true;
}

The constructor is straightforward. It captures a contextual instance of the Serilog logger (which is the logger used by VaraniumSharp.Initiator) with the AppInsightClient set as the property. In this way all logs captured by the AppInsightClient is easy to trace in our logs.

The TrackTelemetry property is also set to true so that tracking will be enable by default once the class has been initialized

Initialization Method
/// <summary>
/// Initialize the Telemetry client with appropriate settings
/// </summary>
/// <param name="instrumentationKey">Application Insight instrumentation key</param>
/// <param name="userKey">Key that uniquely identify the user</param>
public static async Task InitializeAsync(string instrumentationKey, string userKey)
{
    try
    {
        await StartupLock.WaitAsync();
        if (IsInitialized)
        {
            LogInstance.Warning("Client can only be initialized once");
            return;
        }
        _telemetryClient = new TelemetryClient
        {
            InstrumentationKey = instrumentationKey
        };

        _telemetryClient.Context.User.AuthenticatedUserId = userKey;
        _telemetryClient.Context.Session.Id = Guid.NewGuid().ToString();
        _telemetryClient.Context.Device.OperatingSystem = GetWindowsFriendlyName();
        _telemetryClient.Context.Device.Model = GetDeviceModel();
        _telemetryClient.Context.Device.OemName = GetDeviceManufacturer();
        _telemetryClient.Context.Component.Version = GetComponentVersion();
        IsInitialized = true;
    }
    finally
    {
        StartupLock.Release();
    }
}

To pass data to the Application Insight servers the TelemetryClient that is provided by the Application Insight library will be used. The initialization method will prevent multiple initialization attempts by checking and setting the IsInitialized variable. It also ensures that multiple threads calling the methods does not cause multiple instances of the TelemetryClient to be instantiated by making use of a SemaphoreSlim called StartupLock to lock the initialization method to a single thread.

The InitializeAsync method takes the InstrumentationKey, which is acquired from the Azure portal as well as a userKey which is a unique value used to identify the user in Application Insights. How this key is defined or stored is outside the scope of this article.

The Session Key uniquely identifies the current application session and as such it is simply a Guid that is generated the first time that the AppInsightClient is initialized, this value will change each time the application is restarted and correlates telemetry data to that single run.

The method calls several sub-methods that are used to set certain properties of the TelemetryClient that will be posted to the Application Insight server and provide some additional details about the user's environment. The details that will be gathered are:

  • Operating system friendly name
  • Device model
  • Device manufacturer
  • Application version

The implementation of these sub-methods is trivial and will not be explored in further detail this is left as an exercise for the reader.

Properties
/// <summary>
/// The OS that the TelemetryClient will report
/// </summary>
public static string OperatingSystem => _telemetryClient?.Context.Device.OperatingSystem;

There are multiple Properties that expose some of the details that the TelemetryClient has been configured with. All of these Properties guard against the TelemetryClient not having been initialized. All the properties are well documented and will not be discussed in further detail as their implementation is trivial.

Tracking Methods
/// <summary>
/// User actions and other events. Used to track user behavior or to monitor performance.
/// </summary>
/// <param name="name">Name of the event</param>
/// <param name="properties">Dictionary of event properties</param>
/// <param name="metrics">Dictionary of event metrics</param>
public static void TrackEvent(string name, IDictionary<string, string> properties = null,
    IDictionary<string, double> metrics = null)
{
    if (TelemetryCanBePosted())
    {
        _telemetryClient.TrackEvent(name, properties, metrics);
    }
}

The AppInsightClient provides pass-through methods for all tracking methods that are exposed by the TelemetryClient. The only thing that is special about these methods is that all calls to the TelemetryClient is wrapped with a statement to check if data can be posted to the Application Insights servers.

 /// <summary>
/// Check if we can post Telemetry data.
/// This method checks if the client has been initialized and if posting of telemetry data is allowed
/// </summary>
/// <returns>True - Telemetry data can be posted</returns>
private static bool TelemetryCanBePosted()
{
    var canPost = true;
    if (!IsInitialized)
    {
        LogInstance.Warning("Cannot track telemetry - Client has not been initialized");
        canPost = false;
    }
    if (!TrackTelemetry)
    {
        LogInstance.Verbose("Telemetry data posting is disabled");
        canPost = false;
    }
    return canPost;
}

The TelemetryCanBePosted methods ensures that telemetry data can be posted by checking that the TelemetryClient instance has been initialized and that the TrackTelemetry property is true. In this way it prevents posting data if the user has turned tracking off or if the client has not been initialized yet. In both cases an entry is logged so that it is clear why data is not being posted to the Application Insights server.

Unit Testing

All of the AppInsightClient methods have been unit tested. Most of the tests are fairly straightforward serving only to ensure that a method calls the correct underlying methods.

The test are written with the NUnit test framework with Fluent Assertions for asserting the correctness of statements. It also leverages HttpMock to verify that the TelemetryClient actually makes the calls to the Application Insight servers (The data isn't checked, just the fact that the call is made) as well as Moq for instances where mocking is required.

Test Configuration

To adjust the endpoint to which the TelemetryClient posts we need to add an ApplicationInsights.config file to the test project. The content of the file is as follows

<?xml version="1.0" encoding="utf-8" ?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  <TelemetryChannel>
    <EndpointAddress>http://localhost:8888/v2/track</EndpointAddress>
  </TelemetryChannel>
</ApplicationInsights>
Testing
Setup

Because the AppInsightClient is a static class we need to go to some extra trouble during testing. While we are not overly interested in the exact data that is posted we would like to ensure that setup only occurs once. To do that we use the OneTimeSetup attribute from NUnit.

[OneTimeSetUp]
public async Task SetupWithCheckThatPostCannotOccurPriorToInitialization()
{
    // arrange
    _logMock = LoggerFixture.SetupLogCatcher();

    // act
    AppInsightClient.TrackEvent("BeforeStart");

    // assert
    _logMock.Verify(t => t.Warning("Cannot track telemetry - Client has not been initialized"), Times.Once);

    await AppInsightClient.InitializeAsync(TestKey, TestUserKey);
}

This method includes a test (which is weird) but there is no other practical way to work around it as we do not want to force our tests to execute in a specific order. Because of this we will verify that tracking does not occur before the AppInsightClient has been set up before setting it up. Should this "test" fail the first test to call this method will fail.

TelemetryClient setup tests

There are multiple tests that ensure that the TelemetryClient data has been correctly configured. Only a single example will be shown as the rest follow the same pattern.

[Test]
public void TelemetryClientCorrectlyRetrievesOperatingSystem()
{
    // arrange
    var expectedOs = RetrieveValueFromManagementInformation("Caption", "Win32_OperatingSystem", "Unknown");

    // act
    // assert
    AppInsightClient.OperatingSystem.Should().Be(expectedOs);
}

To test that the OS name has been correctly configured the test pulls the OS name for itself and then compares it to the name that has been configured for the TelemetryClient. This is required as the OS friendly name will change depending on which machine the test is executed.

Testing TelemetryClient posts

Before testing if the TelemetryClient correctly post data the HttpMock server has to be configured to capture the call. As all calls go to the same URL a simple helper method has been created to ease setup

private static IHttpServer SetupServer()
{
    const string url = "http://localhost:8888";

    var httpMock = HttpMockRepository.At(url);
    httpMock.Start();
    httpMock.Stub(t => t.Post(UrlPath))
        .OK();
    return httpMock;
}

This method ensure that the HttpMock libary is listening on the endpoint that we have configured the TelemetryClient to post to

[Test]
public void TrackingRequestWorksCorrectly()
{
    // arrange
    var httpMock = SetupServer();

    // act
    AppInsightClient.TrackRequest("Test", DateTimeOffset.Now, TimeSpan.Zero, "202", true);
    AppInsightClient.Flush();

    // assert
    httpMock.AssertWasCalled(t => t.Post(UrlPath));
}

The test itself is fairly trivial, simply request that the AppInsightClient should post some telemetry data, provide it with dummy data and then ensure that the endpoint was called on the HttpMock.

Testing Double Initialization

This test ensures that the TelemetryClient will not be initialized twice. To verify this test we will capture the log entry that states that a second initialization has not occur.

 public static Mock<ILogger> SetupLogCatcher()
        {
            var loggerFixture = new Mock<ILogger>();
            loggerFixture.Setup(t => t.ForContext("Module", It.IsAny<string>(), false)).Returns(loggerFixture.Object);
            Log.Logger = loggerFixture.Object;
            return loggerFixture;
        }

The method above is part of the test fixtures and is used to simplify the setup of the ILogger mock that is used to verify that the log entry has been posted.

[Test]
public async Task InitializationCanOccurOnlyOnce()
{
    // arrange
    // act
    await AppInsightClient.InitializeAsync(TestKey, TestUserKey);

    // assert
    AppInsightClient.IsInitialized.Should().BeTrue();
    _logMock.Verify(t => t.Warning("Client can only be initialized once"), Times.Once);
}

Because the setup method has already initialized the AppInsightClient a second call to it will log the warning that it has already been initialized.

AppInsightClient Usage

Azure Setup

If you do not already have an Azure account head here to sign up for a free trial otherwise head to the Azure Portal and log in to your account. Next add a new Application Insight instance by heading to Application Insight in the side panel then selecting Add
Create Application Insight instance

Next, enter all relevant information in the creation tab then click Create and wait for Azure to set up your instance.
Creation options

Once set up, click on your instance to get its properties
Application Insight Instance

To get your Instrumentation Key scroll down and select Properties then copy your instrumentation key
Grab Instrumentation Key

Consuming Project Setup

The example project can be viewed in this repository

VaraniumSharp.Initiator Nuget Package

Right-click your project and select the Manage NuGet Packages item
Add Nuget Package
Next enter "VaraniumSharp.Initiator" in the search box, then select the VaraniumSharp.Initiator package and click the Install arrow
VaraniumSharp.Initiator Nuget Package
This will add the VaraniumSharp.Initiator package to your project and allow the use of the AppInsightClient class, if required accept the changes that will be made to your project and accept any license agreements then wait for Visual Studio to install the packages.
Accept changes

Initialize AppInsightClient

Navigate to the App.xaml backing code
App.xaml backing code
Override the OnStartup method and set up the AppInsightClient with the following code

protected override async void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    const string instrumentationKey = "[Redacted]";
    const string userKey = "[Temporary Key]";

    await AppInsightClient.InitializeAsync(instrumentationKey, userKey);

    var currentDomain = AppDomain.CurrentDomain;
    currentDomain.UnhandledException += CurrentDomainOnUnhandledException;
    currentDomain.ProcessExit += CurrentDomainOnProcessExit;
}

Because the initialize method is asynchronous we need to make the overridden method async.
Do not be tempted to make use of the Wait method on async Tasks, this can cause your application to deadlock!

We need to attach handlers for the current AppDomain so that we can flush the AppInsightClient data before the application is closed. Failure to do this will result in the data that is still in the TelemetryClient's memory buffer to be lost

The implementation for the CurrentDomainOnUnhandledException code is as follows

private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs unhandledExceptionEventArgs)
{
    AppInsightClient.TrackException((Exception)unhandledExceptionEventArgs.ExceptionObject);
    AppInsightClient.Flush();
}

with the code to handle the CurrentDomainOnProcessExit below

private static void CurrentDomainOnProcessExit(object sender, EventArgs eventArgs)
{
    AppInsightClient.Flush();
}
Posting Telemetry Data with AppInsightClient

To track telemetry data we can simply invoke the appropriate method on the AppInsightClient from anywhere in our code. In the example below we add a page view when our main window is displayed

public MainWindow()
{
    InitializeComponent();
    AppInsightClient.TrackPageView(nameof(MainWindow));
}

Viewing Telemetry Data on Azure

Once you have captured some telemetry data you can view that data in the Azure Application Insight dashboard.
Scroll down to Metrics Explorer then click on one of the graphs to configure it
Show me the metrics

This will show the metrics that have been posted with the ability to view the individual values that have been posted
Metrics

There are plenty of ways to visualize your metrics but I won't dig into the details here, instead it is left as an exercise.