#!/usr/bin/env bash # pull-crash-logs — Download recent Crashlytics crash reports via Firebase CLI # # Usage: ./scripts/pull-crash-logs [--limit N] [--app-id APP_ID] [--out DIR] # Example: ./scripts/pull-crash-logs --limit 20 --out /tmp/crashes # # Requirements: # - firebase-tools installed and authenticated (firebase login) # - FIREBASE_PROJECT set in env, or edit PROJECT_ID below # - firebase-admin or curl + gcloud token for REST fallback set -euo pipefail # ── Config ────────────────────────────────────────────────────────────────── PROJECT_ID="${FIREBASE_PROJECT:-nav-test-c2872}" LIMIT=10 OUT_DIR="$(pwd)/crash-logs" # ── Arg parsing ───────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --limit) LIMIT="$2"; shift 2 ;; --app-id) APP_ID="$2"; shift 2 ;; --out) OUT_DIR="$2"; shift 2 ;; *) echo "Unknown arg: $1" >&2; exit 1 ;; esac done mkdir -p "$OUT_DIR" echo "=== Firebase Crashlytics crash log pull ===" echo " Project : $PROJECT_ID" echo " Limit : $LIMIT" echo " Output : $OUT_DIR" echo "" # ── Check tooling ──────────────────────────────────────────────────────────── require() { command -v "$1" &>/dev/null || { echo "ERROR: '$1' not found. Install it first." >&2; exit 1; }; } # Prefer firebase CLI → fall back to REST via gcloud token if command -v firebase &>/dev/null; then MODE="firebase-cli" elif command -v gcloud &>/dev/null; then MODE="rest" else echo "ERROR: neither firebase-tools nor gcloud found." >&2 echo " Install firebase-tools: npm install -g firebase-tools" >&2 echo " Or gcloud SDK: https://cloud.google.com/sdk/docs/install" >&2 exit 1 fi echo "Mode: $MODE" echo "" # ── Pull via firebase CLI ──────────────────────────────────────────────────── if [[ "$MODE" == "firebase-cli" ]]; then # List Android apps in project to find app ID if not provided if [[ -z "${APP_ID:-}" ]]; then echo "Fetching app list..." APP_ID=$(firebase apps:list --project "$PROJECT_ID" --json 2>/dev/null \ | python3 -c " import json, sys apps = json.load(sys.stdin) android = [a for a in apps.get('result', []) if a.get('platform') == 'ANDROID'] if not android: print('', end='') else: print(android[0]['appId'], end='') " 2>/dev/null || true) if [[ -z "$APP_ID" ]]; then echo "WARNING: Could not auto-detect app ID. Set --app-id manually." >&2 echo " Run: firebase apps:list --project $PROJECT_ID" >&2 else echo " Auto-detected app ID: $APP_ID" fi fi # Crashlytics data via firebase crashlytics:symbols or REST # firebase CLI doesn't expose crash issue listing directly; # use the REST API path below with a gcloud-obtained token. echo "" echo "NOTE: firebase CLI does not expose crash issue listing." echo "Switching to REST API (requires gcloud auth)..." MODE="rest" fi # ── Pull via Crashlytics REST API ──────────────────────────────────────────── if [[ "$MODE" == "rest" ]]; then require curl require python3 # Try gcloud first, then fall back to firebase-tools stored token TOKEN=$(gcloud auth print-access-token 2>/dev/null) || TOKEN="" if [[ -z "$TOKEN" ]]; then FIREBASE_CONFIG="$HOME/.config/configstore/firebase-tools.json" if [[ -f "$FIREBASE_CONFIG" ]]; then TOKEN=$(python3 -c " import json, sys with open('$FIREBASE_CONFIG') as f: d = json.load(f) tokens = d.get('tokens', {}) print(tokens.get('access_token', ''), end='') " 2>/dev/null) fi fi if [[ -z "$TOKEN" ]]; then echo "ERROR: Not authenticated. Run: firebase login --no-localhost" >&2 exit 1 fi if [[ -z "${APP_ID:-}" ]]; then echo "Fetching app list from Firebase Management API..." APPS_JSON=$(curl -s \ -H "Authorization: Bearer $TOKEN" \ "https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/androidApps") APP_ID=$(echo "$APPS_JSON" | python3 -c " import json, sys data = json.load(sys.stdin) apps = data.get('apps', []) if not apps: print('', end='') else: print(apps[0]['appId'], end='') " 2>/dev/null || true) if [[ -z "$APP_ID" ]]; then echo "ERROR: No Android apps found in project $PROJECT_ID" >&2 echo "Response: $APPS_JSON" >&2 exit 1 fi echo " Auto-detected app ID: $APP_ID" fi # Crashlytics issue list via v1alpha1 API ISSUES_URL="https://firebasecrashlytics.googleapis.com/v1alpha1/projects/${PROJECT_ID}/apps/${APP_ID}/issues?pageSize=${LIMIT}&orderBy=eventCount+desc" echo "" echo "Fetching top $LIMIT crash issues..." ISSUES_FILE="$OUT_DIR/issues.json" curl -s \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ "$ISSUES_URL" \ -o "$ISSUES_FILE" ISSUE_COUNT=$(python3 -c " import json, sys with open('$ISSUES_FILE') as f: data = json.load(f) issues = data.get('issues', []) print(len(issues)) " 2>/dev/null || echo "0") echo " Found $ISSUE_COUNT issues. Saved to $ISSUES_FILE" echo "" # Pretty-print summary python3 -c " import json, sys with open('$ISSUES_FILE') as f: data = json.load(f) issues = data.get('issues', []) if not issues: print('No crash issues found (or API returned an error).') print('Raw response:') print(json.dumps(data, indent=2)) sys.exit(0) print(f'{'ID':<30} {'EVENTS':>8} {'USERS':>7} TITLE') print('-' * 90) for issue in issues: issue_id = issue.get('issueId', 'N/A')[:30] title = issue.get('title', 'N/A')[:50] event_cnt = issue.get('eventCount', '?') user_cnt = issue.get('impactedUsersCount', '?') print(f'{issue_id:<30} {str(event_cnt):>8} {str(user_cnt):>7} {title}') " # Pull individual events for each issue echo "" echo "Fetching events per issue..." python3 -c " import json, sys, subprocess, os with open('$ISSUES_FILE') as f: data = json.load(f) issues = data.get('issues', []) token = '$TOKEN' project = '$PROJECT_ID' app_id = '$APP_ID' out_dir = '$OUT_DIR' for issue in issues[:5]: # cap at 5 issues for detail pull issue_id = issue.get('issueId', '') if not issue_id: continue url = (f'https://firebasecrashlytics.googleapis.com/v1alpha1/' f'projects/{project}/apps/{app_id}/issues/{issue_id}/events?pageSize=5') out_file = os.path.join(out_dir, f'events_{issue_id[:20]}.json') result = subprocess.run( ['curl', '-s', '-H', f'Authorization: Bearer {token}', url], capture_output=True, text=True ) with open(out_file, 'w') as f: f.write(result.stdout) try: events = json.loads(result.stdout).get('events', []) print(f' {issue_id[:30]}: {len(events)} events → {out_file}') except Exception: print(f' {issue_id[:30]}: parse error → {out_file}') " fi echo "" echo "Done. Files written to: $OUT_DIR" ls -lh "$OUT_DIR"