sentry-unity icon indicating copy to clipboard operation
sentry-unity copied to clipboard

Spans for HTTP requests - `UnityWebRequest` and `HttpMessageHandler`

Open bruno-garcia opened this issue 3 years ago • 3 comments
trafficstars

When a user makes an HTTP request to their backend, we want to measure how long that took. And propagate the trace id so we can have end to end tracing.

This can be split in:

HttpMessageHandler

The .NET approach. Used in Sentry Defenses: https://github.com/getsentry/sentry-defenses/blob/2989949f857f1cccb46814ce0a05d50f603f9222/game/Assets/Scripts/Manager/BugSpawner.cs#L37

We need to find a way add the handler automatically. At least on the one assembly created by Unity when compiling the scripts.

Steps:

  • [ ] A PoC that gets a DLL that has new HttpClient(...) and adds the middleware (as done in Sentry defenses) @SimonCropp
  • [x] Find out which Unity hooks can we rely on to get the GameAssembly.dll @bitsandfoxes
  • [ ] With the PoC we can discuss next steps such as: Is this a .NET Global Tool? Do we have the code in Sentry.Unity.Editor?

UnityWebRequests (Unity Client)

Example HTTP client usage: https://github.com/getsentry/sentry-unity/blob/7774137c35ce4a3653b934e15ed15f0078a2ee36/src/Sentry.Unity/UnityWebRequestTransport.cs#L75-L81 We need a way to plug Sentry in there. First 'manually' with some C# code. Then automatically, possibly with IL weaving.

  • [ ] We need a strategy to get spans/crumbs similar to SentryHttpMessageHandler @bitsandfoxes
  • [ ] PoC with IL weaving a DLL using UnityWebRequest and add the new interceptor @SimonCropp
  • [ ] With the PoC we can discuss next steps such as: Is this a .NET Global Tool? Do we have the code in Sentry.Unity.Editor?

bruno-garcia avatar May 09 '22 12:05 bruno-garcia

i took a look into implementing the custom uploadHandler and downloadHandler and hit a wall.

The IL replacement plan is to detect usage of existing handlers and wrap them in our own. but that wrapping does not seem possible. The reason is that the the Dispose in the base classes is not virtual. So if we wrap an exiting instance, we override Dispose on our implementation so as to then call Dispose on the inner handler.

I considered re-implementing each of the DownloadHandler implementations that ship with Unity. Unfortunately they all use internal methods in the base class. This might be possible with some reflection. Note it would only work for the known subset of handlers that ship with unity

we cant inherit and override Unitys default handlers since they are all sealed

SimonCropp avatar Jun 21 '22 05:06 SimonCropp

Our goal is to wrap that HTTP request into a span/crumb creation logic, like we do here: https://github.com/getsentry/sentry-dotnet/blob/36ccacf43b8aeedc493585ea57bc05880b285773/src/Sentry/SentryHttpMessageHandler.cs#L52-L96

Like if the HTTP request code would be here: https://github.com/getsentry/sentry-dotnet/blob/36ccacf43b8aeedc493585ea57bc05880b285773/src/Sentry/SentryHttpMessageHandler.cs#L76

So during IL weaving we would find:

var www = new UnityWebRequest 
{ 
    url = message.RequestUri.ToString(), 
    method = message.Method.Method.ToUpperInvariant(), 
    uploadHandler = new UploadHandlerRaw(contentMemoryStream.ToArray()), 
    downloadHandler = new DownloadHandlerBuffer() 
}; 

// REST OF CODE

Then move the two parameters up to an assignment, so we can easily copy its values, add span:

var myUrlValue = message.RequestUri.ToString(), 
var myMethodValue = message.Method.Method.ToUpperInvariant(), 

var span = _hub.GetSpan()?.StartChild(
                "http.client",
                // e.g. "GET https://example.com/"
                $"{requestMethod} {url}");
try
{
    var www = new UnityWebRequest 
    { 
        url = myUrlValue.
        method = myMethodValue,
        uploadHandler = new UploadHandlerRaw(contentMemoryStream.ToArray()), 
        downloadHandler = new DownloadHandlerBuffer() 
    }; 

    // REST OF CODE

    var breadcrumbData = new Dictionary<string, string>
    {
        { "url", myUrlValue },
        { "method", myMethodValue },
        { "status_code", ((int)response.StatusCode).ToString() }
    };
    _hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData);

    // This will handle unsuccessful status codes as well
    span?.Finish(SpanStatusConverter.FromHttpStatusCode(response.StatusCode));

    return response;
}
catch (Exception ex)
{
    span?.Finish(ex);
    throw;
}

bruno-garcia avatar Jun 21 '22 16:06 bruno-garcia