Files
daniel-c-harvey 2af0d8650b 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.
2026-06-26 21:11:43 -04:00

66 lines
2.4 KiB
C#

using Microsoft.JSInterop;
namespace DeepDrftPublic.Client.Services;
/// <summary>
/// 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
{
private readonly IJSRuntime _js;
public BeaconInterop(IJSRuntime js)
{
_js = js;
}
/// <summary>Queue a fire-and-forget POST of a JSON body to the given absolute URL.</summary>
public async Task SendAsync(string url, string json)
{
try
{
await _js.InvokeAsync<bool>("DeepDrftLifecycle.send", url, json);
}
catch
{
// Module not loaded / not interactive yet — drop the event silently.
}
}
/// <summary>Register a .NET unload callback (fires on pagehide / visibility→hidden) under a key.</summary>
public async Task RegisterUnloadAsync<T>(string key, DotNetObjectReference<T> dotNetRef, string methodName)
where T : class
{
try
{
await _js.InvokeVoidAsync("DeepDrftLifecycle.registerUnload", key, dotNetRef, methodName);
}
catch
{
// Best-effort — without the unload handler, mid-play tab-close simply isn't recorded.
}
}
/// <summary>Detach a previously-registered unload callback.</summary>
public async Task UnregisterUnloadAsync(string key)
{
try
{
await _js.InvokeVoidAsync("DeepDrftLifecycle.unregisterUnload", key);
}
catch
{
// Disposal best-effort.
}
}
}