Mobile Corner

How To Inject Analytics Code into Windows Phone 8.1 Apps

Nick Randolph dissects a Windows Phone 8.1 application package in order to inject analytics tracking code into a pre-build application.

Adding analytics code is a necessary part of all good application development in order to provide data about the usage of the application and the frequency of which pages within the application are accessed. There are plenty of other data points that can be captured within an application, but page-level tracking is considered to be a bare minimum.

In a lot of applications, adding analytics code is done by developers toward the end of the first iteration, usually just after a product manager realizes they're going to need to know how many people are using the app -- and is typically right before an application is submitted for certification. In those frantic last cycles, in addition to eradicating any remaining bugs the analytics code has to be sprinkled throughout the application, hoping that all the appropriate events have been recorded.

In this article I'll show you how to inject analytics tracking code into a pre-build application package. The approach here lends itself nicely to a post-build step as part of an automated build process.

I'll quickly walk through how you would normally add page-level analytics tracking to a Windows Phone 8.1 application. There are a couple of different NuGet packages available; in this case, we'll use the Google Analytics SDK by Tim Greenfield, shown in Figure 1.

[Click on image for larger view.] Figure 1. Google Analytics SDK from Tim Greenfield

After adding a reference to the NuGet package you'll notice there are two references added to your project, along with analytics.xml and analytic.xsd files. You can think of the analytics.xml file as being the configuration file for the Google Analytics tracking and it's where you'll need to enter your Google Analytics tracking Id. Now all you need to do is write tracking code, such as the following that tracks when the "MainPage" has been viewed:

GoogleAnalytics.EasyTracker.GetTracker().SendView("MainPage");

Because you want to track each page that's viewed, it makes sense to centralize this logic so that it's invoked on each navigation. This can be done by trapping the Navigated event on the frame of the application. You also want to move this logic into a separate helper library so you can use it across any of your applications. The result is a helper library, GoogleHelper, with a class called Track, which has an Init method that needs to be called at the end of the OnLaunched method in App.xaml.cs, as shown in Listing 1.

Listing 1: The GoogleHelper Library
public static class Track
{
  public async static void Init()
  {
    // Retrieve the main frame of the application
    var frame = Window.Current.Content as Frame;
    while (frame == null)
    {
      // If the frame doesn't yet exist, yield to allow startup to continue,
      // then check again
      await Task.Yield();
      frame = Window.Current.Content as Frame;
    }
            
    frame.Navigated += FrameNavigated;
            
    // At this point there may already be a page, so 
    // you want to make sure you track that, too
    if (frame.Content!= null)
    {
      TrackView(frame.SourcePageType);
    }
  }

  private static void FrameNavigated(object sender, NavigationEventArgs e)
  {
    TrackView(e.SourcePageType);
  }

  private static void TrackView(Type  pageType)
  {
    Task.Run(() => EasyTracker.GetTracker().SendView(pageType.Name));
  }
}

Instead of having to manually add NuGet references, the extra reference to the GoogleHelper library and then call Track.Init from the OnLaunched method, you can create a simple program that injects this logic. The call will be added to the .appx file created by Visual Studio by invoking Project | Store | Create App Packages. This program is going to have four steps: extracting the pre-build appx; copying the additional libraries and files; wiring up the call to the Track.Init method; and packaging up the new .appx. I'll walk these through in more detail.

First, there are a few constants you'll need to reference, which include the location of the command-line tool makeappx that will be used to unpack and repack the .appx file:

private const string MakeAppxPath = @"Windows Kits\8.1\bin\x86\makeappx.exe";
private const string MakeAppxUnpackArgsTemplate = "unpack /p \"{0}\" /d \"{1}\" /l /o";
private const string MakeAppxPackArgsTemplate = "pack /p \"{0}\" /d \"{1}\" /l /o /nv";

private readonly static string MakeAppxCommandPath = 
  Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), 
  MakeAppxPath);
private readonly static string AppxFilenameIn = 
  Path.Combine(Environment.CurrentDirectory, "AppWithoutAnalytics.appx");
private readonly static string AppxFilenameOut = 
  Path.Combine(Environment.CurrentDirectory, "AppWithInsertedAnalytics.appx");

The first step is to extract or unpack the .appx file using the makeappx command-line utility:

/*** Step 1: Extract the Appx ***/
// Create a unique folder to extract the .appx contents too
var extractedAppxFolder = 
  Path.Combine(Environment.CurrentDirectory, Guid.NewGuid().ToString());
            
// Extract the .appx
var makeAppxUnpackArgs = 
  string.Format(MakeAppxUnpackArgsTemplate, AppxFilenameIn, extractedAppxFolder);
RunProcess(Environment.CurrentDirectory, MakeAppxCommandPath, makeAppxUnpackArgs);

Next, you need to copy four files that will make up the Google Analytics integration into the same folder to which the .appx file was extracted. This includes the two .winmd files that form the Google Analytics SDK, the helper library created earlier and the configuration file, analytics.xml, as shown in Listing 2.

Listing 2: Copying Google Analytics Integration Files
/*** Step 2: Copy in required files ***/
var files = new[]{
  // Configuration file for Google Analytics
  "analytics.xml",                
  // Helper class library for wiring up Google Analytics
  "GoogleHelper.dll", 
  // Google Analytics SDK files
  "GoogleAnalytics.Core.winmd",
  "GoogleAnalytics.winmd"};
foreach (var file in files)
{
  var src = Path.Combine(Environment.CurrentDirectory, file);
  var dest = Path.Combine(extractedAppxFolder, file);
  File.Copy(src,dest);
}

The third step is the most complex, as it requires injecting code into the correct place within the application. To do this you first need to determine which file contains the entry point of the application. For Windows Phone 8.1 this is most likely the .exe file within the .appx file, but to ensure logic is injected into the correct file, the appxmanifest.xml file must be queried first to determine both the entry point file and the entry point class. This information will be passed into the WireInHelperLibrary method, which I'll come back to:

/*** Step 3: Wire up call to helper library ***/
            
// Find the entrypoint assembly and class
var appxManifest = Path.Combine(extractedAppxFolder, "appxmanifest.xml");
var appxManifestXml = XElement.Load(appxManifest);
var dns = XNamespace.Get("http://schemas.microsoft.com/appx/2010/manifest");
var app = appxManifestXml.FirstDescendant(dns.GetName("Application"));
var entryAssembly = app.Attribute("Executable").Value;
var entryType = app.Attribute("EntryPoint").Value;
            
// Wire up the call to Track.Init, which is in the helper library
WireInHelperLibrary(extractedAppxFolder, entryAssembly, entryType);

The final step is to repack the .appx, again using the makeappx command-line utility:

/*** Step 4: Pack up the modified contents ***/
if (File.Exists(AppxFilenameOut)) File.Delete(AppxFilenameOut); 
makeAppxUnpackArgs = 
  string.Format(MakeAppxPackArgsTemplate, AppxFilenameOut, extractedAppxFolder);
RunProcess(Environment.CurrentDirectory, MakeAppxCommandPath, makeAppxUnpackArgs);

The WireInHelperLibrary method uses the NuGet package, Mono.Cecil, to locate the correct point within the entry point file. A call to the Track.Init method is inserted, which is contained within the GoogleHelper library, as the end of the OnLaunched method of the application (that is, the entry point class), as shown in Listing 3.

Listing 3: Wiring in the Helper Library
private static void WireInHelperLibrary(
  string extractFolder, string entryPointAssembly, string entryPointClass)
{
  // Get an IL reference to the Track.Init method that you want to add a reference to
  var helperLibraryPath = Path.Combine(extractFolder, "GoogleHelper.dll");
  var helperLibraryModule = ModuleDefinition.ReadModule(helperLibraryPath);
  var trackTypeRef = helperLibraryModule.Types.Single(t => t.Name == "Track");
  var trackInitMethodRef = trackTypeRef.Methods.Single(m => m.Name == "Init");

  // Get an IL reference to the entry point assembly
  var entryPointAssemblyPath = Path.Combine(extractFolder, entryPointAssembly);
  var entryPointModule = ModuleDefinition.ReadModule(entryPointAssemblyPath);
  var entryPointTypeRef = entryPointModule.Types.Single(t => t.FullName == entryPointClass);

  // Locate the last method in the OnLaunched method
  var entryPointOnLaunchedRef = 
    entryPointTypeRef.Methods.FirstOrDefault(m => m.Name == "OnLaunched");
  var entryPointProcesor = entryPointOnLaunchedRef.Body.GetILProcessor();
  var onLaunchedLastMethodRef = entryPointOnLaunchedRef.Body.Instructions.Last();

  // Add call to the Track.Init method
  var importedMethodRef = entryPointModule.Import(trackInitMethodRef);
  var methodCallRef = entryPointProcesor.Create(OpCodes.Call, importedMethodRef);
  entryPointProcesor.InsertBefore(onLaunchedLastMethodRef, methodCallRef);

  // Write the modified IL back out to the entry point assembly
  entryPointModule.Write(entryPointAssemblyPath);
}

For completeness, include the RunProcess method, which is used to invoke the makeappx command-line utility (see Listing 4).

Listing 4: Including the RunProcess Method
public static void RunProcess(string executionFolder, string generatorPath, string generatorArgs)
{
  var psi = new ProcessStartInfo(generatorPath, generatorArgs)
  {
    UseShellExecute = false,
    ErrorDialog = false,
    CreateNoWindow = true,
    WorkingDirectory = executionFolder,
  };


  using (var dllHost = new Process())
  {
    dllHost.StartInfo = psi;
    dllHost.Start();
    dllHost.WaitForExit();
  }
}

Running this process takes the original .appx file, injects a call to the Google Analytics SDK and repacks the .appx file. To adjust the Google Analytics tracking Id you just need to supply a different analytics.xml file. The repackaged .appx can be submitted to the Windows Phone Store or can be deployed to an emulator or device using the Windows Phone Application Deployment tool (comes with the Windows Phone SDK).

That's It? Yep.
Not too complicated, right? All I did was demonstrate how to wire up Google Analytics using an automated process that can easily be added to a build process. The key to this process is the third-party library, Mono.Cecil, which makes modifying or injecting code into an existing assembly possible. This technique can be used to inject additional code logic including additional UI elements, alternative navigation flow and much more.

About the Author

Nick Randolph runs Built to Roam, a consulting company that specializes in training, mentoring and assisting other companies build mobile applications. With a heritage in rich client applications for both the desktop and a variety of mobile platforms, Nick currently presents, writes and educates on the Windows Phone platform.

comments powered by Disqus
Most   Popular
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.