feat(metrics): bucketed surface families + manual-plays baseline (#1248, milestone 127)
The recommendation metrics table was observable but not actionable: raw source strings (album:<uuid> one-offs) drowned the stable surfaces, and manual plays were excluded so skip rates had no control group. - SQL: include NULL-source rows (the baseline) and carry completion_n so family merges can weight avg_completion correctly. - Handler buckets raw sources into stable families (radio:<uuid> → Radio, album:/artist: → direct plays, etc.) grouped by surface intent: go-to / discovery / direct — each band judged against its job, since discovery mixes are expected to skip hotter. Families under 20 plays are flagged low-confidence, not hidden. - Settings card renders the baseline row and per-surface deltas in percentage points vs baseline (worse-than-baseline deltas in danger color), intent hint copy per group, low-data rows dimmed. - Pure-unit test for the bucketing/merge; DB test updated to the new contract (baseline included, radio:<uuid> collapse). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
This commit is contained in:
+17
-20
@@ -1,18 +1,31 @@
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { api } from './client';
|
||||
|
||||
// Mirrors internal/api/me_recommendation_metrics.go.
|
||||
export type RecommendationMetric = {
|
||||
source: string;
|
||||
// Mirrors internal/api/me_recommendation_metrics.go: raw play sources are
|
||||
// bucketed server-side into stable surface families, grouped by intent, and
|
||||
// anchored by the manual-plays baseline (milestone #127).
|
||||
export type SurfaceMetric = {
|
||||
key: string;
|
||||
label: string;
|
||||
plays: number;
|
||||
skips: number;
|
||||
skip_rate: number;
|
||||
avg_completion: number;
|
||||
low_confidence: boolean;
|
||||
};
|
||||
|
||||
export type SurfaceIntent = 'go_to' | 'discovery' | 'direct';
|
||||
|
||||
export type SurfaceGroup = {
|
||||
intent: SurfaceIntent;
|
||||
label: string;
|
||||
surfaces: SurfaceMetric[];
|
||||
};
|
||||
|
||||
export type RecommendationMetrics = {
|
||||
window_days: number;
|
||||
sources: RecommendationMetric[];
|
||||
baseline: SurfaceMetric | null;
|
||||
groups: SurfaceGroup[];
|
||||
};
|
||||
|
||||
export function getRecommendationMetrics(): Promise<RecommendationMetrics> {
|
||||
@@ -28,19 +41,3 @@ export function createRecommendationMetricsQuery() {
|
||||
staleTime: 60_000
|
||||
});
|
||||
}
|
||||
|
||||
// Friendly labels for the system-playlist source keys (play_events.source).
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
for_you: 'For You',
|
||||
discover: 'Discover',
|
||||
deep_cuts: 'Deep cuts',
|
||||
rediscover: 'Rediscover',
|
||||
new_for_you: 'New for you',
|
||||
on_this_day: 'On this day',
|
||||
first_listens: 'First listens',
|
||||
songs_like_artist: 'Songs like…'
|
||||
};
|
||||
|
||||
export function sourceLabel(source: string): string {
|
||||
return SOURCE_LABELS[source] ?? source;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user