Skip to content

Commit

Permalink
Merge branch 'main' into deps/scripts/update-java.ps1/6.27.0
Browse files Browse the repository at this point in the history
  • Loading branch information
bitsandfoxes authored Aug 1, 2023
2 parents e62f783 + 592dfd8 commit 0515240
Show file tree
Hide file tree
Showing 10 changed files with 542 additions and 275 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
- Align SDK with docs regarding session update for dropped events ([#2496](https://github.com/getsentry/sentry-dotnet/pull/2496))
- Introduced `HttpMessageHandler` in favor of the now deprecated `HttpClientHandler` on the options. This allows the SDK to support NSUrlSessionHandler on iOS ([#2503](https://github.com/getsentry/sentry-dotnet/pull/2503))
- Using `Activity.RecordException` now correctly updates the error status of OpenTelemetry Spans ([#2515](https://github.com/getsentry/sentry-dotnet/pull/2515))
- Fixed Transaction name not reporting correctly when using UseExceptionHandler ([#2511](https://github.com/getsentry/sentry-dotnet/pull/2511))

### Dependencies

- Bump Java SDK from v6.25.1 to v6.27.0 ([#2484](https://github.com/getsentry/sentry-dotnet/pull/2484), [#2517](https://github.com/getsentry/sentry-dotnet/pull/2517))
- Bump Java SDK from v6.25.1 to v6.27.0 ([#2484](https://github.com/getsentry/sentry-dotnet/pull/2484), [#2498](https://github.com/getsentry/sentry-dotnet/pull/2498)), [#2517](https://github.com/getsentry/sentry-dotnet/pull/2517))
- [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#6270)
- [diff](https://github.com/getsentry/sentry-java/compare/6.25.1...6.27.0)
- Bump CLI from v2.19.4 to v2.20.1 ([#2509](https://github.com/getsentry/sentry-dotnet/pull/2509), [#2518](https://github.com/getsentry/sentry-dotnet/pull/2518))
Expand Down
85 changes: 85 additions & 0 deletions src/Sentry.AspNetCore/ExceptionHandlerFeatureProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Routing;
using Sentry.Extensibility;

namespace Sentry.AspNetCore;

#if NET6_0_OR_GREATER
internal class ExceptionHandlerFeatureProcessor : ISentryEventExceptionProcessor
{
private readonly string _originalMethod;
private readonly IExceptionHandlerFeature _exceptionHandlerFeature;

public ExceptionHandlerFeatureProcessor(string originalMethod, IExceptionHandlerFeature exceptionHandlerFeature)
{
_originalMethod = originalMethod;
_exceptionHandlerFeature = exceptionHandlerFeature;
}

public void Process(Exception exception, SentryEvent sentryEvent)
{
// When exceptions get caught by the UseExceptionHandler feature we reset the TransactionName and Tags
// to reflect the route values of the original route (not the global error handling route)
ApplyTransactionName(sentryEvent, _originalMethod);
ApplyRouteTags(sentryEvent);
}

internal void ApplyRouteTags(SentryEvent evt)
{
var endpoint = _exceptionHandlerFeature.Endpoint as RouteEndpoint;

var actionName = endpoint?.DisplayName;
if (actionName is not null)
{
evt.SetTag("ActionName", actionName);
}

if (_exceptionHandlerFeature.RouteValues is {} routeValues)
{
if (routeValues.TryGetValue("controller", out var controller))
{
evt.SetTag("route.controller", $"{controller}");
}
if (routeValues.TryGetValue("action", out var action))
{
evt.SetTag("route.action", $"{action}");
}
}
}

internal void ApplyTransactionName(SentryEvent evt,
string method)
{
// If no route template details are available, fall back to the Path
var route = TryGetRouteTemplate() ?? _exceptionHandlerFeature.Path;
if (!string.IsNullOrWhiteSpace(route))
{
evt.TransactionName = $"{method} {route}"; // e.g. "GET /pets/{id}"
}
}

internal string? TryGetRouteTemplate()
{
// Requires .UseRouting()/.UseEndpoints()
var endpoint = _exceptionHandlerFeature.Endpoint as RouteEndpoint;
var routePattern = endpoint?.RoutePattern.RawText;
if (string.IsNullOrWhiteSpace(routePattern))
{
return null;
}

var routeValues = _exceptionHandlerFeature.RouteValues;
if (routeValues is null)
{
return null;
}

if (RouteUtils.NewRouteFormat(routePattern, routeValues) is { } formattedRoute)
{
return formattedRoute;
}

return RouteUtils.LegacyRouteFormat(routeValues);
}
}
#endif
139 changes: 10 additions & 129 deletions src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
using Sentry.Extensibility;

#if !NETSTANDARD2_0
Expand All @@ -11,147 +10,29 @@ namespace Sentry.AspNetCore.Extensions;

internal static class HttpContextExtensions
{
public static string? TryGetRouteTemplate(this HttpContext context)
internal static string? TryGetRouteTemplate(this HttpContext context)
{
#if !NETSTANDARD2_0 // endpoint routing is only supported after ASP.NET Core 3.0
// Requires .UseRouting()/.UseEndpoints()
var endpoint = context.Features.Get<IEndpointFeature?>()?.Endpoint as RouteEndpoint;
var routePattern = endpoint?.RoutePattern.RawText;

if (NewRouteFormat(routePattern, context) is { } formattedRoute)
// GetRouteData can return null on netstandard2 (despite annotations claiming otherwise)
if (RouteUtils.NewRouteFormat(routePattern, context.GetRouteData()?.Values, context.Request.PathBase)
is { } formattedRoute)
{
return formattedRoute;
}
#endif
return LegacyRouteFormat(context);
}

public static string? TryGetCustomTransactionName(this HttpContext context) =>
context.Features.Get<TransactionNameProvider>()?.Invoke(context);

// Internal for testing.
internal static string? NewRouteFormat(string? routePattern, HttpContext context)
{
if (string.IsNullOrWhiteSpace(routePattern))
{
return null;
}

var builder = new StringBuilder();
if (context.Request.PathBase.HasValue)
{
builder.Append(context.Request.PathBase.Value?.TrimStart('/'))
.Append('/');
}

// Skip route pattern if it resembles to a MVC route or null e.g.
// {controller=Home}/{action=Index}/{id?}
if (RouteHasMvcParameters(routePattern))
{
builder.Append(ReplaceMvcParameters(routePattern, context));
}
else
{
if (builder is { Length: > 1 } && builder[^1].Equals('/') && routePattern[0] == '/')
{
builder.Length--;
}

builder.Append(routePattern);
}

return builder.ToString();
}

// Internal for testing.
internal static string? LegacyRouteFormat(HttpContext context)
{
// Fallback for legacy .UseMvc().
// Uses context.Features.Get<IRoutingFeature?>() under the hood and CAN be null,
// despite the annotations claiming otherwise.
var routeData = context.GetRouteData();

// GetRouteData can return null on netstandard2
if (routeData == null)
{
return null;
}

var values = routeData.Values;

if (values["action"] is not string action)
{
// If the handler doesn't use routing (i.e. it checks `context.Request.Path` directly),
// then there is no way for us to extract anything that resembles a route template.
return null;
}

var builder = new StringBuilder();
if (context.Request.PathBase.HasValue)
{
builder.Append(context.Request.PathBase.Value?.TrimStart('/'))
.Append('.');
}

if (values["area"] is string area)
{
builder.Append(area)
.Append('.');
}

if (values["controller"] is string controller)
{
builder.Append(controller)
.Append('.');
}

builder.Append(action);

return builder.ToString();
// Note: GetRouteData can return null on netstandard2
return (context.GetRouteData() is { } routeData)
? RouteUtils.LegacyRouteFormat(routeData.Values, context.Request.PathBase)
: null;
}

// Internal for testing.
internal static string ReplaceMvcParameters(string route, HttpContext context)
{
var routeData = context.GetRouteData();

// GetRouteData can return null on netstandard2
if (routeData == null)
{
return route;
}

var values = routeData.Values;

if (values["controller"] is string controller)
{
route = Regex.Replace(route, "{controller=[^}]+}", controller);
}

if (values["action"] is string action)
{
route = Regex.Replace(route, "{action=[^}]+}", action);
}

if (values["area"] is string area)
{
route = Regex.Replace(route, "{area=[^}]+}", area);
}

if (values["version"] is string version)
{
route = Regex.Replace(route, "{version:[^}]+}", version);
}

return route;
}

// Internal for testing.
internal static bool RouteHasMvcParameters(string route)
=> route.Contains("{controller=") ||
route.Contains("{action=") ||
route.Contains("{version:") ||
route.Contains("{area=");
internal static string? TryGetCustomTransactionName(this HttpContext context) =>
context.Features.Get<TransactionNameProvider>()?.Invoke(context);

public static string? TryGetTransactionName(this HttpContext context)
{
Expand Down
113 changes: 113 additions & 0 deletions src/Sentry.AspNetCore/RouteUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Microsoft.AspNetCore.Routing;

namespace Sentry.AspNetCore;

internal static class RouteUtils
{
// Internal for testing.
internal static string? NewRouteFormat(string? routePattern, RouteValueDictionary? values, string? pathBase = null)
{
if (string.IsNullOrWhiteSpace(routePattern))
{
return null;
}

var builder = new StringBuilder();
if (!string.IsNullOrWhiteSpace(pathBase))
{
builder.Append(pathBase.TrimStart('/'))
.Append('/');
}

// Skip route pattern if it resembles to a MVC route or null e.g.
// {controller=Home}/{action=Index}/{id?}
if (RouteHasMvcParameters(routePattern))
{
builder.Append(ReplaceMvcParameters(routePattern, values));
}
else
{
if (builder is { Length: > 1 } && builder[^1].Equals('/') && routePattern[0] == '/')
{
builder.Length--;
}

builder.Append(routePattern);
}

return builder.ToString();
}

// Internal for testing.
internal static string? LegacyRouteFormat(RouteValueDictionary values, string? pathBase = null)
{
if (values["action"] is not string action)
{
// If the handler doesn't use routing (i.e. it checks `context.Request.Path` directly),
// then there is no way for us to extract anything that resembles a route template.
return null;
}

var builder = new StringBuilder();
if (!string.IsNullOrWhiteSpace(pathBase))
{
builder.Append(pathBase.TrimStart('/'))
.Append('.');
}

if (values["area"] is string area)
{
builder.Append(area)
.Append('.');
}

if (values["controller"] is string controller)
{
builder.Append(controller)
.Append('.');
}

builder.Append(action);

return builder.ToString();
}

// Internal for testing.
internal static string ReplaceMvcParameters(string route, RouteValueDictionary? values)
{
// GetRouteData can return null on netstandard2
if (values == null)
{
return route;
}

if (values["controller"] is string controller)
{
route = Regex.Replace(route, "{controller=[^}]+}", controller);
}

if (values["action"] is string action)
{
route = Regex.Replace(route, "{action=[^}]+}", action);
}

if (values["area"] is string area)
{
route = Regex.Replace(route, "{area=[^}]+}", area);
}

if (values["version"] is string version)
{
route = Regex.Replace(route, "{version:[^}]+}", version);
}

return route;
}

// Internal for testing.
internal static bool RouteHasMvcParameters(string route)
=> route.Contains("{controller=") ||
route.Contains("{action=") ||
route.Contains("{version:") ||
route.Contains("{area=");
}
Loading

0 comments on commit 0515240

Please sign in to comment.