diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-25 05:00:31 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-25 05:00:31 +0000 |
| commit | ee3d0c200797d858e016aefabc8e62e1bd524b4f (patch) | |
| tree | 9eb27b9c8caeb55e28d66a5718664995b1367a23 /scripts | |
| parent | 75ec688eb2d2754b77ff18946412bca434eb503a (diff) | |
chore: add .gitignore, pull-crash-logs script, updated agent permissions
- .gitignore: exclude agent artifacts (.claudomator-env, .agent-home/, crash-logs/)
- scripts/pull-crash-logs: download Crashlytics crash reports via Firebase CLI
- .claude/settings.local.json: add Android SDK/emulator/adb permission rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/pull-crash-logs | 227 |
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" |
