Observability + warming guardrails (Codex)
* client_error details, not just a count: new client_errors table + POST /api/client-error (reason/path/user-agent/time) + GET /api/admin/client-errors. The boot-seatbelt beacon now sends the reason + path (once per page); the admin Overview lists the recent errors so we can tell chunk vs SW vs API vs JS — the truth meter for the next day as the new SW propagates. * Deploy warming now also hits the shell, routes (/play /account /admin), SW, version.json, word lists, and icons/logo/font — not just immutable chunks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -344,6 +344,12 @@ class WordPoolBody(BaseModel):
|
||||
word: str
|
||||
|
||||
|
||||
class ClientErrorBody(BaseModel):
|
||||
reason: str = ""
|
||||
path: str = ""
|
||||
version: str = ""
|
||||
|
||||
|
||||
class EmailStartRequest(BaseModel):
|
||||
email: str
|
||||
|
||||
@@ -919,6 +925,28 @@ def create_app() -> FastAPI:
|
||||
conn.commit()
|
||||
return {"ok": True} # always identical; dedup'd by the unique key
|
||||
|
||||
@app.post("/api/client-error")
|
||||
def record_client_error(body: ClientErrorBody, request: Request) -> dict:
|
||||
# Boot-failure seatbelt telemetry — what blank-risk looks like in the wild.
|
||||
ua = (request.headers.get("user-agent") or "")[:300]
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO client_errors (reason, path, user_agent, app_version) VALUES (?, ?, ?, ?)",
|
||||
((body.reason or "")[:300], (body.path or "")[:200], ua, (body.version or "")[:60]),
|
||||
)
|
||||
conn.execute("DELETE FROM client_errors WHERE created_at < datetime('now','-14 days')")
|
||||
conn.commit()
|
||||
return {"ok": True}
|
||||
|
||||
@app.get("/api/admin/client-errors")
|
||||
def admin_client_errors(request: Request) -> list[dict]:
|
||||
with get_conn() as conn:
|
||||
_require_admin(conn, request)
|
||||
rows = conn.execute(
|
||||
"SELECT reason, path, user_agent, created_at FROM client_errors ORDER BY id DESC LIMIT 20"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.post("/api/feedback")
|
||||
def submit_feedback(body: FeedbackBody, request: Request, background_tasks: BackgroundTasks) -> dict:
|
||||
if body.hp: # honeypot tripped → accept silently, store nothing
|
||||
|
||||
@@ -260,6 +260,15 @@ CREATE TABLE IF NOT EXISTS feedback_replies (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_feedback_replies_fid ON feedback_replies(feedback_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS client_errors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
path TEXT NOT NULL DEFAULT '',
|
||||
user_agent TEXT NOT NULL DEFAULT '',
|
||||
app_version TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS word_pool (
|
||||
word TEXT NOT NULL,
|
||||
variant TEXT NOT NULL, -- '5' | '6'
|
||||
|
||||
+2
-2
@@ -567,8 +567,8 @@ def admin_stats(conn: sqlite3.Connection, days: int = 30) -> dict:
|
||||
"daily": daily,
|
||||
# Boot-failure seatbelt signal — blank-screen risk surfacing.
|
||||
"client_errors": {
|
||||
"today": scalar("SELECT COUNT(*) FROM events WHERE kind='client_error' AND day=date('now')"),
|
||||
"window": kc.get("client_error", 0),
|
||||
"today": scalar("SELECT COUNT(*) FROM client_errors WHERE date(created_at)=date('now')"),
|
||||
"window": scalar("SELECT COUNT(*) FROM client_errors WHERE created_at>=date('now',?)", (since,)),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user