Guides
Detection guide

How to Detect Dead Clicks: Clicks That Do Nothing

By Sazabi Team

Last verified on Nov 8, 2026.

A dead click is a click that does nothing. The user presses a button, a link or a card, and the page stays exactly as it was. Nothing errors, nothing loads and nothing is saved. Because no exception fires, your error tracker never sees it, and the user either retries or leaves.

There are two kinds. On the client, the handler never runs or changes nothing. On the server, the request succeeds but the write never happens. Both look like silence in your usual dashboards, so you need to log the outcome of an action, not just the action.

What the evidence looks like

For the client case, you want a click event followed by no effect. This example shows a click event with an outcome field you compute in the browser:

{
  "event": "click",
  "ts": "2026-11-08T14:02:11.482Z",
  "page": "/settings/profile",
  "target": "button#save-profile",
  "effect": "none",
  "ms_to_effect": null,
  "session_id": "s_8f2c"
}

For the server case, the request log shows success and the data layer shows nothing changed. This example is a request log line with a row count:

2026-11-08T14:02:11.601Z POST /api/profile status=200 rows_affected=0 user=u_4411 duration_ms=38

How to detect client-side dead clicks

  1. Capture clicks with a stable selector. Use a data-testid or an id, not a generated class name.
  2. After each click, start a short timer and watch for any effect. A MutationObserver reports DOM changes. Also count navigation, scroll and outgoing requests as effects.
  3. If the timer ends with none of these, emit the click with effect: none.
  4. Aggregate by selector and page.
SELECT page, target,
       count(*) AS clicks,
       count(*) FILTER (WHERE effect = 'none') AS dead,
       round(100.0 * count(*) FILTER (WHERE effect = 'none') / count(*), 1) AS dead_pct
FROM click_events
WHERE ts > now() - interval '1 day'
GROUP BY page, target
HAVING count(*) >= 50
ORDER BY dead_pct DESC
LIMIT 20;

Some analytics tools ship this already. PostHog documents a dead click as a click not followed by a change to the page and exposes a capture_dead_clicks option. Sentry registers dead clicks on button, input and anchor elements when no DOM update or scroll follows within 7 seconds. If you roll your own, copy that idea and apply it to every interactive element you care about.

How to detect server-side dead clicks

Log the number of rows each write touched, then look for successful writes that touched none.

SELECT route,
       count(*) AS writes,
       count(*) FILTER (WHERE status BETWEEN 200 AND 299 AND rows_affected = 0) AS no_op
FROM request_log
WHERE method IN ('POST', 'PUT', 'PATCH', 'DELETE')
  AND ts > now() - interval '1 hour'
GROUP BY route
HAVING count(*) FILTER (WHERE status BETWEEN 200 AND 299 AND rows_affected = 0) > 0
ORDER BY no_op DESC;

Check the client too. A fetch call only rejects on a network failure. A 404 or 500 still resolves with a Response, so code that never checks response.ok shows a success state after a failure. The MDN guide to fetch describes this behavior.

Thresholds and pitfalls

Compare a control against its own history, not against zero. Plenty of clicks are meant to do nothing: copy buttons that only touch the clipboard, text a user clicks while reading and controls in a disabled state. Exclude these by selector. PostHog supports a .ph-no-deadclick class and Sentry supports an ignore list for the same reason.

Set the wait window above your slow-path response time. A 3 second timer flags every request that is merely slow. Require a minimum sample per selector, such as 50 clicks, or one confused user will top your list. Optimistic interfaces update the DOM before the server answers, so a visible change does not prove the write landed. Pair the client check with the server check.

Watch for jumps after a release. A dead click rate that doubles on one control the day after a deploy is a regression, even if the absolute rate is small.

What to do when it fires

  1. Open a recorded session or reproduce the click with the browser console open.
  2. Look for an overlay covering the control, a disabled attribute set too early or a handler that threw. Check client errors from the same session and time.
  3. If the user clicked repeatedly, the same control will also show up in rage clicks.
  4. On the server, return a non-2xx status when nothing was written, and add an alert on the no-op query above.

The Sazabi Browser SDK captures navigation, clicks, errors and network calls with trace context, which gives you the raw events these queries need. You can also send server logs over OpenTelemetry so click events and request logs sit side by side.

Frequently Asked Questions

What is a dead click?

A dead click is a click or tap that produces no visible change: no DOM update, no navigation and no request. It usually means a broken handler, a disabled control or an element that only looks clickable.

How is a dead click different from a rage click?

A dead click is one click with no effect. A rage click is a burst of repeated clicks on the same spot, often caused by dead clicks. Both point to controls that do not respond.

Can the server cause dead clicks?

Yes. If an endpoint returns 200 but writes nothing, the page looks fine and the click still did nothing. Log rows written next to the status code to catch it.

How long should I wait before calling a click dead?

It depends on your slowest legitimate action. Sentry documents a 7 second window for its own detection. Pick a window above your p95 response time and tune it per control.

Sources

  1. Definition of a dead click as a click not followed by a change to the page, the capture_dead_clicks option and the ignore class and selector list (accessed Nov 8, 2026)
  2. Dead clicks are called slow clicks; they are detected on button, input and anchor elements and count when no DOM update or scroll follows within 7 seconds; selectors can be ignored (accessed Nov 8, 2026)
  3. MutationObserver watches for changes to the DOM tree (accessed Nov 8, 2026)
  4. A fetch promise rejects on network errors but fulfills with a Response for HTTP error statuses such as 404, so the status must be checked (accessed Nov 8, 2026)