From 5e4471d21666c225904989830aca746b6e6417db Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 13 Feb 2026 11:45:38 +1300 Subject: [PATCH 01/13] Update solution filters --- .generated.NoMobile.slnx | 3 +++ Sentry.slnx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.generated.NoMobile.slnx b/.generated.NoMobile.slnx index 4b4ac6e63e..690fbc2d8f 100644 --- a/.generated.NoMobile.slnx +++ b/.generated.NoMobile.slnx @@ -150,6 +150,9 @@ + + + diff --git a/Sentry.slnx b/Sentry.slnx index 4b4ac6e63e..690fbc2d8f 100644 --- a/Sentry.slnx +++ b/Sentry.slnx @@ -150,6 +150,9 @@ + + + From 173c2c90120d188c92fc7052cd2774190e2eac57 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 13 Feb 2026 12:46:22 +1300 Subject: [PATCH 02/13] Fixing my mess --- .generated.NoMobile.slnx | 3 --- Sentry.slnx | 3 --- 2 files changed, 6 deletions(-) diff --git a/.generated.NoMobile.slnx b/.generated.NoMobile.slnx index 690fbc2d8f..4b4ac6e63e 100644 --- a/.generated.NoMobile.slnx +++ b/.generated.NoMobile.slnx @@ -150,9 +150,6 @@ - - - diff --git a/Sentry.slnx b/Sentry.slnx index 690fbc2d8f..4b4ac6e63e 100644 --- a/Sentry.slnx +++ b/Sentry.slnx @@ -150,9 +150,6 @@ - - - From df8aadee09b2a332760b475516540cabcae61556 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 9 Feb 2026 23:04:41 -0500 Subject: [PATCH 03/13] test(blazor): Add Playwright E2E tests for navigation breadcrumbs Add browser-level tests using Playwright to verify that navigation breadcrumbs are created end-to-end in a real Blazor WebAssembly app. The test navigates between pages, triggers SentrySdk.CaptureMessage, intercepts the Sentry envelope via Playwright's route API, and verifies the event contains navigation breadcrumbs with correct from/to paths. Includes a minimal Blazor WASM test host app with a fake DSN. Co-Authored-By: Claude Opus 4.6 --- .../App.razor | 8 ++ .../Pages/Index.razor | 4 + .../Pages/Second.razor | 4 + .../Pages/TriggerCapture.razor | 11 ++ .../Program.cs | 16 +++ ...WebAssembly.PlaywrightTests.TestApp.csproj | 38 +++++++ .../Shared/MainLayout.razor | 5 + .../_Imports.razor | 4 + .../wwwroot/index.html | 16 +++ .../BlazorWasmTestApp.cs | 81 ++++++++++++++ .../NavigationBreadcrumbTests.cs | 100 ++++++++++++++++++ ....Blazor.WebAssembly.PlaywrightTests.csproj | 33 ++++++ .../SentryEnvelopeParser.cs | 32 ++++++ 13 files changed, 352 insertions(+) create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/App.razor create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Index.razor create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Second.razor create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/TriggerCapture.razor create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Program.cs create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Shared/MainLayout.razor create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/_Imports.razor create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/wwwroot/index.html create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.csproj create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/App.razor b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/App.razor new file mode 100644 index 0000000000..93b6831e5e --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/App.razor @@ -0,0 +1,8 @@ + + + + + +

Not found

+
+
diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Index.razor b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Index.razor new file mode 100644 index 0000000000..a5c06a63be --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Index.razor @@ -0,0 +1,4 @@ +@page "/" + +

Home

+Go to Second diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Second.razor b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Second.razor new file mode 100644 index 0000000000..2ad425c0a3 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/Second.razor @@ -0,0 +1,4 @@ +@page "/second" + +

Second Page

+Go to Trigger diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/TriggerCapture.razor b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/TriggerCapture.razor new file mode 100644 index 0000000000..e504e9f017 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Pages/TriggerCapture.razor @@ -0,0 +1,11 @@ +@page "/trigger-capture" + +

Trigger Capture

+ + +@code { + private void Capture() + { + SentrySdk.CaptureMessage("playwright-test"); + } +} diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Program.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Program.cs new file mode 100644 index 0000000000..2f88219148 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.UseSentry(options => +{ + // Fake DSN — Playwright intercepts requests before they reach the network + options.Dsn = "https://key@o0.ingest.sentry.io/0"; + options.AutoSessionTracking = false; +}); + +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +await builder.Build().RunAsync(); diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj new file mode 100644 index 0000000000..530f804349 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + enable + enable + false + false + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Shared/MainLayout.razor b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Shared/MainLayout.razor new file mode 100644 index 0000000000..724fc91b60 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Shared/MainLayout.razor @@ -0,0 +1,5 @@ +@inherits LayoutComponentBase + +
+ @Body +
diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/_Imports.razor b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/_Imports.razor new file mode 100644 index 0000000000..263f7a0fd9 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp +@using Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.Shared diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/wwwroot/index.html b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/wwwroot/index.html new file mode 100644 index 0000000000..5eafdd6ae8 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/wwwroot/index.html @@ -0,0 +1,16 @@ + + + + + + + Playwright Test App + + + + +
Loading...
+ + + + diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs new file mode 100644 index 0000000000..817bebfd77 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs @@ -0,0 +1,81 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; + +namespace Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests; + +internal sealed class BlazorWasmTestApp : IAsyncDisposable +{ + private Process? _process; + + public string BaseUrl { get; private set; } = null!; + + public async Task StartAsync() + { + var port = GetFreePort(); + BaseUrl = $"http://localhost:{port}"; + + var projectPath = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, + "..", "..", "..", "..", + "Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp")); + + _process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"run --no-build --project \"{projectPath}\" --urls {BaseUrl}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + } + }; + _process.Start(); + + // Discard stdout/stderr to prevent buffer deadlock + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + + using var http = new HttpClient(); + var timeout = TimeSpan.FromSeconds(60); + var sw = Stopwatch.StartNew(); + while (sw.Elapsed < timeout) + { + try + { + var response = await http.GetAsync(BaseUrl); + if (response.IsSuccessStatusCode) + { + return; + } + } + catch + { + // Server not ready yet + } + await Task.Delay(500); + } + + throw new TimeoutException($"Blazor WASM test app did not start within {timeout.TotalSeconds}s at {BaseUrl}"); + } + + private static int GetFreePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + public async ValueTask DisposeAsync() + { + if (_process is { HasExited: false }) + { + _process.Kill(entireProcessTree: true); + await _process.WaitForExitAsync(); + } + _process?.Dispose(); + } +} diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs new file mode 100644 index 0000000000..785945bcbd --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using Microsoft.Playwright; + +namespace Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests; + +public class NavigationBreadcrumbTests : IAsyncLifetime +{ + private readonly BlazorWasmTestApp _app = new(); + private IPlaywright _playwright = null!; + private IBrowser _browser = null!; + + public async Task InitializeAsync() + { + await _app.StartAsync(); + + _playwright = await Playwright.CreateAsync(); + _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + Headless = true + }); + } + + public async Task DisposeAsync() + { + await _browser.DisposeAsync(); + _playwright.Dispose(); + await _app.DisposeAsync(); + } + + [Fact] + public async Task Navigation_CreatesBreadcrumbs_WithCorrectFromAndTo() + { + var page = await _browser.NewPageAsync(); + + // Collect all intercepted envelopes + var envelopes = new List(); + var envelopeReceived = new TaskCompletionSource(); + + await page.RouteAsync("**/api/0/envelope/**", async route => + { + var body = route.Request.PostData; + if (body != null) + { + envelopes.Add(body); + if (body.Contains("\"breadcrumbs\"")) + { + envelopeReceived.TrySetResult(body); + } + } + await route.FulfillAsync(new RouteFulfillOptions + { + Status = 200, + ContentType = "application/json", + Body = "{}" + }); + }); + + // 1. Navigate to app root + await page.GotoAsync(_app.BaseUrl); + await page.WaitForSelectorAsync("#page-title"); + + // 2. Navigate to /second (creates first navigation breadcrumb: / -> /second) + await page.ClickAsync("#nav-second"); + await page.WaitForSelectorAsync("h1:has-text('Second Page')"); + + // 3. Navigate to /trigger-capture (creates second breadcrumb: /second -> /trigger-capture) + await page.ClickAsync("#nav-trigger"); + await page.WaitForSelectorAsync("h1:has-text('Trigger Capture')"); + + // 4. Click button to trigger SentrySdk.CaptureMessage — sends event with breadcrumbs + await page.ClickAsync("#btn-capture"); + + // 5. Wait for the envelope containing breadcrumbs + var envelopeBody = await envelopeReceived.Task.WaitAsync(TimeSpan.FromSeconds(10)); + + // 6. Parse and verify + var eventPayload = SentryEnvelopeParser.ExtractEventFromEnvelope(envelopeBody); + eventPayload.Should().NotBeNull("expected an event payload in the Sentry envelope"); + + var breadcrumbs = eventPayload!.Value.GetProperty("breadcrumbs").EnumerateArray().ToList(); + + var navBreadcrumbs = breadcrumbs + .Where(b => + b.TryGetProperty("type", out var t) && t.GetString() == "navigation" && + b.TryGetProperty("category", out var c) && c.GetString() == "navigation") + .ToList(); + + navBreadcrumbs.Should().HaveCount(2, "expected two navigation breadcrumbs (/ -> /second -> /trigger-capture)"); + + // First navigation: / -> /second + var first = navBreadcrumbs[0]; + first.GetProperty("data").GetProperty("from").GetString().Should().Be("/"); + first.GetProperty("data").GetProperty("to").GetString().Should().Be("/second"); + + // Second navigation: /second -> /trigger-capture + var second = navBreadcrumbs[1]; + second.GetProperty("data").GetProperty("from").GetString().Should().Be("/second"); + second.GetProperty("data").GetProperty("to").GetString().Should().Be("/trigger-capture"); + } +} diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.csproj b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.csproj new file mode 100644 index 0000000000..87bb45f2a2 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs new file mode 100644 index 0000000000..e4bb56d20f --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs @@ -0,0 +1,32 @@ +using System.Text.Json; + +namespace Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests; + +internal static class SentryEnvelopeParser +{ + /// + /// Extracts the first event payload from a Sentry envelope body. + /// Envelope format: newline-delimited JSON lines. + /// Line 0 = envelope header, then pairs of (item header, item payload). + /// + public static JsonElement? ExtractEventFromEnvelope(string envelopeBody) + { + var lines = envelopeBody.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + // lines[0] = envelope header + // lines[1..] = pairs of (item header, item payload) + for (var i = 1; i < lines.Length - 1; i += 2) + { + using var itemHeaderDoc = JsonDocument.Parse(lines[i]); + var itemHeader = itemHeaderDoc.RootElement; + + if (itemHeader.TryGetProperty("type", out var typeEl) && + typeEl.GetString() == "event") + { + return JsonDocument.Parse(lines[i + 1]).RootElement; + } + } + + return null; + } +} From b2a1015cc6de153b5f56548616aa27d0d41ab2fe Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 9 Feb 2026 23:18:27 -0500 Subject: [PATCH 04/13] Add Playwright test projects to solution and regenerate filters Co-Authored-By: Claude Opus 4.6 --- Sentry-CI-Build-Linux-NoMobile.slnf | 2 ++ Sentry-CI-Build-Linux.slnf | 2 ++ Sentry-CI-Build-Windows-arm64.slnf | 2 ++ Sentry-CI-Build-Windows.slnf | 2 ++ Sentry-CI-Build-macOS.slnf | 2 ++ SentryAspNetCore.slnf | 2 ++ SentryNoMobile.slnf | 2 ++ SentryNoSamples.slnf | 1 + 8 files changed, 15 insertions(+) diff --git a/Sentry-CI-Build-Linux-NoMobile.slnf b/Sentry-CI-Build-Linux-NoMobile.slnf index 69921c2bcb..517b08bf30 100644 --- a/Sentry-CI-Build-Linux-NoMobile.slnf +++ b/Sentry-CI-Build-Linux-NoMobile.slnf @@ -49,6 +49,8 @@ "src\\Sentry.Serilog\\Sentry.Serilog.csproj", "src\\Sentry\\Sentry.csproj", "test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf index 14ffc09833..040ab0b411 100644 --- a/Sentry-CI-Build-Linux.slnf +++ b/Sentry-CI-Build-Linux.slnf @@ -56,6 +56,8 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-Windows-arm64.slnf b/Sentry-CI-Build-Windows-arm64.slnf index 41d924ac5f..751471a013 100644 --- a/Sentry-CI-Build-Windows-arm64.slnf +++ b/Sentry-CI-Build-Windows-arm64.slnf @@ -58,6 +58,8 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf index 1b158939c9..00b79f97a2 100644 --- a/Sentry-CI-Build-Windows.slnf +++ b/Sentry-CI-Build-Windows.slnf @@ -58,6 +58,8 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index 08b1295241..e5ff038bab 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -63,6 +63,8 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/SentryAspNetCore.slnf b/SentryAspNetCore.slnf index bbaecea920..ccd8904461 100644 --- a/SentryAspNetCore.slnf +++ b/SentryAspNetCore.slnf @@ -24,6 +24,8 @@ "src\\Sentry.Serilog\\Sentry.Serilog.csproj", "src\\Sentry\\Sentry.csproj", "test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf index 5284aab01f..a5db92897e 100644 --- a/SentryNoMobile.slnf +++ b/SentryNoMobile.slnf @@ -52,6 +52,8 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", + "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/SentryNoSamples.slnf b/SentryNoSamples.slnf index 5ca7fb6d84..81a7c4243b 100644 --- a/SentryNoSamples.slnf +++ b/SentryNoSamples.slnf @@ -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.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", From 7bae6d87273e2648deade412ca875d6cb3e0a9ad Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 9 Feb 2026 23:34:44 -0500 Subject: [PATCH 05/13] Increase Blazor WASM test app startup timeout to 120s CI environments can be slow; 60s was not enough. Co-Authored-By: Claude Opus 4.6 --- .../BlazorWasmTestApp.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs index 817bebfd77..e1448f51bf 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs @@ -38,7 +38,7 @@ public async Task StartAsync() _process.BeginErrorReadLine(); using var http = new HttpClient(); - var timeout = TimeSpan.FromSeconds(60); + var timeout = TimeSpan.FromSeconds(120); var sw = Stopwatch.StartNew(); while (sw.Elapsed < timeout) { @@ -57,7 +57,7 @@ public async Task StartAsync() await Task.Delay(500); } - throw new TimeoutException($"Blazor WASM test app did not start within {timeout.TotalSeconds}s at {BaseUrl}"); + throw new TimeoutException($"Blazor WASM test app did not start within {(int)timeout.TotalSeconds}s at {BaseUrl}"); } private static int GetFreePort() From e902aa4dab1839861cbf9b6b0ac7fd1b53d475fb Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 9 Feb 2026 23:37:10 -0500 Subject: [PATCH 06/13] Improve test app startup: capture output, detect early exit, 180s timeout - Capture stdout/stderr into a queue for diagnostics - Detect early process exit and fail immediately with logs - Increase timeout to 180s for slow CI environments - Include process output in both error and timeout messages Co-Authored-By: Claude Opus 4.6 --- .../BlazorWasmTestApp.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs index e1448f51bf..63618cdfc4 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Diagnostics; using System.Net; using System.Net.Sockets; @@ -7,6 +8,7 @@ namespace Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests; internal sealed class BlazorWasmTestApp : IAsyncDisposable { private Process? _process; + private readonly ConcurrentQueue _output = new(); public string BaseUrl { get; private set; } = null!; @@ -31,17 +33,24 @@ public async Task StartAsync() UseShellExecute = false, } }; + _process.OutputDataReceived += (_, e) => { if (e.Data != null) _output.Enqueue($"[stdout] {e.Data}"); }; + _process.ErrorDataReceived += (_, e) => { if (e.Data != null) _output.Enqueue($"[stderr] {e.Data}"); }; _process.Start(); - - // Discard stdout/stderr to prevent buffer deadlock _process.BeginOutputReadLine(); _process.BeginErrorReadLine(); using var http = new HttpClient(); - var timeout = TimeSpan.FromSeconds(120); + var timeout = TimeSpan.FromSeconds(180); var sw = Stopwatch.StartNew(); while (sw.Elapsed < timeout) { + if (_process.HasExited) + { + var logs = string.Join(Environment.NewLine, _output); + throw new InvalidOperationException( + $"Blazor WASM test app exited with code {_process.ExitCode} before becoming ready. Output:{Environment.NewLine}{logs}"); + } + try { var response = await http.GetAsync(BaseUrl); @@ -57,7 +66,9 @@ public async Task StartAsync() await Task.Delay(500); } - throw new TimeoutException($"Blazor WASM test app did not start within {(int)timeout.TotalSeconds}s at {BaseUrl}"); + var timeoutLogs = string.Join(Environment.NewLine, _output); + throw new TimeoutException( + $"Blazor WASM test app did not start within {(int)timeout.TotalSeconds}s at {BaseUrl}. Output:{Environment.NewLine}{timeoutLogs}"); } private static int GetFreePort() From d68af9b356d6e6711ddd19744684aec10120c135 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 9 Feb 2026 23:52:43 -0500 Subject: [PATCH 07/13] Remove --no-build from dotnet run for test app The Blazor WASM DevServer needs the staticwebassets.endpoints.json manifest which is only generated during build of the project itself, not when built from the solution level. Letting dotnet run build the project ensures this manifest is generated. Co-Authored-By: Claude Opus 4.6 --- .../BlazorWasmTestApp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs index 63618cdfc4..10e9b3c1bb 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs @@ -27,7 +27,7 @@ public async Task StartAsync() StartInfo = new ProcessStartInfo { FileName = "dotnet", - Arguments = $"run --no-build --project \"{projectPath}\" --urls {BaseUrl}", + Arguments = $"run --project \"{projectPath}\" --urls {BaseUrl}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, From 8e06c82c1d9d90dbe50a7a90ea6dd487f45a7b9d Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Tue, 10 Feb 2026 00:09:38 -0500 Subject: [PATCH 08/13] Install Playwright Chromium in test setup CI doesn't have Playwright browsers pre-installed. Call Program.Main(["install", "chromium"]) in InitializeAsync so the test is self-contained. The download is cached, so subsequent runs are a no-op. Co-Authored-By: Claude Opus 4.6 --- .../NavigationBreadcrumbTests.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs index 785945bcbd..ec170795b4 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs @@ -11,6 +11,13 @@ public class NavigationBreadcrumbTests : IAsyncLifetime public async Task InitializeAsync() { + // Ensure Chromium is installed (no-op if already cached) + var exitCode = Microsoft.Playwright.Program.Main(["install", "chromium"]); + if (exitCode != 0) + { + throw new InvalidOperationException($"Playwright browser install failed with exit code {exitCode}"); + } + await _app.StartAsync(); _playwright = await Playwright.CreateAsync(); From 3bf77018ff852908b23ca0ec2b6597a6acf8c08f Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Tue, 10 Feb 2026 00:14:56 -0500 Subject: [PATCH 09/13] Exclude Playwright tests from CI solution filters Playwright requires browser binaries and system dependencies that aren't available across all CI platforms (ARM64, musl). Exclude the PlaywrightTests projects from all CI build filters. They remain in local dev filters (SentryAspNetCore, SentryNoMobile, etc.) and can be run manually or in a dedicated CI job later. Co-Authored-By: Claude Opus 4.6 --- Sentry-CI-Build-Linux-NoMobile.slnf | 2 -- Sentry-CI-Build-Linux.slnf | 2 -- Sentry-CI-Build-Windows-arm64.slnf | 2 -- Sentry-CI-Build-Windows.slnf | 2 -- Sentry-CI-Build-macOS.slnf | 2 -- scripts/generate-solution-filters-config.yaml | 7 +++++++ .../NavigationBreadcrumbTests.cs | 9 ++------- 7 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Sentry-CI-Build-Linux-NoMobile.slnf b/Sentry-CI-Build-Linux-NoMobile.slnf index 517b08bf30..69921c2bcb 100644 --- a/Sentry-CI-Build-Linux-NoMobile.slnf +++ b/Sentry-CI-Build-Linux-NoMobile.slnf @@ -49,8 +49,6 @@ "src\\Sentry.Serilog\\Sentry.Serilog.csproj", "src\\Sentry\\Sentry.csproj", "test\\Sentry.Analyzers.Tests\\Sentry.Analyzers.Tests.csproj", - "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", - "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf index 040ab0b411..14ffc09833 100644 --- a/Sentry-CI-Build-Linux.slnf +++ b/Sentry-CI-Build-Linux.slnf @@ -56,8 +56,6 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", - "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-Windows-arm64.slnf b/Sentry-CI-Build-Windows-arm64.slnf index 751471a013..41d924ac5f 100644 --- a/Sentry-CI-Build-Windows-arm64.slnf +++ b/Sentry-CI-Build-Windows-arm64.slnf @@ -58,8 +58,6 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", - "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf index 00b79f97a2..1b158939c9 100644 --- a/Sentry-CI-Build-Windows.slnf +++ b/Sentry-CI-Build-Windows.slnf @@ -58,8 +58,6 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", - "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index e5ff038bab..08b1295241 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -63,8 +63,6 @@ "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.PlaywrightTests.TestApp\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj", - "test\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests\\Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.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", diff --git a/scripts/generate-solution-filters-config.yaml b/scripts/generate-solution-filters-config.yaml index 6526054ea9..e3eaf125bc 100644 --- a/scripts/generate-solution-filters-config.yaml +++ b/scripts/generate-solution-filters-config.yaml @@ -32,6 +32,8 @@ groupConfigs: trimTests: - "**/Sentry.TrimTest.csproj" - "**/Sentry.MauiTrimTest.csproj" + playwrightTests: + - "**/*PlaywrightTests*.csproj" mobileOnly: - "**/*Android*.csproj" - "**/*Ios*.csproj" @@ -50,6 +52,7 @@ filterConfigs: - "windowsOnly" - "artefacts" - "trimTests" + - "playwrightTests" patterns: - "**/*AndroidTestApp.csproj" - "**/*DeviceTests*.csproj" @@ -67,6 +70,7 @@ filterConfigs: - "artefacts" - "trimTests" - "mobileOnly" + - "playwrightTests" patterns: - "**/*Android*.csproj" - "**/*DeviceTests*.csproj" @@ -83,6 +87,7 @@ filterConfigs: groups: - "artefacts" - "trimTests" + - "playwrightTests" patterns: - "**/*AndroidTestApp.csproj" - "**/*DeviceTests*.csproj" @@ -100,6 +105,7 @@ filterConfigs: - "macOnly" - "artefacts" - "trimTests" + - "playwrightTests" patterns: - "**/*AndroidTestApp.csproj" # AssemblyReader tests are flaky on Windows: https://github.com/getsentry/sentry-dotnet/issues/4091 @@ -121,6 +127,7 @@ filterConfigs: - "macOnly" - "artefacts" - "trimTests" + - "playwrightTests" patterns: - "**/*AndroidTestApp.csproj" # AssemblyReader tests are flaky on Windows: https://github.com/getsentry/sentry-dotnet/issues/4091 diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs index ec170795b4..c136818e4b 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs @@ -40,19 +40,14 @@ public async Task Navigation_CreatesBreadcrumbs_WithCorrectFromAndTo() var page = await _browser.NewPageAsync(); // Collect all intercepted envelopes - var envelopes = new List(); var envelopeReceived = new TaskCompletionSource(); await page.RouteAsync("**/api/0/envelope/**", async route => { var body = route.Request.PostData; - if (body != null) + if (body != null && body.Contains("\"breadcrumbs\"")) { - envelopes.Add(body); - if (body.Contains("\"breadcrumbs\"")) - { - envelopeReceived.TrySetResult(body); - } + envelopeReceived.TrySetResult(body); } await route.FulfillAsync(new RouteFulfillOptions { From 485775a7a5507d32333c3ac90fa8e561f594730c Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Wed, 11 Feb 2026 18:39:20 -0500 Subject: [PATCH 10/13] Address PR review comments - Fix JsonDocument leak: clone RootElement and dispose document - Make _playwright/_browser nullable, null-check in DisposeAsync - Close page after test to avoid resource leak - Use using on TcpListener Co-Authored-By: Claude Opus 4.6 --- .../BlazorWasmTestApp.cs | 2 +- .../NavigationBreadcrumbTests.cs | 15 ++++++++++----- .../SentryEnvelopeParser.cs | 3 ++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs index 10e9b3c1bb..9461a7a35d 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs @@ -73,7 +73,7 @@ public async Task StartAsync() private static int GetFreePort() { - var listener = new TcpListener(IPAddress.Loopback, 0); + using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs index c136818e4b..7c13bdd244 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs @@ -6,8 +6,8 @@ namespace Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests; public class NavigationBreadcrumbTests : IAsyncLifetime { private readonly BlazorWasmTestApp _app = new(); - private IPlaywright _playwright = null!; - private IBrowser _browser = null!; + private IPlaywright? _playwright; + private IBrowser? _browser; public async Task InitializeAsync() { @@ -29,15 +29,18 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - await _browser.DisposeAsync(); - _playwright.Dispose(); + if (_browser != null) + { + await _browser.DisposeAsync(); + } + _playwright?.Dispose(); await _app.DisposeAsync(); } [Fact] public async Task Navigation_CreatesBreadcrumbs_WithCorrectFromAndTo() { - var page = await _browser.NewPageAsync(); + var page = await _browser!.NewPageAsync(); // Collect all intercepted envelopes var envelopeReceived = new TaskCompletionSource(); @@ -98,5 +101,7 @@ await route.FulfillAsync(new RouteFulfillOptions var second = navBreadcrumbs[1]; second.GetProperty("data").GetProperty("from").GetString().Should().Be("/second"); second.GetProperty("data").GetProperty("to").GetString().Should().Be("/trigger-capture"); + + await page.CloseAsync(); } } diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs index e4bb56d20f..cd2de0c9a1 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs @@ -23,7 +23,8 @@ internal static class SentryEnvelopeParser if (itemHeader.TryGetProperty("type", out var typeEl) && typeEl.GetString() == "event") { - return JsonDocument.Parse(lines[i + 1]).RootElement; + using var eventDoc = JsonDocument.Parse(lines[i + 1]); + return eventDoc.RootElement.Clone(); } } From 61bdf9865c69a4eed028385e7ea96c184a9eed46 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Thu, 12 Feb 2026 23:01:48 -0500 Subject: [PATCH 11/13] Add Playwright test projects to SLNX solution files The base branch migrated from .sln to .slnx format. Update the new solution files to include the Playwright test projects and fix backslash path separators. Co-Authored-By: Claude Opus 4.6 --- .generated.NoMobile.slnx | 4 +++- Sentry.slnx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.generated.NoMobile.slnx b/.generated.NoMobile.slnx index 4b4ac6e63e..3ed20edf11 100644 --- a/.generated.NoMobile.slnx +++ b/.generated.NoMobile.slnx @@ -179,6 +179,8 @@ - + + +
diff --git a/Sentry.slnx b/Sentry.slnx index 4b4ac6e63e..3ed20edf11 100644 --- a/Sentry.slnx +++ b/Sentry.slnx @@ -179,6 +179,8 @@ - + + +
From 16c7fe3f9be164f510dc9b494c9e820e695957fc Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Fri, 13 Feb 2026 00:08:14 -0500 Subject: [PATCH 12/13] ci: Add Playwright workflow for Blazor WASM E2E tests Adds a lightweight CI workflow that runs the Blazor WebAssembly Playwright tests. Only triggers on PRs that touch Blazor WASM, core SDK, or ASP.NET Core code. Uses a minimal setup (just .NET SDK + Chromium) instead of the heavy shared environment action that installs Java, Android SDK, Mono, etc. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/playwright-blazor-wasm.yml | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/playwright-blazor-wasm.yml diff --git a/.github/workflows/playwright-blazor-wasm.yml b/.github/workflows/playwright-blazor-wasm.yml new file mode 100644 index 0000000000..1b0a03323b --- /dev/null +++ b/.github/workflows/playwright-blazor-wasm.yml @@ -0,0 +1,62 @@ +name: Blazor WASM Playwright Tests + +on: + push: + branches: + - main + - release/* + pull_request: + paths: + - 'src/Sentry/**' + - 'src/Sentry.AspNetCore/**' + - 'src/Sentry.AspNetCore.Blazor.WebAssembly/**' + - 'src/Sentry.Extensions.Logging/**' + - 'test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/**' + - 'test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/**' + - 'global.json' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'nuget.config' + - '.github/workflows/playwright-blazor-wasm.yml' + workflow_dispatch: + +jobs: + playwright: + name: Blazor WASM E2E + runs-on: ubuntu-latest + env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: 1 + steps: + - name: Cancel Previous Runs + if: github.ref_name != 'main' && !startsWith(github.ref_name, 'release/') + uses: styfle/cancel-workflow-action@3155a141048f8f89c06b4cdae32e7853e97536bc # 0.13.0 + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: recursive + + - name: Install .NET SDK + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + with: + global-json-file: global.json + + - name: Install .NET Workloads + run: dotnet workload restore --temp-dir "${{ runner.temp }}" --skip-sign-check + + - name: Build + run: dotnet build test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests -c Release + + - name: Install Playwright Browsers + run: pwsh test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/bin/Release/net10.0/playwright.ps1 install chromium --with-deps + + - name: Run Playwright Tests + run: dotnet test test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests -c Release --no-build --logger "trx;LogFileName=results.trx" + + - name: Upload Test Results + if: failure() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: playwright-blazor-wasm-results + path: test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/TestResults/ From 55bc42d43b0eb019a642b854e0112921f68974e9 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Fri, 13 Feb 2026 17:23:48 -0500 Subject: [PATCH 13/13] Address PR review feedback - Add src/*, test/*, test/Sentry.Testing/** to CI workflow paths (Flash0ver) - Use --configuration Release in dotnet run to match CI build (Flash0ver) - Add empty Directory.Build.props/targets for TestApp to stop inheriting test-specific packages, removing Using Remove hacks (Flash0ver) - Remove redundant ! null-forgiving on Nullable (Flash0ver) - Remove StringSplitOptions.RemoveEmptyEntries from envelope parser to preserve header/payload pair alignment (cursor bugbot) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/playwright-blazor-wasm.yml | 3 +++ .../Directory.Build.props | 3 +++ .../Directory.Build.targets | 3 +++ ...WebAssembly.PlaywrightTests.TestApp.csproj | 21 ------------------- .../BlazorWasmTestApp.cs | 2 +- .../NavigationBreadcrumbTests.cs | 2 +- .../SentryEnvelopeParser.cs | 2 +- 7 files changed, 12 insertions(+), 24 deletions(-) create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.props create mode 100644 test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.targets diff --git a/.github/workflows/playwright-blazor-wasm.yml b/.github/workflows/playwright-blazor-wasm.yml index 1b0a03323b..47b12619eb 100644 --- a/.github/workflows/playwright-blazor-wasm.yml +++ b/.github/workflows/playwright-blazor-wasm.yml @@ -7,12 +7,15 @@ on: - release/* pull_request: paths: + - 'src/*' - 'src/Sentry/**' - 'src/Sentry.AspNetCore/**' - 'src/Sentry.AspNetCore.Blazor.WebAssembly/**' - 'src/Sentry.Extensions.Logging/**' + - 'test/*' - 'test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/**' - 'test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/**' + - 'test/Sentry.Testing/**' - 'global.json' - 'Directory.Build.props' - 'Directory.Build.targets' diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.props b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.props new file mode 100644 index 0000000000..f81630fc42 --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.props @@ -0,0 +1,3 @@ + + + diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.targets b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.targets new file mode 100644 index 0000000000..70545e531e --- /dev/null +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj index 530f804349..aad41e58d8 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj @@ -8,27 +8,6 @@ false - - - - - - - - - - - - - - - - - - - false - - diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs index 9461a7a35d..8411e888e8 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs @@ -27,7 +27,7 @@ public async Task StartAsync() StartInfo = new ProcessStartInfo { FileName = "dotnet", - Arguments = $"run --project \"{projectPath}\" --urls {BaseUrl}", + Arguments = $"run --project \"{projectPath}\" --configuration Release --urls {BaseUrl}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs index 7c13bdd244..be0092b550 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs @@ -82,7 +82,7 @@ await route.FulfillAsync(new RouteFulfillOptions var eventPayload = SentryEnvelopeParser.ExtractEventFromEnvelope(envelopeBody); eventPayload.Should().NotBeNull("expected an event payload in the Sentry envelope"); - var breadcrumbs = eventPayload!.Value.GetProperty("breadcrumbs").EnumerateArray().ToList(); + var breadcrumbs = eventPayload.Value.GetProperty("breadcrumbs").EnumerateArray().ToList(); var navBreadcrumbs = breadcrumbs .Where(b => diff --git a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs index cd2de0c9a1..6ddc8f3496 100644 --- a/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs +++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs @@ -11,7 +11,7 @@ internal static class SentryEnvelopeParser /// public static JsonElement? ExtractEventFromEnvelope(string envelopeBody) { - var lines = envelopeBody.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var lines = envelopeBody.Split('\n'); // lines[0] = envelope header // lines[1..] = pairs of (item header, item payload)