Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TransactionNameProvider to allow the name definition from Unknown transactions on ASP.NET Core #1421

Merged
merged 16 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## Features

- Add the delegate TransactionNameProvider to allow the name definition from Unknown transactions on ASP.NET Core ([#1421](https://github.com/getsentry/sentry-dotnet/pull/1421))
- SentrySDK.WithScope is now obsolete in favour of overloads of CaptureEvent, CaptureMessage, CaptureException ([#1412](https://github.com/getsentry/sentry-dotnet/pull/1412))
- Add Sentry to global usings when ImplicitUsings is enabled (`<ImplicitUsings>true</ImplicitUsings>`) ([#1398](https://github.com/getsentry/sentry-dotnet/pull/1398))

Expand Down
4 changes: 2 additions & 2 deletions src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ internal static class HttpContextExtensions
return legacyFormat;
}

var sentryRouteName = context.Features.Get<ISentryRouteName>();
var sentryRouteName = context.Features.Get<TransactionNameProvider>();

return sentryRouteName?.GetRouteName();
return sentryRouteName?.Invoke(context);
}

// Internal for testing.
Expand Down
6 changes: 0 additions & 6 deletions src/Sentry.AspNetCore/ISentryRouteName.cs

This file was deleted.

9 changes: 9 additions & 0 deletions src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using Sentry.Extensibility;
using Sentry.Extensions.Logging;

Expand Down Expand Up @@ -43,6 +44,14 @@ public class SentryAspNetCoreOptions : SentryLoggingOptions
/// </summary>
public TimeSpan FlushTimeout { get; set; } = TimeSpan.FromSeconds(2);

/// <summary>
/// The strategy to define the name of a transaction based on the <see cref="HttpContext"/>.
/// </summary>
/// <remarks>
/// The SDK can name transactions automatically when using MVC or Endpoint Routing. In other cases, like when serving static files, it will fallback to Unknown Route. This hook allows custom code to define a transaction name given a <see cref="HttpContext"/>.
/// </remarks>
public TransactionNameProvider? TransactionNameProvider { get; set; }

/// <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
5 changes: 5 additions & 0 deletions src/Sentry.AspNetCore/SentryTracingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ public async Task InvokeAsync(HttpContext context)
return;
}

if (_options.TransactionNameProvider is { } route)
{
context.Features.Set(route);
}

var transaction = TryStartTransaction(context);
var initialName = transaction?.Name;

Expand Down
11 changes: 11 additions & 0 deletions src/Sentry.AspNetCore/TransactionNameProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Http;

namespace Sentry.AspNetCore;

/// <summary>
/// Provides the strategy to define the name of a transaction based on the <see cref="HttpContext"/>.
/// </summary>
/// <remarks>
/// The SDK can name transactions automatically when using MVC or Endpoint Routing. In other cases, like when serving static files, it fallback to Unknown Route. This hook allows custom code to define a transaction name given a <see cref="HttpContext"/>.
/// </remarks>
public delegate string? TransactionNameProvider(HttpContext context);
14 changes: 4 additions & 10 deletions src/Sentry.Google.Cloud.Functions/SentryStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public override void ConfigureLogging(WebHostBuilderContext context, ILoggingBui
{
// Make sure all events are flushed out
options.FlushBeforeRequestCompleted = true;
// K_SERVICE is where the name of the FAAS is stored.
// It'll return null. if GCP Function is running locally.
var serviceName = Environment.GetEnvironmentVariable("K_SERVICE");
options.TransactionNameProvider = _ => serviceName;
});

logging.Services.AddSingleton<IConfigureOptions<SentryAspNetCoreOptions>, SentryAspNetCoreOptionsSetup>();
Expand Down Expand Up @@ -114,17 +118,7 @@ private class SentryGoogleCloudFunctionsMiddleware
/// </summary>
public async Task InvokeAsync(HttpContext httpContext)
{
httpContext.Features.Set<ISentryRouteName>(new SentryGoogleCloudFunctionsRouteName());
await _next(httpContext).ConfigureAwait(false);
}
}

private class SentryGoogleCloudFunctionsRouteName : ISentryRouteName
{
private static readonly Lazy<string?> RouteName = new(() => Environment.GetEnvironmentVariable("K_SERVICE"));

// K_SERVICE is where the name of the FAAS is stored.
// It'll return null. if GCP Function is running locally.
public string? GetRouteName() => RouteName.Value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ namespace Sentry.AspNetCore
public System.TimeSpan FlushTimeout { get; set; }
public bool IncludeActivityData { get; set; }
public Sentry.Extensibility.RequestSize MaxRequestBodySize { get; set; }
public Sentry.AspNetCore.TransactionNameProvider? TransactionNameProvider { get; set; }
}
public class SentryAspNetCoreOptionsSetup : Microsoft.Extensions.Options.ConfigureFromConfigurationOptions<Sentry.AspNetCore.SentryAspNetCoreOptions>
{
Expand All @@ -60,4 +61,5 @@ namespace Sentry.AspNetCore
public SentryStartupFilter() { }
public System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> Configure(System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> next) { }
}
public delegate string? TransactionNameProvider(Microsoft.AspNetCore.Http.HttpContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ namespace Sentry.AspNetCore
public System.TimeSpan FlushTimeout { get; set; }
public bool IncludeActivityData { get; set; }
public Sentry.Extensibility.RequestSize MaxRequestBodySize { get; set; }
public Sentry.AspNetCore.TransactionNameProvider? TransactionNameProvider { get; set; }
}
public class SentryAspNetCoreOptionsSetup : Microsoft.Extensions.Options.ConfigureFromConfigurationOptions<Sentry.AspNetCore.SentryAspNetCoreOptions>
{
Expand All @@ -60,4 +61,5 @@ namespace Sentry.AspNetCore
public SentryStartupFilter() { }
public System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> Configure(System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> next) { }
}
public delegate string? TransactionNameProvider(Microsoft.AspNetCore.Http.HttpContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ namespace Sentry.AspNetCore
public System.TimeSpan FlushTimeout { get; set; }
public bool IncludeActivityData { get; set; }
public Sentry.Extensibility.RequestSize MaxRequestBodySize { get; set; }
public Sentry.AspNetCore.TransactionNameProvider? TransactionNameProvider { get; set; }
}
public class SentryAspNetCoreOptionsSetup : Microsoft.Extensions.Options.ConfigureFromConfigurationOptions<Sentry.AspNetCore.SentryAspNetCoreOptions>
{
Expand All @@ -60,4 +61,5 @@ namespace Sentry.AspNetCore
public SentryStartupFilter() { }
public System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> Configure(System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> next) { }
}
public delegate string? TransactionNameProvider(Microsoft.AspNetCore.Http.HttpContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ namespace Sentry.AspNetCore
public System.TimeSpan FlushTimeout { get; set; }
public bool IncludeActivityData { get; set; }
public Sentry.Extensibility.RequestSize MaxRequestBodySize { get; set; }
public Sentry.AspNetCore.TransactionNameProvider? TransactionNameProvider { get; set; }
}
public class SentryAspNetCoreOptionsSetup : Microsoft.Extensions.Options.ConfigureFromConfigurationOptions<Sentry.AspNetCore.SentryAspNetCoreOptions>
{
Expand All @@ -60,4 +61,5 @@ namespace Sentry.AspNetCore
public SentryStartupFilter() { }
public System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> Configure(System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> next) { }
}
public delegate string? TransactionNameProvider(Microsoft.AspNetCore.Http.HttpContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,9 @@ public void TryGetRouteTemplate_NoRoute_NullOutput()
public void TryGetRouteTemplate_WithSentryRouteName_RouteName()
{
// Arrange
var sentryRouteName = Substitute.For<ISentryRouteName>();
var httpContext = Fixture.GetSut();
var expectedName = "abc";
sentryRouteName.GetRouteName().Returns(expectedName);
TransactionNameProvider sentryRouteName = _ => expectedName;
var httpContext = Fixture.GetSut();
httpContext.Features.Set(sentryRouteName);

// Act
Expand Down
87 changes: 87 additions & 0 deletions test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,93 @@ public async Task Transaction_binds_exception_thrown()
Assert.True(hub.ExceptionToSpanMap.TryGetValue(exception, out var span));
Assert.Equal(SpanStatus.InternalError, span.Status);
}

[Fact]
public async Task Transaction_TransactionNameProviderSetSet_TransactionNameSet()
{
// Arrange
Transaction transaction = null;

var expectedName = "My custom name";

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<Transaction>()))
.Do(callback => transaction = callback.Arg<Transaction>());
var options = new SentryAspNetCoreOptions()
{
Dsn = DsnSamples.ValidDsnWithoutSecret,
TracesSampleRate = 1
};

var hub = new Hub(options, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseSentry(aspNewOptions =>
{
aspNewOptions.TransactionNameProvider = _ => expectedName;
})
.ConfigureServices(services =>
{
services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
}).Configure(app => app.UseSentryTracing()));

var client = server.CreateClient();

// Act
try
{
await client.GetStringAsync("/person/13.bmp");
}
// Expected error.
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{ }

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

[Fact]
public async Task Transaction_TransactionNameProviderSetUnset_UnknownTransactionNameSet()
{
// Arrange
Transaction transaction = null;

var sentryClient = Substitute.For<ISentryClient>();
sentryClient.When(x => x.CaptureTransaction(Arg.Any<Transaction>()))
.Do(callback => transaction = callback.Arg<Transaction>());
var options = new SentryAspNetCoreOptions
{
Dsn = DsnSamples.ValidDsnWithoutSecret,
TracesSampleRate = 1
};

var hub = new Hub(options, sentryClient);

var server = new TestServer(new WebHostBuilder()
.UseSentry()
.ConfigureServices(services =>
{
services.RemoveAll(typeof(Func<IHub>));
services.AddSingleton<Func<IHub>>(() => hub);
}).Configure(app => app.UseSentryTracing()));

var client = server.CreateClient();

// Act
try
{
await client.GetStringAsync("/person/13.bmp");
}
// Expected error.
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{ }

// Assert
transaction.Should().NotBeNull();
transaction?.Name.Should().Be("Unknown Route");
}
}

#endif