Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Added non-allocating `ConfigureScope` and `ConfigureScopeAsync` overloads ([#4244](https://github.com/getsentry/sentry-dotnet/pull/4244))

### Fixes

- The HTTP instrumentation uses the span created for the outgoing request in the sentry-trace header, fixing the parent-child relationship between client and server ([#4264](https://github.com/getsentry/sentry-dotnet/pull/4264))
Expand Down
18 changes: 9 additions & 9 deletions src/Sentry.AspNetCore/SentryMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
context.Items.TryAdd(BaggageHeaderItemKey, baggageHeader);
context.Items.TryAdd(TransactionContextItemKey, transactionContext);

hub.ConfigureScope(scope =>
hub.ConfigureScope(static (scope, arg) =>
{
// At the point lots of stuff from the request are not yet filled
// Identity for example is added later on in the pipeline
Expand All @@ -131,16 +131,16 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
// when the event fires. Use `activeScope`, not `scope` or `hub`.
scope.OnEvaluating += (_, activeScope) =>
{
SyncOptionsScope(activeScope);
PopulateScope(context, activeScope);
arg.middleware.SyncOptionsScope(activeScope);
arg.middleware.PopulateScope(arg.context, activeScope);
};
});
}, (middleware: this, context));

// Pre-create the Sentry Event ID and save it on the scope it so it's available throughout the pipeline,
// even if there's no event actually being sent to Sentry. This allows for things like a custom exception
// handler page to access the event ID, enabling user feedback, etc.
var eventId = SentryId.Create();
hub.ConfigureScope(scope => scope.LastEventId = eventId);
hub.ConfigureScope(static (scope, eventId) => scope.LastEventId = eventId, eventId);

try
{
Expand All @@ -167,7 +167,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// The middleware pipeline finishes up before the Otel Activity.OnEnd callback is invoked so we need
// so save a copy of the scope that can be restored by our SentrySpanProcessor
hub.ConfigureScope(scope => activity.SetFused(scope));
hub.ConfigureScope(static (scope, activity) => activity.SetFused(scope), activity);
}

// When an exception was handled by other component (i.e: UseExceptionHandler feature).
Expand All @@ -179,12 +179,12 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
"The web server likely returned a customized error page as a result of this exception.";

#if NET6_0_OR_GREATER
hub.ConfigureScope(scope =>
hub.ConfigureScope(static (scope, arg) =>
{
scope.ExceptionProcessors.Add(
new ExceptionHandlerFeatureProcessor(originalMethod, exceptionFeature)
new ExceptionHandlerFeatureProcessor(arg.originalMethod, arg.exceptionFeature)
);
});
}, (originalMethod, exceptionFeature));
#endif
CaptureException(exceptionFeature.Error, eventId, "IExceptionHandlerFeature", description);
}
Expand Down
5 changes: 1 addition & 4 deletions src/Sentry.AspNetCore/SentryTracingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,7 @@ public async Task InvokeAsync(HttpContext context)

// Expose the transaction on the scope so that the user
// can retrieve it and start child spans off of it.
hub.ConfigureScope(scope =>
{
scope.Transaction = transaction;
});
hub.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction);

Exception? exception = null;
try
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry.Extensions.Logging/SentryLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal SentryLoggerProvider(
if (hub.IsEnabled)
{
_scope = hub.PushScope();
hub.ConfigureScope(s =>
hub.ConfigureScope(static s =>
{
if (s.Sdk is { } sdk)
{
Expand Down
6 changes: 3 additions & 3 deletions src/Sentry.OpenTelemetry/AspNetCoreEnricher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ public void Enrich(ISpan span, Activity activity, IHub hub, SentryOptions? optio
{
if (options?.SendDefaultPii is true)
{
hub.ConfigureScope(scope =>
hub.ConfigureScope(static (scope, enricher) =>
{
if (!scope.HasUser() && _userFactory.Create() is { } user)
if (!scope.HasUser() && enricher._userFactory.Create() is { } user)
{
scope.User = user;
}
});
}, this);
}
}
}
2 changes: 1 addition & 1 deletion src/Sentry.OpenTelemetry/SentrySpanProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private void CreateRootSpan(Activity data)
tracer.Contexts.Trace.Origin = OpenTelemetryOrigin;
tracer.StartTimestamp = data.StartTimeUtc;
}
_hub.ConfigureScope(scope => scope.Transaction = transaction);
_hub.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction);
transaction.SetFused(data);
_map[data.SpanId] = transaction;
}
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,23 @@ public void ConfigureScope(Action<Scope> configureScope)
{
}

/// <summary>
/// No-Op.
/// </summary>
public void ConfigureScope<TArg>(Action<Scope, TArg> configureScope, TArg arg)
{
}

/// <summary>
/// No-Op.
/// </summary>
public Task ConfigureScopeAsync(Func<Scope, Task> configureScope) => Task.CompletedTask;

/// <summary>
/// No-Op.
/// </summary>
public Task ConfigureScopeAsync<TArg>(Func<Scope, TArg, Task> configureScope, TArg arg) => Task.CompletedTask;

/// <summary>
/// No-Op.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,27 @@ private HubAdapter() { }
public void ConfigureScope(Action<Scope> configureScope)
=> SentrySdk.ConfigureScope(configureScope);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
public void ConfigureScope<TArg>(Action<Scope, TArg> configureScope, TArg arg)
=> SentrySdk.ConfigureScope(configureScope, arg);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
public Task ConfigureScopeAsync(Func<Scope, Task> configureScope)
=> SentrySdk.ConfigureScopeAsync(configureScope);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
public Task ConfigureScopeAsync<TArg>(Func<Scope, TArg, Task> configureScope, TArg arg)
=> SentrySdk.ConfigureScopeAsync(configureScope, arg);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
Expand Down
18 changes: 10 additions & 8 deletions src/Sentry/HubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,11 @@ public static ITransactionTracer StartTransaction(
/// </summary>
public static ISpan StartSpan(this IHub hub, string operation, string description)
{
ITransactionTracer? currentTransaction = null;
hub.ConfigureScope(s => currentTransaction = s.Transaction);
return currentTransaction is { } transaction
return hub.GetTransaction() is { } transaction
Copy link
Member

@Flash0ver Flash0ver Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test: Should we have a test for StartSpan in HubExtensionsTests.cs

To be fair, these method was recently moved and made public, where we should have added a test for this, but missed it.
Now, that this method calls an internal method, I'm wondering if this is adding more "worth" of adding a test for this extension method.

@KnapSac, only if you feel like adding a test for this method.
To be fair, though, it should have been added in a preceding PR.

? transaction.StartChild(operation, description)
: hub.StartTransaction(operation, description); // this is actually in the wrong order but changing it may break other things
}


/// <summary>
/// Adds a breadcrumb to the current scope.
/// </summary>
Expand Down Expand Up @@ -158,8 +155,8 @@ public static void AddBreadcrumb(
}

hub.ConfigureScope(
s => s.AddBreadcrumb(breadcrumb, hint ?? new SentryHint())
);
static (s, arg) => s.AddBreadcrumb(arg.breadcrumb, arg.hint ?? new SentryHint()),
(breadcrumb, hint));
}

/// <summary>
Expand All @@ -175,13 +172,13 @@ public static void AddBreadcrumb(
/// like Loggers which guarantee log messages are not lost.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public static void LockScope(this IHub hub) => hub.ConfigureScope(c => c.Locked = true);
public static void LockScope(this IHub hub) => hub.ConfigureScope(static s => s.Locked = true);

/// <summary>
/// Unlocks the current scope to allow subsequent calls to <see cref="ISentryScopeManager.PushScope"/> create new scopes.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static void UnlockScope(this IHub hub) => hub.ConfigureScope(c => c.Locked = false);
public static void UnlockScope(this IHub hub) => hub.ConfigureScope(static s => s.Locked = false);

private sealed class LockedScope : IDisposable
{
Expand Down Expand Up @@ -247,6 +244,11 @@ internal static ITransactionTracer StartTransaction(

internal static ITransactionTracer? GetTransaction(this IHub hub)
{
if (hub is Hub fullHub)
{
return fullHub.ScopeManager.GetCurrent().Key.Transaction;
}

ITransactionTracer? transaction = null;
hub.ConfigureScope(scope => transaction = scope.Transaction);
return transaction;
Expand Down
23 changes: 19 additions & 4 deletions src/Sentry/ISentryScopeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,33 @@ namespace Sentry;
public interface ISentryScopeManager
{
/// <summary>
/// Configures the current scope.
/// Configures the current scope through the callback.
/// </summary>
/// <param name="configureScope">The configure scope.</param>
/// <param name="configureScope">The configure scope callback.</param>
public void ConfigureScope(Action<Scope> configureScope);

/// <summary>
/// Asynchronously configure the current scope.
/// Configures the current scope through the callback.
/// </summary>
/// <param name="configureScope">The configure scope.</param>
/// <param name="configureScope">The configure scope callback.</param>
/// <param name="arg">The argument to pass to the configure scope callback.</param>
public void ConfigureScope<TArg>(Action<Scope, TArg> configureScope, TArg arg);

/// <summary>
/// Configures the current scope through the callback asynchronously.
/// </summary>
/// <param name="configureScope">The configure scope callback.</param>
/// <returns>A task that completes when the callback is done or a completed task if the SDK is disabled.</returns>
public Task ConfigureScopeAsync(Func<Scope, Task> configureScope);

/// <summary>
/// Configures the current scope through the callback asynchronously.
/// </summary>
/// <param name="configureScope">The configure scope callback.</param>
/// <param name="arg">The argument to pass to the configure scope callback.</param>
/// <returns>A task that completes when the callback is done or a completed task if the SDK is disabled.</returns>
public Task ConfigureScopeAsync<TArg>(Func<Scope, TArg, Task> configureScope, TArg arg);

/// <summary>
/// Sets a tag on the current scope.
/// </summary>
Expand Down
26 changes: 25 additions & 1 deletion src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ public void ConfigureScope(Action<Scope> configureScope)
}
}

public void ConfigureScope<TArg>(Action<Scope, TArg> configureScope, TArg arg)
{
try
{
ScopeManager.ConfigureScope(configureScope, arg);
}
catch (Exception e)
{
_options.LogError(e, "Failure to ConfigureScope<TArg>");
}
}

public async Task ConfigureScopeAsync(Func<Scope, Task> configureScope)
{
try
Expand All @@ -111,6 +123,18 @@ public async Task ConfigureScopeAsync(Func<Scope, Task> configureScope)
}
}

public async Task ConfigureScopeAsync<TArg>(Func<Scope, TArg, Task> configureScope, TArg arg)
{
try
{
await ScopeManager.ConfigureScopeAsync(configureScope, arg).ConfigureAwait(false);
}
catch (Exception e)
{
_options.LogError(e, "Failure to ConfigureScopeAsync<TArg>");
}
}

public void SetTag(string key, string value) => ScopeManager.SetTag(key, value);

public void UnsetTag(string key) => ScopeManager.UnsetTag(key);
Expand Down Expand Up @@ -272,7 +296,7 @@ public TransactionContext ContinueTrace(
string? operation = null)
{
var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader, _replaySession);
ConfigureScope(scope => scope.SetPropagationContext(propagationContext));
ConfigureScope(static (scope, propagationContext) => scope.SetPropagationContext(propagationContext), propagationContext);

return new TransactionContext(
name: name ?? string.Empty,
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Internal/SentryScopeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,24 @@ public void ConfigureScope(Action<Scope>? configureScope)
configureScope?.Invoke(scope);
}

public void ConfigureScope<TArg>(Action<Scope, TArg>? configureScope, TArg arg)
{
var (scope, _) = GetCurrent();
configureScope?.Invoke(scope, arg);
}

public Task ConfigureScopeAsync(Func<Scope, Task>? configureScope)
{
var (scope, _) = GetCurrent();
return configureScope?.Invoke(scope) ?? Task.CompletedTask;
}

public Task ConfigureScopeAsync<TArg>(Func<Scope, TArg, Task>? configureScope, TArg arg)
{
var (scope, _) = GetCurrent();
return configureScope?.Invoke(scope, arg) ?? Task.CompletedTask;
}

public void SetTag(string key, string value)
{
var (scope, _) = GetCurrent();
Expand Down
6 changes: 3 additions & 3 deletions src/Sentry/Internal/UnsampledTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ public override void Finish()

// Clear the transaction from the scope and regenerate the Propagation Context, so new events don't have a
// trace context that is "older" than the transaction that just finished
_hub.ConfigureScope(scope =>
_hub.ConfigureScope(static (scope, transactionTracer) =>
{
scope.ResetTransaction(this);
scope.ResetTransaction(transactionTracer);
scope.SetPropagationContext(new SentryPropagationContext());
});
}, this);

// Record the discarded events
var spanCount = Spans.Count + 1; // 1 for each span + 1 for the transaction itself
Expand Down
37 changes: 35 additions & 2 deletions src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,14 +381,47 @@ public static void ConfigureScope(Action<Scope> configureScope)
=> CurrentHub.ConfigureScope(configureScope);

/// <summary>
/// Configures the scope asynchronously.
/// Configures the scope through the callback.
/// <example>
/// <code>
/// object someValue = ...;
/// SentrySdk.ConfigureScope(static (scope, arg) => scope.SetExtra("key", arg), someValue);
/// </code>
/// </example>
/// </summary>
/// <param name="configureScope">The configure scope callback.</param>
/// <returns>The Id of the event.</returns>
/// <param name="arg">The argument to pass to the configure scope callback.</param>
public static void ConfigureScope<TArg>(Action<Scope, TArg> configureScope, TArg arg)
=> CurrentHub.ConfigureScope(configureScope, arg);

/// <summary>
/// Configures the scope through the callback asynchronously.
/// </summary>
/// <param name="configureScope">The configure scope callback.</param>
/// <returns>A task that completes when the callback is done or a completed task if the SDK is disabled.</returns>
[DebuggerStepThrough]
public static Task ConfigureScopeAsync(Func<Scope, Task> configureScope)
=> CurrentHub.ConfigureScopeAsync(configureScope);

/// <summary>
/// Configures the scope through the callback asynchronously.
/// <example>
/// <code>
/// object someValue = ...;
/// SentrySdk.ConfigureScopeAsync(static async (scope, arg) =>
/// {
/// scope.SetExtra("key", arg);
/// }, someValue);
/// </code>
/// </example>
/// </summary>
/// <param name="configureScope">The configure scope callback.</param>
/// <param name="arg">The argument to pass to the configure scope callback.</param>
/// <returns>A task that completes when the callback is done or a completed task if the SDK is disabled.</returns>
[DebuggerStepThrough]
public static Task ConfigureScopeAsync<TArg>(Func<Scope, TArg, Task> configureScope, TArg arg)
=> CurrentHub.ConfigureScopeAsync(configureScope, arg);

/// <summary>
/// Sets a tag on the current scope.
/// </summary>
Expand Down
Loading
Loading