[MAUI] Local scope tags leak into subsequent HttpClient auto captured events
Package
Sentry
.NET Flavor
.NET
.NET Version
8.0.16
OS
macOS
OS Version
No response
Development Environment
Visual Studio v17.x
SDK Version
5.16.0
Self-Hosted Sentry Version
No response
Workload Versions
n/a
UseSentry or SentrySdk.Init call
.UseSentry(options =>
{
options.Dsn = "";
options.TracesSampleRate = 1.0;
options.CaptureFailedRequests = true;
options.FailedRequestStatusCodes.Add((400, 499));
})
Steps to Reproduce
Reported via Zendesk ticket: Tags set inside the local scope callback of SentrySdk.CaptureException(error, scope => { ... }) are appearing on automatically captured HttpClient events. The tag (lats.EventName) is only set within the local callback and not configured globally, and it bleeds into later auto captured events.
Make a failing HTTP request (404) inside a try/catch. In catch, call:
SentrySdk.CaptureException(ex, scope => { scope.SetTags(SetExtra(apiMethod, "http.error")); });
This correctly sends an event with tag lats.EventName=testtag.
Make another failing HTTP request (500). The HttpClient integration automatically captures an event. That event still includes the tag lats.EventName=testtag even though it wasn’t set globally.
Expected Result
Tags set in a local scope callback should not bleed to other automatically captured events.
Actual Result
The automatically captured HttpClient event inherits tags from the local scope, even though that scope should have been isolated.
Looking at the code, I can't see how this would be possible: https://github.com/getsentry/sentry-dotnet/blob/9e2f5f223c8651891ff9760abb9fd16163f3c820/src/Sentry/Internal/Hub.cs#L515-L533
Can we get a bit of sample code that reproduces the problem to investigate further?
Hi @jamescrosswell. I'm the one who reported it. I'll just copy and paste my email with all the details I sent earleir to @kerenkhatiwada:
===========
Here is exact code how all http related error events are being communicated from my app to Sentry.
public override void LogApiError(Exception error, string apiMethod, string apiFullUrl,
[Optional] string? payload,
[CallerMemberName] string? callerFunction = null,
[CallerFilePath] string? callerFile = null)
{
base.LogApiError(error, apiMethod, apiFullUrl, payload, callerFunction, callerFile);
if (settingsService.HandledErrorsTelemetry)
{
SentrySdk.CaptureException(error, scope =>
{
scope.Contexts.Add("Error Payload", BuildContext(null, apiFullUrl, callerFunction, callerFile, Sanitize(payload!, apiMethod), null));
// Here I set tags, one of them is lats.EventName that sparked this help request
scope.SetTags(SetExtra(apiMethod, "http.error"));
});
// disregard this block - it's my attempt to mitigate the situation
//SentrySdk.ConfigureScope(scope =>
//{
// scope.SetTag("lats.EventName", "PreviousNameIsReset");
//});
}
}
The above function is called from the try/catch block when some exception arises, like this:
// function Try { execute httpClient post or get something that will fail on the server } catch (HttpRequestException ex) { diagnostics.LogApiError(ex, request.MethodName, targetUrl, $"\nREQUEST {jsonRequest}\nRESPONSE {jsonResponse}", caller); }
So, the above setup is pretty standard and boring. Now, to replicate, we need to execute two sequential calls that will all fail on the server:
Call #1: https://my server/NegotiateApiVersion will result in 404 because such a page doesn't exist. The error is caught by the HttpRequestException catch block LogApiError will be triggered and Sentry event will be sent with a tag "Lats.EventName" set to the value of "NegotiateApiVersion" Here is the actual event: https://cma-inc.sentry.io/issues/6810027947/events/5f03878d536f4d58a826eba4acc0f0d1/?project=4507097791463424
Call #2: https://my server/sqlTest/api/V5/TDS/AddTDS will result in 500 because of, let's say, mismatch of the expected return type. The reason is truly irrelevant here. BEFORE the error is eventually caught by HttpRequestException catch block (in step #3) the HTTPClientError is somehow being reported to Sentry LogApiError is NOT triggered yet Here is the actual event. Well, it's actually two events and they are very similar: https://cma-inc.sentry.io/issues/5243722030/events/6486f3c48aa547b2931587d37b977913/?project=4507097791463424 https://cma-inc.sentry.io/issues/5243722030/events/d5a367b738554125bc01ec2af305edfe/?project=4507097791463424 HERE, in the above events that were NOT sent by me, by try/catch and my LogApiErrorfunction explicitly, you can clearly see that the tag "Lats.EventName" still set to the value of "NegotiateApiVersion" eventhough I played no role in setting nor keeping this tag.
Finally, the above error (the 500) is being caught by my HttpRequestException try/catch LogApiError will be triggered and Sentry event will be sent with a tag "Lats.EventName" set to the value of "TDS/AddTDS" Here is the actual event: https://cma-inc.sentry.io/issues/6916320887/events/4e94e91bdbb140b4baf9cf90c627454e/?project=4507097791463424
So, to reiterate, my question is about step #2 - why does the Sentry report retains the tag that was set on the previous call presumably on a local to the call scope?
===========
Something like that. I hope I am not misreading the documentation about hub and scope. The way I understand it, the line SentrySdk.CaptureException(error, scope => ... makes whatever tags are set to be local to this particular call.
Let me know if I can answer any more questions.
@Flash0ver I think you'll have to look into this. I don't have access to any of the events @MichaelShapiro is referencing.
As I say, I can't see logically how the result of the scope delegate could end up being applied to a different event (our code creates a clone of the underlying scope before invoking the delegate).
@jamescrosswell would it help if I attach json files for all the events I'm mentioning above?
@MichaelShapiro possibly but even with the JSON files, that just confirms that you have what looks like a problem (not how to fix it).
Ideally you could give us a little console app or a minimal ASP.NET Core app that demonstrates the behaviour... that way we could step into the code and see what's going on.
One way to create that would be to take a fork of your actual app and just delete stuff until all that's left is the minimum amount of code to reproduce (and scramble/scrub any sensitive data, if necessary).
@jamescrosswell You are right - I should've be prepared with a repo, my bad. Sorry for that. So, as I was concocting small repro last night I stumbled on an important detail that I missed before - I think it's important to surface it here. Console program won't be able to help here because ... this situation occurs ONLY when executing http request on MAUI/iOS.
So, same code base, same Sentry settings etc. Calling two sequential https request. both should fail. On Android each failure is "catch"ed predictably by my code and Sentry event sent the way I intended with tags I supplied. On iOS, in addition to predictably "catch"ed exception and correctly sent to Sentry, there are two additional events sent to Sentry. These are the events in question and the reason for this issue.
Additionally, if I explicitly turn off this option sentryOptions.CaptureFailedRequests = false then iOS behaves identical to Android - no extra and uncontrolled events are sent to Sentry. I didn't realize that the above property was true by default.
So, if still needed, I'd have to create small repro but it has to be a full MAUI mobile app program. But I still hope that the additional details above might help you to isolate the issue as I am not sure how quickly I can do this small repro this week.
Thank you.
@jamescrosswell I have a repo for you. It requires a security token to run against my server here. I couldn't make it replicatable without the actual connection:( I'll provide both to you - repository and the temp token - whenever you have a chance to take a look at it. Thank you so much.
Thanks @MichaelShapiro, that would be fantastic. I can take a look this week some time. You could DM me the connection details via Discord if you like. My handle is @wingfoiladdict.
wingfoiladdict
Would it be passible to send you email instead? I'm not on discord and I tried to send you friend request. That place seems very confusing (or maybe I'm just old fashioned:)
@MichaelShapiro thanks for providing the repro. I couldn't get it working with the token that you provided (everything just timed out after 60 seconds when I used that) but I got the general gist of what you were trying to do and managed to reproduce using a fake message handler instead of your real world service + token.
using System.Net;
using System.Net.Http.Json;
namespace SentryMAUI4610;
public partial class MainPage
{
private readonly HttpClient _httpClient;
public MainPage()
{
InitializeComponent();
var fakeHandler = new FakeHttpMessageHandler(request =>
{
var responseCode = request.RequestUri.AbsolutePath.Contains("NotPresent") ? 404 : 500;
return new HttpResponseMessage((HttpStatusCode)responseCode);
});
var sentryHandler = new SentryHttpMessageHandler(fakeHandler);
_httpClient = new HttpClient(sentryHandler);
}
private void ApiRequest01(object? sender, EventArgs e)
{
// this call simulates a 404 response and reports such to Sentry
CallApi("https://localhost/NotPresent", "NotPresent");
}
private void ApiRequest02(object? sender, EventArgs e)
{
// this call simulates a 500 response and reports such to Sentry
CallApi("https://localhost/Catastrophic", "Catastrophic");
}
private async void CallApi(string url, string requestName)
{
try
{
using var httpResponse = await _httpClient.PostAsJsonAsync(url, "foo" );
httpResponse?.EnsureSuccessStatusCode();
Console.WriteLine($"CallAPI - success");
}
catch (Exception ex)
{
Console.WriteLine($"CallAPI - error: {ex.Message}");
SentrySdk.CaptureException(ex, scope =>
{
scope.SetTag("MyTag", requestName);
scope.SetTag("MyUrl", url);
});
}
}
}
public class FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> getResponse) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
Task.FromResult(getResponse?.Invoke(request) ?? new HttpResponseMessage(HttpStatusCode.OK));
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
getResponse?.Invoke(request) ?? new HttpResponseMessage(HttpStatusCode.OK);
}
From what I can tell, everything is working as it should in the SDK and Tags are not bleeding over from one event to another.
However the UI in Sentry is confusing.
Firstly, it groups all of the events together... not in and of itself a problem:
When you click on an event however, you can click on the next/previous buttons to view subsequent/previous versions of that event. When doing so, the title itself doesn't update (it shows the description for whatever the first event was). So you can end up with a discrepancy between the event you think you're looking at and the one you're actually looking at.
Below, for example, is what I see when having clicked on the Latest event from the dashboard (so that was the first event I looked at - that was a 500 Internal Server Error) and subsequently having navigated to view the First event in that series/group (which was a 404 Not Found):
At that point, if you navigate to the Tags and look at them, what you see in the tags will no longer match the title that is shown in the UI - which makes you think the tags are bleeding over from one event to the next.
Short term workaround
Ignore the title - this can't be trusted... only look at the Stack Trace (this is always accurate).
Long term workaround
I'll raise an issue with the team that maintains the Sentry portal itself.