Automating Firebase Crashlytics Analysis with Antigravity
Use Antigravity to automate Firebase Crashlytics crash analysis. Covers BigQuery export setup, AI-powered root cause detection, and a GitHub Actions workflow for daily mobile crash reporting.
Every Monday morning, the first tab I open is Crashlytics. And every Monday morning, the count is higher than it was on Friday.
If you're maintaining a mobile app solo, crash report triage is the kind of work that quietly piles up while you're focused on shipping features. Reading stack traces one by one, trying to reproduce, patching, repeat — it's draining in a way that's hard to explain until you've lived it.
I started using Antigravity to handle this differently. Instead of manually reading Crashlytics reports, I now pull the data via BigQuery, feed the structured crash context into Antigravity, and let it scan the codebase to pinpoint which method is responsible. This article walks through the exact workflow.
Setting Up Crashlytics BigQuery Export
Before anything else, enable the Crashlytics BigQuery export in your Firebase Console under Crashlytics → BigQuery export. Once enabled, crash events start flowing into a firebase_crashlytics dataset in BigQuery automatically.
Note: it takes 24–48 hours for the first data to appear after enabling. If you haven't set this up yet, do it now — it's a one-time configuration.
With the export running, here's a Python script to fetch your top 10 fatal crashes from the last 7 days:
# crash_fetcher.pyfrom google.cloud import bigqueryfrom datetime import datetime, timedeltadef get_top_crashes(project_id: str, app_id: str, days: int = 7) -> list[dict]: """ Fetch the top 10 fatal crashes from Crashlytics via BigQuery. Prerequisites: - Firebase Crashlytics BigQuery export enabled - google-cloud-bigquery installed: pip install google-cloud-bigquery - GCP auth set up: gcloud auth application-default login Args: project_id: Your Firebase project ID (e.g., "my-app-12345") app_id: Your Firebase App ID from Project Settings > General Format: "1:123456789012:ios:abcdef1234567890" days: Number of days to look back (default 7) """ client = bigquery.Client(project=project_id) # BigQuery table names replace ":" in app_id with "_" table_id = app_id.replace(":", "_") table = f"{project_id}.firebase_crashlytics.{table_id}" query = f""" SELECT issue_id, issue_title, issue_subtitle, COUNT(*) AS crash_count, ARRAY_AGG( STRUCT( application.display_version AS version, device.manufacturer AS manufacturer, device.model AS model, os.display_version AS os_version, ARRAY( SELECT CONCAT(frame.file, ':', CAST(frame.line AS STRING)) FROM UNNEST(exceptions[OFFSET(0)].frames) AS frame WHERE frame.file IS NOT NULL LIMIT 10 ) AS stack_frames ) ORDER BY event_timestamp DESC LIMIT 3 ) AS recent_examples FROM `{table}*` WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL {days} DAY)) AND FORMAT_DATE('%Y%m%d', CURRENT_DATE()) AND is_fatal = TRUE GROUP BY issue_id, issue_title, issue_subtitle ORDER BY crash_count DESC LIMIT 10 """ results = client.query(query).result() return [dict(row) for row in results]if __name__ == "__main__": crashes = get_top_crashes( project_id="your-firebase-project-id", app_id="1:000000000000:ios:aaaaaaaaaaaaaaaa" ) for crash in crashes: print(f"[{crash['crash_count']} crashes] {crash['issue_title']}") # Example output: # [247 crashes] Fatal Exception: NSInvalidArgumentException # [89 crashes] Fatal Exception: EXC_BAD_ACCESS SIGSEGV
Formatting Crash Data for Antigravity
The key insight here is that dumping raw crash output into Antigravity doesn't work as well as you'd hope. What actually helps is giving Antigravity a structured prompt that includes crash frequency, device context, and specific questions to answer.
# crash_analyzer.pyfrom crash_fetcher import get_top_crashesdef format_crash_for_ai(crash: dict) -> str: """ Format a Crashlytics crash report as a structured Antigravity prompt. Adding frequency counts, device info, and explicit questions significantly improves Antigravity's analysis accuracy. Tested with NSInvalidArgumentException, EXC_BAD_ACCESS, NSRangeException. """ examples = crash.get("recent_examples", []) # Collect unique device/OS combinations devices = list({ f"{ex['manufacturer']} {ex['model']} (OS {ex['os_version']})" for ex in examples if ex.get("manufacturer") })[:3] # Stack frames from the most recent crash stack_frames = [] if examples and examples[0].get("stack_frames"): stack_frames = examples[0]["stack_frames"] prompt = f"""## Crashlytics Crash Analysis Request**Occurrences (last 7 days):** {crash['crash_count']}**Error type:** {crash['issue_title']}**Subtitle:** {crash.get('issue_subtitle', 'N/A')}**Primary devices affected:** {', '.join(devices) if devices else 'Unknown'}**Stack trace (most recent crash, top 10 frames):**{chr(10).join(f' {i+1}. {frame}' for i, frame in enumerate(stack_frames))}**Please:**1. Identify the most likely root cause based on the stack trace2. Point to the specific file(s) and method(s) in the codebase to investigate3. Suggest three fixes in priority order4. Estimate the minimal reproduction steps""" return promptif __name__ == "__main__": crashes = get_top_crashes("your-firebase-project-id", "1:000000000000:ios:aaaaaaaaaaaaaaaa") # Output top 3 crashes as Antigravity prompts for i, crash in enumerate(crashes[:3], 1): print(f"\n{'=' * 60}\n### Crash #{i}") print(format_crash_for_ai(crash))
Paste the output into Antigravity's chat, and it will scan your codebase for relevant files before generating the analysis.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Triage crashes by real user impact (deduplicated installation_uuid), not raw count
✦Ready-to-use code to format both iOS and Android stack traces before handing them to Antigravity
✦Measured crash-free-rate and turnaround gains, plus three pitfalls of AI crash analysis
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
For the GCP_SA_KEY_JSON secret, create a service account in Google Cloud with BigQuery read access, download its JSON key, and paste the entire JSON string as a GitHub Actions secret.
A Real Fix: NSInvalidArgumentException on NSDictionary Mutation
Let me walk through what this looks like in practice. One of the most common iOS crash patterns I've seen — and fixed via this workflow — is NSInvalidArgumentException: mutating method sent to immutable object.
When Antigravity analyzes this crash and scans the codebase, it typically finds code that treats an API response dictionary as mutable when it's actually an immutable NSDictionary at runtime:
// ❌ Crashes at runtime// The JSON deserializer often returns NSDictionary (immutable)func updateUserLastSeen(response: [String: Any]) { var userInfo = response["user"] as! [String: Any] userInfo["lastSeen"] = Date().timeIntervalSince1970 // ← crash here saveToCache(userInfo)}// ✅ Antigravity's suggested fix// In Swift, assigning to a var makes a copy — explicitly cast to a value typefunc updateUserLastSeen(response: [String: Any]) { guard let rawUserInfo = response["user"] as? [String: Any] else { return } var userInfo = rawUserInfo // Swift value semantics gives us a mutable copy userInfo["lastSeen"] = Date().timeIntervalSince1970 saveToCache(userInfo)}
What I appreciate about using Antigravity here is that it doesn't just fix the one line I pointed at — it searches the entire codebase for the same pattern and flags every other instance. That kind of systematic fix is hard to do manually when you're working alone.
Prioritizing Crashes by User Impact
Crash count alone will steer you wrong. Across the wallpaper apps I've run for years, a high-count crash concentrated on a handful of users on an old OS version matters far less than a mid-count crash firing in the main onboarding flow. Without knowing how many people, on which screen, you end up fixing the wrong issue first.
Add a unique-affected-users column to the crash_fetcher.py query by deduplicating on installation_uuid:
# Add to the SELECT clause in crash_fetcher.py and reorder the results COUNT(DISTINCT installation_uuid) AS affected_users, SAFE_DIVIDE(COUNT(*), COUNT(DISTINCT installation_uuid)) AS crashes_per_user, # ...(keep the existing aggregate columns) # replace the ORDER BY after GROUP BY with: ORDER BY affected_users DESC, crash_count DESC
A high crashes_per_user value means the same users hit the crash repeatedly — a strong signal the app is effectively unusable for them. I triage on "affected users × is-this-a-core-flow" and treat raw count as secondary. You can also hand Antigravity the full top-10 list and ask it to rank by likely business impact based on which flow each crash sits in; because it reads the codebase first, the ordering is far more grounded than a plain count sort.
What Works Well and What Doesn't
After running this workflow for several months, some honest observations:
High accuracy: Crashes where the stack trace includes real file names and line numbers — NSInvalidArgumentException, NSRangeException, index-out-of-bounds, force-unwrap failures. Antigravity consistently pinpoints the right file and suggests a reasonable fix.
Lower accuracy: Crashes originating inside third-party SDK binaries, or memory corruption issues where the stack trace is mostly unresolvable symbols. In these cases, I switch to asking Antigravity to review how I'm calling the SDK rather than asking it to fix the SDK itself.
My preferred rhythm has settled into a once-a-week review on Monday mornings. I pull up the Slack notification, paste the top three crashes into Antigravity, work through the fixes, and push. It takes about 45 minutes for what used to take a full afternoon.
The numbers moved too. On my main apps, the crash-free user rate climbed from around 99.2% to the high 99.7% range, and the per-issue turnaround shrank dramatically — crash work that used to eat a whole afternoon now fits into a 45-minute Monday-morning pass over the top three. Crashes feed directly into review ratings, retention, and ultimately AdMob revenue, so cutting time-to-resolution shows up in the business numbers, not just the dashboard.
Three Pitfalls to Avoid with AI Crash Analysis
After running this workflow across several projects, three recurring mistakes stand out.
Structuring the prompt poorly: Raw Crashlytics export dumps contain device metadata, percentile stats, and formatting noise that adds context length without improving Antigravity's analysis. The format_crash_for_ai() function strips this down to what matters — error type, frequency, stack frames, and specific questions. The more focused the prompt, the more specific the answer.
Missing codebase context: Antigravity's real edge is that it searches your project while analyzing the crash. If you paste a stack trace into a fresh session without the project indexed and loaded, you get advice that reads like documentation. With your codebase active, it points to specific files and methods. Always run crash analysis from an Antigravity session where your project context is loaded.
Treating all crashes as equally urgent: Crash count alone is a rough signal. A crash hitting 200 users on a deprecated OS is different from 200 crashes on the latest iOS inside your main onboarding flow. I supplement the BigQuery query with COUNT(DISTINCT installation_uuid) to measure unique affected users, which gives a better picture of real impact. Antigravity can also help triage: paste the full top-10 list and ask it to rank by likely business impact based on which user flows each crash occurs in.
Adapting This for Android
The workflow above uses iOS examples, but Firebase Crashlytics works identically for Android — same BigQuery schema, same export process. The difference is stack trace format: Android uses Java or Kotlin class paths rather than file-line notation.
// Android: filter system frames before passing to Antigravity// Crashlytics BigQuery returns stack frames as "com.example.Class.method(File.kt:line)"// Keep only app-owned frames (skip android.*, java.*, kotlin.*, com.google.*)fun formatAndroidStack(frames: List<String>): String { val systemPrefixes = listOf("android.", "java.", "kotlin.", "okhttp3.", "com.google.") return frames .filter { frame -> systemPrefixes.none { frame.startsWith(it) } } .take(8) .mapIndexed { i, frame -> " ${i + 1}. $frame" } .joinToString("\n")}
Kotlin-specific crash patterns — NPE on nullable returns, CancellationException from coroutines, IllegalStateException from LiveData observation outside lifecycle — are cases where Antigravity's Kotlin understanding produces reliable suggestions. Analysis quality between iOS and Android is comparable in my experience.
Get Started Today
Enable the Crashlytics BigQuery export if you haven't already — that's the one non-negotiable prerequisite. Once the first 24 hours of data comes in, run crash_fetcher.py manually and paste the output into Antigravity. You don't need the GitHub Actions automation to get value from this workflow.
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.