-
-
Notifications
You must be signed in to change notification settings - Fork 229
feat(blazor): Add navigation breadcrumbs for Blazor WebAssembly #4907
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
Changes from all commits
b5c4aa8
06ba684
fdcf455
11ada43
cfe932a
8ce9b0d
7a27be0
a8333c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| using Microsoft.AspNetCore.Components; | ||
| using Microsoft.AspNetCore.Components.WebAssembly.Hosting; | ||
| using Microsoft.Extensions.Options; | ||
| using Sentry.Extensibility; | ||
|
|
||
| namespace Sentry.AspNetCore.Blazor.WebAssembly.Internal; | ||
|
|
||
| internal sealed class BlazorWasmOptionsSetup : IConfigureOptions<SentryBlazorOptions> | ||
| { | ||
| private readonly NavigationManager _navigationManager; | ||
| private readonly IHub _hub; | ||
| private bool _initialized; | ||
|
|
||
| public BlazorWasmOptionsSetup(NavigationManager navigationManager) | ||
| : this(navigationManager, HubAdapter.Instance) | ||
| { | ||
| } | ||
|
|
||
| internal BlazorWasmOptionsSetup(NavigationManager navigationManager, IHub hub) | ||
| { | ||
| _navigationManager = navigationManager; | ||
| _hub = hub; | ||
| } | ||
|
|
||
| public void Configure(SentryBlazorOptions options) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(options); | ||
|
|
||
| if (_initialized) | ||
| { | ||
| return; | ||
| } | ||
| _initialized = true; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a potential race condition here? Does this need to be thread safe? I think Better to use But if this is all compiling down to WASM, maybe there are no threads? There are none in javascript - I assume WASM is the same? @Flash0ver this sounds like your area of expertise... edit: Turns out .NET does have experimental support for threading in WASM... maybe we use Interlocked here just to be on the safe side then (even though it might not be strictly required)? I think for navigation events in particular, it's extremely unlikely to cause problems, so not pushing super hard for this.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This runs single threaded, since it runs on the main thread when the app starts up
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit uncertain if this is (or may be) an issue, |
||
|
|
||
| var previousUrl = _navigationManager.Uri; | ||
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Set the initial scope request URL | ||
| _hub.ConfigureScope(scope => | ||
| { | ||
| scope.Request.Url = ToRelativePath(previousUrl); | ||
| }); | ||
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| _navigationManager.LocationChanged += (_, args) => | ||
| { | ||
| var from = ToRelativePath(previousUrl); | ||
| var to = ToRelativePath(args.Location); | ||
|
|
||
| _hub.AddBreadcrumb( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: @Flash0ver
|
||
| new Breadcrumb( | ||
| type: "navigation", | ||
| category: "navigation", | ||
| data: new Dictionary<string, string> | ||
| { | ||
| { "from", from }, | ||
| { "to", to } | ||
| })); | ||
|
|
||
| _hub.ConfigureScope(scope => | ||
| { | ||
| scope.Request.Url = to; | ||
| }); | ||
|
Comment on lines
+58
to
+61
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: we could avoid the closure here, as |
||
|
|
||
| previousUrl = args.Location; | ||
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private string ToRelativePath(string url) | ||
| { | ||
| var relative = _navigationManager.ToBaseRelativePath(url); | ||
| return "/" + relative; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Options; | ||
| using Sentry; | ||
| using Sentry.AspNetCore.Blazor.WebAssembly.Internal; | ||
| using Sentry.Extensions.Logging; | ||
|
|
||
| // ReSharper disable once CheckNamespace - Discoverability | ||
|
|
@@ -30,6 +33,8 @@ public static WebAssemblyHostBuilder UseSentry(this WebAssemblyHostBuilder build | |
| blazorOptions.IsGlobalModeEnabled = true; | ||
| }); | ||
|
|
||
| builder.Services.AddSingleton<IConfigureOptions<SentryBlazorOptions>, BlazorWasmOptionsSetup>(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: with that change,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. however, the Unit Tests and Playwright Tests (#4908) do pass |
||
|
|
||
| return builder; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| using Microsoft.AspNetCore.Components.WebAssembly.Hosting; | ||
| using Sentry.AspNetCore.Blazor.WebAssembly.Internal; | ||
|
|
||
| namespace Sentry.AspNetCore.Blazor.WebAssembly.Tests; | ||
|
|
||
| public class BlazorWasmOptionsSetupTests | ||
| { | ||
| private readonly FakeNavigationManager _navigationManager; | ||
| private readonly IHub _hub; | ||
| private readonly Scope _scope; | ||
| private readonly BlazorWasmOptionsSetup _sut; | ||
|
|
||
| public BlazorWasmOptionsSetupTests() | ||
| { | ||
| _navigationManager = new FakeNavigationManager( | ||
| baseUri: "https://localhost:5001/", | ||
| initialUri: "https://localhost:5001/"); | ||
|
|
||
| _hub = Substitute.For<IHub>(); | ||
| _scope = new Scope(new SentryOptions()); | ||
| _hub.SubstituteConfigureScope(_scope); | ||
|
|
||
| _sut = new BlazorWasmOptionsSetup(_navigationManager, _hub); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Configure_SetsInitialRequestUrl() | ||
| { | ||
| // Act | ||
| _sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Assert | ||
| _scope.Request.Url.Should().Be("/"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Configure_SetsInitialRequestUrl_WithPath() | ||
| { | ||
| // Arrange | ||
| var nav = new FakeNavigationManager( | ||
| baseUri: "https://localhost:5001/", | ||
| initialUri: "https://localhost:5001/counter"); | ||
| var sut = new BlazorWasmOptionsSetup(nav, _hub); | ||
|
|
||
| // Act | ||
| sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Assert | ||
| _scope.Request.Url.Should().Be("/counter"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Navigation_CreatesBreadcrumbWithCorrectTypeAndCategory() | ||
| { | ||
| // Arrange | ||
| _sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Act | ||
| _navigationManager.NavigateTo("/dashboard"); | ||
|
|
||
| // Assert | ||
| var crumb = _scope.Breadcrumbs.Should().ContainSingle().Subject; | ||
| crumb.Type.Should().Be("navigation"); | ||
| crumb.Category.Should().Be("navigation"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Navigation_BreadcrumbHasNoMessage() | ||
| { | ||
| // Arrange | ||
| _sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Act | ||
| _navigationManager.NavigateTo("/dashboard"); | ||
|
|
||
| // Assert | ||
| var crumb = _scope.Breadcrumbs.Should().ContainSingle().Subject; | ||
| crumb.Message.Should().BeNull(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Navigation_CreatesBreadcrumbWithRelativePaths() | ||
| { | ||
| // Arrange | ||
| _sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Act | ||
| _navigationManager.NavigateTo("/dashboard"); | ||
|
|
||
| // Assert | ||
| var crumb = _scope.Breadcrumbs.Should().ContainSingle().Subject; | ||
| crumb.Data.Should().ContainKey("from").WhoseValue.Should().Be("/"); | ||
| crumb.Data.Should().ContainKey("to").WhoseValue.Should().Be("/dashboard"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Navigation_UpdatesRequestUrl() | ||
| { | ||
| // Arrange | ||
| _sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Act | ||
| _navigationManager.NavigateTo("/dashboard"); | ||
|
|
||
| // Assert | ||
| _scope.Request.Url.Should().Be("/dashboard"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void MultipleNavigations_TrackFromCorrectly() | ||
| { | ||
| // Arrange | ||
| _sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Act | ||
| _navigationManager.NavigateTo("/page1"); | ||
| _navigationManager.NavigateTo("/page2"); | ||
|
|
||
| // Assert | ||
| var breadcrumbs = _scope.Breadcrumbs.ToList(); | ||
| breadcrumbs.Should().HaveCount(2); | ||
|
|
||
| var first = breadcrumbs[0]; | ||
| first.Data.Should().ContainKey("from").WhoseValue.Should().Be("/"); | ||
| first.Data.Should().ContainKey("to").WhoseValue.Should().Be("/page1"); | ||
|
|
||
| var second = breadcrumbs[1]; | ||
| second.Data.Should().ContainKey("from").WhoseValue.Should().Be("/page1"); | ||
| second.Data.Should().ContainKey("to").WhoseValue.Should().Be("/page2"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Navigation_FromInitialPath_TracksCorrectFrom() | ||
| { | ||
| // Arrange - start on /login | ||
| var nav = new FakeNavigationManager( | ||
| baseUri: "https://localhost:5001/", | ||
| initialUri: "https://localhost:5001/login"); | ||
| var sut = new BlazorWasmOptionsSetup(nav, _hub); | ||
| sut.Configure(new SentryBlazorOptions()); | ||
|
|
||
| // Act | ||
| nav.NavigateTo("/home"); | ||
|
|
||
| // Assert | ||
| var crumb = _scope.Breadcrumbs.Should().ContainSingle().Subject; | ||
| crumb.Data.Should().ContainKey("from").WhoseValue.Should().Be("/login"); | ||
| crumb.Data.Should().ContainKey("to").WhoseValue.Should().Be("/home"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| using Microsoft.AspNetCore.Components; | ||
|
|
||
| namespace Sentry.AspNetCore.Blazor.WebAssembly.Tests; | ||
|
|
||
| internal sealed class FakeNavigationManager : NavigationManager | ||
| { | ||
| public FakeNavigationManager(string baseUri = "https://localhost/", string initialUri = "https://localhost/") | ||
| { | ||
| Initialize(baseUri, initialUri); | ||
| } | ||
|
|
||
| protected override void NavigateToCore(string uri, bool forceLoad) | ||
| { | ||
| var absoluteUri = ToAbsoluteUri(uri).ToString(); | ||
| Uri = absoluteUri; | ||
| NotifyLocationChanged(isInterceptedLink: false); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFrameworks>$(CurrentTfms)</TargetFrameworks> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\Sentry.AspNetCore.Blazor.WebAssembly\Sentry.AspNetCore.Blazor.WebAssembly.csproj" /> | ||
| <ProjectReference Include="..\Sentry.Testing\Sentry.Testing.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: We don't strictly require a changelog entry anymore - it should get generated automatically from the commit description since #4896
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I need to remove it? it's already there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah you can leave it in... it would be generated automatically for the PR (from the title) when making a release otherwise. Just FYI that you don't need to add this anymore.