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
@@ -3,11 +3,16 @@ using Microsoft.JSInterop;
namespace DeepDrftPublic.Client.Services;
/// <summary>
/// Thin C# wrapper over the <c>window.DeepDrftBeacon</c> TS interop (Phase 16 §2.2). Wraps the
/// <c>navigator.sendBeacon</c> POST and the page-unload registration so the rest of the client never
/// touches <see cref="IJSRuntime"/> string identifiers directly. All calls are best-effort: a JS
/// failure (module not yet loaded, interop unavailable during prerender) is swallowed — telemetry must
/// never throw into the UI or the playback path.
/// Thin C# wrapper over the <c>window.DeepDrftLifecycle</c> TS interop. Wraps the <c>navigator.sendBeacon</c>
/// POST and the page-unload registration so the rest of the client never touches <see cref="IJSRuntime"/>
/// string identifiers directly. After the transport-resilience split this is the <b>unload-edge transport
/// only</b>: normal play closes and shares go over the first-party <see cref="IEventPoster"/> fetch, and
/// <c>sendBeacon</c> is retained solely for the page-unload path (pagehide / visibility→hidden) where an
/// awaited fetch would be cancelled. The module is named off the former <c>telemetry/beacon</c> path
/// (<c>DeepDrftLifecycle</c>, served from <c>js/session/lifecycle.js</c>) so even this retained fallback is
/// not caught by name-based tracking/fingerprinting blockers. All calls are best-effort: a JS failure
/// (module not yet loaded, interop unavailable during prerender) is swallowed — telemetry must never throw
/// into the UI or the playback path.
/// </summary>
public sealed class BeaconInterop
{
@@ -23,7 +28,7 @@ public sealed class BeaconInterop
{
try
{
await _js.InvokeAsync<bool>("DeepDrftBeacon.send", url, json);
await _js.InvokeAsync<bool>("DeepDrftLifecycle.send", url, json);
}
catch
{
@@ -37,7 +42,7 @@ public sealed class BeaconInterop
{
try
{
await _js.InvokeVoidAsync("DeepDrftBeacon.registerUnload", key, dotNetRef, methodName);
await _js.InvokeVoidAsync("DeepDrftLifecycle.registerUnload", key, dotNetRef, methodName);
}
catch
{
@@ -50,7 +55,7 @@ public sealed class BeaconInterop
{
try
{
await _js.InvokeVoidAsync("DeepDrftBeacon.unregisterUnload", key);
await _js.InvokeVoidAsync("DeepDrftLifecycle.unregisterUnload", key);
}
catch
{