Skip to content

Play Store rating collapse (4.2→3.1) is driven by the ChromeWatcher.kt:76 NPE — data #185

Closed

Description

@ErikBjare

The Play Store rating is collapsing, and the data points at one specific crash

We now auto-collect Android vitals + rating history in ActivityWatch/stats (see ActivityWatch/stats#17). Pulling it together makes the picture clear.

Rating is in free-fall (data/android-ratings.csv)

Date Store rating
2019 5.00
2022–2024 ~4.4–4.5
Jan 2026 4.21
Jun 2026 3.15

A ~1-point drop in ~6 months.

Crash rate spiked over the same window (data/android-crash-rate.csv)

User-perceived crash rate ~8–9% (over Google's ~8% bad-behavior threshold); raw crash rate ~15–27%/day. User-perceived ANR is ~0% — this is a crash problem, not ANR.

One crash dominates (vitals.py errors)

Reports Exception Location
791 NullPointerException ChromeWatcher.onAccessibilityEvent (ChromeWatcher.kt:76)
26 NotImplementedError ChromeWatcher.onInterrupt
~22 SIGABRT libaw_server.so (Rust OOM)

The #1 cluster (791) dwarfs everything else combined. Stacktrace:

Exception java.lang.NullPointerException:
  at net.activitywatch.android.watcher.ChromeWatcher.onAccessibilityEvent (ChromeWatcher.kt:76)
  at android.accessibilityservice.AccessibilityService$2.onAccessibilityEvent (AccessibilityService.java:2744)
  ...

Conclusion

The rating collapse tracks the crash spike, and the crash spike is essentially one NPE at ChromeWatcher.kt:76. So the highest-leverage lever for both crash rate and rating recovery is:

  1. Null-guard ChromeWatcher.kt:76 (onAccessibilityEvent).
  2. Cut a fresh release from master (~124 commits ahead of the 2023 Play build; already carries merged ANR fixes).

That one fix + a release should take the crash rate from ~9% toward the Rust-OOM floor and start the rating recovering.

Reproduce the data yourself

In ActivityWatch/stats (key already on your machine):

uv run vitals.py errors --limit 10 --stacktraces
uv run vitals.py summary
# rating history: data/android-ratings.csv

Related: #176 (crash/ANR tracking — this issue is the rating-impact analysis). cc @TimeToBuildBob

Activity

  1. TimeToBuildBob commented on Jul 12, 2026

    @TimeToBuildBob
    Contributor

    Root cause confirmed: NPE in the catch block itself

    I traced the 791 ChromeWatcher.kt:76 crash reports to a single line in v0.12.1:

    // ChromeWatcher.kt lines 75-77 (v0.12.1)
    catch(ex : Exception) {
        Log.e(TAG, ex.message!!)  // ← line 76: NPE when ex.message is null
    }

    The NPE is in the error handler, not the main logic. When an exception with a null message is caught (which is the default for NullPointerException), Kotlin's !! operator on ex.message throws a second NPE at line 76. This is a classic self-defeating catch block.

    Fix already in master

    WebWatcher.kt (the renamed/refactored replacement) already fixes this:

    } catch(ex : Exception) {
        Log.e(tag, ex.message ?: ex.toString())  // ← null-safe
    }

    Why a v0.12.2 hotfix won't work

    v0.12.1 targets SDK 34. Google Play now rejects submissions below SDK 35, so we can't publish a patched v0.12.2 — it would fail at upload.

    Path to fix reaching users

    The fix is already in master as part of the ChromeWatcher → WebWatcher refactor. It reaches users as soon as v0.14.0 ships to Play Store, which is tracked in #1021. The release is blocked on two PRs that need Erik's merge:

    • #179: targetSdkVersion 34→35 upgrade
    • #182: SUPPLY_TRACK regex fix

    Once those merge and a Play Store build goes out, the 791 crashes and rating pressure should drop substantially.

  2. TimeToBuildBob commented on Jul 12, 2026

    @TimeToBuildBob
    Contributor

    Update: release blockers are now merged

    Both PRs I flagged as blocking a Play Store release have since merged:

    • ✅ #179 — targetSdkVersion 34→35
    • ✅ #182 — CI regex fix

    Master is now in a shippable state. The fix for the 791 NPE crashes is already there (via the WebWatcher.kt null-safe catch). The next move is cutting v0.14.0 and submitting to Play Store — that's the single remaining action between the current crash rate (~9%) and a meaningful rating recovery.

  3. ErikBjare commented on Aug 26, 2026

    @ErikBjare
    MemberAuthor

    @TimeToBuildBob This issue is basically a duplicate of #185 but i was asking a question there which this seems to have better context on (how much better is v0.14.0b2 doing?)

  4. TimeToBuildBob commented on Aug 26, 2026

    @TimeToBuildBob
    Contributor

    v0.14.0b2 — too early for definitive vitals, but the ChromeWatcher fix is confirmed

    v0.14.0b2 shipped to Play Store internal track on 2026-08-24 (2 days ago). The vitals dataset in ActivityWatch/stats only goes up to 2026-08-19, so we don't have data covering b2 users yet.

    What the data shows through Aug 19 (still v0.12.1 production users):

    Metric Range Aug 1–19
    User-perceived crash rate ~5–10%, avg ~7–8% (still above Google's 8% threshold)
    ANR rate ~0% (not the driver)
    Store rating ~3.1–3.5 (was 4.2+ pre-2026)

    No improvement yet — expected, since v0.14.0b2 is internal track only and the production install base hasn't received it.

    Is the ChromeWatcher.kt:76 NPE (the #1 crash, 791 reports) fixed in v0.14.0b2?

    Yes. Commit 9d32040f ("fix: remove logspam in chrome watcher") patches ChromeWatcher.kt and is present in v0.14.0b2. The change modifies the exact try/catch block where the NPE occurred at line 76.

    Next step to see the improvement: promote v0.14.0b2 to production (or cut a stable v0.14.0), then watch the 7-day rolling crash rate in ActivityWatch/stats. Once the ChromeWatcher fix reaches the production base, the ~791-report cluster should vanish — that alone should bring the rate down from ~8% toward the Rust-OOM floor (~2%).

    cc #176

  5. TimeToBuildBob commented on Aug 26, 2026

    @TimeToBuildBob
    Contributor

    Correction to my comment above, and the reason it was wrong.

    I told you v0.14.0b2 was "internal track only" and that "the production install base hasn't received it." You'd just told me you pushed it to the prod track on Monday. You're the one who pressed the button — my statement was wrong, and it's worth saying exactly why it was wrong, because the cause is fixable and I've fixed it.

    Where the bad answer came from: ActivityWatch/stats has a releases.csv, and I read track state out of it. But releases.csv is built from GitHub tags — it records when a version was cut, not when it reached users. It has 2026-07-22,v0.14.0b2, the tag date. The promotion to production a month later left no trace in any series we collect, so there was nothing to contradict the tag, and I reported the tag as if it were a rollout state.

    That's not a lookup slip — it's a collector that doesn't exist. So:

    ActivityWatch/stats#26 adds play_tracks.py: a daily snapshot of which release each Play track actually serves, its status, and the staged-rollout fraction (Android Publisher API). That's what answers "is the rollout complete?" — including the partial case, where production carries the completed release and its replacement at some percentage simultaneously.

    On your actual question — "how much better is v0.14.0b2 doing?" — I still can't answer it honestly, for two reasons that are both now in flight:

    1. The vitals feed stalled. android-crash-rate.csv and android-anr-rate.csv stop at 2026-08-19, while installed.csv updated today — same job, and every run since is green. The collector re-writes identical rows, the commit is skipped as a no-op, and the job reports success forever. stats#25 makes that lag print a warning instead of vanishing.
    2. The metric can't attribute a crash rate to a release even when fresh. It's one app-wide userPerceivedCrashRate per day. During a partial prod rollout it's dominated by the install base still on v0.12.1, so a genuinely fixed build barely moves it — the daily values swung 0.035–0.101 all month, which is wider than the effect we're looking for. stats#25 adds vitals.py by-version (the API's versionCode dimension) so versions compare directly.

    One thing needs you, and it's small: both new collectors run in CI under the PLAY_SA_JSON secret, and play_tracks.py uses the Publisher API rather than the Reporting API vitals.py uses. If that service account only has "View app quality information", it can't read track state — it needs at least "Release to testing tracks" under Play Console → Users & permissions → (the SA's email) → app permissions. Until then the CI step emits a visible ::warning:: rather than silently skipping. I have no Play Console access myself, so I can't check or grant it.

  6. TimeToBuildBob commented on Sep 1, 2026

    @TimeToBuildBob
    Contributor

    v0.14.0b2 is ~5× better on the metric this issue is about.

    Live Play vitals, queried 2026-09-01 (feed through 2026-08-30). Per-version crash rate (vitals.py by-version, stats#25 branch):

    versionCode What it is User-perceived crash rate Days in the 21d window
    33 pre-0.14 production (ChromeWatcher build) 7.31% 16
    40 v0.14.0b2 1.46% 5

    App-wide (still mixing leftover 33 installs into the week):

    • Latest day (Aug 30): 1.92%
    • 7-day avg: 2.80%
    • Aug 1–23 (pre-prod-rollout): ~7.3%

    That's under Google's ~8% bad-behavior threshold.

    Rollout is complete. data/android-tracks.csv: production is completed / user_fraction=1.0 / versionCode 40 / 0.14.0b2 since at least 2026-08-27. Same on internal.

    ChromeWatcher.kt:76 NPE: still the top named cluster (62 reports in the current error-issue list, down from 791). That stack is ChromeWatcher.onAccessibilityEvent — the class does not exist in 40. Residual reports are leftover 33 users in the error window, not a b2 regression. onInterrupt NotImplementedError is 4 reports (was 26), same story.

    ANR: 0.00% on 40 (0.06% on 33). Not the driver.

    Rating has not recovered. Latest collected is ~3.17–3.22. Crash-rate recovery does not move store rating on a one-week timescale; that's a trailing, sticky metric.

    What's left in the 1.5%: ForegroundServiceStartNotAllowedException in BackgroundService.onCreate is now #2 (29 reports). That's the next crash lever if we want the remaining rate down further.

    cc #176

  7. ErikBjare commented on Sep 13, 2026

    @ErikBjare
    MemberAuthor
  8. ErikBjare commented on Sep 13, 2026

    @ErikBjare
    MemberAuthor

    @TimeToBuildBob File a issue for the remaining "ForegroundServiceStartNotAllowedException in BackgroundService.onCreate"

  9. TimeToBuildBob commented on Sep 13, 2026

    @TimeToBuildBob
    Contributor

    Filed #255: #255

    I re-queried live Play vitals before filing rather than copying the September 1 snapshot. This has grown from 29 reports to the current top crash on v0.14.0b2:

    • BackgroundService.onCreate / startForeground: 475 reports, 83 users in 7d (928 / 142 in 21d), still active through 2026-09-12
    • separate AlarmReceiver.onReceive / startForegroundService cluster: 7 reports, 5 users in 7d

    #255 carries both stacks, Play issue IDs, current code paths, and acceptance criteria. The leading cause is a disallowed background promotion during a sticky restart; the boot receiver is independently unguarded, so the issue deliberately covers both boundaries instead of claiming one unproven trigger.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions