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:
@@ -10,6 +10,8 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
||||
[Inject] public required AudioInteropService AudioInterop { get; set; }
|
||||
[Inject] public required TrackMediaClient TrackMediaClient { get; set; }
|
||||
[Inject] public required ILogger<StreamingAudioPlayerService> Logger { get; set; }
|
||||
[Inject] public required BeaconInterop Beacon { get; set; }
|
||||
[Inject] public required IPlayEventSink PlayEventSink { get; set; }
|
||||
|
||||
private IStreamingPlayerService? _audioPlayerService;
|
||||
private QueueService? _queueService;
|
||||
@@ -23,7 +25,16 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
||||
// EnsureInitializedAsync — that path is correct because audio contexts
|
||||
// require a user gesture anyway. Initializing eagerly here causes 4+
|
||||
// SignalR round-trips before any content is stable.
|
||||
_audioPlayerService = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);
|
||||
var player = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);
|
||||
|
||||
// Phase 16: bind the play-session tracker to the player after construction, the same way the
|
||||
// queue binds — the player is built with `new`, not DI, so threading telemetry through its
|
||||
// constructor would force the provider to over-resolve. The tracker owns the floor/bucket logic
|
||||
// and emits via the injected sink (the beacon in production); the beacon also drives the
|
||||
// page-unload close so a mid-play tab-close still records the listen. Attached on the concrete
|
||||
// type before it is exposed through the IStreamingPlayerService field.
|
||||
player.AttachTracker(new PlayTracker(PlayEventSink), Beacon);
|
||||
_audioPlayerService = player;
|
||||
|
||||
// Provider is the SOLE owner of OnStateChanged. When the service fires,
|
||||
// the provider re-renders, which cascades to its children automatically.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user