diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d042183a6..a89e3bdd6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## vNext + +* Add SentryScopeStateProcessor #603 + ## 3.0.0-alpha.5 * Replaced `BaseScope` with `IScope`. (#590) @Tyrrrz diff --git a/samples/Sentry.Samples.Console.Customized/Program.cs b/samples/Sentry.Samples.Console.Customized/Program.cs index 2574d8db49..ede8e48c1e 100644 --- a/samples/Sentry.Samples.Console.Customized/Program.cs +++ b/samples/Sentry.Samples.Console.Customized/Program.cs @@ -107,6 +107,9 @@ await SentrySdk.ConfigureScopeAsync(async scope => { client.DefaultRequestHeaders.TryAddWithoutValidation("CustomHeader", new[] { "my value" }); }; + + // Control/override how to apply the State object into the scope + o.SentryScopeStateProcessor = new MyCustomerScopeStateProcessor(); })) { // Ignored by its type due to the setting above @@ -222,7 +225,7 @@ public AdminPartMiddleware(ISentryClient adminClient, dynamic middleware) public void Invoke(dynamic request) { - using (SentrySdk.PushScope()) + using (SentrySdk.PushScope(new SpecialContextObject())) { SentrySdk.AddBreadcrumb(request.Path, "request-path"); @@ -265,4 +268,27 @@ protected override void ProcessException(ArgumentException exception, SentryEven sentryEvent.SetTag("parameter-name", exception.ParamName); } } + + private class MyCustomerScopeStateProcessor : ISentryScopeStateProcessor + { + private readonly ISentryScopeStateProcessor _fallback = new DefaultSentryScopeStateProcessor(); + + public void Apply(IScope scope, object state) + { + if (state is SpecialContextObject specialState) + { + scope.SetTag("SpecialContextObject", specialState.A + specialState.B); + } + else + { + _fallback.Apply(scope, state); + } + } + } + + private class SpecialContextObject + { + public string A { get; } = "hello"; + public string B { get; } = "world"; + } } diff --git a/src/Sentry/Internal/SentryScopeManager.cs b/src/Sentry/Internal/SentryScopeManager.cs index a41eb583f2..1aeaed620f 100644 --- a/src/Sentry/Internal/SentryScopeManager.cs +++ b/src/Sentry/Internal/SentryScopeManager.cs @@ -58,8 +58,14 @@ public IDisposable PushScope(TState state) if (scope.Key.Locked) { - // TODO: keep state on current scope? - _options.DiagnosticLogger?.LogDebug("Locked scope. No new scope pushed."); + _options?.DiagnosticLogger?.LogDebug("Locked scope. No new scope pushed."); + + // Apply to current scope + if (state != null) + { + scope.Key.Apply(state); + } + return DisabledHub.Instance; } diff --git a/src/Sentry/Protocol/DefaultSentryScopeStateProcessor.cs b/src/Sentry/Protocol/DefaultSentryScopeStateProcessor.cs new file mode 100644 index 0000000000..b53d899247 --- /dev/null +++ b/src/Sentry/Protocol/DefaultSentryScopeStateProcessor.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Sentry.Protocol +{ + /// + /// Defines the logic for applying state onto a scope. + /// + public class DefaultSentryScopeStateProcessor : ISentryScopeStateProcessor + { + /// + /// Applies state onto a scope. + /// + public void Apply(IScope scope, object state) + { + switch (state) + { + case string scopeString: + // TODO: find unique key to support multiple single-string scopes + scope.SetTag("scope", scopeString); + break; + case IEnumerable> keyValStringString: + scope.SetTags(keyValStringString + .Where(kv => !string.IsNullOrEmpty(kv.Value))); + break; + case IEnumerable> keyValStringObject: + { + scope.SetTags(keyValStringObject + .Select(k => new KeyValuePair( + k.Key, + k.Value?.ToString()!)) + .Where(kv => !string.IsNullOrEmpty(kv.Value))); + + break; + } +#if HAS_VALUE_TUPLE + case ValueTuple tupleStringString: + if (!string.IsNullOrEmpty(tupleStringString.Item2)) + { + scope.SetTag(tupleStringString.Item1, tupleStringString.Item2); + } + break; +#endif + default: + scope.SetExtra("state", state); + break; + } + } + } +} diff --git a/src/Sentry/Protocol/IScopeOptions.cs b/src/Sentry/Protocol/IScopeOptions.cs index 8b2262c6ae..d7629b4154 100644 --- a/src/Sentry/Protocol/IScopeOptions.cs +++ b/src/Sentry/Protocol/IScopeOptions.cs @@ -7,6 +7,11 @@ namespace Sentry.Protocol /// public interface IScopeOptions { + /// + /// Configured scope processor. + /// + ISentryScopeStateProcessor SentryScopeStateProcessor { get; set; } + /// /// Gets or sets the maximum breadcrumbs. /// diff --git a/src/Sentry/Protocol/ISentryScopeStateProcessor.cs b/src/Sentry/Protocol/ISentryScopeStateProcessor.cs new file mode 100644 index 0000000000..8a49ef8b9b --- /dev/null +++ b/src/Sentry/Protocol/ISentryScopeStateProcessor.cs @@ -0,0 +1,13 @@ +namespace Sentry.Protocol +{ + /// + /// Defines the logic for applying state onto a scope. + /// + public interface ISentryScopeStateProcessor + { + /// + /// Applies state onto a scope. + /// + void Apply(IScope scope, object state); + } +} diff --git a/src/Sentry/ScopeExtensions.cs b/src/Sentry/ScopeExtensions.cs index 233655249e..73f4c00d5d 100644 --- a/src/Sentry/ScopeExtensions.cs +++ b/src/Sentry/ScopeExtensions.cs @@ -2,8 +2,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Sentry.Extensibility; +using System.Linq; using Sentry.Internal; using Sentry.Protocol; using Constants = Sentry.Protocol.Constants; @@ -354,39 +354,8 @@ public static void Apply(this IScope from, IScope to) /// The state object to apply. public static void Apply(this IScope scope, object state) { - switch (state) - { - case string scopeString: - // TODO: find unique key to support multiple single-string scopes - scope.SetTag("scope", scopeString); - break; - case IEnumerable> keyValStringString: - scope.SetTags(keyValStringString - .Where(kv => !string.IsNullOrEmpty(kv.Value))); - break; - case IEnumerable> keyValStringObject: - { - scope.SetTags(keyValStringObject - .Select(k => new KeyValuePair( - k.Key, - k.Value?.ToString()!)) - .Where(kv => !string.IsNullOrEmpty(kv.Value))); - - break; - } -#if HAS_VALUE_TUPLE - case ValueTuple tupleStringString: - if (!string.IsNullOrEmpty(tupleStringString.Item2)) - { - scope.SetTag(tupleStringString.Item1, tupleStringString.Item2); - } - - break; -#endif - default: - scope.SetExtra("state", state); - break; - } + var processor = scope.ScopeOptions?.SentryScopeStateProcessor ?? new DefaultSentryScopeStateProcessor(); + processor.Apply(scope, state); } /// diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index d68727c55d..de9b3766c9 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -59,6 +59,9 @@ public class SentryOptions : IScopeOptions internal ISentryHttpClientFactory? SentryHttpClientFactory { get; set; } + /// + public ISentryScopeStateProcessor SentryScopeStateProcessor { get; set; } = new DefaultSentryScopeStateProcessor(); + /// /// A list of namespaces (or prefixes) considered not part of application code ///