User Context Not Submitting Consistently

I have a WPF (.NET Framework) app and I am trying to tie user information to my Sentry events. The user information is only intermittently submitted and in most cases is not included in the event JSON. I have not been able to discern what is causing the user information to be missing from the context or why in some cases it is submitted successfully.

My setup is fairly simple. When a user logs in, I set the user information on the context like so:

SentrySdk.ConfigureScope(scope => scope.User = new User
    Email = user.Email,
    Id = user.Id,
    Username = user.UserName,
    Other = new Dictionary<string, string>() { { "Roles", userRoles } },

And similarly, I set scope.User to null when the user logs out of the application.

Any ideas on what I might be doing wrong would be greatly appreciated!

Is it possible you’ve configure a scope different than the one you’re capturing errors?
Do you use the PushScope and PopScope APIs of the Sentry SDK?

The only time I heard something similar happening it involved unmanaged threads calling into to C# (i.e IIS) which caused problems with the AsyncLocal which is used to hold the Scope (and hence, the data you’ve set to it).

Do you create Threads by newing them up? Or all your background work is done via Tasks?

Could you please try to create a reproducible example and share it with us as a github repository perhaps?

I’ve created a simple demo app for the issue here:

I am not using PushScope or PopScope, and I confirmed with the debug mode turned on that no other scopes are being created.

In WPF commands do not execute on a new thread, they execute on the main UI thread, unless you explicitly create a new thread from them to execute on. I am not creating any new Threads manually at all in this scenario so I don’t think that is an issue either.

From what I can tell, the issue arises from using async/await. In my demo app everything works as expected when the user context is set in a normal method, but stops working when I convert that method to use async/await.

Any insight you can provide would be very helpful! I don’t think I have any good way to convert my login flow to not use async/await, and even if I did I would really prefer not to hack something that way.

Thank you a lot for the repro! It was a great start to debug this.

So the problem isn’t really related to the fact there’s a ConfigureAwait(false) but instead with Sentry being initialized on OnStartup.

After a lot of debugging I finally realized that values set to a AsyncLocal on OnStartup are actually reset at the end of its invocation. So the Hub that the Sentry SDK initializes is gone by the time the UI events start coming in.

That means that each time an async UI event comes in, there’s no Scope stack in the SDK’s AsyncLocal, so the SDK creates one. Since AsyncLocal’s only flow downwards, the data is just lost.

I managed to work around that on your sample by moving the Sentry SDK initialization to the class’ (App) constructor. The value set to an AsyncLocal sticks when done there. The value though is not visible on OnStartup since it seems to have a new ExecutionContext that is later cleared.

Note that there’s no need to use Application.Current.Dispatcher.Invoke.
You can ConfigureScope directly on whatever thread your continuation executed following the ConfigureAwait(false).

On the other hand, WPF seems to be resetting any AsyncLocal data you set on OnStartup so avoid calling ConfigureScope there.

Thank you for figuring that out! I’ve taken your recommendations, applied them to my app and everything seems to be working fine now :slight_smile:

You’ve already seen it, but I also opened a PR to update the docs with this info:

For anyone looking at this in the future: the Application constructor posts an event to the dispatcher to execute the OnStartup event which is not the correct context for the AsyncLocal used by the Sentry SDK. Placing the SDK initialization in the app constructor instead sets up the SDK in the correct context and still executes before everything else in OnStartup() (since OnStartup is still queued with the dispatcher when the derived constructor executes)

1 Like