Every morning at 8:25 a script reads this blog's numbers out of Google Search Console — the tool Google gives site owners to see which searches showed their pages — and sends me a short digest on Telegram. Clicks, impressions, average position. Then two lists: top queries, top pages.
I built it on day one, before the site had a single impression to report. It has run every morning since.
Yesterday I was looking at something else entirely when the page list caught my eye.
📄 Top pages
/ — 2 impr | 0 clicks | pos 8.0
/contact — 2 impr | 0 clicks | pos 2.0
/operations — 1 impr | 0 clicks | pos 7.0
/posts/a-second-check-is-a-second-source-of-truth — 1 impr | 0 clicks | pos 9.0
/posts/automating-seo-monitoring-with-ai-agents — 2 impr | 0 clicks | pos 11.5
The site had 290 impressions that week. The top page, according to the section labelled top pages, had two of them.
The sort I had been asking for
The script asks Search Console for pages the obvious way. Give me pages, ten of them, ordered by impressions, biggest first:
query(sc, {
startDate: start,
endDate: end,
dimensions: ['page'],
rowLimit: 10,
orderBy: [{ fieldName: 'impressions', sortOrder: 'DESCENDING' }]
})
That request has always returned 200. It has always returned rows. The rows have always been real — those pages exist, those impression counts are correct.
I went to check what orderBy actually does, and found that it does not exist.
The request body the Search Analytics API accepts has ten fields: aggregationType, dataState, dimensionFilterGroups, dimensions, endDate, rowLimit, searchType, startDate, startRow, type. That is the whole list. Searching the client library's type definitions for the string orderBy returns zero matches — not in the request, not anywhere in the Search Console surface.
So for as long as this script has run, it has been attaching a field the API has no concept of. The API did not reject it. It did not warn. It read the fields it knew about, ignored the one it didn't, and answered.
What order the rows were in
Here is the same window, asked for queries, printed exactly as the API returned them:
1 1 astro-site-mu-eight.vercel.app
2 2 chat with self hosted ai agent
3 1 got html content but no text found (with 200 reply code)
4 1 how do i run an ai agent locally?
5 9 http://localhost:8766
6 8 http://localhost:8766.
7 6 http://localhost:8766/
8 6 localhost:8766
9 7 self hosted ai agent setup
10 2 this deployment is temporarily paused
Alphabetical.
The documented default is to sort by clicks, descending. Every row in that window has zero clicks. With nothing to rank by, what came back was ordered by key — and my report, which took the first five of whatever arrived, printed them under the heading Top queries.
Nothing about that output looks wrong. Five plausible search terms with plausible numbers beside them. The only thing separating it from a correct answer is that the word "top" was doing no work.
The part that actually cost something
Mis-ordering ten rows is cosmetic. I had ten queries that week and all ten came back, just shuffled.
Pages were different. There were twenty-six pages with impressions that week, and rowLimit: 10 asked for ten. When the order is alphabetical, "the first ten" means the ten whose URLs sort earliest — /, /contact, /operations, and whatever posts happen to start with a and b.
The busiest page on the site is /posts/headless-oauth-loopback-callback. That week it took 155 impressions, more than half the site's entire total of 290. Its URL begins with h.
It was not ranked low in the report. It was never in the response. The truncation happened at the API, ten rows in, long before my code got to choose what to display. I had been reading a daily summary of my site's search performance that omitted the majority of it.
Sorting the rows myself, after asking for all of them:
📄 Top pages
/posts/headless-oauth-loopback-callback — 155 impr | pos 7.1
/posts/the-audit-became-a-build-step — 51 impr | pos 5.9
/posts/how-to-set-up-local-ai-agent — 17 impr | pos 37.4
/posts/vercel-silent-build-failure — 10 impr | pos 12.9
/posts/claude-built-my-astro-blog — 9 impr | pos 13.1
Same API, same week, same window. A different site.
It failed because the numbers were small
This is the part I keep turning over.
The fallback to alphabetical only happened because every row had zero clicks. On a site with traffic, the default sort — clicks, descending — would have put the busiest pages at the top on its own. The report would have been right. Not because the parameter worked, but because the API's default happened to agree with what I wanted.
I would have gone on passing a field that does nothing, reading correct output, for as long as the site had clicks.
The report was wrong in exactly the conditions it exists to report on. It is a monitor for a site that is not yet getting traffic, and the absence of traffic is what broke it. Had it ever started working, it would have started working silently, and I would have had no more reason to check it then than I did on any of the mornings it was lying.
An ignored input is a default you didn't choose
I have a rule on this site that defaults are policy: an unset value is not an absence, because something downstream always picks one. This is the same rule arriving from a direction I hadn't considered.
I did set the value. I set it explicitly, in the request, with the right intent. It just went to a system that had no field to put it in, and a system with no field to put it in cannot tell you that you set nothing — it has already forgotten you tried.
A strict API rejects unknown fields. A permissive one accepts everything and gives every request the same confident shape. That permissiveness is usually described as robustness. What it actually does is move the entire class of "you asked for something that doesn't exist" out of the error channel and into the results, where it arrives looking like an answer.
I've written before about an API that reported 44 updates and performed 24. That one accepted a real field and undid the write inside the same operation. This one is a step further back: the field was never real, so there was nothing to undo. Both return 200. Both hand you well-formed output. The difference only exists in documentation you have to go and read.
Two things I do now
Check the parameter against the schema, not against the response. A 200 tells you the request was accepted. It cannot tell you the request was understood. The only place that distinction lives is the field list, and reading it takes a minute — considerably less than the months I spent not reading it.
Sort where you control the sorting. If ranking matters, pull a generous row limit and order the rows in your own code. Ordering done remotely is ordering you are trusting on someone else's terms, and — as here — possibly not happening at all. Ordering done locally is something you can look at.
The report was never broken. Every morning it fetched real data and formatted it correctly and delivered it on time. It just answered a question I hadn't asked, under a heading that said I had.
Top comments (0)