Symptom
New instrumentation measured whether a modal was actually seen (as opposed to merely mounted), using IntersectionObserver on the dialog node. Under browser automation it reported reach = 0 while the dialog was unambiguously rendered: getBoundingClientRect() returned 622x439 at top: 260, and element.checkVisibility() returned true.
The observer callback was never invoked at all — not with isIntersecting: false, not once.
Cause
IntersectionObserver delivery is tied to the rendering lifecycle: the spec runs the intersection steps in "update the rendering", which browsers skip for pages that are not being composited — backgrounded tabs, non-foregrounded windows, and several headless/automation configurations. No frames, no observations, no callback. requestAnimationFrame starves the same way, so anything built on it inherits the bug.
This is the dangerous shape for a metric: it produces a confident zero, which reads exactly like a real product finding ("nobody sees the prompt"). We were investigating a funnel step stuck at 0%, so a phantom zero would have corroborated the wrong conclusion and closed the investigation.
Procedure
For "was this actually visible to a user" instrumentation, use a bounded poll on synchronous geometry rather than a lifecycle-driven observer:
const started = Date.now();
const timer = setInterval(() => {
const r = node.getBoundingClientRect();
const visible = r.width > 0 && r.height > 0 &&
(node.checkVisibility?.({ checkOpacity: true, checkVisibilityCSS: true }) ?? true);
if (visible) { clearInterval(timer); emit_shown(); }
else if (Date.now() - started > 10_000) { clearInterval(timer); } // always bound it
}, 50);getBoundingClientRect() and checkVisibility() are synchronous and force layout on demand, so they answer correctly in a background tab. Bound the poll so a never-visible node cannot leak a timer.
Validating any zero-valued metric
- Establish a positive control first. Make the thing happen deliberately and confirm the event fires. A zero is only evidence about a metric once you have separately shown the emitter is reachable on that path.
- Cross-check with a mechanism that has different failure modes. Geometry (
getBoundingClientRect) and lifecycle (IntersectionObserver) fail for unrelated reasons; agreement between them is meaningful, and disagreement localizes the bug. - Be suspicious when a new metric immediately confirms your hypothesis. That is the moment to test the instrument rather than believe it.
Related trap in the same work
Do not mix visibility-triggered and intent-triggered events in one funnel rate. An IntersectionObserver-driven "seen" event is close to automatic, while a button tap is deliberate; a ratio spanning both is not a conversion rate, and it will swing wildly with no behavioural change.