Telemetry in Desktop Apps

Summary

Learn how to collect telemetry in desktop applications for .NET Framework and .NET Core.

Intro

One of the key parts of product development is the ability to get telemetry out of your apps. This is critical for understanding how your users use your app and what errors happen. It’s part of the “ops” of DevOps and feeds data back into the development cycle to make informed decisions.

Taking a step back, let’s define “telemetry,” so we’re on the same page. I mean events, pages/views, metrics, and exceptions that occur as a user uses the app. This is data about how your app is running, not data profiling a user based on content. The goal is to be able to answer questions like “what parts of my app do people use the most,” or “what path do users take to get to feature X or feature Y?” It’s explicitly not about answering questions like “Find me users in Seattle that shop at Contoso” or “What is Jane Doe’s favorite color?” I believe all apps can benefit from the former while the latter is a business choice with ethical/moral implications.

Application Insights provides a way to collect and explore app usage and statistics. Application Insights used to have support for desktop and devices, but they ended that in 2016 in favor of HockeyApp. HockeyApp was since moved into Visual Studio App Center, where it supports iOS, Android, and UWP. Left out were desktop apps. I should note that there are backlog items, but the SDK alone isn’t enough, it needs updates server-side as well to be useful (particularly around crash dumps). In the end, even App Center recommends analyzing your data in Application Insights.

If you were building a .NET Framework-based desktop app, you could try to use the Windows Server SDK as described by the docs. There are a couple of downsides to that SDK vs the old Windows Desktop SDK they had:

  • It’s big and pulls in many more dependencies than you need, and thus increases the size of your redistributable.
  • There are several types that are only in the .NET Framework target and not in their .NET Standard target (one key missing item is the DeviceTelemetryInitializer).
  • PersistenceChannel doesn’t exist anymore. This channel was designed to store telemetry on disk and send the next time the app started with connectivity. See the team’s blog post for more information on how it works. The ServerTelemetryChannel does have network resilience, but does not persist across app instances in case of crash.

Fortunately Microsoft open sourced the Application Insights SDK, and I’ve been able to revive the PersisteceChannel along with taking a few key telemetry modules from the Server SDk and create a new AppInsights.WindowsDesktop package (code).

Getting started

You’ll need an Azure subscription (free to sign up) and there’s a basic plan for Application Insights that’s free until you have a lot of data.

  1. Create an Application Insights resource and take note of the InstrumentationKey as you’ll need it later.
  2. Add the AppInsights.WindowsDesktop NuGet package to your project. I usually put it in a core/low-level library so that I can use its types throughout my code.
  3. Add a file called ApplicationInsights.config to your application and ensure the build action it set to Copy if newer. You can adjust many things in it, but a good starting point is here:
    <?xml version="1.0" encoding="utf-8"?>
    <ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
     <TelemetryInitializers>
       <Add Type="Microsoft.ApplicationInsights.WindowsDesktop.DeviceTelemetryInitializer, AppInsights.WindowsDesktop"/>
       <Add Type="Microsoft.ApplicationInsights.WindowsDesktop.SessionTelemetryInitializer, AppInsights.WindowsDesktop"/>
       <Add Type="Microsoft.ApplicationInsights.WindowsDesktop.VersionTelemetryInitializer, AppInsights.WindowsDesktop"/>
     </TelemetryInitializers>
     <TelemetryModules>
       <Add Type="Microsoft.ApplicationInsights.WindowsDesktop.DeveloperModeWithDebuggerAttachedTelemetryModule, AppInsights.WindowsDesktop"/>
       <Add Type="Microsoft.ApplicationInsights.WindowsDesktop.UnhandledExceptionTelemetryModule, AppInsights.WindowsDesktop"/>
       <Add Type="Microsoft.ApplicationInsights.WindowsDesktop.UnobservedExceptionTelemetryModule, AppInsights.WindowsDesktop" />
       <!--<Add Type="Microsoft.ApplicationInsights.WindowsDesktop.FirstChanceExceptionStatisticsTelemetryModule, AppInsights.WindowsDesktop" />-->
     </TelemetryModules>
     <TelemetryProcessors>
       <Add Type="Microsoft.ApplicationInsights.Extensibility.AutocollectedMetricsExtractor, Microsoft.ApplicationInsights"/>
     </TelemetryProcessors>
     <TelemetryChannel Type="Microsoft.ApplicationInsights.Channel.PersistenceChannel, AppInsights.WindowsDesktop"/>
    </ApplicationInsights>
    

    This will add in telemetry capture of unhandled exceptions and unobserved tasks. If you want to capture first chance exceptions, uncomment out the FirstChanceExceptionStatisticsTelemetryModule, though be warned that it can be noisy and often does not matter.

  4. Set your InstrumentationKey in the configuration as an <InstrumentationKey></<InstrumentationKey> element, or set TelemetryConfiguration.Active.InstrumentationKey in code.
  5. You’ll need to set some per-session property values that get applied to all outgoing data for correlation. A telemetry initializer is a good way to do it, and that’s what the SessionTelemetryInitializer does in the config.

    Note: many of the samples show using Environment.Username for the user id. As it is common to have all or part of a person’s name as the username, that can lead to sending PII over to Application Insights and is not recommended. The SessionTelemetryInitializer class referenced above sends a SHA-2 hash of the username, domain, and machine to achieve the desired result without sending personally identifiable information over.

  6. Consider what additional telemetry might be useful to collect. I have another telemetry initializer to capture the application version and CLR version in VersionTelemetryInitializer. This lets me generate reports split by application version. It uses the AssemblyInformationalVersionAttribute of the main exe. You can always override it by providing your own telemetry initializer afterwards.

  7. Add a disclosure about the data you’re collecting to your privacy policy. Feel free to borrow from NuGet Package Explorer’s privacy policy.

In your app

Application Insights primarily uses PageView‘s and Events to trace user behavior in the app, and it’s up to you to add those into your code. I’ll typically put a TrackPageView call into every form, or view. If your app has internal navigation to different views, that’s a great place to put page tracking too. I put a TrackEvent call on every action a user can take — menu item, context menu, command, button, etc. It represents something the user does. Together, you can get a picture of how your users use your app, and what things they do the most…or see if there are features that your users aren’t using.

If you choose to set your InstrumentationKey in code, then do so as early as you can in the app startup. Here’s how I do it. Finally, call Flush with a short sleep on exit to give a chance for unsent telemetry be sent. If the user is offline or it’s not enough time, the PersistenceChannel will attempt to send the next time the application is launched.

Wrapping up

This starts collecting telemetry, next up is analyzing it. Stay tuned for next week, when I’ll explore the kind of data we can see Application Insights for NuGet Package Explorer.