●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Automating Firebase Crashlytics Analysis with Gemini API — A Real-World Implementation from an Indie App
A real-world implementation record of automating Firebase Crashlytics log analysis with Gemini API, validated during development of an indie wallpaper app (v2.1.0). Includes Before/After code for a RecyclerView crash fix and a production cost breakdown.
Twenty-eight days. Over fifty users hitting the same crash. I noticed it one evening while scrolling through the Firebase Crashlytics dashboard.
When you run a wallpaper app solo, crash triage tends to get pushed aside by everything else. Just after shipping the Android v2.0.0 update of my wallpaper app, Beautiful HD Wallpapers, a java.lang.IndexOutOfBoundsException: Inconsistent detected. Invalid view holder adapter position entry started accumulating quietly in the Issues tab. Something was wrong with the RecyclerView adapter. But what, exactly, was not visible from the stack trace alone.
This article is the record of building a Gemini API-powered pipeline to analyze that crash and actually identify the root cause. All code here is tested and working. I have structured this so you can build the Firebase Crashlytics REST API + Gemini API analysis system from scratch.
Why the Crashlytics Dashboard Is Not Enough
Firebase Crashlytics is a strong tool. Crash counts, affected users, stack traces, device model and OS version distributions — all in one place. What it does not tell you is why the crash is happening.
A RecyclerView IndexOutOfBoundsException is particularly ambiguous. There are multiple plausible causes: async data updates racing with the UI thread, mixing notifyDataSetChanged() with individual notify calls, mutating the data list from a background thread, or the adapter holding a reference to the same list object that is being modified externally. Any of these could be the culprit, and testing each hypothesis takes time.
For an indie developer, debugging time directly impacts the release cycle. Beyond the technical diagnosis, deciding which crash to fix first is also harder than it looks. High occurrence count does not always mean the highest user impact. Sometimes the crashes that drive churn are the ones that happen during a specific user action, not the most frequent ones. I wanted to automate some of that prioritization too.
So I tried passing crash logs directly to Gemini API and getting structured analysis back. The result: Gemini 2.5 Pro identified a missing defensive copy as the root cause, with working fix code, faster than I could have narrowed it down through manual hypothesis testing.
Setting Up Firebase Crashlytics REST API Access
First, I needed a way to pull crash logs programmatically. Crashlytics is often treated as a dashboard-only tool, but you can access issues via the Firebase Admin SDK and REST API.
In the Firebase console, go to Project Settings, then Service Accounts, and generate a new private key. Store the downloaded JSON file securely. Register it as a GitHub Actions Secret for the automation pipeline, and make sure to add the file path to your .gitignore so it never ends up in version control.
from google.oauth2 import service_accountimport google.auth.transport.requestsimport requestsimport jsonSCOPES = ["https://www.googleapis.com/auth/cloud-platform"]SERVICE_ACCOUNT_FILE = "serviceAccountKey.json"credentials = service_account.Credentials.from_service_account_file( SERVICE_ACCOUNT_FILE, scopes=SCOPES)def get_access_token(credentials) -> str: if not credentials.valid: credentials.refresh(google.auth.transport.requests.Request()) return credentials.token
For fetching issues, combining occurrenceCount and affectedUsersCount filters works best in practice. A crash with thousands of occurrences but only one affected device model is lower priority than a crash with fifty occurrences spread across all OS versions.
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
✦Get a working pipeline today that sends Firebase Crashlytics logs to Gemini API and auto-generates root cause analysis with fix suggestions
✦See the actual Before/After Kotlin code for a RecyclerView IndexOutOfBoundsException fixed via defensive copy — and how accurately Gemini 2.5 Pro identified the root cause
✦Learn the concrete cost breakdown and Flash vs Pro decision criteria for running this analysis pipeline in production on an app with 3M monthly active users
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.
The key decision here is to get structured data back from Gemini, not free-form text. JSON output means the analysis results flow directly into GitHub Issues, Slack, or any downstream system without additional parsing. When you use natural language output, Gemini's response style can shift across versions, and your parsing logic breaks. Defining a Pydantic schema and setting response_mime_type="application/json" guarantees a consistent format every time.
from pydantic import BaseModel, Fieldfrom typing import Optionalimport google.generativeai as genaiclass CrashAnalysis(BaseModel): root_cause: str = Field(description="Root cause of the crash (1-3 concise sentences)") affected_components: list[str] = Field(description="Components involved in the crash") fix_approach: str = Field(description="How to fix it and why (3-5 sentences)") code_fix_snippet: str = Field(description="Working Kotlin code demonstrating the fix") confidence: str = Field(description="Analysis confidence: high / medium / low") additional_info_needed: Optional[str] = Field( default=None, description="What context would improve accuracy" )genai.configure(api_key="YOUR_GEMINI_API_KEY")model = genai.GenerativeModel("gemini-2.5-pro")
How I Iterated on the Prompt
My first prompt version just passed the stack trace. The response was generic: "verify that notifyDataSetChanged() is called on the main thread." Technically accurate but not actionable for identifying the specific fix location.
The second version added the OS version distribution. Because the crash was spread across Android 10, 11, and 12 with similar proportions, Gemini started narrowing toward an architecture problem rather than an OS-specific issue. The response became more specific, but still fell short of pinpointing the defensive copy problem.
The third version included a summary of WallpaperGridFragment.onLoadFinished() — the method responsible for triggering the adapter update. With that context, Gemini immediately identified the missing defensive copy as the root cause. This iteration process taught me the single most important lesson: pass code context alongside the stack trace. Even 50–100 lines from the crash location makes a meaningful difference.
def analyze_crash(issue_data: dict, code_context: str = "") -> CrashAnalysis: stack_trace = issue_data.get("stackTrace", "Not available") exception_type = issue_data.get("exceptionType", "Unknown") occurrence_count = issue_data.get("occurrenceCount", 0) affected_users = issue_data.get("affectedUsersCount", 0) os_versions = issue_data.get("affectedOsVersions", []) context_block = "" if code_context: context_block = f"\n## Relevant Code\n```kotlin\n{code_context}\n```\n" prompt = f"""You are an expert Android developer. Analyze this Crashlytics reportand provide root cause diagnosis with a Kotlin fix example.Exception: {exception_type}Occurrences: {occurrence_count}Affected users: {affected_users}OS versions: {', '.join(str(v) for v in os_versions)}Stack trace:{stack_trace}{context_block}Provide a working Kotlin code example, not pseudocode.""" response = model.generate_content( prompt, generation_config=genai.GenerationConfig( response_mime_type="application/json", response_schema=CrashAnalysis.model_json_schema(), ) ) return CrashAnalysis(**json.loads(response.text))
The RecyclerView Case — What Gemini Actually Said
Here is the real Crashlytics data I fed into the pipeline, and Gemini 2.5 Pro's response. The stack trace is based on the actual crash, lightly simplified.
The OS version breakdown was: Android 10 (38%), Android 11 (29%), Android 12 (22%), other (11%). When Gemini saw this even distribution across OS versions, it correctly concluded this was not a compatibility issue — it was an architecture issue.
Gemini 2.5 Pro returned confidence: "high" and diagnosed the root cause as: "WallpaperAdapter holds a direct reference to a list that is being mutated from a background thread. The defensive copy is missing — the adapter stores the reference passed in rather than creating its own copy, so when the source list is modified externally, the adapter's internal state becomes inconsistent with the ViewHolder positions."
I checked the code. The diagnosis was accurate.
// Before: Holding a direct reference — the crash causeclass WallpaperAdapter : RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() { private var wallpapers: MutableList<Wallpaper> = mutableListOf() fun updateWallpapers(newData: List<Wallpaper>) { wallpapers.clear() wallpapers.addAll(newData) // Background thread can modify the same source list notifyDataSetChanged() } override fun getItemCount() = wallpapers.size}
// After: Defensive copy — fixed in v2.1.0class WallpaperAdapter : RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() { private var wallpapers: List<Wallpaper> = emptyList() fun updateWallpapers(newData: List<Wallpaper>) { wallpapers = newData.toList() // New list — immune to external mutations notifyDataSetChanged() } override fun getItemCount() = wallpapers.size}
The more robust long-term fix is migrating to ListAdapter + DiffUtil, where submitList() handles thread safety internally. After shipping v2.1.0 with both the defensive copy fix and the ListAdapter migration, the crash that had hit 50+ users over 28 days disappeared from Crashlytics.
Second Case — Glide 5.0.5 + AGP 9.x Java 8 Desugaring
Another crash from the same v2.1.0 development cycle. Symptom: java.lang.NoClassDefFoundError: Failed resolution of: Ljava/util/function/Supplier — only on Android 6.0.1 (API 23), not reproducible on Android 10 and above.
The OS version distribution data was the key signal here. The crash was exclusive to Android 6.0.1, which told Gemini immediately: compatibility problem with an older OS, not a code logic issue. Gemini connected "Glide 5.0.5 uses Java 8 API + minSdk=23 + no desugaring configured" into a single root cause diagnosis: coreLibraryDesugaring was not enabled, and AGP 9.x changed the default desugaring behavior, breaking a configuration that previously worked.
The fix was adding one line to the build configuration.
Every Android 6.0.1 user's crash was gone after deploying this. What impressed me was how Gemini connected the Glide version, the AGP version, and the OS version distribution into a single coherent diagnosis — something that would have taken me considerably longer to piece together manually.
Production Pipeline Architecture
Running analysis on individual crashes manually is useful for validating the approach, but the real value is automation. My production setup runs on GitHub Actions and follows this flow: Firebase Crashlytics REST API pulls open issues daily at 09:00 JST, a Python filter selects issues above the severity threshold, Gemini analyzes each one with the appropriate model tier, and the results are filed as GitHub Issues with Slack notifications.
Using a two-tier model approach keeps costs down while directing the most complex crashes to the most capable model.
def should_analyze(issue: dict) -> tuple[bool, str]: occurrence_count = issue.get("occurrenceCount", 0) affected_users = issue.get("affectedUsersCount", 0) is_regression = issue.get("isRegression", False) if affected_users >= 20 or occurrence_count >= 50: return True, "gemini-2.5-pro" # Critical: full analysis if occurrence_count >= 10 or affected_users >= 5: return True, "gemini-2.5-flash" # Moderate: initial screening if is_regression: return True, "gemini-2.5-pro" # Regressions always get Pro return False, ""
With this filtering on an app at 3M monthly active users, between three and seven issues qualify for analysis per day. The workflow runs in around two minutes.
Slack Notification
Routing the analysis results to Slack makes it harder to miss new crashes.
Per analysis, token usage is roughly 2,000–4,000 tokens input (stack trace, OS distribution, optional code context) and 500–1,200 tokens output (structured JSON).
At May 2026 pricing, Gemini 2.5 Flash costs approximately $0.001 per analysis, and Gemini 2.5 Pro costs approximately $0.010 per analysis. For five analyses per day — three Flash, two Pro — the monthly total runs around $0.69. That is well under the threshold where cost becomes a meaningful consideration.
Where Flash falls short is on architecture-specific crashes. For the RecyclerView defensive copy issue, Flash gave a generic thread-safety suggestion. Pro connected the dots to the specific missing copy. My current heuristic is to use Flash for initial screening on moderate crashes, then escalate to Pro when Flash returns confidence: "low" or when the issue affects twenty or more users. That two-stage approach captures most of the cost savings while preserving diagnostic quality where it matters most.
Limitations and Compensating Strategies
Gemini performs well on known patterns — RecyclerView inconsistencies, NullPointerException, library compatibility issues identified through OS version distribution. It struggles with crashes rooted in app-specific business logic where the code is not included in the prompt, race conditions where the stack trace alone lacks timing information, and issues deep in custom or proprietary libraries.
The workaround for hard cases is to include the full relevant class in the prompt — 300 to 500 lines fits comfortably in Gemini 2.5 Pro's context window. I also use confidence: "low" as a routing signal: when Gemini is uncertain, the issue goes into a manual review queue rather than auto-filing as a GitHub Issue. The model that works is not full automation but "Gemini screens first, human reviews the hard cases." That hybrid keeps the time savings without sacrificing reliability.
Looking ahead, I plan to integrate this with Google Play Console's Android Vitals section. When the Crash-free users rate drops below a threshold, triggering the GitHub Actions workflow immediately — rather than waiting for the next daily run — would further shorten the time from problem detection to diagnosis.
Next Step: Try One Real Crash Today
The simplest starting point: open your Crashlytics dashboard, find the open issue with the most affected users, copy the stack trace and OS version breakdown, and pass it to Gemini using the prompt format from this article. Verify the diagnosis quality on real data from your own app before building the automation pipeline.
The hardest part of crash work is that you often notice a problem only after the affected users are already gone. Handing the triage to Gemini shortens that gap structurally: instead of staring at a dashboard assembling hypotheses, you get a structured diagnosis as the first move. Verify it on one real crash, and if it holds up, widen it into the semi-automated flow. I hope it is useful for your stack too.
Share
Thank You for Reading
Gemini 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.