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 Thread
s by new
ing them up? Or all your background work is done via Task
s?
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: https://gitlab.com/senti.mark.monteiro/sentry-demo-app
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
You’ve already seen it, but I also opened a PR to update the docs with this info: https://github.com/getsentry/sentry-docs/pull/2060
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