DEV Community

Dumb Game Lab
Dumb Game Lab

Posted on

Our crash reporter's first day: three web standards that killed boot on old Android WebViews

We run about a dozen small HTML5 games wrapped as Android apps. Last week we finally put a crash reporter in all of them: a tiny script at the very top of <head> that sends any uncaught error to our own worker.

The first day of data had a pattern we didn't expect. The top boot crashes weren't bugs in our game code. They were standard functions that simply don't exist on older Android WebViews:

  • CanvasRenderingContext2D.roundRect — killed boot in our claw machine game. We use it for rounded buttons and the share card.
  • Array.prototype.at — arr.at(-1) is so convenient that it crept into a dozen places.
  • structuredClone — used to copy save data in two tycoon games.

All three work on any phone from the last couple of years, and that's why our own testing never caught them. Players on older devices (still around Chrome 90 inside the WebView) got a white screen and left. No review, no report. The reporter was the first time we saw them at all.

What we did

We didn't patch each game. We wrote one small compat file that gets injected in the same spot as the crash reporter, before any game script runs:

var def = function (o, k, v) {
  if (o && !o[k]) Object.defineProperty(o, k, { value: v, writable: true, configurable: true });
};
function at(n) {
  n = Math.trunc(n) || 0;
  if (n < 0) n += this.length;
  return n < 0 || n >= this.length ? undefined : this[n];
}
[Array.prototype, String.prototype].forEach(function (p) { def(p, 'at', at); });
if (typeof structuredClone === 'undefined')
  window.structuredClone = function (v) { return v === undefined ? v : JSON.parse(JSON.stringify(v)); };
// roundRect: moveTo/lineTo/arcTo with radii clamped to half the shorter side,
// installed on CanvasRenderingContext2D, OffscreenCanvasRenderingContext2D and Path2D
Enter fullscreen mode Exit fullscreen mode

The structuredClone fallback is lossy (no Maps, Dates or cycles), but our save data is plain JSON anyway, so it's fine for us. Check yours before copying it.

Rules we set for ourselves:

  1. If the reporter shows "X is not a function" and X is a web standard, the fix goes in the compat file, not in the game. The next game gets it for free.
  2. There's a self-test that creates a canvas context with roundRect removed and checks that the shim fills it in. It runs with the rest of our checks.
  3. The shim only ships in store builds. The dev server doesn't load it, so we don't hide the problem from ourselves while developing.

The claw machine game got a hotfix the same day (1.0.4) and the boot errors from that version stopped.

The takeaway

If you ship web games inside a WebView, your minimum browser is whatever the oldest phone in your player base has, not your own phone. A crash reporter at the top of <head> is about 2 KB and pays for itself on day one.

If you want to see the claw machine game that started this: https://play.google.com/store/apps/details?id=com.dumbgamelab.bangguseokclaw

Top comments (1)

Collapse
 
marcusykim profile image
Marcus Kim •

The store-only shim makes the packaged build the important test target. I'd launch that exact bundle on the oldest supported WebView and check first playable frame, arr.at(-1), and a save/load round trip with your actual JSON fixture. The isolated roundRect self-test proves installation, but not that the compat script runs before the game after packaging. Keeping that boot-order check would protect the next game that inherits the shared fix.