diff --git a/Scripts/analyze_crashlytics.py b/Scripts/analyze_crashlytics.py new file mode 100644 index 0000000..90c3e3f --- /dev/null +++ b/Scripts/analyze_crashlytics.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Crashlytics report for Portfolio Journal via the BigQuery export. + +Setup (one-time, already done 2026-07-13): + - Firebase console → project portfoliojournal-ef2d7 → Integrations → BigQuery: + enable the Crashlytics export (requires Blaze plan). + - Grant the service account sirius@sirius-vazquez.iam BigQuery Data Viewer + + Job User on project portfoliojournal-ef2d7. + +Creds come from `pass show google/sirius-service-account`. The export table is +created on the first crash after enabling, named like +`firebase_crashlytics.com_alexandrevazquez_PortfolioJournal_IOS`. + +Usage: python3 Scripts/analyze_crashlytics.py [days] # default 30 +""" +import subprocess, json, sys, urllib.request, urllib.error + +PROJECT = "portfoliojournal-ef2d7" +DATASET = "firebase_crashlytics" +DAYS = int(sys.argv[1]) if len(sys.argv) > 1 else 30 + + +def creds_token(): + raw = subprocess.run(["pass", "show", "google/sirius-service-account"], + capture_output=True, text=True).stdout + try: + info = json.loads(raw) + except Exception: + out, ins, esc = [], False, False + for c in raw: + if esc: out.append(c); esc = False + elif c == "\\": out.append(c); esc = True + elif c == '"': ins = not ins; out.append(c) + elif ins and c == "\n": out.append("\\n") + elif ins and c == "\r": pass + else: out.append(c) + info = json.loads("".join(out)) + from google.oauth2 import service_account + import google.auth.transport.requests + c = service_account.Credentials.from_service_account_info( + info, scopes=["https://www.googleapis.com/auth/cloud-platform"]) + c.refresh(google.auth.transport.requests.Request()) + return c.token + + +def api(token, path): + req = urllib.request.Request( + f"https://bigquery.googleapis.com/bigquery/v2/projects/{PROJECT}{path}", + headers={"Authorization": f"Bearer {token}"}) + return json.load(urllib.request.urlopen(req)) + + +def query(token, sql): + body = json.dumps({"query": sql, "useLegacySql": False, "timeoutMs": 60000}).encode() + req = urllib.request.Request( + f"https://bigquery.googleapis.com/bigquery/v2/projects/{PROJECT}/queries", + data=body, headers={"Authorization": f"Bearer {token}", + "Content-Type": "application/json"}) + d = json.load(urllib.request.urlopen(req)) + cols = [f["name"] for f in d.get("schema", {}).get("fields", [])] + rows = [[c.get("v") for c in r["f"]] for r in d.get("rows", [])] + return cols, rows + + +def main(): + token = creds_token() + try: + td = api(token, f"/datasets/{DATASET}/tables?maxResults=100") + except urllib.error.HTTPError as e: + print(f"No access to {DATASET} (HTTP {e.code}).") + return + tables = [t["tableReference"]["tableId"] for t in td.get("tables", [])] + crash_tables = [t for t in tables if "IOS" in t or "ios" in t.lower()] + if not crash_tables: + print("No Crashlytics export tables yet — no crashes exported since the " + "export was enabled. (Good news.)") + return + table = crash_tables[0] + print(f"== Crashlytics {table} — últimos {DAYS}d ==\n") + + sql = f""" + SELECT + issue_title, issue_subtitle, + application.display_version AS version, + COUNT(*) AS events, + COUNT(DISTINCT installation_uuid) AS users, + COUNTIF(error_type = 'FATAL') AS fatal + FROM `{PROJECT}.{DATASET}.{table}` + WHERE event_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {DAYS} DAY) + GROUP BY issue_title, issue_subtitle, version + ORDER BY events DESC + LIMIT 25 + """ + cols, rows = query(token, sql) + if not rows: + print("Sin crashes en el periodo.") + return + for r in rows: + title, sub, ver, events, users, fatal = r + print(f"[{ver}] {title} — {sub}") + print(f" {events} eventos · {users} usuarios · {fatal} fatales\n") + + +if __name__ == "__main__": + main()