telemetría: tiempo por pantalla (columna seconds) + Aprende/subsecciones

telemetry_daily gana columna `seconds` (migración idempotente ALTER ADD COLUMN
IF NOT EXISTS para el Postgres ya desplegado; create_all para SQLite fresco). El
POST acepta seconds_by_module y lo agrega por clave (tope 86400/clave/envío); el
summary suma y devuelve seconds por módulo/vista. Las claves ya eran genéricas,
así que Aprende ('aprende', 'aprende:<tema>') y las vistas ('today', '__other__')
entran sin cambios de esquema. Tests actualizados.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-07-04 00:01:46 +02:00
parent 3092e19b73
commit d9f3e91aa8
3 changed files with 46 additions and 10 deletions

45
main.py
View File

@@ -59,6 +59,20 @@ def _migrate_recipes():
pass
def _migrate_telemetry():
"""Añade en Postgres la columna `seconds` de `telemetry_daily` si falta
(la tabla ya existe desde v2.27 sin esa columna; create_all no la altera)."""
if not engine.url.get_backend_name().startswith("postgres"):
return
try:
with engine.begin() as conn:
conn.execute(text(
"ALTER TABLE telemetry_daily ADD COLUMN IF NOT EXISTS "
"seconds INTEGER DEFAULT 0"))
except Exception:
pass
def init_db(retries: int = 10):
"""Crea las tablas, reintentando por si la BD tarda en levantar. No tumba
el arranque si falla: /health seguirá respondiendo."""
@@ -67,6 +81,7 @@ def init_db(retries: int = 10):
try:
Base.metadata.create_all(bind=engine)
_migrate_recipes()
_migrate_telemetry()
return True
except Exception:
time.sleep(2)
@@ -265,28 +280,31 @@ class TelemetryIn(BaseModel):
app_version: str | None = None
active_modules: list[str] = []
opens_by_module: dict[str, int] = {}
seconds_by_module: dict[str, int] = {}
@app.post("/telemetry")
def add_telemetry(body: TelemetryIn, db: Session = Depends(get_db)):
"""Recibe un lote diario y agregado de uso. NO lleva identificador: solo
incrementa contadores por día y módulo. No guarda IP, nombre ni contenido."""
incrementa contadores por día y módulo/vista. No guarda IP, nombre ni
contenido."""
day = (body.day or "").strip()[:10]
if len(day) != 10:
raise HTTPException(status_code=400, detail="day inválido (YYYY-MM-DD)")
# Agregamos los incrementos de ESTE envío por módulo antes de tocar la BD:
# un módulo puede aparecer en active_modules y en opens_by_module, y crear
# dos filas pendientes con la misma PK reventaría el INSERT.
# Agregamos los incrementos de ESTE envío por clave antes de tocar la BD:
# una clave puede aparecer en varios mapas, y crear dos filas pendientes con
# la misma PK reventaría el INSERT.
agg: dict[str, dict[str, int]] = {}
def add(module: str, active: int = 0, opens: int = 0):
def add(module: str, active: int = 0, opens: int = 0, seconds: int = 0):
module = str(module).strip()[:40]
if not module:
return
e = agg.setdefault(module, {"active": 0, "opens": 0})
e = agg.setdefault(module, {"active": 0, "opens": 0, "seconds": 0})
e["active"] += active
e["opens"] += opens
e["seconds"] += seconds
# Un envío = una instalación activa ese día que cede estadística.
add("__submissions__", active=1)
@@ -299,14 +317,23 @@ def add_telemetry(body: TelemetryIn, db: Session = Depends(get_db)):
continue
if c > 0:
add(m, opens=min(c, 100000))
for m, s in (body.seconds_by_module or {}).items():
try:
s = int(s)
except (TypeError, ValueError):
continue
if s > 0:
# Tope por clave y envío (~1 día): evita valores absurdos.
add(m, seconds=min(s, 86400))
for module, delta in agg.items():
row = db.get(TelemetryDaily, (day, module))
if row is None:
row = TelemetryDaily(day=day, module=module, active=0, opens=0)
row = TelemetryDaily(day=day, module=module, active=0, opens=0, seconds=0)
db.add(row)
row.active = (row.active or 0) + delta["active"]
row.opens = (row.opens or 0) + delta["opens"]
row.seconds = (row.seconds or 0) + delta["seconds"]
db.commit()
return {"ok": True}
@@ -323,6 +350,7 @@ def telemetry_summary(days: int = 30, db: Session = Depends(get_db)):
TelemetryDaily.module,
func.sum(TelemetryDaily.active),
func.sum(TelemetryDaily.opens),
func.sum(TelemetryDaily.seconds),
)
.filter(TelemetryDaily.day >= cutoff)
.group_by(TelemetryDaily.module)
@@ -330,7 +358,7 @@ def telemetry_summary(days: int = 30, db: Session = Depends(get_db)):
)
submissions = 0
modules = []
for module, active, opens in rows:
for module, active, opens, seconds in rows:
if module == "__submissions__":
submissions = int(active or 0)
continue
@@ -338,6 +366,7 @@ def telemetry_summary(days: int = 30, db: Session = Depends(get_db)):
"module": module,
"activeInstallDays": int(active or 0),
"opens": int(opens or 0),
"seconds": int(seconds or 0),
})
modules.sort(key=lambda x: x["opens"], reverse=True)
return {"sinceDays": days, "submissions": submissions, "modules": modules}