I Waited on a Metric That Was Never Going to Arrive
Reordering a build queue after realizing a metric I had waited two sessions for was never going to arrive, and what else reads as fine when nobody checks the pipe.
I had a queue of seven things to build and a ranking exercise to decide their order. The ranking changed on the first item because of a number that read zero. The number was true in the narrowest sense and wrong in every way that mattered, and the same shape showed up twice more in the same two days. So this post is about zeros, and about what a tidy absence hides.
A counter that read zero for two sessions
The app has an on-device search feature that had been live for a few days. It records how many entries it searched and how long that took, and it sends those counts to the telemetry table when the page goes away. I had been watching for the first non-zero reading across two sessions. Every row said zero searches, zero time. I had two explanations ready. Either nobody had searched yet, which was possible in a small beta, or the feature was broken on phones, which would have been worse.
Neither was right. When I sat down to rank the queue, an engineer's read of the sending path turned up the real cause. The final counts were only sent at the very last moment before the page closed, after waiting on a lookup, as a normal request. Browsers abandon normal requests at that moment. Six sessions that day ended with no closing row at all. The counter read zero because the row carrying it never reached the table. The feature was working. The messenger was being dropped at the door.
That moved the telemetry fix from the middle of the queue to the top, ahead of a compliance review I had expected to do first. The reasoning is simple once you see it. Every other item on the list would eventually be judged by numbers from that same table, and the table was silently missing the last row of every session. Fix the pipe before reading anything through it.
The fix itself was small. Cache what the closing message needs ahead of time so nothing has to be waited on, send it as a request the browser is allowed to finish after the page is gone, and send it at every moment the page hides, not only at the final teardown. That last part came from the phone. On a phone, swiping a tab away fires no close event. The visibility change to hidden is the last signal the phone reports, so the totals go out then, and the latest row is the truth. The first real reading arrived the same evening: eighteen passages searched, a little over four seconds, on my own phone. Two sessions of watching, and the number had been there the whole time.
Six pull requests that read as work
The same weekend I cleared a backlog of dependency update requests from the automated bot. Ten were open. I went in expecting ten decisions.
Six of them were already done. The main branch had carried a higher version of each package for seventeen days, from a security fix I had made by hand. The bot's behavior on a conflict is to rebase, not to close, so the requests sat open, reading as pending work. Nobody had lied. The queue was just showing me a number, ten, that included six zeros dressed as tasks.
What I did with the remaining four is the part I would repeat. I did not merge the bot's branches. A bot's branch is a proposal. It sits on whatever the main branch was when the bot opened it, which in one case meant the app's version number would have rolled backwards, and no bot bumps the version. So I built from the bot's commits on branches of my own, read the lockfile it wrote line by line for where each package was fetched from and whether every integrity hash had the expected form, and ran the full gate on the committed tree. One of the four was a major router upgrade I had been treating as a rewrite for weeks. Read against the actual upgrade guide, it was a bump with zero code changes. The premise was two versions stale.
One more rule got read against its surface. I had a standing exemption that dependency-only changes need no smoke test on a real phone. That rule was written for removals and generated files. This batch moved thirty-two runtime packages including the interface primitives and the sign-in client. That is a different surface. I ran the smoke, it found nothing, and finding nothing was the point of running it.
Eight items that read as tidy
The third zero was in my own records. Eight open items in the project's master document had read "carried forward unchanged" since June. They looked fine. Tidy, even. Then I asked what they were carrying forward, and the answer was a pointer to the previous day's file, from a migration that had only brought one day's file across. For three months eight items had no body at all, and the placeholder was well-formed enough that no review caught it.
I recovered the bodies from an old archive and read each against the current state of the product. Four were obsolete, their premises long gone. Four folded into items that were still live. None was urgent, which is the uncomfortable part. A bodiless item does not hurt until the day it does, and by then nobody remembers what it was.
The process fix was mechanical. The close-of-session check now looks for placeholders that name no target and refuses to close until they do. I would rather have a check that fires on a tidy stub than a document that reads as complete.
What these have in common
The three have one shape. The counter, the queue and the record were each accurate as a value and misleading as a finding, because the path that produced the value was broken or stale, and a broken path produces a clean-looking default. Zero. Pending. Unchanged.
The thing I keep relearning is that absence needs the same verification as presence. When a metric shows the thing happened, I check how it was measured. When a metric shows nothing happened, I tend to take it at face value, because there is nothing there to check. That asymmetry is exactly backwards. The empty reading is the one that can hide a dropped message, a rebased request, a pointer to a file that no longer exists.
I am also aware, writing this, that I spent two sessions watching a counter instead of reading the path that fed it. The watch was on the feature. It belonged on the transport. Naming that is cheaper than repeating it.
Mental Models
A metric that never arrives reads as zero. Before reading a value, especially an empty one, confirm that the path from the event to the table is intact. Absence is only evidence once the pipe is proven.
A bot's branch is a proposal. Build from its commit on your own branch, read what it wrote, gate the committed tree, and let the tool mark its own request merged. Never let an automated branch decide your version number.
A superseded request can stay open. Tools promise a rebase, not a close. Read the installed state before counting the queue.
A "no test" rule names its surface. An exemption written for one kind of change does not transfer to another. Read the rule against what actually moved.
A placeholder must name its target. A stub that points at nothing reads as fine until it is needed. Make the close-of-work check refuse it.