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
+26
View File
@@ -0,0 +1,26 @@
using System.Text.Json.Serialization;
namespace DeepDrftModels.Enums;
/// <summary>
/// Completion bucket for a recorded play (Phase 16 §1a / D1). The three buckets are exhaustive and
/// non-overlapping, classified by the high-water playback fraction reached before the session closed:
/// <c>Partial</c> [0, 30%), <c>Sampled</c> [30%, 80%], <c>Complete</c> (80%, 100%]. The headline
/// "Plays" figure is the sum of all three — every started listen that crosses the engagement floor
/// is a play; the buckets are the texture beneath it.
///
/// Serialized as its string name on the wire — the converter on the type makes the
/// client to proxy to API JSON contract string-based regardless of host serializer config.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PlayBucket>))]
public enum PlayBucket
{
/// <summary>Reached &lt; 30% of duration — a skip or a brief partial listen (still past the floor).</summary>
Partial,
/// <summary>Reached 30%80% of duration — a real listen that was neither a skip nor a finish.</summary>
Sampled,
/// <summary>Reached &gt; 80% of duration — effectively a finished listen.</summary>
Complete
}
+33
View File
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace DeepDrftModels.Enums;
/// <summary>
/// The channel a share was performed through (Phase 16 §1b). Today both originate from
/// <c>SharePopover</c>'s clipboard actions; a future native/Web-Share button would add a channel
/// without reshaping the metric. Serialized as its string name on the wire.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<ShareChannel>))]
public enum ShareChannel
{
/// <summary>Copy-link — the canonical track or release URL placed on the clipboard.</summary>
Link,
/// <summary>Copy-embed — the <c>&lt;iframe&gt;</c> snippet for the single-track FramePlayer.</summary>
Embed
}
/// <summary>
/// What a share targets (Phase 16 §1b). Tracks and releases are both shareable; the popover knows
/// which it is at the point of the action, so no server-side resolution is needed for shares.
/// Serialized as its string name on the wire.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<ShareTargetType>))]
public enum ShareTargetType
{
/// <summary>The share targets a single track, addressed by its vault <c>EntryKey</c>.</summary>
Track,
/// <summary>The share targets a release, addressed by its public <c>EntryKey</c>.</summary>
Release
}