feat(phase-16): anonymous play & share telemetry substrate (wave 16.1)

Player-service play-session tracker (floor + 3-bucket classify), SharePopover share tracker with debounce, sendBeacon interop, proxied rate-limited POST api/event/{play,share}, append-only event logs + incremental play_counter with server-side release resolution. Migration authored, not applied. No anonId, no read surface.
This commit is contained in:
daniel-c-harvey
2026-06-19 12:59:00 -04:00
parent 1931574ad4
commit dbd90ee52a
35 changed files with 2460 additions and 2 deletions
@@ -260,6 +260,9 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
private async Task OnProgressCallback(double currentTime)
{
CurrentTime = currentTime;
// Telemetry hook (Phase 16 §2.1): a subclass advances the play-session high-water mark here, on
// the same throttled tick the UI already consumes. Base implementation is a no-op.
OnProgressTick(currentTime);
await NotifyStateChanged();
}
@@ -270,6 +273,10 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
IsLoaded = false;
CurrentTime = 0;
Duration = null;
// Telemetry hook: organic end closes the play session (the bucket reflects how far they got)
// BEFORE the state notification and TrackEnded fan-out, so the session that just ended is the
// one recorded — not whatever a queue auto-advance opens next. Base implementation is a no-op.
OnPlaybackEnded();
await NotifyStateChanged();
// Fire AFTER the state notification so any queue orchestrator that advances on this
@@ -279,6 +286,18 @@ public abstract class AudioPlayerService : IPlayerService, IAsyncDisposable
TrackEnded?.Invoke();
}
/// <summary>
/// Telemetry seam (Phase 16): called on each progress tick with the current playback position. The
/// streaming subclass overrides this to advance the play-session high-water mark. No-op in the base.
/// </summary>
protected virtual void OnProgressTick(double currentTime) { }
/// <summary>
/// Telemetry seam (Phase 16): called on organic end-of-stream, before <see cref="TrackEnded"/> fires.
/// The streaming subclass overrides this to close the play session. No-op in the base.
/// </summary>
protected virtual void OnPlaybackEnded() { }
protected async Task EnsureInitializedAsync()
{