So we built this library/framework thing full of code related to business processes and common elements that are shared across multiple applications (C#, .Net 4.7.1,WPF, MVVM). Our Logging stuff is all set up through this framework so naturally it felt like the best place for Sentry. All the references in our individual applications are manually pointed to the dlls the folder where our shared library thingy installs itself. So far so good.
When we set up Sentry initially everything seemed to work great. We do some updates and errors seem to be going way down. That's cause we are awesome and Sentry helped us be more awesome, right? Nope! Well I mean kind of.
The scope is being disposed of so we are no longer getting Unhandled exceptions. We didn't notice at first because we are still getting sentry logs when we are handling errors through our Logging.Log() method. This logging method calls SentrySdk.Init() which I suspect is disposing the client in the executing assembly.
We also started using Sentry for some simple Usage tracking by spinning up a separate project in Sentry called Usage-Tracker and passing a simple "DoThingApplication has been launched"
with an ApplicationName.UsageTracker
Enum as a parameter to our Logging method.
Question: What is a good way to handle this where my setup can have a Sentry instance that wraps my using(sentryClientStuff){ ComposeObjects(); }
and still have my logging method look for the existing client and use it if it exists?
Caveats:
Maybe there is a better way to handle references that could solve both this and some other pains of when they have become mismatched between client and shared framework/library thing
Maybe the answer can be found through adding some Unit Tests but I could use a Sentry specific example or a nudge there because I don't know a muc about that.
Maybe there is a way to use my shared library to return a Sentry Client or Scope that I could use in my client assembly that would not be so fragile and the library could somehow also use it.
Maybe there is a better solution I can't conceive because I'm just kind of an OK programmer and it escapes me. I'm open to any advice/correction/ridicule.
Maybe there is a smarter way to handle "Usage-Tracker" type signals in Sentry
Really I want a cross-assembly singleton kind of thing in practice.
There are really many things going on here. Also without looking at any code it's hard to picture how things are laid out. There's a better chance you can get the answer your are looking for if you share some (dummy even) example of the structure of your project.
I'll try to break it down and address what I can anyway:
With regards to:
Usage-Tracker:
You can create a new client and bind to a scope. That way any use of the SentrySdk
static class (which I assume your Logger.Log
routes to) will pick up.
In other words, call SentrySdk.Init
as you currently do, with the options that are shared across any application using your shared library, and after that create a client using the DSN
of your Usage-Tracker project in sentry. Push a scope, bind the client and you can use SentrySdk
with it.
There's an example in the GitHub repo of the SDK:
using (SentrySdk.PushScope())
{
SentrySdk.AddBreadcrumb(request.Path, "request-path");
// Change the SentryClient in case the request is to the admin part:
if (request.Path.StartsWith("/admin"))
{
// Within this scope, the _adminClient will be used instead of whatever
// client was defined before this point:
SentrySdk.BindClient(_adminClient);
}
SentrySdk.CaptureException(new Exception("Error at the admin section"));
// Else it uses the default client
_middleware?.Invoke(request);
} // Scope is disposed.
The SDK only has to be initialized once but you can always create a new client with new SentryClient
, push a new scope (SentrySdk.PushScope()
) and bind it to that new scope (SentrySdk.BindClient
). Once you pop the scope the client is no longer accessdible via SentrySdk.CaptureException
or any other method on the static class SentrySdk
.
You can also use the client directly, without binding it to the scope at all.
using (var c = new SentryClient(new SentryOptions { Dsn = new Dsn("...") })) {
c.CaptureMessage("hello world!");
}
The using
block is there to make sure the background thread flushes the event.
Central place to initialize the SDK: There will be configuration which you want to have fixed in your shared framework/library but surely each application (composition root) will have its own setting. Release is auto-discovered. From docs.sentry.io:
The SDK will firstly look at the entry assembly’s AssemblyInformationalVersionAttribute, which accepts a string as value and is often used to set a GIT commit hash. If that returns null, it’ll look at the default AssemblyVersionAttribute which accepts the numeric version number.
If you patch your assemblies in your build server, the correct Release
should be reported automatically. If not, you could define it per application by taking a delegate that passes the SentryOptions
as argument.
Something like:
Framework code
:
public class MyLogging
{
void Init(Action<SentryOptions> configuration)
{
var o = new SentryOptions();
// Add things that should run for all users of this library:
o.AddInAppExclude("SomePrefixTrueForAllApplications");
o.AddEventProcessor(new GeneralEventProessor());
// Give the application a chance to reconfigure anything it needs:
configuration?.Invoke(o);
}
}
App code
:
void Main()
{
MyLogging.Init(o => o.Environment = "my env");
}
The scope is being disposed of so we are no longer getting Unhandled exceptions."
Not sure I understand what's going on here. Pushing and popping (disposing) scopes don't affect the ability of the SDK to capture unhandled exceptions. Could you please share a repro?
This logging method calls SentrySdk.Init() which I suspect is disposing the client in the executing assembly.:
Unless you create a client "by hand" with new SentryClient
, there's only 1 client in the running process. Please note I said running process and not assembly. Instances are not held within an assembly. The assembly only contains the code that can be executed. If you call SentrySdk.CaptureException
it will dispatch the call to the SentryClient
bound to the current scope. If you didn't PushScope
, there's always an implicit scope, the root scope. In this case it's all transparent enough you shouldn't care there's a scope in there. You also can't dispose of that scope since you never got a handle to do so (you didn't call PushScope
so you didn't get what it returns to call Dispose
on).
All the references in our individual applications are manually pointed to the dlls the folder where our shared library thingy installs itself.:
One thing to consider, depending on your environment is to distribute packages via NuGet. I'm unsure whether you expect to use these libraries in non .NET Framework applications (like .NET Core). But considering .NET Core 3.0 is bringing Windows Desktop framework support like WPF and WinForm, it's possible that eventually you will. If that's the case, consider targeting .NET Standard
instead of .NET Framework
for your code libraries.