●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
11 Days in Crashlytics: A Claude Code Debug Loop Across Two Android Wallpaper Apps
After shipping Beautiful Wallpapers v2.0.0 and Ukiyo-e Wallpapers v1.7.0 in early May, Crashlytics and Play Console threw more than 30 new issues at me in 11 days. This is the operations log of how I drove the fix list down to v2.1.1 / v1.8.1 using Claude Code as a triage partner.
On the evening of May 19, after I had just about finished writing the v2.1.1 hotfix for one of my Android wallpaper apps, I checked Play Vitals while making coffee. The "user-perceived crash rate" had climbed to 1.08%. Google's "bad behaviour" threshold sits at 1.09%. 0.01% to spare.
The two flagship wallpaper apps (Beautiful 4K/HDR Wallpapers and Ukiyo-e Wallpapers) had just gone through a major refresh in early May (v2.0.0 / v1.7.0), and Crashlytics afterwards looked like a fireworks display of new issues. Over 11 days I worked through 30+ stack traces, committed fixes for 13 of them, and parked 6 as "won't fix" because they live inside the OS or third-party SDKs. Solo development means the same person does the triage, the implementation, and the call on whether to ship. This article is the working log of that period, told through how I used Claude Code as a triage partner.
If you keep Crashlytics open every morning but feel a quiet pile of "I'll get to it later" stack traces accumulating, or you've ever scrambled a hotfix because Play Vitals nearly tripped on you, this is for you.
The 11-day map — what got fixed, what didn't
Here's the high-level path from v2.0.0 release through v2.1.1 submission.
5/12 (Day 1) — Triaged 12 new Crashlytics issues post-v2.0.0. Found RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline IOOBE climbing through 50+ users over 28 days
5/13 (Day 2) — Caught j.u.function.Supplier NoClassDefFoundError killing every Android 6.0.1 user 3 seconds into the app. Single-line fix: enable coreLibraryDesugaringEnabled
5/13 (Day 2, afternoon) — Untangled Gallery.onResume's nested back-press logic, where rewarded-ad inducement was always beating regular interstitials on back
5/14–18 (Day 3–7) — Shipped v2.1.0, observation window. BW-015 (PageJumpSlider NPE) climbed from 1 user to 9. BW-009 (R8 strict mode + density split stripping a drawable) kept firing
5/19 (Day 8) — The 1.08% spike. Killed ViewPager2 internal RecyclerView fling-time IOOBE (BW-017) with two lines of setItemAnimator(null) + setHasFixedSize(true). Replaced a 220KB JPG Glide placeholder with an XML ColorDrawable to kill the synchronous decode ANR (BW-027)
5/22 (Day 11) — Reproduced an API 37 emulator silent UX bug where Material You's overlay regeneration recreated CategoryActivity to a blank screen, and added graceful finish() fallback
5/25 (Day 14) — Re-audited Play Console + Crashlytics end to end. Pulled the hotfix forward instead of waiting for the July targetSdk 37 bundle
5/27 (Today) — Submitted v2.1.1 / v1.8.1 for Play Store review
In that period I started 27 Claude Code sessions, 30 minutes to 2 hours each. One discipline I learned: don't mix "writing code" and "reading Crashlytics" inside the same session. More on that later.
How I shape a stack trace before showing it to Claude Code
A Crashlytics or Play Console crash detail page gives you 200–400 lines of raw stack. Pasting that wholesale burns context and produces vague hypotheses. I've settled on this 4-block format before I ever hit enter:
## Environment- App: Beautiful Wallpapers v2.1.0 (vc=49)- Device: Sharp AQUOS wish4 (DOCOMO SH-52E)- OS: Android 15 (SDK 35)- Impact: 117 events / 99 users in 7 days (74.4% of all crashes)## Top 5 frames (only the ones I think matter)RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline (line 7160)LinearLayoutManager.scrollByRecyclerView.scrollHorizontallyByRecyclerView.scrollStepRecyclerView$ViewFlinger.run## MessageInconsistency detected. Invalid item position 1(offset:1).state:1030## My matching row in the known-patterns lookup- "Fling-time IOOBE inside ViewPager2's internal RV"- Candidate fixes: setItemAnimator(null), setHasFixedSize(true), kill Predictive Animation## What I think I'd touchIn ThumbnailActivity, after mViewPager.setAdapter(...), grabmViewPager.getChildAt(0) and tune the internal RecyclerView.Apply preventively in Gallery too.
Writing my own 3-line hypothesis before pasting the trace tilts Claude Code's output away from "80% explanation, 20% code" toward "here is the patch." In maintenance work, the latter is what I need.
With that input shape, Claude Code reliably comes back with something like this:
That is the exact patch I shipped in v2.1.1. Before: 117 events / 99 users in 7 days, 74.4% of total crashes. After: projected crash-free user rate to climb from 97.98% into >99% territory.
setItemAnimator(null) has side effects. In my case, the PageJumpSlider drag started calling setCurrentItem(largePosition, false) against a LayoutManager that now skipped remeasurement, and the wallpaper would silently stop changing (BW-032). Asking Claude Code "what changes when I disable ItemAnimator on a ViewPager2's internal RecyclerView, any side effects?" up front would have caught this in advance.
✦
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
✦A repeatable loop for pasting raw Crashlytics stack traces into Claude Code and grinding ViewPager2 internal RecyclerView IndexOutOfBoundsException and Glide synchronous-decode ANRs down to reproducible, fixable patterns
✦How to decide what goes into a hotfix and what gets deferred when Play Vitals user-perceived crash rate sits 0.01% below the 1.09% threshold — setItemAnimator(null), R8 keep.xml, and ColorDrawable placeholder choices explained from production data
✦A known-bugs-tracker pattern for triaging 100+ Crashlytics issues per month solo, plus the 21÷affected-users rule that tells you the date you are actually allowed to call a fix confirmed
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.
Keep your known-bugs tracker in a shape Claude can actually use
I maintain one known-bugs-tracker.md per app. The file is plain Markdown, but it is structured for one purpose: when I paste a new stack trace, I want Claude Code to identify the match in seconds.
### v2.1.0 (live, vc=49) known bugs| ID | Symptom | Stack origin | Status | Impact | Fix ||---|---|---|---|---|---|| BW-017 | RecyclerView IOOBE during fling | `RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline` → `RecyclerView$ViewFlinger.run` | Fixed in v2.1.1 | 117 events / 99 users in 7 days, 74.4% of v2.1.0 crashes | setItemAnimator(null) + setHasFixedSize(true) || BW-027 | Glide placeholder synchronous decode ANR | `libhwui ImageDecoder_nDecodeBitmap` → `Glide.SingleRequest.getPlaceholderDrawable` | Fixed in v2.1.1 | 1 event / 1 user / Sharp AQUOS SX4 | Swap 220KB JPG placeholder for XML ColorDrawable |
The second half of the file is a quick-lookup table that maps Crashlytics-style stack signature substrings to the bug ID.
| Crashlytics title fragment | Bug ID | Note ||---|---|---|| `RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline` IOOBE | BW-002 / BW-007 / BW-017 / UE-004 / UE-009 | ViewPager2 internal RV fling. setItemAnimator(null) applied in v2.1.1 / v1.8.1 || `Glide.SingleRequest.getPlaceholderDrawable` + ANR | BW-027 / UE-016 | JPG placeholder sync-decode. Swapped to XML ColorDrawable in v2.1.1 / v1.8.1 || `[libart.so] art::DumpNativeStack` ANR | UE-013 | ART runtime stack dump. Low-DAU statistical noise |
When a new Crashlytics notification arrives, I text-search the lookup first. Hit → known. No hit → spin up Claude Code, generate a hypothesis, and append a new row. Roughly half of the 30 issues I saw in 11 days were already known. Of the unknown half, only about half could be fixed in my code. The rest were OS or SDK problems I marked "won't fix." That coarse 4-way split is what keeps the morning Crashlytics check down to 5 minutes most days.
Be willing to mark issues "not mine to fix"
Six of the new bugs across this 11-day stretch are categorically not fixable inside my code. They show up with a "new" flag every morning, which is why writing down the reason matters. Here is the list, organized by why I gave up on them.
LinkedHashMap$LinkedHashIterator.nextNode + zzbiv (play-services-ads 25.2.0) — concurrency bug inside the Google Mobile Ads SDK. Nothing I do at app level changes it. SDK update only
com.applovin.impl.p.onCreate NPE / com.applovin.impl.g0.a NPE — null reference inside AppLovin SDK. Obfuscated, unreachable from my code
GoogleCertificatesRslt SecurityException — Google Play Services can't verify the apk signature (devices without GMS, rooted, or sideloaded from a third-party store)
[libc.so] __ioctl ANR with "slow binder call" tag — display HAL latency on specific Sharp AQUOS handsets. OS-side
BaseDexClassLoader.findClass com.rini.xanrn.Wish — repackaged/modded APK. Affects malware users, not real customers
The point isn't the list, it's writing the "won't fix" call down in known-bugs-tracker.md with the reason, so the next time the same stack signature shows up, past me has already done the diagnosis. The status legend at the top of my tracker reserves a row called "out of scope (external)" precisely so I can park these without guilt.
Calling something "out of scope" by feel drifts over time, so I reduced it to three tests. My own package name never appears anywhere in the trace. The same signature shows up at roughly the same rate across other apps and other versions. Stratifying by SDK or OS version collapses it onto one cluster. If none of the three hold, it's probably my code after all. Time spent inside an SDK's null reference isn't time spent improving anything I can actually touch.
What I put into v2.1.1 versus what I held back
When the 1.08% reading appeared on 5/19, I'd been planning to roll v2.1.1 into the July targetSdk 37 release. I scrapped that plan. With Claude Code I drafted a release-plan-v2.1.1-v1.8.1.md and listed every candidate fix with a "ship now / defer" call.
In v2.1.1 / v1.8.1, I shipped these seven:
BW-017 / UE-009 — ViewPager2 fling-time IOOBE. setItemAnimator(null) + setHasFixedSize(true) applied in ThumbnailActivity and preventively in Gallery
BW-027 / UE-016 — Glide placeholder ANR. 220KB JPG (R.drawable.background_silver) swapped for R.drawable.background_silver_placeholder (XML ColorDrawable). Glide listener and .into() wrapped in try-catch
BW-015 / UE-007 — PageJumpSlider.hide() animation-end NPE. Null check on mContainer in onAnimationEnd
BW-009 / UE-021 — R8 strict mode + density split stripping background_silver.jpg. Added res/raw/keep.xml with tools:keep="@drawable/background_silver,@drawable/background_silver_placeholder"
BW-030 / UE-017 — API 37 / Android 14+ Material You overlay regeneration making CategoryActivity blank. Detect empty + unrecoverable image list, then finish() back to ThumbnailActivity (silent graceful exit)
UE-015 — WallpaperPagerAdapter.onBindViewHolder assigned mCurrentHolder = holder directly, so off-screen pre-bind overwrote the current holder and the "set as wallpaper" action set the neighbour. Centralized mCurrentHolder updates in onPageSelected → setCurrentHolder()
Liftoff (Vungle) early-reward defense — AdMobRewardedAd / AdMobRewardedIntersAd now reject reward callbacks under MIN_WATCH_DURATION_MS = 3_000L with Suspicious reward callback after only NNms logged
Held back for v2.2.0 or until SDK updates arrive:
BW-003 / BW-023 — startup MessageQueue ANR. Needs async ad SDK init; too risky for a hotfix
BW-005 — Mintegral SDK lock contention. Wait for SDK update
BW-016 / BW-019 — 1 user / 1 event scatter. Let signal accumulate
The discipline of "don't cram everything into a hotfix" matters more than which specific items you defer. I asked Claude Code to take the candidate list and split it into ship-now versus defer with reasons. Outside review, even from an LLM, prevents the single-developer bias of "let me just slip this one more thing in."
Treat ANRs as a separate loop from crashes
The toughest Crashlytics class is ANRs. Stack traces are heavy with native frames — art_jni_trampoline, libhwui, libc syscall — and the application's accountability surface is fuzzy. Mixing ANR triage with crash triage in one session causes my judgment to drift.
I now run a 3-step framing for every ANR before showing it to Claude Code: (A) device / OS profile, (B) what is main thread blocked on, (C) which SDK init looks synchronous. For BW-027 (the Glide synchronous decode), the question I asked was:
On Sharp AQUOS SX4 / Android 15, after WallpaperPagerAdapter.onBindViewHolder:145, the stack descends through Glide.SingleRequest.getPlaceholderDrawable into libhwui ImageDecoder_nDecodeBitmap+512 and the app ANRs. I suspect we're synchronously decoding a 220KB JPG on the main thread because we hand R.drawable.background_silver to Glide as a placeholder. Should I replace it with an XML ColorDrawable, and is it correct that ColorDrawable bypasses ImageDecoder?
Claude Code confirmed that ColorDrawable resolves immediately as android.graphics.drawable.ColorDrawable without going through ImageDecoder, and explained that JPGs go through density resampling and decoding that lands on the main thread. With that, the XML ColorDrawable swap was a confident commit, not a guess.
The general trick with ANRs: compress "what is the main thread doing" into a single sentence before you ask. art_jni_trampoline followed by IPackageManager$Stub$Proxy.getNameForUid+252 is "blocked on system_server binder IPC." Above Display.getRefreshRate is "display HAL latency." Pre-classify like that and the SDK-vs-OS attribution gets fast.
Use Claude Code as a meeting minutes-taker, not a code generator
The biggest shift across 27 sessions in 11 days was leaning on Claude Code less as a "write me the code" tool and more as a "write down what I just decided" tool.
Specifically, during a debug session you hit decision points like:
"BW-017 will be killed with setItemAnimator(null). Side-effect risk: PageJumpSlider drag may break, so apply setHasFixedSize(true) together."
"BW-009 survived the v2.1.0 drawable-nodpi placement. The density-split + R8 strict mode hypothesis matters more than I thought. Add res/raw/keep.xml."
"BW-030 only reproduces in the API 37 emulator. configChanges += assetsPaths would have worked but has API compatibility risk. Use graceful finish() instead."
After I verbalize each of those, I ask Claude Code to "summarize what I just said in the form of the Fix column of known-bugs-tracker.md for BW-XXX." Claude Code is good at minutes-taking; the operational notes end up filed next to the Crashlytics evidence. Six months from now when the same stack trace pops up, past-me's reasoning is there to read. For solo maintenance work, this is more valuable than the code itself.
What paid off most in those notes wasn't the conclusion — it was the option I didn't take. One line explaining why I rejected adding assetsPaths to configChanges for BW-030 saves future me from re-running the entire evaluation. Fixes I shipped are legible from the diff; the discarded alternatives live nowhere else.
Where the numbers landed
After the 11-day loop and submitting v2.1.1 / v1.8.1, here is where Crashlytics and Play Console sat on 2026-05-25:
BW v2.1.0 7-day: crash-free users 98.62% (+0.48%), crashes 67 / users 57 (-42.7% / -43.6% versus the prior week)
BW v2.1.0 Play Console 28-day: 15 issues, all known. The top 4 (BW-017 / BW-015 / BW-027 / RecyclerView IOOBE variant) are already fixed in v2.1.1
UE v1.8.0 7-day: 7 issues, all known. Zero new unknowns
A +0.48% crash-free-users improvement reads as small, but in raw counts it is 67 crashes across 57 users, down 42.7% and 43.6% week over week. Two lines of code (setItemAnimator(null) + setHasFixedSize(true)) plus one drawable swap moved a bit over 40% of the volume. The compounding effect of small, evidence-grounded fixes is bigger than I expected coming into the loop.
Work out the date you're allowed to call it fixed
The day after I submitted v2.1.1 for review, the Crashlytics dashboard showed the new version at a 100% crash-free rate, and for about ten seconds that felt great. Then I looked at adoption: a few percent of users had v2.1.1 at that point. With a denominator that small, 100% shows up whether or not anything was actually fixed.
I used to paint the known-bugs-tracker.md row green ("fixed") right there. A few weeks later the same signature would come back and I'd be peeling the green off again. After doing that twice, I started calculating the date I'm allowed to paint it green before I ship.
The arithmetic is small. Say k users hit the bug over 7 days on the pre-fix version. After the fix, watching a comparable population with zero occurrences, the number of days before "zero" stops being luck at the 95% level looks like this.
#!/usr/bin/env python3"""fix-confidence.py — days of observation before "it stopped" is evidence."""import mathdef observation_days(k: float, adoption: float = 1.0, confidence: float = 0.95) -> float: """k: affected users over 7 days pre-fix / adoption: rollout share of the new build""" if k <= 0 or not 0 < adoption <= 1: raise ValueError("k > 0 and 0 < adoption <= 1") return -7.0 * math.log(1 - confidence) / k / adoptionif __name__ == "__main__": print(f"{'k':>5} {'100%':>8} {'20%':>8} {'5%':>8}") for k in (99, 20, 9, 1): row = [observation_days(k, a) for a in (1.0, 0.2, 0.05)] print(f"{k:>5} " + " ".join(f"{d:>8.1f}" for d in row))
Here is what it printed on my machine (values are days).
k (affected users over 7 days, pre-fix)
100% adoption
20% adoption
5% adoption
99
0.2
1.1
4.2
20
1.0
5.2
21.0
9
2.3
11.7
46.6
1
21.0
104.9
419.4
The interesting part is what's missing: the denominator. The 7-day active user count for that version cancels out as long as it doesn't change across the fix. I checked the exact form (ln(1-C) / ln(1-k/population)) against populations of 2,000, 5,000, 20,000 and 100,000, and every one of them lands on 21÷k days. So the only input you need is the affected-user count already printed on the Crashlytics issue page — no DAU export required to set a review date.
Applied to the actual list, the bugs from those 11 days split cleanly into three kinds.
BW-017 (99 users in 7 days) — five hours at full adoption, a little over a day at 20%. Safe to mark green two days after release
BW-015 (grew from 1 user to 9) — about 12 days at 20% adoption. Stays in "observing" until the next scheduled release
BW-027 (1 event / 1 user in 7 days) — 21 days at full adoption, 105 days at 20%. Observation will never confirm this one in a release cycle
The third case is the awkward one. The BW-027 fix (swapping a 220KB JPG placeholder for an XML ColorDrawable) is mechanically correct: the main thread no longer goes through ImageDecoder at all, so the cause is gone. But demonstrating the absence with numbers would take until the next major update. Painting it green would put two very different qualities of evidence in the same colour.
The tracker excerpt earlier in this article still shows BW-027 as green — that was the snapshot at the time. The status legend now has three tiers instead of one, and that row has since moved to blue.
Status
Evidence
Condition to move it
Green (confirmed by observation)
Zero occurrences across 21÷k days divided by adoption
Review date passed with zero
Blue (confirmed by mechanism, unobservable)
The offending call was removed from the code path
Diff and reason fit in one line
Yellow (observing)
Still inside the window
Always carry an explicit review date
This changed how I ask Claude Code, too. Instead of "can I mark this one fixed?", I now ask "given the affected-user count, is this bug confirmable by observation or only by mechanism?" The first phrasing reliably returns a generic "please test thoroughly on real devices." The second one produces different answers depending on k, because I handed over the material the judgement actually depends on.
What I'd set up first next time
If I were starting the same debug cycle on a fresh app today, I'd put these in place before any stack traces arrive.
Create known-bugs-tracker.md and write the status legend first (open / in progress / fixed / out of scope / observing)
Pre-build the "quick-lookup table" header so the first new bug just appends a row
Don't mix "writing code" and "reading Crashlytics" in one session — at minimum keep them in separate tabs
Format every stack trace into 4 blocks (environment / top 5 frames / message / your hypothesis) before pasting into Claude Code
Keep "won't fix" entries with reasons in known-bugs-tracker.md; don't just delete them from your mental list
Before painting a fix green, compute 21÷affected-users divided by adoption and write that date into the tracker row
When Play Vitals thresholds (1.09% crashes, 0.47% ANRs) come into view, ask Claude Code to re-rank the deferred list as ship-now versus hold
Crashlytics will eat your morning if you let it, but with this scaffolding it becomes a fast filter. Most days I can read a new issue, find the BW-017 variant, file it in the tracker, and close the tab before my coffee cools.
Hope it helps your own debug loop. Thanks for reading.
Share
Thank You for Reading
Claude 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.