DEV Community

Bharath Kumar
Bharath Kumar

Posted on

The Silent Bug: How a DOM Click Target Issue Was Breaking Formbricks Surveys

Here's something that will frustrate you once you see it.
You set up a Formbricks survey trigger. Configure it to fire when a user clicks .submit-btn. Deploy it. Test it yourself — works perfectly. Ship it.
Then nothing happens. Zero surveys triggered. No errors. No warnings. Just silence.
That's the bug I fixed in PR #7327. And the reason it's interesting isn't the fix itself — it's what it taught me about how SDKs fail in the real world.

What Was Actually Breaking
The Formbricks JS SDK lets you trigger surveys based on user actions — including CSS selector click actions. You tell it "when someone clicks .feedback-btn, show this survey."
The SDK listened for click events and checked if the clicked element matched your selector:
typescriptif (!targetElement.matches(".feedback-btn")) {
return false // action dropped, survey never shows
}
Looks fine. Works fine — until your button has any content inside it.
html
...
Give Feedback

Now when a user clicks the SVG icon inside the button, event.target is the — not the .feedback-btn. The .matches() check runs against the SVG. It returns false. The survey is dropped silently.
The only way to trigger the survey was to click the exact 1-2px padding of the button where no child element exists. Which nobody does.

Why Nobody Reported It Directly
This is the part that stuck with me.
The bug had almost certainly been there for a while. But nobody filed an issue saying "event.target doesn't match the selector for nested elements." They filed issues saying "the survey trigger doesn't work reliably" or "only fires sometimes." They assumed it was a configuration problem and gave up.
The bug was invisible because it failed silently. No console error. No warning. Just... nothing.
This is a classic SDK failure mode — the kind that's hard to debug because the feedback loop is broken. The user did everything right. The SDK said nothing. The survey never showed.

How Common Was This Really?
Extremely common. This affects virtually every real-world button.
Modern design systems — shadcn/ui, Radix UI, MUI, Headless UI — almost always put content inside buttons. Icon buttons. Buttons with text wrappers. Buttons with badges. Every single one of these would silently fail with the old behavior.
When I demonstrated the reproduction to Dhruwang:

Click the SVG → Survey does not trigger ❌
Click the text → Survey does not trigger ❌
Click the 1-2px button edge → Survey triggers ✅

His response: "Looks good 🚀" — merged.

The Fix: .closest() as a Fallback
The solution is a DOM method called .closest(). It walks up the DOM tree from the clicked element until it finds an ancestor that matches the selector.
typescript// Before — only checks the exact clicked element
if (!targetElement.matches(selector)) return false

// After — falls back to checking ancestors
const matchesDirectly = targetElement.matches(cssSelector)

if (!matchesDirectly) {
const ancestor = targetElement.closest(cssSelector)
if (!ancestor) return false
matchedElement = ancestor // use the button, not the SVG
}
When the user clicks the SVG icon, .closest(".feedback-btn") walks up the DOM, finds the parent button, and returns it. The survey fires correctly.
Performance note: .closest() is only called as a fallback. If the direct match succeeds — which it does for simple elements — the code takes the same fast path as before. No regression for the common case.

What This Taught Me About SDK Design
Three things that I keep coming back to:

  1. Silent failures are worse than loud failures. An error in the console is annoying. A survey that silently never fires is a support ticket three weeks later when the customer asks why they have zero responses. SDKs that fail silently destroy trust slowly. If the fix fails for some reason, it should say so.
  2. The gap between "works in testing" and "works in production" is the DOM. In testing you click the button. In production users click whatever their cursor lands on — which is almost always a child element. The SDK has to handle the messy reality of how people actually interact with interfaces, not the clean version you test with.
  3. Event delegation is harder than it looks. event.target gives you the most specific element that was clicked. That's often not the element you care about. Any SDK that listens to click events and matches CSS selectors needs to account for this — otherwise it breaks on every button with an icon.

The Regression Tests
I added three tests that fail on the old code and pass on the new:
✅ Clicking a child inside .my-btn → action fires correctly
✅ Clicking an element with no matching ancestor → correctly returns false

✅ Clicking the target directly → .closest() is not called (fast path preserved)
The third test matters. It confirms the fix doesn't slow down the common case. .closest() is only invoked when the direct match fails.
232 tests. 19 files. All passing.

Why I Picked This Up
I was exploring the Formbricks codebase looking for reliability gaps — places where the SDK could fail silently without the developer knowing. This was one of the clearest examples I found.
The issue (#7314) had been sitting open. The reproduction wasn't obvious unless you thought about how click events actually propagate through the DOM. Once I understood it, the fix was clear.
That's usually how it goes with SDK bugs. Understanding the problem takes 90% of the time. Writing the fix takes 10%.

Links

PR #7327: https://github.com/formbricks/formbricks/pull/7327
My GitHub: https://github.com/bharathkumar39293
WebhookDrop (another project in this space): https://web-hook-drop-t4k6.vercel.app

Top comments (0)