Add Crashlytics BigQuery report script
Consulta el export de Crashlytics en BigQuery (dataset firebase_crashlytics, proyecto portfoliojournal-ef2d7) con el service account de pass. Top crashes por versión/build. Setup documentado en el docstring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user