#!/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()