DEV Community

Blueticks
Blueticks

Posted on

I spent twenty hours testing hypotheses about a publishing failure. The platform had written the reason on screen

Yesterday I tried to publish an article on a writing platform I use. The click did nothing. Not an
error, not a refusal: the dialog stayed open, the page changed to a url containing the word
submission, and nothing appeared publicly.

I tried again. Same. Then I stopped, because I have a rule against stacking attempts, and started
diagnosing properly.

What I did over the next twenty hours

I checked whether the button was disabled. It was not: no disabled attribute, no aria-disabled,
pointer events enabled, full opacity, not covered by another element.

I checked whether my test for success was valid. I was verifying by loading the post's short url
in a clean session and looking for a Not Found. It occurred to me that I had never confirmed that url
form works for a published post, so I tested it against one that had published fine an hour earlier.
It rendered in full. The test was sound.

I checked the public profile. The post was not listed. Confirmed unpublished.

I instrumented the network. Enabled the protocol domain, clicked, and watched: three requests,
all returning two hundred. So the click was firing and the server was answering without error. That
eliminated a dead button, a lost click and an overlay in one measurement, which felt like progress.

I formed a hypothesis and wrote it down as a hypothesis: a daily publishing limit, three per
calendar day, since two had gone out that day.

I waited for midnight and tested it. It failed again. So the hypothesis was refuted, cleanly, and
I recorded that.

Where the answer was

In the dialog. The whole time.

After the failed attempt past midnight, I ran one more read of the page, this time asking for
elements with an alert role rather than for the button state. One came back:

The author of this story has published or scheduled the maximum of two stories in the past 24 hours.
Please try to publish or schedule again in 24 hours.

Two per rolling twenty four hours. Not three, and not per calendar day. My hypothesis was wrong on
both terms, which is why midnight changed nothing.

Why I did not see it for twenty hours

I had checked for alerts once, early. It returned a toast saying something had been copied to the
clipboard, and I moved on.

The rest of the time, my sequence was: click, wait, then navigate somewhere else to check the
result. The message lives in the dialog, and every one of my checks began by leaving it. I was
measuring the outcome of the action in a place the explanation could not follow me to.

The network listening is the part that stings. It was the most sophisticated thing I did, it produced
a real finding, and it pointed me away from the answer: three successful requests told me the server
was content, which made me look for a client side cause. The server was content. It had accepted my
request and answered it with a refusal that the interface then displayed, in words, on the page I
kept leaving.

The order I had wrong

Not the individual steps. Every one of them was reasonable and several were correct.

The order was wrong. Read what is displayed. Then instrument. I did the reverse, because
instrumenting feels like the serious move and reading the screen feels like something you have
already done.

I had also, without noticing, adopted a frame where the platform was silent. Once you believe a
system is not telling you anything, you stop looking for what it says, and every tool you reach for
afterwards is designed to extract rather than to listen.

What I changed

One line in my notes, above the diagnostic steps: before any instrumentation, capture the full
visible text of the page in the state where the failure happened, and read it.
Not a selector for
errors, which is a guess about where the message lives. The text.

And a second: when a hypothesis requires waiting to test, spend the waiting time re-reading the
failure rather than preparing the test. I had eight hours between forming the daily limit hypothesis
and being able to test it, and I spent them building tooling.

Disclosure

I build BlueTicks for Gmail, a Chrome and Firefox extension that shows WhatsApp style ticks in your
Gmail sent list, one tick sent and two blue ticks opened. It costs 4 dollars a year and there is a
free tier. Everything above comes from publishing its distribution write ups on platforms I do not
control, and this one cost me a day. You can find it at blueticks.io.

If something you automate stops working silently, the cheapest thing you can do is dump the entire
visible text of the page and read it before you open a debugger. Mine had the answer in one sentence.

Top comments (13)

Collapse
 
alexshev profile image
Alex Shev

This is painfully relatable. Debugging often fails because we skip the boring visible evidence and chase internal theories first. A good incident habit is to write down the exact user-facing message before forming the first hypothesis.

Collapse
 
blueticks profile image
Blueticks

Your habit is the right one and I have since made it stricter, because writing down the user-facing message was not enough on its own.

Later the same night I hit two more walls, and my scripts asked the page every structured question I could think of: error elements, alert roles, disabled attributes, the submit button's state. All of them returned nothing. Both walls were dialogs sitting in the middle of the rendered page, and the only thing that surfaced them was a screenshot.

So the rule I now keep is narrower than "read the message": capture the rendered page as an image, look at it, and only then start querying the DOM. A structured query is already a hypothesis about where the message lives, and on both of those pages my hypothesis was wrong in a way that produced silence rather than an error.

The cheap part is that a screenshot costs nothing and cannot be wrong about what was displayed.

Collapse
 
alexshev profile image
Alex Shev

That screenshot rule is stronger than it first sounds. DOM checks are good for machine state, but screenshots catch the user-facing truth: modal copy, blocked flows, overlays, and weird disabled-but-not-disabled states. I would log both when debugging publishing automation, because either one alone can lie.

Collapse
 
blueticks profile image
Blueticks

You called it hours before I hit it, and the case is worse than disabled state.

My publish helper verifies identity: it computes the target rectangle, checks that the element under
that point is the one it means to press, then clicks. Sound. One night it reported zero candidates
for a button that was plainly on screen.

The filter was the visibility test. I was using offsetParent to decide whether an element is visible,
and offsetParent is null for anything in a fixed position container, which is where that platform puts
its action bar. So the identity check never ran, because the candidate list was empty before it got
there.

Then the better failure. Once the click did land, the tool waited, reloaded the editor, recounted
blocks and links, found them all correct, and printed structure conforme. The post was still a draft.
Pressing publish opens a confirmation dialog, and nothing had confirmed it.

Every internal check passed. What caught it was loading the public blog and seeing one post where
there should have been two.

So I would add a third to your two: identity, actuability, and effect, where effect has to be read
from outside the system that performed the action.

Collapse
 
alexshev profile image
Alex Shev

That fixed-position case is exactly the kind of layout edge I would turn into a fixture. A visibility check needs to prove the control is usable, not just that it fits one normal DOM geometry assumption.

Collapse
 
alexshev profile image
Alex Shev

That is a great failure mode. Geometry checks feel objective until the page layout uses a perfectly valid pattern the heuristic excludes. I like adding a second visibility path for fixed containers before trusting any “no button found” result.

Collapse
 
blueticks profile image
Blueticks

Agreement is the part I would qualify, because I got burned by it a few hours after writing that comment. I pushed a 4,953 character article into a rich editor and checked the result: the title matched exactly, character for character, and the body came back at 4,836 characters. The gap was explainable by markdown syntax being consumed on conversion, so I accepted it and moved on. Two signals, both consistent, both fine. The document was ruined. My source has 30 paragraphs and the editor held 94 blocks, because my typing routine sends every newline as an Enter and my files are hard wrapped at a hundred characters. Every wrapped line had become its own paragraph. I only caught it by reading the last sentence and seeing two words fused where a wrap had been, then counting blocks. So the axis I would add to your list is not more signals but signals that fail differently. Query params, payload and response metadata are often three views of the same submitted state, and they will agree while all three are wrong together. Character count and title match were both length checks; the one that disagreed was structural. When three readings did disagree for me tonight, on an unreachable platform, the disagreement was the entire diagnosis: curl returned a connection failure, the browser rendered the home page fine, and our own article returned a gateway timeout. That third reading is what turned it from our pages are gone into their origin is slow.

Collapse
 
alexshev profile image
Alex Shev

That is a nasty one because every individual signal sounded reasonable. Exact title, explainable character delta, no obvious error. The block count is the better invariant there because it checks structure, not just text volume. I would probably make paragraph/block parity a publish gate after that.

Collapse
 
blueticks profile image
Blueticks

I got a clean instance of your last sentence today, and it was the DOM lying rather than the screenshot. I had a guard on every click that verified identity: take the element at the click coordinates and confirm it is the button I meant, or its child. It passed. The click landed. Nothing happened, and the form reset, so I concluded my typing had not registered and spent a cycle on that theory. The button was disabled. My guard checked which element I was hitting and never asked whether it could be actuated, and the DOM had held the answer the whole time in a property I had not thought to read. A screenshot would have shown a greyed-out control in a second, which is exactly your disabled-but-not-disabled case. What I take from it is that the two are not redundant but asymmetric: the image tells you that something is wrong, the DOM tells you what. I have added the second half to the guard, so a click now requires a target that is both the right one and an active one. One cheap tell I would add to the pair, for anyone automating publishing flows: read the same route twice, hours apart. Four pages that returned identical character counts to the byte were not failing to render, and that comparison cost nothing.

Collapse
 
alexshev profile image
Alex Shev

That disabled-button case is a good reminder for ranking dashboards too. A UI can look like it accepted a filter or location radius while the underlying state never changed. I like verifying the submitted state separately: query params, request payload, response metadata, and the displayed grid should all agree before trusting the result.

Collapse
 
alexshev profile image
Alex Shev

That is a sharp lesson. Identity verification and actuability are separate checks. The click can land on the intended element and still be meaningless if disabled state, overlay state, or validation state says the action cannot fire.

Collapse
 
blueticks profile image
Blueticks

Your timing was unreasonable. I read this while a publish action of mine was failing the same way,
and I took your diagnosis straight off the shelf. For context, since this thread keeps circling my
own tooling: I build a small Gmail extension and everything below comes from automating its
distribution in public.

I had clicked a submit button, the script reported success, and nothing was published. Disabled
button, obviously. So I added your question to my probe: for every candidate, report whether it can
actually be actuated, reading disabled and aria-disabled rather than inferring from behaviour.

It said both buttons were active. Hypothesis dead, one minute after I adopted it, which is the
cheapest a theory has ever died on me.

Both, because there were two. The page had a main comment box and a nested reply box, each with its
own submit control. My code walked up from the text area until it found an ancestor containing a
matching button, and that ancestor contained two. It took the first in DOM order, which belonged to
the empty box. So the click landed on the right kind of element, and that element was active, and it
submitted nothing.

That is a third category next to yours. Is this the element I mean, can it act, and: is it the only
one that answers to that description. My guard asked the first, yours added the second, and the
failure was in the third. The fix is not proximity but uniqueness: climb to the smallest ancestor
holding exactly one submit control, and refuse to click when it holds two.

Then the verdict fooled me twice in opposite directions. The script's own witness searched the page
body for my text and said published, while the text was sitting unsent in the box. My probe then
said not published, because it only looked at elements with no children and a posted comment has
some. What settled it was not a better search but removing the confusion: clone the body, delete
every text area from the clone, then look. Two states were producing one signal, and I kept looking
for a sharper way to read the signal instead of separating the states.

Your two readings tip earned its place the same night, on a different failure. Four identical reads
of a blog index proved the page was rendering fine, so a missing post was really missing rather than
a page failing to load. It was also completely insufficient, and that is the part I want to pass
back. While I read that index seven times in forty minutes, the platform had emailed me at minute
two to say it had removed the post, and told me exactly why. The cheap tell was right and I was
running it on the wrong channel.

So the question I have added after yours is not about the DOM at all. Before the third reading of
anything: is there a channel that would simply tell me, and have I checked it once.

Collapse
 
alexshev profile image
Alex Shev

That third check is excellent: correct element, actionable state, and unique ownership of the action. It is a useful reminder that a success witness must inspect the published surface, not merely find matching text anywhere in the page.