feat(phase-16.3): light up anonId unique-listener layer
Mint a first-party localStorage anonId, thread it onto play/share beacons, persist it via EventController, and add all-time distinct-listener counts (site/track/release). Storage columns + indexes already existed from 16.1.
This commit is contained in:
@@ -22,6 +22,12 @@ public class EventController : ControllerBase
|
||||
// payloads are a track key + an enum, well under 1 KB.
|
||||
private const int MaxBodyBytes = 1024;
|
||||
|
||||
// The anonId is a client-minted GUID string (~36 chars); the anon_id column is varchar(64). Reject
|
||||
// anything longer as malformed rather than silently truncating — an over-long token is either a bug
|
||||
// or an inflation attempt, and a truncated id would corrupt the distinct-listener count by colliding
|
||||
// distinct listeners onto one prefix. Whitespace-only is treated as absent.
|
||||
private const int MaxAnonIdLength = 64;
|
||||
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ILogger<EventController> _logger;
|
||||
|
||||
@@ -43,9 +49,10 @@ public class EventController : ControllerBase
|
||||
return BadRequest("trackEntryKey is required");
|
||||
if (!Enum.IsDefined(payload.Bucket))
|
||||
return BadRequest("bucket is invalid");
|
||||
if (!TryNormalizeAnonId(payload.AnonId, out var anonId))
|
||||
return BadRequest("anonId is invalid");
|
||||
|
||||
// Wave 16.1 writes no anonId — defend the substrate by dropping any the client sends early.
|
||||
var result = await _eventService.RecordPlay(payload.TrackEntryKey, payload.Bucket, anonId: null, ct);
|
||||
var result = await _eventService.RecordPlay(payload.TrackEntryKey, payload.Bucket, anonId, ct);
|
||||
if (!result.Success)
|
||||
{
|
||||
// A telemetry failure must never surface to the listener as an error they can act on, but
|
||||
@@ -69,8 +76,10 @@ public class EventController : ControllerBase
|
||||
return BadRequest("targetType is invalid");
|
||||
if (!Enum.IsDefined(payload.Channel))
|
||||
return BadRequest("channel is invalid");
|
||||
if (!TryNormalizeAnonId(payload.AnonId, out var anonId))
|
||||
return BadRequest("anonId is invalid");
|
||||
|
||||
var result = await _eventService.RecordShare(payload.TargetType, payload.TargetKey, payload.Channel, anonId: null, ct);
|
||||
var result = await _eventService.RecordShare(payload.TargetType, payload.TargetKey, payload.Channel, anonId, ct);
|
||||
if (!result.Success)
|
||||
{
|
||||
_logger.LogWarning("RecordShare failed: {Error}", result.Messages.FirstOrDefault()?.Message);
|
||||
@@ -79,4 +88,27 @@ public class EventController : ControllerBase
|
||||
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
// Normalize an incoming anonId (wave 16.3): whitespace-only / empty / null collapses to a null token
|
||||
// (the listener didn't send one, or storage was unavailable — a valid, anonId-less event). A token
|
||||
// over the column width is rejected (400) rather than truncated, since truncation would collide
|
||||
// distinct listeners. Returns false only on the over-long case; null and a valid token both pass.
|
||||
private static bool TryNormalizeAnonId(string? raw, out string? anonId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
anonId = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
var trimmed = raw.Trim();
|
||||
if (trimmed.Length > MaxAnonIdLength)
|
||||
{
|
||||
anonId = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
anonId = trimmed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user