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/.github/workflows/playwright-blazor-wasm.yml b/.github/workflows/playwright-blazor-wasm.yml
new file mode 100644
index 0000000000..47b12619eb
--- /dev/null
+++ b/.github/workflows/playwright-blazor-wasm.yml
@@ -0,0 +1,65 @@
+name: Blazor WASM Playwright Tests
+
+on:
+ push:
+ branches:
+ - main
+ - 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'
+ - '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/
diff --git a/Sentry.slnx b/Sentry.slnx
index 4b4ac6e63e..3ed20edf11 100644
--- a/Sentry.slnx
+++ b/Sentry.slnx
@@ -179,6 +179,8 @@
-
+
+
+
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",
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.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/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/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..aad41e58d8
--- /dev/null
+++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests.TestApp.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net10.0
+ enable
+ enable
+ 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..8411e888e8
--- /dev/null
+++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/BlazorWasmTestApp.cs
@@ -0,0 +1,92 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+
+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!;
+
+ 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 --project \"{projectPath}\" --configuration Release --urls {BaseUrl}",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ 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();
+ _process.BeginOutputReadLine();
+ _process.BeginErrorReadLine();
+
+ using var http = new HttpClient();
+ 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);
+ if (response.IsSuccessStatusCode)
+ {
+ return;
+ }
+ }
+ catch
+ {
+ // Server not ready yet
+ }
+ await Task.Delay(500);
+ }
+
+ 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()
+ {
+ using 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..be0092b550
--- /dev/null
+++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/NavigationBreadcrumbTests.cs
@@ -0,0 +1,107 @@
+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;
+ private IBrowser? _browser;
+
+ 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();
+ _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
+ {
+ Headless = true
+ });
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (_browser != null)
+ {
+ 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 envelopeReceived = new TaskCompletionSource();
+
+ await page.RouteAsync("**/api/0/envelope/**", async route =>
+ {
+ var body = route.Request.PostData;
+ if (body != null && 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");
+
+ await page.CloseAsync();
+ }
+}
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..6ddc8f3496
--- /dev/null
+++ b/test/Sentry.AspNetCore.Blazor.WebAssembly.PlaywrightTests/SentryEnvelopeParser.cs
@@ -0,0 +1,33 @@
+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');
+
+ // 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")
+ {
+ using var eventDoc = JsonDocument.Parse(lines[i + 1]);
+ return eventDoc.RootElement.Clone();
+ }
+ }
+
+ return null;
+ }
+}