Monitoring Xamarin Android Applications

In a previous post we completed all the basic tasks building and publishing an app for Android using Xamarin Android. Part of deploying a mobile app includes the ability to be able to monitor crashes and report on analytics.

To do this monitoring I declared two interfaces one for analytics and one for crashes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface ICrashReporter
{
    void TestReporting();
    void LogNonFatalException(Exception ex);
}

public interface IAnalyticsEngine
{
    void DownloadFeedEvent(int numberOfItems);
    void DownloadSpecificFeedEvent(int numberOfItems, string folder);
    void DownloadEpisodeEvent(long sizeInMB);
    void DownloadEpisodeCompleteEvent();
    void LoadControlFileEvent();
    void LifecycleLaunchEvent();
    void LifecycleErrorEvent();
    void LifecycleErrorFatalEvent();
    void GeneratePlaylistEvent(PlaylistFormat format);
    void GeneratePlaylistCompleteEvent(int numberOfItems);
    void PurgeScanEvent(int numberOfItems);
    void PurgeDeleteEvent(int numberOfItems);
}

Google Analytics and Firebase Crashlytics

The automatic choice for most android applications is Google Analytics and Firbase Crashlytics

Adding these to a Xamarin Android application is pretty straightforward. Simply add NuGet package references for Xamarin.Firebase.Analytics and Xamarin.Firebase.Crashlytics to the project.

There are instructions for setting up Crashlytics but essentially you create a project in the developer console, download the generated google-services.json and add the file to your project

1
2
3
<ItemGroup>
  <AndroidAsset Include="Assets\NLog.config" />
  <GoogleServicesJson Include="google-services.json" />

There is a small gotcha as explained here, but if you add this string to your resources then all should be well.

1
2
3
4
5
6
7
8
<resources>
 <!-- 
 Bonkers - but see
 https://github.com/a-imai/XamarinCrashlyticsUpgradeSample
 https://docs.microsoft.com/en-us/answers/questions/450181/android-firebase-crashlytics-build-id-is-missing.html
 -->
 <string name="com.google.firebase.crashlytics.mapping_file_id">none</string>
</resources>

The crash reporter looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CrashlyticsReporter : ICrashReporter
{
  private IAnalyticsEngine AnalyticsEngine;

  public CrashlyticsReporter(IAnalyticsEngine analyticsEngine)
  {
    AnalyticsEngine = analyticsEngine;
  }

  public void LogNonFatalException(Exception ex)
  {
    var throwable = Java.Lang.Throwable.FromException(ex);
    FirebaseCrashlytics.Instance.RecordException(throwable);

    AnalyticsEngine?.LifecycleErrorEvent();
  }

  public void TestReporting()
  {
    throw new NotImplementedException("Test Crash Reporting");
  }
}

The analytics looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class FirebaseAnalyticsEngine : IAnalyticsEngine
{
  private Context ApplicationContext;
  private IAndroidApplication Application;

  public FirebaseAnalyticsEngine(Context applicationContext, IAndroidApplication application)
  {
    ApplicationContext = applicationContext;
    Application = application;
  }

  private void SendEvent(string eventName, string eventCategory, string eventAction, string eventLabel, string eventValue)
  {
    var firebaseAnalytics = FirebaseAnalytics.GetInstance(ApplicationContext);
    var bundle = new Bundle();
    bundle.PutString(FirebaseAnalytics.Param.ItemId, eventCategory);
    if (eventAction != null)
    {
      bundle.PutString(FirebaseAnalytics.Param.ItemName, eventAction);
    }
    if (eventLabel != null)
    {
      bundle.PutString(FirebaseAnalytics.Param.ContentType, eventLabel);
    }
    if (eventValue != null)
    {
      bundle.PutString(FirebaseAnalytics.Param.Value, eventValue);
    }
    firebaseAnalytics.LogEvent(eventName, bundle);
  }

  private const string Seperator = "_";
  private const string Category_Lifecycle = "Lifecycle";
  private const string Action_Lifecycle_Error = "Lifecycle_Error";

  public void LifecycleErrorEvent()
  {
    SendEvent(
      FirebaseAnalytics.Event.SelectContent,
      Category_Lifecycle,
      Action_Lifecycle_Error,
      Action_Lifecycle_Error + Seperator + Application.DisplayVersion,
      null
    );
  }

The crashes appear like this

Crashlytics

The stack trace is a little mangled almost certainly caused by passing it from .NET to Java environments

AppCenter Crashes and Analytics

The main issue with using Google Analytics and Firebase Crashlytics is that it ties you into using Google Play Services and that means that you can only run on Google’s Android, Amazon Fire OS and Windows Subsystem for Android are not available without hacking.

Microsoft AppCenter is an free alternative from Microsoft that, amongst other things, has AppCenter Crashes and AppCenter Analytics that are direct replacements that work in all Android forks.

Getting setup is also very simple. Add NuGet package references for Microsoft.AppCenter.Analytics and Microsoft.AppCenter.Crashes Then create a new project in the AppCenter Console and then initialise the SDK like this

1
2
3
4
5
6
  [Application]
  public class AndroidApplication : Application, IAndroidApplication
  {
    public override void OnCreate()
    {
      AppCenter.Start(Secrets.APP_CENTER_SECRET, typeof(Analytics), typeof(Crashes));

The crash reporter looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AppCenterCrashReporter : ICrashReporter
{
  private IAnalyticsEngine AnalyticsEngine;

  public AppCenterCrashReporter(IAnalyticsEngine analyticsEngine)
  {
    AnalyticsEngine = analyticsEngine;
  }

  public void LogNonFatalException(Exception ex)
  {
    Crashes.TrackError(ex);
    AnalyticsEngine?.LifecycleErrorEvent();
  }

  public void TestReporting()
  {
    // Note - this will only do anything in a debug build
    Crashes.GenerateTestCrash();
  }
}

The analytics looks like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AppCenterAnalyticsEngine : IAnalyticsEngine
{
  private IAndroidApplication Application;

  public AppCenterAnalyticsEngine(IAndroidApplication application)
  {
    Application = application;
  }

  private const string Event_Lifecycle_Error = "Lifecycle_Error";
  private const string Property_Version = "Version";

  public void LifecycleErrorFatalEvent()
  {
    Analytics.TrackEvent(Event_Lifecycle_ErrorFatal, new Dictionary<string, string> {
      { Property_Version, Application.DisplayVersion}
    });
  }

The crashes appear like this

Crashlytics

Both mechanisms work pretty much as you would expect