A Green API Is Not a Working Page

javascript dev.to

I spent a morning connecting a session-recording tool to OneFindMe, the AliExpress product search engine I run in twelve languages, expecting to learn something about user behaviour. Instead it handed me a list of JavaScript errors, and every single one turned out to be a bug that had been in production for weeks while every check I had was green.

That is the part worth writing down. Not the bugs — bugs are ordinary. The fact that my entire measurement apparatus was structurally incapable of seeing them.

The one that cost the most

The error text was Unexpected identifier 's'. Twenty-eight occurrences.

The card that renders each product built its click payload like this:

const pJson = JSON.stringify({ id, title, price })
  .replace(/'/g, "'")
  .replace(/"/g, """);

return `<a href="${url}" onclick="onProductClick('${pJson}')">…</a>`;
Enter fullscreen mode Exit fullscreen mode

That escaping is correct — for an HTML attribute. It is wrong for a JavaScript string, and the attribute is both.

The HTML parser decodes ' back into a bare apostrophe before the browser compiles the attribute as JavaScript. So for a product titled Women's Vacation Dress, what the engine actually tries to compile is:

onProductClick('{"id":1,"title":"Women's Vacation Dress"}')
Enter fullscreen mode Exit fullscreen mode

The string ends at Women'. Syntax error. The handler never compiles.

HTML entity escaping does not protect a JavaScript string literal in an inline handler. Entities are decoded first.

I proved it in the live page rather than reasoning about it, with a control:

const title = "Women's Vacation Dress";           // vs "Elegant Satin Dress"
host.innerHTML = `<a onclick="probeClick('${pJson}')">card</a>`;
document.getElementById('probeCard').click();
// apostrophe title  → handler did NOT run
// clean title       → handler ran
Enter fullscreen mode Exit fullscreen mode

Then I counted how often it mattered: 9 of 24 products on one search, 14 of 42 on another. Roughly a third of every result page.

The damage is worth being precise about, because the obvious guess is wrong. The `` was untouched, so no sale was lost — shoppers still reached the retailer. What broke was everything the handler did: the favourite button and the share button silently did nothing on those products, and the click never reached my analytics.

Which means every click-through rate I had measured, for months, was an undercount — and I had been making product decisions on those numbers.

The fix is the one you already know: get the data out of the JavaScript entirely.

`js
<a href="…" data-act="open" data-p="${pJson}">…</a>
`

`js
document.addEventListener("click", (e) => {
const el = e.target.closest("[data-act][data-p]");
if (!el) return;
dispatch(el.dataset.act, el.dataset.p); // parser treats it as text; nothing compiles
});
`

The button that had not existed for months

Next error: Cannot read properties of null (reading 'classList').

`js
function toggleDeals() {
dealsActive = !dealsActive;
const btn = document.querySelector(".deals-btn");
btn.classList.add("active"); // btn is null
if (currentQuery) reSearch(); // never reached
}
`

.deals-btn did not exist anywhere on the page. It had been renamed at some point and this function never updated. It threw on line three, before the line that actually did the work — so the two visible buttons that called it were completely inert.

Then I opened the session recording attached to that error, and it stopped being an abstraction:

`js
01:02 clicked "Show all"
01:07 clicked "Show all"
01:27 clicked "Show all"
01:28 clicked "Show all"
01:28 clicked "Show all"
`

Five clicks in 26 seconds, inside a seven-minute session where the same person also hit "load more" nine times. That is not a confused user. That is someone who wanted to buy something, pressing a button that did nothing, until they gave up.

A handler that throws leaves no trace on the server. No 500, no slow query, no error log. The only signature it leaves is a human being clicking the same thing over and over — and you can only see that if something is recording the page.

Three more, all invisible to the API

The product rows had no CSS at all. The page carried its own inline styles and had stopped loading the shared stylesheet; nobody noticed that the rules for one component never came with it. Cards rendered as full-width inline elements with uncapped images — one product per phone screen. The check that found it is one line:

`js
[...document.styleSheets].some(s =>
[...s.cssRules].some(r => (r.selectorText || "").includes("product-card")))
// false
`

Then the cards collapsed to one pixel. The message list is a column flexbox that overflows, and flex-shrink defaults to 1, so every child gets squeezed to make the container fit. The images were fully downloaded and completely invisible:

`js
card.getBoundingClientRect().height // 1
card.querySelector('img').naturalWidth // 480 ← the image was fine all along
`

flex-shrink: 0 took the card from 1px to 167px.

And loading="lazy" never fired inside that scroll container. Nine cards sat in the viewport with zero decoded images, while the exact same URL loaded instantly when fetched directly:

`js
new Image().src = img.src; // loads, 480px
img.complete // false, indefinitely
`

Forcing eager on one loaded it immediately. Lazy loading is the wrong default for a handful of thumbnails the user explicitly asked to see; I removed it.

What I assert on now

The common thread is not carelessness. It is that my assertions were about the wrong layer. I was checking that the server returned the right JSON quickly, and it always did. The bugs all lived between the JSON and the pixels.

So after any change that reaches a screen, I now check the rendered DOM, not the response:

  • the order of nodes, not just their presence — reordering a streaming response once cut one sentence into three pieces across two product rows, and every timing measurement stayed perfect while it happened;
  • each element's measured boxgetBoundingClientRect(), because "the element exists" and "the element is one pixel tall" are the same to a selector;
  • whether images actually decodedimg.complete && img.naturalWidth > 1, because a broken image and a lazy image are indistinguishable from src;
  • whether a CSS rule for the class exists at all, before debugging the layout it supposedly produces;
  • and all of it at a phone viewport, since that is where most of the traffic is.

Two of these bugs were years old. Both were found within five minutes of pointing something at the rendered page instead of the API — the same engine I described in an earlier piece about what broke when I put an LLM in front of product search, where the lesson was also that the interesting failures were in the seams rather than the model.

There is a broader version of this. Backend observability got very good — traces, structured logs, percentile latency — and it is all measuring the half of the system that was not broken here. The frontend half gets a synthetic Lighthouse run on a fast laptop and, if you are lucky, an error counter nobody reads.

The cheapest fix is not a new tool. It is to stop treating a green endpoint as evidence about a page, and to write one assertion that can only pass if the thing the customer sees is actually there.

The engine is OneFindMe — free, no signup. Every bug above came out of its production logs, and it is still finding new ways to break.

Source: dev.to

arrow_back Back to Tutorials