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

detect MVC versioning in route #1731

Merged
merged 35 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e6a9ad2
.
SimonCropp Jun 14, 2022
2457fb2
.
SimonCropp Jun 14, 2022
58f4009
.
SimonCropp Jun 15, 2022
fab9fec
Update VersioningTests.cs
SimonCropp Jun 15, 2022
f904897
Update HttpContextExtensions.cs
SimonCropp Jun 15, 2022
0ca34f9
Merge branch 'main' into Mvc.Versioning
SimonCropp Jun 15, 2022
c5ea64b
Merge branch 'main' into Mvc.Versioning
SimonCropp Jun 15, 2022
740b5cf
Update VersioningTests.cs
SimonCropp Jun 16, 2022
f58dbc3
.
SimonCropp Jun 17, 2022
120df3f
Merge branch 'main' into Mvc.Versioning
SimonCropp Jun 17, 2022
6766005
.
SimonCropp Jun 17, 2022
5d9a44d
Update CHANGELOG.md
SimonCropp Jun 17, 2022
f69bb64
Update HttpContextExtensions.cs
SimonCropp Jun 17, 2022
12eb2ca
Update HttpContextExtensions.cs
SimonCropp Jun 17, 2022
43b46cc
Update HttpContextExtensions.cs
SimonCropp Jun 17, 2022
47cc57d
Update HttpContextExtensions.cs
SimonCropp Jun 18, 2022
b98f731
.
SimonCropp Jun 18, 2022
d8cc877
Update CHANGELOG.md
SimonCropp Jun 18, 2022
ec3d76c
Revert "."
SimonCropp Jun 18, 2022
acf9e4f
.
SimonCropp Jun 18, 2022
7435f09
Update ScopeExtensions.cs
SimonCropp Jun 18, 2022
b72ff3a
Merge branch 'main' into Mvc.Versioning
SimonCropp Jun 25, 2022
b26cbc1
fix CHANGELOG spacing
bruno-garcia Jun 26, 2022
3105d42
Merge branch 'main' into Mvc.Versioning
SimonCropp Jun 29, 2022
70ca1d9
Merge branch 'main' into Mvc.Versioning
SimonCropp Jul 7, 2022
6aa5b24
Update VersioningTests.cs
SimonCropp Jul 7, 2022
d4ee27d
Merge branch 'main' into Mvc.Versioning
SimonCropp Jul 11, 2022
e7b3b8e
Update VersioningTests.cs
SimonCropp Jul 11, 2022
775de1b
Merge branch 'main' into Mvc.Versioning
mattjohnsonpint Jul 21, 2022
06c4cc4
Merge branch 'main' into Mvc.Versioning
SimonCropp Jul 21, 2022
ceb46aa
Update VersioningTests.cs
SimonCropp Jul 21, 2022
ade293e
Merge branch 'main' into Mvc.Versioning
mattjohnsonpint Jul 22, 2022
1d2b77f
Update versioning test
mattjohnsonpint Jul 22, 2022
5cd5126
Update VersioningTests.cs
mattjohnsonpint Jul 22, 2022
ee775e7
Update VersioningTests.cs
mattjohnsonpint Jul 22, 2022
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 @@ -8,6 +8,7 @@

### Fixes

- Detect MVC versioning in route ([#1731](https://github.com/getsentry/sentry-dotnet/pull/1731))
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
- Fix error with `ConcurrentHashMap` on Android <= 9 ([#1761](https://github.com/getsentry/sentry-dotnet/pull/1761))
- Minor improvements to `BackgroundWorker` ([#1773](https://github.com/getsentry/sentry-dotnet/pull/1773))
- Make GzipRequestBodyHandler respect async ([#1776](https://github.com/getsentry/sentry-dotnet/pull/1776))
Expand Down
94 changes: 62 additions & 32 deletions src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,16 @@ internal static class HttpContextExtensions

// Skip route pattern if it resembles to a MVC route or null e.g.
// {controller=Home}/{action=Index}/{id?}
return RouteHasMvcParameters(routePattern)
? builder.Append(ReplaceMvcParameters(routePattern, context)).ToString()
: builder.Append(routePattern).ToString();
if (RouteHasMvcParameters(routePattern))
{
builder.Append(ReplaceMvcParameters(routePattern, context));
}
else
{
builder.Append(routePattern);
}

return builder.ToString();
}

// Internal for testing.
Expand All @@ -63,64 +70,87 @@ internal static class HttpContextExtensions
// despite the annotations claiming otherwise.
var routeData = context.GetRouteData();

var controller = routeData?.Values["controller"]?.ToString();
var action = routeData?.Values["action"]?.ToString();
var area = routeData?.Values["area"]?.ToString();
// GetRouteData can return null on netstandard2
if (routeData == null)
{
return null;
}

if (!string.IsNullOrWhiteSpace(action))
var values = routeData.Values;

if (values["action"] is not string action)
{
var builder = new StringBuilder();
if (context.Request.PathBase.HasValue)
{
builder.Append(context.Request.PathBase.Value?.TrimStart('/'))
.Append('.');
}
// 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;
}

if (!string.IsNullOrWhiteSpace(area))
{
builder.Append(area)
.Append('.');
}
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('.')
.Append(action);
return builder.ToString();
.Append('.');
}

// 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;
builder.Append(action);

return builder.ToString();
}

// Internal for testing.
internal static string ReplaceMvcParameters(string route, HttpContext? context)
internal static string ReplaceMvcParameters(string route, HttpContext context)
{
// Return RouteData or Null, marking the HttpContext as nullable since the output doesn't
// shows the nullable output.
var routeData = context?.GetRouteData();
var routeData = context.GetRouteData();

if (routeData?.Values["controller"]?.ToString() is { } controller)
// 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 (routeData?.Values["action"]?.ToString() is { } action)
if (values["action"] is string action)
{
route = Regex.Replace(route, "{action=[^}]+}", action);
}

if (routeData?.Values["area"]?.ToString() is { } area)
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("{area=");
=> route.Contains("{controller=") ||
route.Contains("{action=") ||
route.Contains("{version:") ||
route.Contains("{area=");

public static string? TryGetTransactionName(this HttpContext context)
{
Expand Down
15 changes: 9 additions & 6 deletions src/Sentry.AspNetCore/ScopeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,28 @@ public static void Populate(this Scope scope, HttpContext context, SentryAspNetC
try
{
var routeData = context.GetRouteData();
var controller = routeData.Values["controller"]?.ToString();
var action = routeData.Values["action"]?.ToString();
var area = routeData.Values["area"]?.ToString();
var values = routeData.Values;

if (controller != null)
if (values["controller"] is string controller)
{
scope.SetTag("route.controller", controller);
}

if (action != null)
if (values["action"] is string action)
{
scope.SetTag("route.action", action);
}

if (area != null)
if (values["area"] is string area)
{
scope.SetTag("route.area", area);
}

if (values["version"] is string version)
{
scope.SetTag("route.version", version);
}

// Transaction Name may only be available afterward the creation of the Transaction.
// In this case, the event will update the transaction name if captured during the
// pipeline execution, allowing it to match the correct transaction name as the current
Expand Down
1 change: 0 additions & 1 deletion src/Sentry.AspNetCore/Sentry.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
<ProjectReference Include="..\Sentry.Extensions.Logging\Sentry.Extensions.Logging.csproj" />
</ItemGroup>


<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<ProjectReference Include="..\Sentry.DiagnosticSource\Sentry.DiagnosticSource.csproj" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public static HttpContext GetMvcSut(
string area = null,
string controller = null,
string action = null,
string pathBase = null)
string pathBase = null,
string version = null)
{
var httpContext = new DefaultHttpContext();
if (pathBase is not null)
Expand All @@ -37,6 +38,7 @@ public static HttpContext GetMvcSut(
AddRouteValuesIfNotNull(httpContext.Request.RouteValues, "controller", controller);
AddRouteValuesIfNotNull(httpContext.Request.RouteValues, "action", action);
AddRouteValuesIfNotNull(httpContext.Request.RouteValues, "area", area);
AddRouteValuesIfNotNull(httpContext.Request.RouteValues, "version", version);
return httpContext;
}

Expand Down Expand Up @@ -66,7 +68,6 @@ public void ReplaceMcvParameters_ParsedParameters(string routeInput, string asse
{
// Arrange
var httpContext = Fixture.GetMvcSut(area, controller, action);

// Act
var filteredRoute = HttpContextExtensions.ReplaceMvcParameters(routeInput, httpContext);

Expand All @@ -81,6 +82,7 @@ public void ReplaceMcvParameters_ParsedParameters(string routeInput, string asse
[InlineData("abc/{controller=}/")]
[InlineData("{action=Index}/{id?}")]
[InlineData("{area=Index}/{id?}")]
[InlineData("v{version:apiVersion}/Target")]
public void RouteHasMvcParameters_RouteWithMvcParameters_True(string route)
{
// Assert
Expand Down Expand Up @@ -122,17 +124,18 @@ public void NewRouteFormat_MvcRouteWithPathBase_ParsedParameters(string routeInp
}

[Theory]
[InlineData("{area=MyArea}/{controller=Home}/{action=Index}/{id?}", "theArea/house/about/{id?}", "house", "about", "theArea")]
[InlineData("{area=MyArea}/{controller=Home}/{action=Index}/{id?}", "{area=MyArea}/house/about/{id?}", "house", "about", null)]
[InlineData("{area=}/{controller=}/{action=}/{id?}", "{area=}/{controller=}/{action=}/{id?}", "house", "about", "theArea")]
[InlineData("{controller=Home}/{action=Index}/{id?}", "house/about/{id?}", "house", "about", null)]
[InlineData("{controller=Home}/{action=Index}", "house/about", "house", "about", null)]
[InlineData("{controller=Home}/{id?}", "house/{id?}", "house", "about", null)]
[InlineData("{action=Index}/{id?}", "about/{id?}", null, "about", null)]
public void NewRouteFormat_MvcRouteWithoutPathBase_ParsedParameters(string routeInput, string expectedOutput, string controller, string action, string area)
[InlineData("{area=MyArea}/{controller=Home}/{action=Index}/{id?}", "theArea/house/about/{id?}", "house", "about", "theArea", null)]
[InlineData("{area=MyArea}/{controller=Home}/{action=Index}/{id?}", "{area=MyArea}/house/about/{id?}", "house", "about", null, null)]
[InlineData("{area=}/{controller=}/{action=}/{id?}", "{area=}/{controller=}/{action=}/{id?}", "house", "about", "theArea", null)]
[InlineData("{controller=Home}/{action=Index}/{id?}", "house/about/{id?}", "house", "about", null, null)]
[InlineData("{controller=Home}/{action=Index}", "house/about", "house", "about", null, null)]
[InlineData("{controller=Home}/{id?}", "house/{id?}", "house", "about", null, null)]
[InlineData("{action=Index}/{id?}", "about/{id?}", null, "about", null, null)]
[InlineData("v{version:apiVersion}/Target", "v1.1/Target", null, "about", null, "1.1")]
public void NewRouteFormat_MvcRouteWithoutPathBase_ParsedParameters(string routeInput, string expectedOutput, string controller, string action, string area, string version)
{
// Arrange
var httpContext = Fixture.GetMvcSut(area, controller, action);
var httpContext = Fixture.GetMvcSut(area, controller, action, null, version);

// Act
var filteredRoute = HttpContextExtensions.NewRouteFormat(routeInput, httpContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
Request: {
Method: GET,
QueryString:
},
User: {},
TransactionName: GET Ctrl.Actn,
Tags: {
route.action: Actn,
route.controller: Ctrl,
route.version: 1.1,
TraceIdentifier:
}
}
19 changes: 14 additions & 5 deletions test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Microsoft.Extensions.Primitives;

namespace Sentry.AspNetCore.Tests;

[UsesVerify]
public class ScopeExtensionsTests
{
private readonly Scope _sut = new(new SentryOptions());
Expand Down Expand Up @@ -317,14 +317,23 @@ public void Populate_PayloadExtractors_DoesNotConsiderInvalidResponse(object exp
}

[Fact]
public void Populate_RouteData_SetToScope()
public Task Populate_RouteData_SetToScope()
{
// Arrange
const string controller = "Ctrl";
const string action = "Actn";
const string version = "1.1";
var routeFeature = new RoutingFeature
{
RouteData = new RouteData { Values = { { "controller", controller }, { "action", action }, } }
RouteData = new RouteData
{
Values =
{
{ "controller", controller },
{ "action", action },
{ "version", version },
}
}
};
var features = new FeatureCollection();
features.Set<IRoutingFeature>(routeFeature);
Expand All @@ -334,8 +343,8 @@ public void Populate_RouteData_SetToScope()
// Act
_sut.Populate(_httpContext, SentryAspNetCoreOptions);

// Assert
Assert.Equal($"GET {controller}.{action}", _sut.TransactionName);
return Verify(_sut)
.IgnoreStandardSentryMembers();
}

public static IEnumerable<object[]> InvalidRequestBodies()
Expand Down
5 changes: 5 additions & 0 deletions test/Sentry.AspNetCore.Tests/Sentry.AspNetCore.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<ProjectReference Include="..\..\src\Sentry.AspNetCore\Sentry.AspNetCore.csproj" />
<ProjectReference Include="..\Sentry.Testing\Sentry.Testing.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0"/>
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
<PackageReference Include="Verify.AspNetCore" Version="1.5.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1' OR '$(TargetFramework)' == 'net461'">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.2.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
result: Hello world,
payloads: [
{
Source: {
Name: GET v1.1/Target,
Platform: csharp,
Operation: http.server,
Description: ,
Status: Ok,
IsSampled: true,
Request: {
Method: GET,
QueryString:
},
User: {},
Environment: production,
Breadcrumbs: [
{
Message: Request starting HTTP/1.1 GET http://localhost:5000/v1.1/Target - -,
Data: {
eventId: 1
},
Category: Microsoft.AspNetCore.Hosting.Diagnostics
},
{
Message: Executing endpoint 'VersioningTests+TargetController.Method (Sentry.AspNetCore.Tests)',
Data: {
eventId: ExecutingEndpoint
},
Category: Microsoft.AspNetCore.Routing.EndpointMiddleware
},
{
Message: Route matched with {action = "Method", controller = "Target"}. Executing controller action with signature System.String Method() on controller VersioningTests+TargetController (Sentry.AspNetCore.Tests).,
Data: {
eventId: ControllerActionExecuting
},
Category: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker
},
{
Message: Executing ObjectResult, writing value of type 'System.String'.,
Data: {
eventId: ObjectResultExecuting
},
Category: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor
},
{
Data: {
eventId: ActionExecuted
},
Category: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker
},
{
Message: Executed endpoint 'VersioningTests+TargetController.Method (Sentry.AspNetCore.Tests)',
Data: {
eventId: ExecutedEndpoint
},
Category: Microsoft.AspNetCore.Routing.EndpointMiddleware
}
],
Tags: {
ActionId: Guid_1,
ActionName: VersioningTests+TargetController.Method (Sentry.AspNetCore.Tests),
route.action: Method,
route.controller: Target,
route.version: 1.1
},
IsFinished: true
}
}
]
}
Loading