fb4431207d
Milestone #127 step 2 (#1249). For You deliberately blends two populations — a head of top-scored taste picks and a tail sampled from deeper ranking (the freshness injection) — but the metrics judged it as one blob, so its skip rate couldn't distinguish "the taste engine is missing" from "the freshness tax is too high". That number decides the exploration share before we tune it. - Migration 0038: nullable pick_kind ('taste'|'fresh') on both playlist_tracks (stamped at snapshot build) and play_events (frozen at play-ingestion — the snapshot rebuilds daily, so attribution cannot be reconstructed at read time). - Builder: pickHeadAndTail marks head=taste / tail=fresh; the small-pool fallback is all taste (top-N-by-score IS the taste mechanism). Other variants persist NULL. - Ingestion: for_you plays (live + offline replay) look the track up in the user's current snapshot; not found → unattributed, never guessed. - Metrics: For You's row gains a breakdown (taste / fresh / earlier unattributed plays), parent row stays the sum; web card renders the sub-rows indented with the same baseline deltas + low-data dimming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
28 lines
1.4 KiB
SQL
28 lines
1.4 KiB
SQL
-- Recommendation observability (#796 phase 4). Per-source play outcomes so the
|
|
-- operator can see whether each recommendation surface is landing and tune the
|
|
-- taste weights. Source is stamped on play_events when a play is launched from
|
|
-- a recommendation surface; NULL means the user picked the track manually —
|
|
-- those rows are INCLUDED here as the baseline control group the surfaces are
|
|
-- judged against (milestone #127: delta-vs-baseline is what makes the numbers
|
|
-- actionable). Raw source strings are bucketed into stable surface families in
|
|
-- the Go handler; completion_n is carried so family merges can weight
|
|
-- avg_completion correctly.
|
|
|
|
-- name: RecommendationSourceMetricsForUser :many
|
|
-- $1 user_id, $2 window_days. plays/skips are counts; avg_completion is the
|
|
-- mean completion ratio over the completion_n plays that recorded one.
|
|
-- pick_kind splits For You plays into taste/fresh/unattributed (#1249);
|
|
-- it is NULL for every other source, so those still group to one row.
|
|
SELECT
|
|
pe.source,
|
|
pe.pick_kind,
|
|
count(*)::bigint AS plays,
|
|
count(*) FILTER (WHERE pe.was_skipped)::bigint AS skips,
|
|
count(pe.completion_ratio)::bigint AS completion_n,
|
|
COALESCE(avg(pe.completion_ratio), 0)::float8 AS avg_completion
|
|
FROM play_events pe
|
|
WHERE pe.user_id = $1
|
|
AND pe.started_at > now() - ($2::float8 * INTERVAL '1 day')
|
|
GROUP BY pe.source, pe.pick_kind
|
|
ORDER BY plays DESC;
|