feat(stats): per-user API quota attribution + admin usage page
Track who burned how much YouTube API quota. A QuotaEvent audit log (migration 0009) records every spend with the triggering user (NULL = background/system) and an action label, set via a request/job-scoped contextvar (quota.attribute) so no call signatures change. User-initiated work (sync subscriptions, unsubscribe, opt-in recent backfill, manual enrich) attributes to the user; scheduler work to System, split by action. - backend: QuotaEvent model + migration 0009; quota.attribute() contextvar; record_usage logs events; entry points wrapped (routes/sync, routes/channels, scheduler); GET /api/quota/my-usage + GET /api/quota/admin - frontend: admin-only Stats page (header nav, page=stats) with daily bars + per-user breakdown by action and range picker; 'Your API usage' in Settings -> Sync for every user Verified: attribution + endpoints compute correctly; events are per-user vs System.
This commit is contained in:
parent
bcc4371ac7
commit
f255728f75
15 changed files with 451 additions and 26 deletions
|
|
@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import quota
|
||||
from app.auth import current_user, has_write_scope
|
||||
from app.db import get_db
|
||||
from app.models import Channel, ChannelTag, Subscription, Tag, User, Video
|
||||
|
|
@ -121,7 +122,8 @@ def update_channel(
|
|||
# fetched its recent uploads yet, do that one channel now (cheap) instead of waiting for
|
||||
# the scheduler. Deep paging still follows on the scheduler's next run (recent-then-deep).
|
||||
if deep_turned_on and channel is not None and channel.recent_synced_at is None:
|
||||
run_recent_backfill(db, [channel], max_channels=1)
|
||||
with quota.attribute(user.id, "backfill_recent"):
|
||||
run_recent_backfill(db, [channel], max_channels=1)
|
||||
|
||||
return {
|
||||
"id": channel_id,
|
||||
|
|
@ -152,7 +154,7 @@ def unsubscribe(
|
|||
detail="No YouTube subscription id on record — sync subscriptions first.",
|
||||
)
|
||||
try:
|
||||
with YouTubeClient(db, user) as yt:
|
||||
with quota.attribute(user.id, "unsubscribe"), YouTubeClient(db, user) as yt:
|
||||
yt.delete_subscription(sub.yt_subscription_id)
|
||||
except YouTubeError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"YouTube unsubscribe failed: {exc}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue