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
+48 -2
View File
@@ -13,11 +13,27 @@ namespace DeepDrftTests;
[TestFixture]
public class PlayTrackerTests
{
// Captures emitted plays so assertions read the (key, bucket) the tracker classified.
// Captures emitted plays so assertions read the (key, bucket) the tracker classified. The two arms are
// captured separately so a test can assert which transport a given close selected (fetch vs unload).
// Emitted folds both arms for the floor/bucket assertions that don't care about transport.
private sealed class FakeSink : IPlayEventSink
{
public List<(string Key, PlayBucket Bucket)> Emitted { get; } = new();
public void EmitPlay(string trackEntryKey, PlayBucket bucket) => Emitted.Add((trackEntryKey, bucket));
public List<(string Key, PlayBucket Bucket)> FetchEmitted { get; } = new();
public List<(string Key, PlayBucket Bucket)> UnloadEmitted { get; } = new();
public Task EmitPlayAsync(string trackEntryKey, PlayBucket bucket)
{
FetchEmitted.Add((trackEntryKey, bucket));
Emitted.Add((trackEntryKey, bucket));
return Task.CompletedTask;
}
public void EmitPlayOnUnload(string trackEntryKey, PlayBucket bucket)
{
UnloadEmitted.Add((trackEntryKey, bucket));
Emitted.Add((trackEntryKey, bucket));
}
}
private FakeSink _sink = null!;
@@ -202,4 +218,34 @@ public class PlayTrackerTests
PlaySession("t", duration: 100, highWater: 95);
Assert.That(_sink.Emitted, Has.Count.EqualTo(2));
}
// --- Transport-arm selection (telemetry transport-resilience) ---
// A normal close (organic end / track-switch / stop) emits over the first-party fetch arm — the page
// is alive, so the awaitable HttpClient POST is the heuristic-safe transport.
[Test]
public void Close_NormalClose_EmitsOverFetchArm()
{
_tracker.OnPlaybackStarted("t");
_tracker.SetDuration(100);
_tracker.OnProgress(95);
_tracker.Close(); // viaUnload defaults to false
Assert.That(_sink.FetchEmitted, Has.Count.EqualTo(1));
Assert.That(_sink.UnloadEmitted, Is.Empty);
}
// The page-unload close emits over the sendBeacon arm — an awaited fetch would be cancelled as the
// page freezes, so this rare edge keeps the beacon.
[Test]
public void Close_ViaUnload_EmitsOverBeaconArm()
{
_tracker.OnPlaybackStarted("t");
_tracker.SetDuration(100);
_tracker.OnProgress(95);
_tracker.Close(viaUnload: true);
Assert.That(_sink.UnloadEmitted, Has.Count.EqualTo(1));
Assert.That(_sink.FetchEmitted, Is.Empty);
}
}