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:
@@ -8,8 +8,10 @@ using DeepDrftData;
|
||||
using DeepDrftData.Data;
|
||||
using DeepDrftData.Repositories;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NetBlocks.Utilities.Environment;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
// Required credential files — must exist before the app will start.
|
||||
// Production secrets stay gitignored; the *.example.json templates at the project root show the shape.
|
||||
@@ -64,6 +66,14 @@ builder.Services
|
||||
.AddScoped<ITrackService>(sp => sp.GetRequiredService<TrackManager>());
|
||||
builder.Services.AddScoped<UnifiedTrackService>();
|
||||
|
||||
// Phase 16 anonymous telemetry — append-only event logs + incremental play-counter rollup (all SQL).
|
||||
// EventManager is the IEventService boundary; EventRepository owns the EF writes and the
|
||||
// release-resolution + counter-bump transaction.
|
||||
builder.Services
|
||||
.AddScoped<EventRepository>()
|
||||
.AddScoped<EventManager>()
|
||||
.AddScoped<IEventService>(sp => sp.GetRequiredService<EventManager>());
|
||||
|
||||
// Release domain — medium-aware read projection + satellite metadata writes. ReleaseManager is the
|
||||
// IReleaseService implementation; UnifiedReleaseService orchestrates the vault + SQL satellite writes.
|
||||
builder.Services
|
||||
@@ -118,6 +128,25 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
options.KnownProxies.Clear();
|
||||
});
|
||||
|
||||
// Per-IP rate limiting for the anonymous telemetry intake (Phase 16 §2.5). Coarse and stateless —
|
||||
// a fixed window keyed by the (forwarded) remote IP. The substrate sits behind nginx, so the real
|
||||
// client IP is the X-Forwarded-For value UseForwardedHeaders resolves into Connection.RemoteIpAddress.
|
||||
// On limit, reject with 429 (the beacon ignores it; this only blunts casual inflation). The 30-window
|
||||
// budget is generous for a real listening session and only bites on scripted spam.
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.AddPolicy("events", httpContext =>
|
||||
RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
factory: _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 30,
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
QueueLimit = 0,
|
||||
}));
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Apply AuthBlocks EF migrations, seed system roles, seed admin user on first boot.
|
||||
@@ -136,6 +165,11 @@ if (app.Environment.IsDevelopment())
|
||||
|
||||
app.UseCors("ContentApiPolicy");
|
||||
|
||||
// Rate limiter must sit in the pipeline for the [EnableRateLimiting("events")] attribute on
|
||||
// EventController to take effect. Only the telemetry endpoints carry the policy; everything else is
|
||||
// unaffected (no global limiter is set).
|
||||
app.UseRateLimiter();
|
||||
|
||||
// ApiKey middleware only enforces on endpoints tagged [ApiKeyAuthorize] (the track surface); it
|
||||
// passes all other endpoints through. JWT auth/authorization gate the AuthBlocks endpoints, which
|
||||
// carry no [ApiKeyAuthorize] metadata — the two schemes are orthogonal and do not interfere.
|
||||
|
||||
Reference in New Issue
Block a user