Tracker: click handler assumes e.target is an Element, throws on clicks dispatched on document

Open Beginner friendly
#4,473 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
typescript
Domain
analytics

Research direction

Start at the click handler in src/tracker/index.ts around lines 348–351 and reproduce the issue with document.dispatchEvent(new MouseEvent('click', { bubbles: true })). Verify that clicks whose targets are not Elements no longer throw, while ordinary element clicks still reach the tracker's event lookup and tracking behavior.

Written by the indexing model from the issue text.

Description

Summary

The tracker's click handler casts e.target to Element and immediately calls .closest() on it. Any click event whose target is not an Element — most easily document itself — throws TypeError: e.target.closest is not a function from inside the listener.

https://github.com/umami-software/umami/blob/ca661c7057984aa98ed4f7083d84dae2f65bfcb0/src/tracker/index.ts#L348-L351

const onClick = (e: MouseEvent) => {
  const el = e.target as Element;
  const eventEl = el.closest(`[${eventNameAttribute}]`);

The as Element cast is the assumption that breaks: the listener is registered on document in the capture phase, so it receives every click in the page — including ones dispatched programmatically on document or on any other non-Element node.

Steps to reproduce

On any page with the tracker loaded, in the console:

document.dispatchEvent(new MouseEvent('click', { bubbles: true }));

Result (captured on a live page running the tracker, window.onerror):

{
  "message": "Uncaught TypeError: e.target.closest is not a function",
  "filename": "https://<host>/script.js",
  "lineno": 1,
  "colno": 2152
}

typeof document.closest is "undefined"Document does not implement Element, so there is nothing to call.

Why it matters in practice

We did not reproduce this synthetically first — it arrived from a real visitor (Chrome 151 / macOS) and landed in our error tracker attributed to our own application, because the tracker is proxied first-party so ad blockers don't drop it. Browser extensions and third-party widgets dispatch synthetic clicks on document routinely, so this surfaces as unexplained noise in the host site's error reporting rather than anything the site owner can act on.

Functional impact is small — an exception in one listener doesn't stop the others, so pageview beacons are unaffected and only that one click's data-umami-event tracking is lost — but the error is unavoidable and unsilenceable from the host page.

Suggested fix

Guard instead of cast:

const onClick = (e: MouseEvent) => {
  const el = e.target;
  if (!(el instanceof Element)) return;
  const eventEl = el.closest(`[${eventNameAttribute}]`);

instanceof Element also handles text nodes and window targets, not just document.

Environment
  • Tracker served by umami 3.3.0 (ghcr.io/umami-software/umami:postgresql-latest), self-hosted, PostgreSQL
  • Code path unchanged on master (3.3.1) as of ca661c7
  • Chrome 151, macOS
Dominant language
TypeScript
Stars
38.9k
Forks
8.1k
Avg merge
5d 19h
Merged PRs (30d)
21

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from umami-software/umami

All issues in umami-software/umami

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.