fix(telemetry): first-party fetch for play/share, beacon only on unload

Route normal play closes (end/switch/stop) and all shares through a same-origin
HttpClient POST so privacy-hardened browsers stop blocking them; keep sendBeacon
for the tab-unload edge. Rename the JS module off telemetry/beacon to session/
lifecycle so the retained fallback isn't name-matched. No new data or identifiers.
This commit is contained in:
daniel-c-harvey
2026-06-26 21:11:43 -04:00
parent ca44979b08
commit 2af0d8650b
16 changed files with 318 additions and 114 deletions
+7 -13
View File
@@ -1,33 +1,27 @@
using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Microsoft.JSInterop.Infrastructure;
namespace DeepDrftTests;
/// <summary>
/// Unit tests for the Phase 16 share tracker (<see cref="ShareTracker"/>): the per-(target,channel)
/// debounce (§1b — at most one event per target+channel per 60s window per session). The tracker fires
/// through a beacon that wraps <see cref="IJSRuntime"/>; the tests use a no-op JS runtime (the send is
/// through the first-party <see cref="IEventPoster"/>; the tests use a no-op poster (the POST is
/// fire-and-forget and its outcome is irrelevant) and assert on the debounce decision via the bool the
/// recorder returns — true when an event fired, false when debounced.
/// </summary>
[TestFixture]
public class ShareTrackerTests
{
// sendBeacon interop is fire-and-forget; the tracker never reads the result, so a no-op runtime that
// returns default for any invocation is sufficient to exercise the debounce logic.
private sealed class NoopJsRuntime : IJSRuntime
// The first-party POST is fire-and-forget; the tracker never reads the result, so a no-op poster that
// completes immediately is sufficient to exercise the debounce logic.
private sealed class NoopEventPoster : IEventPoster
{
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
=> ValueTask.FromResult<TValue>(default!);
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
=> ValueTask.FromResult<TValue>(default!);
public Task PostAsync(string url, string json) => Task.CompletedTask;
}
// Minimal NavigationManager so the tracker can compose the (unused-in-test) beacon URL.
// Minimal NavigationManager so the tracker can compose the (unused-in-test) event URL.
private sealed class TestNavigationManager : NavigationManager
{
public TestNavigationManager() => Initialize("https://deepdrft.test/", "https://deepdrft.test/");
@@ -49,7 +43,7 @@ public class ShareTrackerTests
[SetUp]
public void SetUp()
=> _tracker = new ShareTracker(
new BeaconInterop(new NoopJsRuntime()),
new NoopEventPoster(),
new StubAnonIdProvider("anon-1"),
new TestNavigationManager());