Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ Public API diffs are stored as Verify snapshot files. After any public API chang
## Changelog

- If the change is not user-facing, add `#skip-changelog` to the PR description
- Otherwise generated automatically from [Commit message conventions](https://develop.sentry.dev/engineering-practices/commit-messages/)
- Otherwise generated automatically from [Commit message conventions](https://develop.sentry.dev/engineering-practices/commit-messages/).
- Do **NOT** add changelogs manually.
Comment thread
jamescrosswell marked this conversation as resolved.

```sh
# Get PR number for current branch
Expand Down
2 changes: 2 additions & 0 deletions src/Sentry.AspNetCore/BindableSentryAspNetCoreOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal class BindableSentryAspNetCoreOptions : BindableSentryLoggingOptions
public bool? AdjustStandardEnvironmentNameCasing { get; set; }
public bool? AutoRegisterTracing { get; set; }
public bool? CaptureBlockingCalls { get; set; }
public bool? AlwaysCallTransactionNameProvider { get; set; }

public void ApplyTo(SentryAspNetCoreOptions options)
{
Expand All @@ -28,5 +29,6 @@ public void ApplyTo(SentryAspNetCoreOptions options)
options.AdjustStandardEnvironmentNameCasing = AdjustStandardEnvironmentNameCasing ?? options.AdjustStandardEnvironmentNameCasing;
options.AutoRegisterTracing = AutoRegisterTracing ?? options.AutoRegisterTracing;
options.CaptureBlockingCalls = CaptureBlockingCalls ?? options.CaptureBlockingCalls;
options.AlwaysCallTransactionNameProvider = AlwaysCallTransactionNameProvider ?? options.AlwaysCallTransactionNameProvider;
}
}
14 changes: 14 additions & 0 deletions src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ public class SentryAspNetCoreOptions : SentryLoggingOptions
/// </remarks>
public TransactionNameProvider? TransactionNameProvider { get; set; }

/// <summary>
/// Controls whether <see cref="TransactionNameProvider"/> runs on every request, or only
/// when routing fails to resolve a template.
/// </summary>
/// <remarks>
/// When <c>false</c> (default), the provider is only invoked as a fallback for unresolved routes —
/// matching the legacy behaviour. When <c>true</c>, the provider is invoked for every request
/// after routing; a non-empty return value sets the transaction name. If the provider returns
/// <c>null</c> or empty for a routed request, the name falls back to the URL path (the route
/// template is <b>not</b> retained); callers that want to preserve the route template should
/// return it themselves from the provider.
/// </remarks>
public bool AlwaysCallTransactionNameProvider { get; set; }
Comment thread
Flash0ver marked this conversation as resolved.
Outdated
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated

/// <summary>
/// Controls whether the casing of the standard (Production, Development and Staging) environment name supplied by <see cref="Microsoft.AspNetCore.Hosting.IHostingEnvironment" />
/// is adjusted when setting the Sentry environment. Defaults to true.
Expand Down
3 changes: 2 additions & 1 deletion src/Sentry.AspNetCore/SentryTracingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ public async Task InvokeAsync(HttpContext context)
var status = SpanStatusConverter.FromHttpStatusCode(context.Response.StatusCode);

// If no Name was found for Transaction, then we don't have the route.
if (transaction.Name == string.Empty)
// Also run this block if the caller has opted into always calling the TransactionNameProvider.
if (transaction.Name == string.Empty || _options.AlwaysCallTransactionNameProvider)
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
{
var method = context.Request.Method.ToUpperInvariant();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace Sentry.AspNetCore
{
public SentryAspNetCoreOptions() { }
public bool AdjustStandardEnvironmentNameCasing { get; set; }
public bool AlwaysCallTransactionNameProvider { get; set; }
public bool AutoRegisterTracing { get; set; }
public bool CaptureBlockingCalls { get; set; }
public bool FlushOnCompletedRequest { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace Sentry.AspNetCore
{
public SentryAspNetCoreOptions() { }
public bool AdjustStandardEnvironmentNameCasing { get; set; }
public bool AlwaysCallTransactionNameProvider { get; set; }
public bool AutoRegisterTracing { get; set; }
public bool CaptureBlockingCalls { get; set; }
public bool FlushOnCompletedRequest { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace Sentry.AspNetCore
{
public SentryAspNetCoreOptions() { }
public bool AdjustStandardEnvironmentNameCasing { get; set; }
public bool AlwaysCallTransactionNameProvider { get; set; }
public bool AutoRegisterTracing { get; set; }
public bool CaptureBlockingCalls { get; set; }
public bool FlushOnCompletedRequest { get; set; }
Expand Down
130 changes: 130 additions & 0 deletions test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -752,4 +752,134 @@ public async Task Transaction_TransactionNameProviderSetUnset_TransactionNameSet
transaction.Name.Should().Be("GET /person/13.bmp");
transaction.NameSource.Should().Be(TransactionNameSource.Url);
}

[Fact]
public async Task Transaction_TransactionNameProviderSetForRoutedRequest_ProviderOverridesRouteName()
{
// Arrange
SentryTransaction transaction = null;

const string expectedName = "My custom name";

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<SentryTransaction>(), Arg.Any<Scope>(), Arg.Any<SentryHint>()))
.Do(callback => transaction = callback.Arg<SentryTransaction>());

var hub = new Hub(new SentryOptions { Dsn = ValidDsn, TracesSampleRate = 1 }, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseDefaultServiceProvider(di => di.EnableValidation())
.UseSentry(aspNetOptions =>
{
aspNetOptions.TransactionNameProvider = _ => expectedName;
aspNetOptions.AlwaysCallTransactionNameProvider = true;
})
.ConfigureServices(services =>
{
services.AddRouting();

services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
})
.Configure(app =>
{
app.UseRouting();

app.UseEndpoints(routes => routes.Map("/person/{id}", _ => Task.CompletedTask));
}));

var client = server.CreateClient();

// Act
await client.GetStringAsync("/person/13");

// Assert
transaction.Should().NotBeNull();
transaction.Name.Should().Be($"GET {expectedName}");
transaction.NameSource.Should().Be(TransactionNameSource.Custom);
}

[Fact]
public async Task Transaction_TransactionNameProviderSetForRoutedRequest_DefaultsToRouteBehaviour()
{
// Arrange
SentryTransaction transaction = null;

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<SentryTransaction>(), Arg.Any<Scope>(), Arg.Any<SentryHint>()))
.Do(callback => transaction = callback.Arg<SentryTransaction>());

var hub = new Hub(new SentryOptions { Dsn = ValidDsn, TracesSampleRate = 1 }, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseDefaultServiceProvider(di => di.EnableValidation())
.UseSentry(aspNetOptions => aspNetOptions.TransactionNameProvider = _ => "My custom name")
.ConfigureServices(services =>
{
services.AddRouting();

services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
})
.Configure(app =>
{
app.UseRouting();

app.UseEndpoints(routes => routes.Map("/person/{id}", _ => Task.CompletedTask));
}));

var client = server.CreateClient();

// Act
await client.GetStringAsync("/person/13");

// Assert
transaction.Should().NotBeNull();
transaction.Name.Should().Be("GET /person/{id}");
transaction.NameSource.Should().Be(TransactionNameSource.Route);
}

[Fact]
public async Task Transaction_AlwaysCallTransactionNameProviderWithNullReturn_FallsBackToUrlPath()
{
// Arrange
SentryTransaction transaction = null;

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<SentryTransaction>(), Arg.Any<Scope>(), Arg.Any<SentryHint>()))
.Do(callback => transaction = callback.Arg<SentryTransaction>());

var hub = new Hub(new SentryOptions { Dsn = ValidDsn, TracesSampleRate = 1 }, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseDefaultServiceProvider(di => di.EnableValidation())
.UseSentry(aspNetOptions =>
{
aspNetOptions.TransactionNameProvider = _ => null;
aspNetOptions.AlwaysCallTransactionNameProvider = true;
})
.ConfigureServices(services =>
{
services.AddRouting();

services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
})
.Configure(app =>
{
app.UseRouting();

app.UseEndpoints(routes => routes.Map("/person/{id}", _ => Task.CompletedTask));
}));

var client = server.CreateClient();

// Act
await client.GetStringAsync("/person/13");

// Assert
transaction.Should().NotBeNull();
transaction.Name.Should().Be("GET /person/13");
transaction.NameSource.Should().Be(TransactionNameSource.Url);
}
}
Loading