Analytics

[SOLVED] Firebase Performance Monitoring in Xamarin Android

Oliver Brown
— This upcoming video may not be available to view yet.

I decided to add Firebase Performance Monitoring to the latest version of Tic-tac-toe Collection.

What is it

This allows you to collect timed traces of things that happen in your app. You can set up custom traces for anything you like, but it also tracks how long HTTP requests take by default. At least it’s supposed to. On iOS it worked well, but Android had nothing.

There is an open issue on the repo with the Xamarin binding that was created in December 2018. It includes suggestions to make sure your app is set to using the native HTTP handlers (which makes sense since whatever Firebase is doing it won’t be expecting a random third party HTTP stack to be used), however it still does not work.

How does it work

So I decided to investigate how the HTTP tracing is actually implemented. How found this Stack Overflow question which lead me to this video. It turns out the implementation works by doing some byte code rewriting that changes any references in your app to the HTTP classes to instead reference Firebase provided wrappers.

Why doesn’t it work for Xamarin Android?

I kind of assumed it was doing something like that, but didn’t really think through the implications. Specifically - it does the byte code rewriting as part of a Gradle plugin that is not executed as part of a Xamarin build.

Solution

It may be possible to get the Gradle plugin to execute it’s transformer, but that seems complicated. Luckily, Firebase does provide support for adding HTTP tracing to other libraries, so I decided to work with that.

I decided to implement my own HttpMessageHandler that sets up the HttpMetric and then delegates to a normal HttpClientHandler. This can be passed into the constructor of HttpClient.

public class FirebaseHttpMessageHandler : DelegatingHandler
{
    public FirebaseHttpMessageHandler() : base(new HttpClientHandler())
    {
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var metric = StartTracking(request);
        var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        StopTracking(metric, response);
        return response;
    }

    private HttpMetric? StartTracking(HttpRequestMessage request)
    {
        try
        {
            var url = new Java.Net.URL(request.RequestUri.AbsoluteUri);
            var metric = Firebase.Perf.FirebasePerformance.Instance.NewHttpMetric(url, request.Method.Method);
            metric.Start();
            if (request.Content?.Headers?.ContentLength is long length)
            {
                metric.SetRequestPayloadSize(length);
            }

            return metric;
        }
        catch (Exception ex)
        {
            Debug.Fail($"URL performance tracking failed: {request.RequestUri.AbsoluteUri} - {ex.Message}");
            return null;
        }
    }

    private void StopTracking(HttpMetric? metric, HttpResponseMessage response)
    {
        if (metric is null)
        {
            return;
        }
        try
        {
            metric.SetHttpResponseCode((int)response.StatusCode);
            if (response.Content?.Headers?.ContentLength is long length)
            {
                metric.SetResponsePayloadSize(length);
            }
            if (response.Content?.Headers?.ContentType?.MediaType is string mediaType)
            {
                metric.SetResponseContentType(mediaType);
            }
        }
        catch (Exception ex)
        {
            Debug.Fail($"URL performance tracking failed: {response.RequestMessage.RequestUri.AbsoluteUri} - {ex.Message}");
        }
        finally
        {
            metric.Stop();
        }
    }
}

Why does it work for Xamarin iOS?

I don’t know the exact method used on iOS, but I do know that iOS has a feature called NSURLProtocol that lets you intercept all HTTP requests by the app, as well as method swizzling which lets you replace basically any method implementation at runtime.

Either of those could be used, and they would both just work in Xamarin.