This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.
When I was building the push notification system for ShelfTalk —...
For further actions, you may consider blocking this person and/or reporting abuse
This one hit home because I shipped the same shape of bug this week in a totally different place: a CI classifier that read test results from stdout. "No output" meant "test died mid-run" — except when the process crashed at exit and the output just lost the flush race. Same trap as document.hidden: a signal pressed into carrying a meaning it was never designed for. Visible != attentive, and silent != failed. My fix rhymed with the delivery-vs-seen idea from the comments — move the truth to a channel that survives the failure mode (a synchronously-written result file) instead of inferring it from a side effect. Great writeup, and the second-monitor detail is exactly the kind of real-user setup no dev machine ever reproduces.
That's a really good parallel. The “signal carrying a meaning it was never designed for” part is exactly what got me here too.
And yeah, the second-monitor setup was the kind of real-world case I completely missed while testing. Glad the story resonated!
the office-machine example makes this a distributed-state bug, not only a visibility bug. each device independently suppresses delivery from local state, but no device has proof that the human saw the message. document.hidden can choose a presentation channel; it should never be allowed to delete the event.
deleting the optimization is the right safe fix. the longer-term state machine is created -> delivered per device -> seen per user, with suppression only after the server has a seen acknowledgement. otherwise one technically visible device vetoes every other device without evidence.
does ShelfTalk already have server-side unread state u could use for that, or is notification state local-only today?
Good catch, and yeah, this is basically @anassBld's distributed-state point taken one step further with an actual engineering answer instead of just "combine signals." You're right that
document.hiddencan only ever be a presentation-channel decision, not a delete-the-event one — that's exactly the trap I fell into, just framed better than I did in the post 😅Went back and checked, and turns out I already halfway did what you're describing:
So no device gets veto power anymore, that part's fixed.
To your actual question: it's split today.
isReadon the Notification model in Mongo, per user, only flipped by an explicitmarkAsReadcall. That's already a legit "seen" signal.So the created → delivered per device → seen per user pipeline you're describing doesn't exist for push yet. If I take this further than a demo project, that's the actual next build, not another local heuristic. Appreciate you laying out the model this clearly.
this is the right split. isRead gives u a user-level fact, while push is only an attempted delivery until each device acknowledges it. the next useful step is a per-device delivery id that folds into one user-level seen state, so presentation choices never decide whether the event exists.
The
document.hiddenfalse positive on multi-monitor setups is a clean example of an API whose name implies a stronger guarantee than it actually delivers — "not hidden" means the OS can render it, not that a human is paying attention. I've run into the same semantic gap in security contexts:document.hasFocus(),Notification.permission === 'granted', and similar presence signals all have different failure modes, and combining them doesn't stack the guarantees the way you'd expect. The delete-the-optimization fix is almost always the right call once you've found the ceiling. One thing worth flagging: LLMs generating notification code tend to reproduce this exact assumption because the MDN prose fordocument.hiddenis technically accurate but elides the multi-monitor nuance entirely — so if you're reviewing AI-assisted PRs in this area, it's worth adding it to the checklist.Thanks @ofri-peretz! for pointing that out! I hadn't considered the LLM-generated code angle, but you're absolutely right. I'll keep that nuance in mind when reviewing AI-assisted code in this area. Really appreciate the insight!
The LLM angle is worth naming precisely, because the failure mode is consistent: models pattern-match on the common service worker examples that call
showNotification()unconditionally, then bolt on foreground detection as an afterthought. Theclients.matchAll()check looks right on inspection but often returns an empty array because the page never registered acontrollerchangelistener — so the worker concludes there's no active client and falls through to the background path every time. You end up with code that passes review, passes manual testing in a fresh tab, and silently does the wrong thing in production. The concede: this isn't unique to AI-generated code, it's just that the subtle async coordination between page and worker is exactly the kind of thing confidence-calibrated generation gets wrong at higher rates.Yeah, this is exactly the nuance I wish I'd included more explicitly in the post. The bug itself definitely isn't “an AI bug” — it's a subtle page ↔ service-worker coordination issue that can survive both review and casual testing.
What AI changes is the probability of that failure mode: the generated code looks completely reasonable because it's assembled from familiar notification/service-worker patterns, while the missing coordination between those pieces is easy to overlook.
That “looks right on inspection, silently fails in the real environment” distinction is probably the biggest lesson I took from this bug. Thanks for articulating it so precisely.
The activation race is the specific mechanism worth naming:
navigator.serviceWorker.controlleris null until the worker reaches the activated state, so any message dispatched before that resolves drops silently — no error, no console warning. In development the race is nearly always pre-won because the worker is already active from a prior session, which masks it completely during manual testing. You're right that this class of bug predates AI tooling entirely; what shifts is that pattern-assembled code tends to get each component correct in isolation while skipping the state-machine coordination between them, so every individual piece looks exactly as it should under review.That activation-race framing is the missing piece — dev masking it because the worker's already warm explains exactly why this kind of bug survives local testing every time. Makes me think the real guard isn't more careful review, it's testing against a cold registration on purpose (hard refresh / unregister first) instead of trusting a warm dev session. Appreciate you naming the actual mechanism instead of leaving it at "coordination issue" — that's the version I'll actually remember next time I touch a service worker.
The useful test for “The Optimization That Was Too Good: Why Our Push Notifications Only Worked When You Weren't Looking” is whether the lesson changes a team decision, not just a local implementation. I’d capture the failure signal, the guardrail that caught it, and the smallest regression test that keeps it from returning. That turns a good postmortem into something another team can actually reuse.
That's a great way to frame it! In my case, though, ShelfTalk was originally a college project and I was the only one building it. I revived it later for the GitHub Finish-Up-A-Thon challenge, which is when I dug much deeper into these edge cases. Still, love the point about turning a bug into something reusable.
Great debugging story. Your "Ah-Ha" moment is actually a specific instance of a general distributed-systems trap: inferring client state from a single signal.
document.hiddentold you about the OS viewport, but you used it to model human attention — and attention was never the thing being measured.The fix that usually survives multi-monitor reality without fully reverting is to combine signals: fire the notification, but let the client acknowledge receipt (focus event, service-worker interaction) mark it as seen. That way you optimize for "don't annoy an attentive user" based on observed behavior rather than predicted attention — closer to how push systems treat delivery vs. read receipts.
The deeper lesson matches what we keep hitting building agent infrastructure: any state inferred from one observation ("visible", "returned 200") should stay provisional until confirmed by a second independent signal. Congratulations on the Finish-Up-A-Thon win, by the way.
This is a really interesting way to look at it. I was basically treating
document.hiddenas “the user isn't looking,” when that's obviously not what it actually tells us. 😅I really like the idea of separating delivery from the “seen” state instead of trying to predict attention. Definitely something I'd implement differently if I were rebuilding this now. Thanks for the detailed response!
The second monitor detail is the whole story. Every analytics tool I use has the same blind spot: a tab open on a side screen keeps logging time on page, so a page nobody has glanced at all afternoon looks like the best performing thing on the site. Same boolean pressed into meaning attention when it only ever meant visible.
Deleting your own clever code is the right ending too.
Exactly! That second-monitor scenario was the one I completely missed.
document.hiddenfelt like the right signal until I realized it was measuring visibility, not attention. Thanks for sharing the analytics perspective too!Great story! I've fallen into the same trap with document.hidden – it's so tempting to "optimize" UX, but the assumption that visible tab = attentive user is dangerous. Your point about multi‑monitor setups and multiple devices is spot on. Sometimes the simplest fix is just to delete the clever code. Thanks for sharing this lesson – it's a great reminder that reliability often beats micro‑optimizations. 👏
Exactly!!
document.hiddenfelt like such a clever little optimization until I actually thought about all the ways “visible” ≠ “user is looking”.And yeah, deleting the clever code was probably my favorite part of the whole bug 😂 Thanks for sharing your experience too, really glad it resonated!
Helpful article ❤️
Nice blog explaining the bug and how you solved it with a good storytelling @dj29
Thanks bro. Actually that's the thing We do a lot of bug fixes, projects in day-to-day life but this is the platform where can talk about it and find people who will vibe with it😅
This is a great example of how a seemingly reasonable optimization can encode the wrong assumption. document.hidden tells you visibility, not attention, and the multi-monitor case makes that distinction painfully clear. The delivery-vs-seen separation is a much more robust mental model. Great debugging story! 👏
Exactly!
document.hiddenlooked like the perfect signal until I realized I was using it to answer a question it was never meant to answer. 😅The delivery-vs-seen distinction is definitely something I'm taking forward from this bug.
Glad it helped! Separating delivery from the 'seen' state makes things a lot more predictable. Let me know if you end up rebuilding it, would love to see what you come up with!
Sure. I'll definitely post if I end up going back to shelftalk again to make it better.
Sounds great, Dhruv. Looking forward to reading about it if you do end up rebuilding it.
If you decide to go the receipts route, I'd love to hear how that changes the architecture for you!
One of those bugs that makes you rethink what “visible” actually means.
I thought I had a clever notification optimization. Turns out document.hidden and “the user is actually paying attention” are two very different things.
Hope this one saves someone else from making the same mistake. 🔔
the
requireHiddenpattern is a classic example of the abstraction leaking. you're assumingvisibilityStatemaps to 'user is reading this' but it actually maps to 'this tab is rendering' — a tab can be visible to the OS but completely out of the user's attention.we hit the inverse in a chat app. suppressed notification sounds when the tab was focused, but our check ran inside an iframe so
document.hiddenwas always false from the iframe's perspective. spent a week thinking the sound suppression logic was broken before realizing the context was wrong.curious what finally surfaced it. did Sentry show the
visibilityStatevalue in the error payload, or did you catch it some other way?Thanks for reading! The interesting part was actually how the bug surfaced — I could reproduce the expected behavior locally, but it was a user mentioning their second-monitor setup that finally exposed what was really going wrong.
document.hiddenwas answering “is this page hidden?” while I was treating it as “is the user looking?” That distinction completely got me.