WPF

Telemetry in Desktop Apps

March 29, 2019 Coding 2 comments , , , ,

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.

Packaging a .NET Core app with the Desktop Bridge

December 4, 2018 Coding 12 comments , , ,

Packaging a .NET Core app with the Desktop Bridge

Update: Starting with Visual Studio 2019 Preview 2, the steps outlined below aren’t necessary as the functionality is built-in. Just create a Packaging Project and add a reference to your desktop application and it’ll “do the right thing.”

The Windows Desktop Bridge is a way to package up Desktop applications for submission to the Microsoft Store or sideloading from anywhere. It’s one of the ways of creating an MSIX package, and there’s a lot more information about the format in the docs. The short version is this: think about it like the modern ClickOnce. It’s a package format that supports automatic updating while users the peace of mind that it won’t put bits all over their system or pollute the registry.

Earlier today, Microsoft announced the first previews of .NET Core 3 and Visual Studio 2019. These previews have support for creating Desktop GUI apps with .NET Core using WPF and Windows Forms. It’s possible to migrate your existing app from the .NET Framework to .NET Core 3. I’ll blog about that in a later post, but it can be pretty straight-forward for many apps. One app that has already made the switch is NuGet Package Explorer; it’s open-source on GitHub and may serve as a reference.

Once you have an application targeting .NET Core 3, some of your next questions may be, “how do I get this to my users?” “.NET Core 3 is brand new, my users won’t have that!” “My IT department won’t roll out .NET Core 3 for a year!”

Sound familiar? One of the really cool things (to me) in .NET Core is that it supports completely self-contained applications. That is to say it has no external dependencies. Nothing needs to be installed on the machine, not even .NET Core itself. You can xcopy the publish output from the project and give it to someone to run. This unlocks a huge opportunity as you, the developer, can use the framework and runtime versions you want, without worrying about interfering with other apps on the machine, or even if the runtime exists on the box.

With the ability to have a completely self-contained app, we can take advantage of the Desktop Bridge to package our app for users to install. As of today, the templates don’t support this scenario out-of-the-box, but with a few tweaks, we can make it work. Read on for the details.

Getting started

You’ll need Visual Studio 2017 15.9, or better yet, the Visual Studio 2019 preview, just released today. In the setup, make sure to select the UWP workload to install the packaging project tools. Grab the .NET Core 3 preview and create your first WPF .NET Core app with it.

Details

The official docs show how to add a Packaging project to your solution, so we’ll pick-up after that article ends. Start with that first. In the future, once the tooling catches up, that’s all you’ll need. For now, as a temporary workaround, the rest of this post describes how to make it work.

I’ve put a sample showing the finished product here. The diff showing the specific changes is here.

The goal here is get the packaging project to do a self-contained publish on the main app and then use those outputs as its inputs for packing. This requires changes to two files

  1. The main application project, NetCoreDesktopBridgeApp.csproj in the sample.
  2. The packaging project, NetCoreDesktopBridgeApp.Package.wapproj in the sample.

Application Project

Let’s start with the main application project, the .csproj or .vbproj file. Add <RuntimeIdentifiers>win-x86</RuntimeIdentifiers> to the first <PropertyGroup>. This ensures that NuGet restore pulls in the runtime-specific resources and puts them in the project.assets.json file. Next, put in the following Target:

<Target Name="__GetPublishItems" DependsOnTargets="ComputeFilesToPublish" Returns="@(_PublishItem)">
  <ItemGroup>
    <_PublishItem Include="@(ResolvedFileToPublish->'%(FullPath)')" TargetPath="%(ResolvedFileToPublish.RelativePath)" OutputGroup="__GetPublishItems" />
    <_PublishItem Include="$(ProjectDepsFilePath)" TargetPath="$(ProjectDepsFileName)" />
    <_PublishItem Include="$(ProjectRuntimeConfigFilePath)" TargetPath="$(ProjectRuntimeConfigFileName)" />
  </ItemGroup>
</Target>

The full project file should look something like this:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UseWPF>true</UseWPF>

    <!-- Use RuntimeIdentifiers so that the restore calculates things correctly
         We'll pass RuntimeIdentifier=win-x86 in the reference from the Packaging Project 
    -->
    <RuntimeIdentifiers>win-x86</RuntimeIdentifiers>
  </PropertyGroup>

  <!-- Add the results of the publish into the output for the package -->
  <Target Name="__GetPublishItems" DependsOnTargets="ComputeFilesToPublish" Returns="@(_PublishItem)">
    <ItemGroup>
      <_PublishItem Include="@(ResolvedFileToPublish->'%(FullPath)')" TargetPath="%(ResolvedFileToPublish.RelativePath)" OutputGroup="__GetPublishItems" />
      <_PublishItem Include="$(ProjectDepsFilePath)" TargetPath="$(ProjectDepsFileName)" />
      <_PublishItem Include="$(ProjectRuntimeConfigFilePath)" TargetPath="$(ProjectRuntimeConfigFileName)" />
    </ItemGroup>
  </Target>

</Project>

Packaging Project

Next up, we need to add a few things to the packaging project (.wapproj). In the <PropertyGroup> that has the DefaultLanguage and EntryPointProjectUniqueName, add another property: <DebuggerType>CoreClr</DebuggerType>. This tells Visual Studio to use the .NET Core debugger. Note: after setting this property, you may have to unload/reload the project for VS to use this setting, if you get a weird debug error after changing this property, restart VS, load the solution and it should be fine.

Next, look for the <ProjectReference ... element. If it’s not there, right click the Application node and add the application reference to your main project. Add the following attributes: SkipGetTargetFrameworkProperties="true" Properties="RuntimeIdentifier=win-x86;SelfContained=true". The full ItemGroup should look something like this:

<ItemGroup>
  <!-- Added Properties to build the RID-specific version and be self-contained -->
  <ProjectReference
    Include="..\NetCoreDesktopBridgeApp\NetCoreDesktopBridgeApp.csproj"
    SkipGetTargetFrameworkProperties="true"
    Properties="RuntimeIdentifier=win-x86;SelfContained=true" />
</ItemGroup>

Finally, and we’re almost done, add the following snippet after the <Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" /> line:

<!-- Additions for .NET Core 3 target -->
<PropertyGroup>
  <PackageOutputGroups>@(PackageOutputGroups);__GetPublishItems</PackageOutputGroups>
</PropertyGroup>
<Target Name="_ValidateAppReferenceItems" />
<Target Name="_FixEntryPoint" AfterTargets="_ConvertItems">
  <PropertyGroup>
    <EntryPointExe>NetCoreDesktopBridgeApp\NetCoreDesktopBridgeApp.exe</EntryPointExe>
  </PropertyGroup>
</Target>
<Target Name="PublishReferences" BeforeTargets="ExpandProjectReferences">
  <MSBuild Projects="@(ProjectReference->'%(FullPath)')"
           BuildInParallel="$(BuildInParallel)"
           Targets="Publish" />
</Target>

In that snippet, change NetCoreDesktopBridgeApp\NetCoreDesktopBridgeApp.exe to match your main project’s name and executable.

VCRedist workaround

Bonus section: as a point-in-time issue, you’ll need to declare a package dependency on the VCRedist in your Package.appxmanifest file. Add the following in the <Dependencies> element: <PackageDependency Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" Name="Microsoft.VCLibs.140.00.UWPDesktop" MinVersion="14.0.26905.0" />. When your users install the app, Windows will automatically pull that dependency from the store.

Build & Debug

With the above pieces in place, you can set the packaging project as the startup project and debug as you normally would. It’ll build the app and deploy it to a local application. You can see the output within your packaging project’s bin\AnyCPU\<configuration>\AppX directory. It should have more files than your main application as it’ll have the self-contained .NET Core runtime in it.

Note: I’ve sometimes found that debugging the packaging project doesn’t cause a rebuild if I’ve changed certain project files. A rebuild of the main app project has fixed that for me and then I’m debugging what I expect.

Deployment

There are two main options for deploying the package:

  1. Sideloading with an AppInstaller file. This is the replacement to ClickOnce.
  2. The Microsoft Store. The package can be submitted to the Store for distribution.

Side-loading

Since Windows 10 1803, sideloaded applications can receive automatic updates using an .appinstaller file. This makes AppInstaller a replacement to ClickOnce for most scenarios. The documentation describes how to create this file during publish, so that you can put it on a UNC path, file share, or HTTPS location.

If you sideload, you’ll need to use a code signing certificate that’s trusted by your users. For an enterprise, that can be a certificate from an internal certificate authority, for the public, it needs to be from a public authority. DigiCert has a great offer for code signing certs, $74/yr for regular and $104/yr for EV at this special link. Disclaimer: DigiCert provides me with free certificates as a Microsoft MVP. I have had nothing but great experiences with them though. Once you have the certificate, you’ll need to update your Package.appxmanifest to use it. Automatic code signing is beyond the scope of this article, but please see my code signing service project for something you can deploy in your organization to handle this.

Microsoft Store

The Microsoft Store is a great way to get your app to your users. It handles the code signing, distribution, and updating. More info on how to submit to the store is here and here.

Further exploration

One of the projects I maintain, NuGet Package Explorer, is a WPF app on .NET Core 3 and is setup with Azure Pipelines. It has a release pipeline that generates a code signed CI build that auto-update, and then promotes packages to the Microsoft Store, Chocolatey, and GitHub. It has a build script that uses Nerdbank.GitVersioning to ensure that each build gets incremented in all the necessary places. I would encourage you to review the project repository for ideas and techniques you may want to use in your own projects.