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
@@ -1,5 +1,6 @@
using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Common;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
@@ -27,6 +28,7 @@ public partial class SharePopover : ComponentBase, IDisposable
[Inject] public required NavigationManager Navigation { get; set; }
[Inject] public required IJSRuntime JS { get; set; }
[Inject] public required ShareTracker ShareTracker { get; set; }
private bool IsReleaseMode => ReleaseEntryKey is not null;
@@ -67,6 +69,14 @@ public partial class SharePopover : ComponentBase, IDisposable
{
if (await CopyToClipboard(LinkUrl))
{
// Record a share only after the clipboard write succeeds (§1b). Release mode targets the
// release EntryKey; track mode targets the track EntryKey. The tracker debounces repeat
// copies of the same (target, channel) into one event.
if (IsReleaseMode)
ShareTracker.RecordShare(ShareTargetType.Release, ReleaseEntryKey!, ShareChannel.Link);
else if (!string.IsNullOrWhiteSpace(EntryKey))
ShareTracker.RecordShare(ShareTargetType.Track, EntryKey, ShareChannel.Link);
_linkCopied = true;
await ResetAfterDelay(() => _linkCopied = false);
}
@@ -76,6 +86,11 @@ public partial class SharePopover : ComponentBase, IDisposable
{
if (await CopyToClipboard(EmbedSnippet))
{
// Embed is a single-track affordance only (release mode hides it), so this always targets a
// track with channel = embed.
if (!string.IsNullOrWhiteSpace(EntryKey))
ShareTracker.RecordShare(ShareTargetType.Track, EntryKey, ShareChannel.Embed);
_embedCopied = true;
await ResetAfterDelay(() => _embedCopied = false);
}