Skip to content
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 .generated.NoMobile.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,6 @@
<Project Path="test/Sentry.Tests/Sentry.Tests.csproj" />
<Project Path="test/Sentry.TrimTest/Sentry.TrimTest.csproj" />
<Project Path="test/SingleFileTestApp/SingleFileTestApp.csproj" />
<Project Path="test\Sentry.AspNetCore.Blazor.WebAssembly.Tests\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj" />
</Folder>
</Solution>
4 changes: 4 additions & 0 deletions CHANGELOG.md
Copy link
Collaborator

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

Copy link
Member Author

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

Copy link
Collaborator

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.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Add navigation breadcrumbs for Blazor WebAssembly ([#4907](https://github.com/getsentry/sentry-dotnet/pull/4907))

### Dependencies

- Bumped Xamarin.Kotlin.StdLib.Jdk8 to 2.2.20 ([#4876](https://github.com/getsentry/sentry-dotnet/pull/4876))
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Linux-NoMobile.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Linux.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.Android.AssemblyReader.Tests\\Sentry.Android.AssemblyReader.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Windows-arm64.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Windows.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-macOS.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.Android.AssemblyReader.Tests\\Sentry.Android.AssemblyReader.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
1 change: 1 addition & 0 deletions Sentry.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,6 @@
<Project Path="test/Sentry.Tests/Sentry.Tests.csproj" />
<Project Path="test/Sentry.TrimTest/Sentry.TrimTest.csproj" />
<Project Path="test/SingleFileTestApp/SingleFileTestApp.csproj" />
<Project Path="test\Sentry.AspNetCore.Blazor.WebAssembly.Tests\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj" />
</Folder>
</Solution>
1 change: 1 addition & 0 deletions SentryAspNetCore.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"src\\Sentry.Serilog\\Sentry.Serilog.csproj",
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
1 change: 1 addition & 0 deletions SentryNoMobile.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"src\\Sentry\\Sentry.csproj",
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
1 change: 1 addition & 0 deletions SentryNoSamples.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj",
"test\\Sentry.Android.AssemblyReader.Tests\\Sentry.Android.AssemblyReader.Tests.csproj",
"test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj",
"test\\Sentry.AspNetCore.Blazor.WebAssembly.Tests\\Sentry.AspNetCore.Blazor.WebAssembly.Tests.csproj",
"test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj",
"test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj",
"test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj",
Expand Down
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;
Copy link
Collaborator

@jamescrosswell jamescrosswell Feb 12, 2026

Choose a reason for hiding this comment

The 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 IOptions<T> gets evaluated lazily the first time it's requested... so if multiple requests hit the server concurrently after it starts, you could get two threads checking it _initialized is true, seeing that they're not and then setting up duplicate event handlers (resulting in duplicate breadcrumbs).

Better to use Interlocked.Exchange?

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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

@Flash0ver Flash0ver Feb 13, 2026

Choose a reason for hiding this comment

The 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,
but considering that the change using our InterlockedBoolean is both cheap in runtime and maintenance (would result in less code, actually),
I vote for a thread-safe guard here using our internal InterlockedBoolean.


var previousUrl = _navigationManager.Uri;

// Set the initial scope request URL
_hub.ConfigureScope(scope =>
{
scope.Request.Url = ToRelativePath(previousUrl);
});

_navigationManager.LocationChanged += (_, args) =>
{
var from = ToRelativePath(previousUrl);
var to = ToRelativePath(args.Location);

_hub.AddBreadcrumb(
Copy link
Member

@Flash0ver Flash0ver Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: we could avoid the closure here, as to is already a string-ified relative path, by passing it as "state"


previousUrl = args.Location;
};
}

private string ToRelativePath(string url)
{
var relative = _navigationManager.ToBaseRelativePath(url);
return "/" + relative;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<ProjectReference Include="..\Sentry\Sentry.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Sentry.AspNetCore.Blazor.WebAssembly.Tests" PublicKey="$(SentryPublicKey)" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" />
</ItemGroup>
Expand Down
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
Expand Down Expand Up @@ -30,6 +33,8 @@ public static WebAssemblyHostBuilder UseSentry(this WebAssemblyHostBuilder build
blazorOptions.IsGlobalModeEnabled = true;
});

builder.Services.AddSingleton<IConfigureOptions<SentryBlazorOptions>, BlazorWasmOptionsSetup>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: with that change, Sentry.Samples.AspNetCore.Blazor.Wasm doesn't fully initialize any longer and just hangs

Copy link
Member

@Flash0ver Flash0ver Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

however, the Unit Tests and Playwright Tests (#4908) do pass


return builder;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Sentry/Sentry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
<InternalsVisibleTo Include="Sentry.Android.AssemblyReader.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.AspNet" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.AspNet.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.AspNetCore.Blazor.WebAssembly" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.AspNetCore" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.AspNetCore.Grpc" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.AspNetCore.Grpc.Tests" PublicKey="$(SentryPublicKey)" />
Expand Down
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>
1 change: 1 addition & 0 deletions test/Sentry.Testing/Sentry.Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<InternalsVisibleTo Include="Sentry.GraphQL.Client.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.Maui.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.Profiling.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.AspNetCore.Blazor.WebAssembly.Tests" PublicKey="$(SentryPublicKey)" />
<InternalsVisibleTo Include="Sentry.Tests" PublicKey="$(SentryPublicKey)" />
</ItemGroup>

Expand Down
Loading