summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/pull-crash-logs227
1 files changed, 227 insertions, 0 deletions
diff --git a/scripts/pull-crash-logs b/scripts/pull-crash-logs
new file mode 100755
index 0000000..7458e18
--- /dev/null
+++ b/scripts/pull-crash-logs
@@ -0,0 +1,227 @@
+#!/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 <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"