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

47 lines
2.3 KiB
C#

using System.Text;
namespace DeepDrftPublic.Client.Services;
/// <summary>
/// Production <see cref="IEventPoster"/>: a first-party, same-origin <see cref="System.Net.Http.HttpClient"/>
/// POST to the site's own <c>api/event/*</c> proxy (telemetry transport-resilience). A fetch to the host's
/// own origin is not third-party tracking and is not caught by the name-based heuristics (Firefox
/// Fingerprinting / Tracking Protection) that block a <c>telemetry/beacon</c>-named <c>sendBeacon</c>
/// module. Used for the awaitable play-close paths (organic end / track-switch / stop) and every share
/// event; only the rare tab-unload edge still goes through <see cref="BeaconInterop"/>, where an awaited
/// fetch would be cancelled as the page freezes.
///
/// <para>Best-effort and non-throwing by contract: a failed POST (offline, blocked, server error) is
/// swallowed so a dropped telemetry event never throws into the UI or the playback path — identical
/// posture to the beacon transport.</para>
/// </summary>
public sealed class HttpEventPoster : IEventPoster
{
// The default factory client carries no base address; the sink/tracker pass an absolute same-origin
// URL built from NavigationManager.BaseUri, so the POST targets the host proxy regardless of how the
// named clients are configured. The default client uses the browser fetch handler in WASM, which is
// exactly the first-party request the heuristic blockers permit.
private readonly IHttpClientFactory _httpClientFactory;
public HttpEventPoster(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task PostAsync(string url, string json)
{
try
{
using var client = _httpClientFactory.CreateClient();
using var content = new StringContent(json, Encoding.UTF8, "application/json");
// Best-effort: the server records the event; we do not act on the status either way.
using var response = await client.PostAsync(url, content);
}
catch
{
// Swallow — a dropped telemetry event is acceptable; telemetry must never throw into the UI
// or the playback path. Mirrors the beacon's fire-and-forget contract.
}
}
}