From d9f3e91aa819c07ce497993a7ffa260389a400f8 Mon Sep 17 00:00:00 2001 From: Carlos Narro Date: Sat, 4 Jul 2026 00:01:46 +0200 Subject: [PATCH] =?UTF-8?q?telemetr=C3=ADa:=20tiempo=20por=20pantalla=20(c?= =?UTF-8?q?olumna=20seconds)=20+=20Aprende/subsecciones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:') y las vistas ('today', '__other__') entran sin cambios de esquema. Tests actualizados. Co-Authored-By: Claude Opus 4.8 --- main.py | 45 +++++++++++++++++++++++++++++++++++++-------- models.py | 3 ++- test_telemetry.py | 8 +++++++- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index d31c7ea..77ee02a 100644 --- a/main.py +++ b/main.py @@ -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} diff --git a/models.py b/models.py index c47a4c9..86638e8 100644 --- a/models.py +++ b/models.py @@ -81,6 +81,7 @@ class TelemetryDaily(Base): __tablename__ = "telemetry_daily" day = Column(String, primary_key=True) # 'YYYY-MM-DD' - module = Column(String, primary_key=True) # nombre del módulo o '__submissions__' + module = Column(String, primary_key=True) # nombre del módulo/vista o '__submissions__' active = Column(Integer, default=0) opens = Column(Integer, default=0) + seconds = Column(Integer, default=0) # tiempo en pantalla acumulado diff --git a/test_telemetry.py b/test_telemetry.py index 5673cd3..1c08729 100644 --- a/test_telemetry.py +++ b/test_telemetry.py @@ -25,21 +25,27 @@ def test_telemetria_agrega_y_resume(): "app_version": "2.27.0", "active_modules": ["fasting", "weight", "alcohol"], "opens_by_module": {"alcohol": 3, "weight": 1}, + "seconds_by_module": {"alcohol": 120, "aprende:sueno": 45}, }) assert r1.status_code < 300, r1.text r2 = client.post("/telemetry", json={ "day": day, "active_modules": ["fasting", "alcohol"], "opens_by_module": {"alcohol": 2}, + "seconds_by_module": {"alcohol": 60}, }) assert r2.status_code < 300, r2.text summary = client.get("/telemetry/summary").json() assert summary["submissions"] == 2 # dos envíos ese día by_module = {m["module"]: m for m in summary["modules"]} - # alcohol: activo en 2 envíos, abierto 3+2 = 5 veces. + # alcohol: activo en 2 envíos, abierto 3+2 = 5 veces, 120+60 = 180 s. assert by_module["alcohol"]["activeInstallDays"] == 2 assert by_module["alcohol"]["opens"] == 5 + assert by_module["alcohol"]["seconds"] == 180 + # Una subsección de Aprende con tiempo, sin ser módulo activo. + assert by_module["aprende:sueno"]["seconds"] == 45 + assert by_module["aprende:sueno"]["opens"] == 0 # fasting: activo en 2 envíos, 0 aperturas. assert by_module["fasting"]["activeInstallDays"] == 2 assert by_module["fasting"]["opens"] == 0