Last week two of our scripts reported numbers that looked clean. One said an app had 0 reviews, three days running. The other said it had received 40 photos with 0 failures. Neither told the whole story. Both numbers meant the script couldn't read something, and neither one said so.
This follows my posts on failures hidden behind exit code 0 and on checks that borrowed from the code they checked. These zeros came from the readers.
At the end I'll ask which warning sign you trust most, because we ran into three in one week.
Zero reviews, three days running
One of our Android apps launched on October 28, and a script checked it six times over 72 hours: search rank, installs, rating and reviews. At 24 hours it wrote "reviews: 0". At 48 hours, the same. A separate morning check on the 31st agreed.
The app had a review, posted on launch day. We found it when someone opened the review page in the developer console.
The two readers failed in different ways. The rank script counted reviews from the "N reviews" label, and the store page only shows that label once an app has a public rating. With one review, the page still showed no rating, so there was no label, so 0. The morning check looked for review IDs that start with gp:. This review's ID was a UUID.
The part that stung: from the third reading on, we kept the raw HTML. The review is in the October 29 and October 30 files. It sat on disk for two days, and the rank script read both files as nothing.
The reader now finds reviews by shape instead of by label or ID format: a star value from 1 to 5, a body, a timestamp and an app version, inside the page's embedded data block. We also treat the console as the source for review and install counts. The public page is a second reading.
40 received, 0 failed
Another script collects the day's photos from a web list of notes, for our blog drafts. On October 29 it printed "received 40, failed 0" (translated). Forty photos came in, 20 from each of two notes. The third note's 22 photos were missing entirely, and so were 4 more from the second note: 26 photos, every one below row 40.
The list's first screen holds 40 rows, and the script read what was rendered and stopped there, so nothing failed and the failure count was honest, but the received count was just the screen size. The run the day before had also received exactly 40. We never checked whether anything was missing that day.
The fix calls scrollIntoView({block: 'end'}) on the last row and reads again while the list keeps growing. It stops when the last row's upload date is before that day, after 3 scrolls with no new rows, or after 20 scrolls. It counts rows as it goes, in case the list drops rows that scroll out of view. And it prints how many rows it read and why it stopped. The rerun that evening printed "first screen 40 rows → 80 rows, 2 scrolls, stopped at a row from the day before" (translated), then 26 new photos and 0 failures. I'd take that line over a clean 40 every time.
Two numbers that can't both be true
The same launch log had a third number worth doubting. The store page showed "0+" installs for all 72 hours. One review means at least one install, so "0+ installs" and "1 review" can't both describe the same app at the same time.
This time the parser was right. The page's data block and the visible label both said "0+". The public figure updates late. But the contradiction is what made us look, and looking took two places and a minute.
Three signs, in the order they'd have helped
- A count that equals a page size. 40 of 40 should have been a question, not a pass.
- A zero that lasts. Three readings of 0 in a row is a reason to check a second source once.
- Two numbers that contradict. A review without an install means one figure is stale or misread.
None of these needs a new tool. Each is a line in the output: how many rows the reader saw and why it stopped, which source a zero came from, and a flag when two figures disagree.
A question
When a script hands you a suspiciously clean number, what tips you off: a count that matches a page or batch size, a zero that has lasted too long, or two numbers that can't both be true? Which one has actually caught something for you?
Sources
- MDN, Element.scrollIntoView() (the
blockoption sets vertical alignment in the scrolling container): https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView, accessed 2026-11-02 - Everything else: our launch log, commit history and lessons log, October 28 to 31.
This post was drafted with AI help. The numbers and facts come from my own logs and repo.
Top comments (10)
Sometimes the script does say so, on stderr. My status line's context bar sat at 0% whenever the percentage had a decimal in it. Under a German locale, bash 3.2's builtin printf rejects 37.4 as an invalid number, so it wrote that error to stderr and the bar got 0. Claude Code shows the script's stdout, and all I ever saw there was a clean 0%. The fix was LC_NUMERIC=C plus rounding in jq, which doesn't depend on the locale.
That's a third way to get a clean 0: the error was written, just to a stream nobody was watching. Ours never got that far. When the "N reviews" label wasn't on the page, the reader found no match and counted 0, three days running. A review ID without the
gp:prefix did the same. No match isn't an error, so there was nothing to send to stderr.So the photo collector now prints how many rows it saw and why it stopped, on the line with the count. Its first screen holds 40 rows and 26 photos sat below them; a 40 printed beside "40 rows seen" is much harder to wave through.
Your case adds a twist we didn't have, since the locale changed what counted as a number. Where would you rather it had failed loudly, at the printf or at the status bar refusing to draw 0% from bad input?
The "received 40, failed 0" one is the nastiest, because a cap that matches a UI page size looks like a healthy number. What helped me: make the reader report two numbers it can't fake from its own output, the count it read and an independent count of what should exist (the list's own "N total" label, or a count from the source system), and alert on the gap rather than on either number. A flat 0 or a suspiciously round number (40, 50, 100) with no total to compare against should be treated as "unreadable", not "zero". Reading by shape instead of by label is the right fix; the independent total is what catches the next page-size change.
The independent total is the part we only half have. For reviews, the developer console is now the source and the public page is a second reading, so a 0 on the page can be checked against a number that didn't come from the same parser.
For the photo list we have no total to compare against. The reader stops on a row from the day before, on 3 scrolls with no new rows, or at 20 scrolls, and it prints which one fired. A run that hits the 20-scroll cap reads differently from one that reached yesterday's rows.
Your round-number rule would also have caught the run the day before. It received exactly 40 too. Nobody looked, because 40 wasn't 0.
When the source has no total at all, where would you put the alert: on the stop reason, or on a count that matches the last few runs too closely?
For the 40-row case, I'd distinguish 'end of data' from 'stopped trying' in the result itself. Three scrolls with no growth could mean a slow request or a stuck loader, so that stop condition deserves an incomplete flag rather than a successful total. Keeping the last visible row ID alongside the stop reason would make a rerun easier to compare.
Fair point. Right now the stop reason is only a label, printed on the same line as the row count, and the run ends the same way whichever of the 3 stops fired: a row older than that day, 3 scrolls with no new rows, or the cap of 20 scrolls. Only the date stop names anything, the upload date of the last row it read.
A stall prints its reason and nothing else. The two runs in our log both stopped on the date, including the October 29 rerun (40 rows to 80 after 2 scrolls). Neither stalled.
Your last-row ID would help most when a rerun reads more rows than the first run did. Would you put the stall in the exit code, or leave it in the output and let the next step decide?
The "40 of 40 should have been a question, not a pass" line is the one that stuck with me — a count that exactly matches a page size is a boundary condition, not a success. I also liked the fix of printing how many rows the reader saw and why it stopped, since the stop reason is the part that usually goes missing from logs. For your closing question, the zero that lasts would tip me off first; a single zero is noise, but three identical readings in a row is a pattern worth checking against a second source.
That's the one we'd pick too, in hindsight. We had three zeros: the 24-hour reading, the 48-hour one, and a separate morning check on the 31st. Nobody compared them to anything. The review turned up only when someone opened the developer console.
The worse part is that two of those readings had the review on disk. From the third reading on we kept the raw HTML, and the October 29 and 30 files both contain it.
So the console is now the source for reviews, and the public page is the second reading. When a zero repeats, would you check it against a different source first, or re-run the reader on the raw page you already saved?
The zero that lasts is the one that tips me off. One clean reading can be real — three in a row after a launch is a reason to check a second source before believing it. For a small business, that matters twice over, because a wrong zero isn't just a bad number, it's a missed review or a listing that looks dead to a real customer. Keeping the raw HTML from the third reading on is the cheap insurance here — it's what turned this from a mystery into a fixable reader bug.
The raw HTML was cheap, but it only paid off after someone had already found the review in the console. Those files sat on disk for two days, and the reader went through both of them. It found nothing.
So the fix had to be in the reader. It used to look for the "N reviews" label or an ID starting with
gp:. Now it looks for the shape of a review inside the page's embedded data block, where a star value from 1 to 5 sits next to a body, a timestamp and an app version.Old pages make a decent test set. Would you re-run a new reader over the saved files before trusting it, or wait for the next live reading?