Sprint M6 — the bug was never where it said it was
I submitted Domi to the App Store on a Friday morning and Apple rejected it that evening. No crash. No bug. No guideline breach. They could not complete the review because the App Review Information section — a form, in the console, that I had left mostly empty — did not tell them how to sign in or what the app was for.
The reflex when a rejection lands is to go and fix something in the code. There was nothing to fix. Worse, the obvious instinct would have been actively harmful: cutting a new build means bumping the version, and in this app the version number is the over-the-air update bucket. Bumping it would have stranded every phone already running 1.0.1 — including the one Apple was about to look at.
Thirty-two PRs merged between August 24th and 31st, 454 tests up from 379, one new binary, seven production over-the-air updates. The app is not in the store yet. But for the first time, nothing on the remaining list is waiting on somebody else.
Three innocent files
Here is the part I actually want to write about, because it cost more than the rejection did and it will happen to you.
The crash reporter filed five bugs against the mobile app this sprint. Three of them named a file that had nothing whatsoever to do with the failure.
Two of them pointed at the reminders code — specific lines, a plausible stack, a real function. An agent read that function closely, found the frames sitting inside a try/catch that had been swallowing everything for three weeks, and said so: an exception thrown there could not possibly have reached the crash reporter. That was the moment the investigation turned from “fix this” to “why is this report lying to me.”
The answer is a detail of how Sentry works that I did not know and now cannot unsee. When you send it a message rather than an exception — a deliberate diagnostic, “the weather service returned 503” — the message has no stack trace of its own. So Sentry attaches whatever JavaScript frame the program happened to be sitting on at that instant. That frame is not the cause. It is a bystander.
One weather 503, fired twice on the same evening, became two separate GitHub issues, both blaming a reminders file that neither throws errors nor knows what weather is. Three agents spent an evening reading innocent code. A third issue did the same thing days later, naming a database helper, and turned out to be an authentication warning from somewhere else entirely.
The fix was not to any of the named files. It was to give every diagnostic in the app a stable fingerprint, so the reporter groups them by what happened instead of by where the program was standing. There are now zero unfingerprinted diagnostics in the codebase.
What pleases me more than the fix is what happened on the third occurrence. By then the shape was recognisable — warning level, no culprit, a bare function name as the title — and instead of confidently repairing the named file, the agent stopped and asked another station to pull the raw event. It said userinfo failed: HTTP 401. The database helper was innocent, exactly as predicted. Two guesses at the real source had already been wrong by that point, including one the mobile agent made itself and corrected in writing.
A fourth report, filed by the same sweep, turned out to have been fixed six days earlier — by a commit whose message names that exact crash-report ID. It stayed open because the sweep filed it three hours before the fix merged and nobody wrote the magic word that closes it. Nothing was wrong with the code. Only the paperwork.
A day lost to a dropdown
The same mistake, in a different costume, earlier in the sprint.
I had it written down in two documents that the App Store listing was English-only and needed its French translation before submission. It did not. Both French localisations, the keywords, the screenshots, the privacy labels, the demo account — all of it had gone in before I ever pressed submit.
The error came from looking at a language picker on a version page and reading it as an inventory. A dropdown showing you the language you have selected is not a list of the languages that exist. That reading got copied into two working documents as fact, and it cost a day.
Both of these are the same failure. A thing that displays one item is not a report of all items. A frame in a stack trace is not a cause. The interface showed something true and I read a claim into it that it was never making.
Two bugs that only existed on a phone
I found both of these myself, in about ninety seconds, doing nothing clever.
The first: on the sign-in screen there is a Cancel link. I tapped it. Nothing happened. The handler was correct — it calls “go back” — but three of the four ways you can arrive at that screen replace the screen you came from rather than stacking on top of it, so there was no back to go to. It worked in exactly one case: opening it deliberately from Settings. On the ordinary path — launch the app while signed out — it rendered perfectly and did nothing at all.
The second is worse, because it is the flow Apple explicitly asked to see. Settings, “Delete my account”, a confirmation, then it should open the deletion page in the browser. It did not. The call had no error handling at all, so the failure was invisible twice over: the app never told the user, and the crash reporter logged something nobody was reading. Tap Continue, the dialog closes, silence.
Neither was findable from the repository. Both controls were on screen, correctly labelled, wired to the right function. There is no test that would have caught either, because the test would have asserted the same wrong thing the code did. Shipped is not the same as rendering is the rule in this repo, and this sprint it earned itself twice in one day.
The reviewer caught one the humans made
There is an AI reviewer on every pull request in this repo now. On the reminders fix it asked for changes twice, and both findings were real.
The second one is the interesting one. Fixing the original bug meant deleting a line that assigned a value too early. That line was wrong for its own reasons — it cached a claim about work that had not happened yet — but it had also been accidentally acting as a lock, preventing two simultaneous runs from stepping on each other. Removing it fixed one bug and created another: two overlapping runs could now schedule every reminder twice, past the operating system’s limit on pending notifications.
I would have shipped that. The regression was invisible unless you noticed that a line being removed had been load-bearing for a reason unrelated to why it was written. That is a genuinely hard thing to see, and a review bot saw it.
Where it stands
The web backend promoted to production on the 30th, which cleared the last thing mobile was waiting on from another repository — the account-deletion link had been dead-ending on a sign-in page until that shipped.
What is left is short and it is all mine: push one over-the-air update, confirm the deletion flow on an actual phone, record it, and answer Apple’s seven questions. The answers are written. The recording is shot. The last blank in the form is a password I have to go and copy.
Which is roughly where I came in.