Accessing and ensuring global/shared scope in .NET

I am working with the .NET SDK, and we are currently on 2.1.4 (the latest at the time our app was released).

I gather that there is such a thing as a global scope, but it does not appear that we are creating it, since I also gather that any logging captured on threads would inherit anything applied on the global scope, such as Scope.User, Scope.Contexts.*, tags, and (I hope) breadcrumbs. Is this correct?

I found one forum post (Scopes and multithreading in python) talking about Hub, and ways to get unambiguously to the global scope. But Hub doesn’t seem to be much of a thing in this .NET version.

So really two questions, and an answer to either would suffice.

  1. On what thread or in what way can we configure scope such that it WOULD be the global one (assuming my idea of global scope is even a thing, where certain core data would always be set, after being set once, across the whole application)?
  2. How can it be accessed from elsewhere (threadwise) and added to, such that the data remains present for all logging? I assume SentrySdk.WithScope would be the way to use it, or some clone of it?

Currently, for both Android and iOS (but .NET by way of Xamarin), we call ConfigureScope on the main UI thread. But many things happen on other threads that we expect to accumulate breadcrumbs, or if errors are logged there we expect to reflect all the breadcrumbs and basic global info.

Should we just retain the scope instance coming off of ConfigureScope, regardless of whether it’s “global” or not, and then use it with WithScope everywhere we want the info to carry across? I feel like this should be clear enough but somehow I’m failing to search using the right language I guess, or what I’m finding doesn’t seem to apply to the .NET SDK, or is ambiguous from docs.

I also gather that any logging captured on threads would inherit anything applied on the global scope, such as Scope.User , Scope.Contexts.* , tags, and (I hope) breadcrumbs. Is this correct?

Today Scope data is backed by AsyncLocal so once a new Thread is spawned, the Scope is cloned and data is copied. If this is a console app for example, and you ConfigureScope in main thread, any thread that is spawned from there will hold that data but have its own Scope.

  1. On what thread or in what way can we configure scope such that it WOULD be the global one (assuming my idea of global scope is even a thing, where certain core data would always be set, after being set once, across the whole application)?

If you do that on the main thread, that would be the case.
We have a PR open to make an actual “Global Hub” as it exists on Android and iOS, which makes total sense for console, mobile and desktop apps. Is your use case a Desktop app or else?

Ultimately there are other ways to add data to all events like through EventProcessor so I wonder what type of app to clarify if the Global Hub is the way to go here or we could discuss BeforeSend or other ways of “adding data to all events”.

  1. How can it be accessed from elsewhere (threadwise) and added to, such that the data remains present for all logging? I assume SentrySdk.WithScope would be the way to use it, or some clone of it?

WithScope is short for:


SentrySdk.PushScope()
try {
  SentrySdk.ConfigureScope(s => s. ....);
  ....
} finally {
 SentrySdk.PopScope()
}

The current behavior is that Hub is AsyncLocal and hence “Thread Local” but aware of asynchronous code. So there isn’t really any “Global Hub”, except at the point where the Main runs and you have access to the root Hub, before any async happens or any Tasks are spawned.

Should we just retain the scope instance coming off of ConfigureScope , regardless of whether it’s “global” or not, and then use it with WithScope everywhere we want the info to carry across?

Having to take the reference of a scope through that call back would be a work around that we definitly won’t suggest users. If you’re having to write such code, we either fail on providing your with a proper API or documentation on how to tackle a problem in a better way.

Could you give some example of the use case you have at hand? This will help us decide next steps here given that we anyway need to cater for “Global State” approach needed by Mobile (Xamarin) and Desktop which has come up before.

1 Like

The use case is a mobile app, so there is iOS and Android at the “bottom” but it’s Xamarin/.NET so I’m not sure how that affects things. From what I understand, the equivalent of main in Android would be the main launcher’s OnCreate method, which is indeed the main UI thread. So that is where we initialize what we hope will carry to other threads. However as you say:

but I don’t know how that bears out with callbacks coming from System.Timers.Timer which may occur on different threads. This is one case where we do not see all of our scope variables showing up in logs.

A second case would be from handling AppDomain.CurrentDomain.UnhandledException. We get some sense that SentrySdk already implicitly handles this event, but it was inconsistent, sometimes producing events, sometimes not. This is a separate concern really, but the point was that it was never logging any of the scope info previously initialized on the main thread.

What we did there certainly is a workaround, but ensured that (almost?) every crash produced a log, and had all the scope info we needed:

void InitLogging() {
	this.sentryClient = SentrySdk.Init(LoggingService.SentryEntryPoint);
	SentrySdk.ConfigureScope(OnConfigureScope);
	AppDomain.CurrentDomain.UnhandledException += OnUnhandledException
}

void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) {
	this.sentryClient?.Dispose(); 		// Saved from our initial SentrySdk.Init
	this.sentryClient = SentrySdk.Init(LoggingService.SentryEntryPoint);

	SentrySdk.WithScope(scope =>
	{
		this.OnConfigureScope(scope); 	// Set OS version, device metrics, build version etc
		SentrySdk.CaptureException((Exception)e.ExceptionObject );
		this.sentryClient.Dispose();
		this.sentryClient = null;
	});
}

Which we could certainly do likewise (at least the re-initializing scope with SentrySdk.WithScope & this.OnConfigureScope ) for logging from the timer-based threads, but it seems like it would have some overhead and shouldn’t be necessary. I don’t necessarily understand which thread spawns from which, so it feels safer to be able to always access the same scope unless/until we actually want separate threads to dictate what’s relevant. But if it’s a hierarchical thing coming down from main, right, I don’t see why our top-level scope info isn’t surviving, other than something peculiar about Android or Xamarin or the combination.

Xamarin Support

Xamarin is a complicated subject. Our current state is “limited support”.

The current SDK indeed will not guarantee delivery with the current crash handler, because on Xamarin you can’t flush the event out to the network (establish a TLS connection) before the app is killed by the OS.

The way we support mobile for React Native, and native (Android, iOS) is that we write the crash data to disk first and when the app restarts, we flush the data to sentry on the background.
On top of that there’s the shared state thing that’s optimized for server, as we discussed.

I’ve recently experimented with this and hacked my way into making this work on Xamarin: https://github.com/bruno-garcia/sentry-dotnet/blob/e71e09b9020ba80faf54a07fa9578ba9347b63c4/src/Sentry/Integrations/AppDomainUnhandledExceptionIntegration.cs#L40-L50

Given that now (on 3.0.0-alpha) we can serialize/deserialize stuff, it could be replace with a File.WriteAllBytes and on app restart, just call Capture.

Work around shared state

As I said above, we need to have a “Global Hub” mode to cater not only Xamarin, but WPF, WinForms and Unity. This is on our list.

Until then, you can work around the “Global State” issue by adding an EventProcesssor like:

SentrySdk.Init(o => o.AddEventProcessor(new MyProcessor());

And inside your MyProcessor you have a hook to all passing events before they are captured to Sentry. So there you can add any data you’d normally add to the scope, straight into the event. You can hold that reference statically if you will, and mutate that as the program runs, in order to pass data that should be applied to the event.

Closing

The .NET SDK recently received support to Envelopes and now for Deserialization so we can add offline caching next.

Next we’d need to add “Global Hub” (The PR from Lucas is a start but we’ll do it a bit differently) support so you can share global state, and make sure the UnhandledException handler writes things to disk first. We want to have that support also for Unity.

If you’re interested in contributing to the .NET SDK we could work together on getting Xamarin support in. But right now Sentry has no plan to add support directly at this time but we’re close from it “just working” with it.

If you’d like to discuss this, you can find us on Discord: https://discord.gg/Ww9hbqr
There’s a #dotnet channel there

1 Like

Right, and we definitely saw that it was “hit or miss” whether crashes got logged. But somewhere on the forums it was suggested that disposing of the client was sufficient to flush it out to the network (before the OS kills everything). So indeed with the code I posted it’s capturing 100%, I believe.

Right, and we definitely saw that it was “hit or miss” whether crashes got logged. But somewhere on the forums it was suggested that disposing of the client was sufficient to flush it out to the network (before the OS kills everything). So indeed with the code I posted it’s capturing 100%, I believe.

That’s true for the .NET, CLR or CoreCLR. But Mono on Android behaves differently sadly.

We’re improving this constantly. Offline caching, storing to disk on Mono (Xamarin, Unity) etc.
You can follow development on: https://github.com/getsentry/sentry-dotnet

image

@ibm5155 is working on a Xamarin SDK: https://github.com/getsentry/sentry-dotnet-xamarin

We solved “Global Mode” on version 3.8.1 of the .NET SDK.

It makes it so that any call to SentrySdk.SetTag or AddBreadcrumb or anything else that you do through SentrySdk.ConfigureScope mutates a single, static Scope object that affects all threads of the app. This is useful for Desktop and Mobile apps when you have a single user session and you add context to Sentry anywhere in the app and want that to be included if it crashes in any other thread.

You can opt-in to this mode through:

options.IsGlobalModeEnabled = true

To clarify: This options doesn’t make sense in any sort of web server where each individual request takes a separate scope (You want each SetTag("url", ...) to take a different request URL, headers etc). Nothing needs to be done for server apps, it continues to work as it did in the past.

Next release of Sentry.Xamarin and Sentry.Xamarin.Forms will include this option already opt-ed in.

We plan to ship Sentry.Wpf and Sentry.WinForms in the future that would already flip IsGlobalModeEnabled = true for you. Until then we’ll add to the docs that you can do that if you prefer to have a single mutable state.