Commit Graph
4 Commits
Author SHA1 Message Date
Stanislaw dcf1b8b8b5 fix(tracker): track clicks on annotated containers and gracefully handle invalid pushState URLs
Three bugs in src/tracker/index.js, all empirically reproduced.

1. handleClicks() missed clicks on container elements: closest('a,button')
   could not find a non-anchor ancestor with data-umami-event, so a click on
   any descendant of <div data-umami-event=...> went untracked. Reworked to
   closest([data-umami-event]) so any annotated ancestor matches.

2. handlePush() called new URL(url, location.href) outside normalize()'s
   try/catch, so a host page calling history.pushState({}, '', invalidUrl)
   would have umami's wrapper throw a TypeError into the host's router.
   normalize(url) already handles base resolution and catches parse errors.

3. The history hook ran the umami callback BEFORE native pushState, so a
   failed native call (SecurityError on invalid URL, etc.) would still mutate
   currentUrl/currentRef and schedule a phantom pageview. Run native first;
   if it throws, the callback never fires and tracker state stays consistent.

Bug 2 verified: 4/10 representative click scenarios missed before
(span inside div, deep span inside div, a with no href, button inside a),
all 10/10 tracked after.

Bug 1+3 verified: pushState({}, '', invalidUrl) now leaves tracker state
unchanged (currentUrl unchanged, no phantom pageview).
2026-05-07 16:04:16 +02:00
Stanislaw 3f9de35907 perf(tracker): defer INP percentile computation to flush time
The INP observer sorted Object.values(interactions) on every event entry,
even though metrics.inp is only read when sendPerformance() flushes.
Defer the sort + p98 computation to flush, and drain queued observer
entries via observer.takeRecords() to capture the most recent
interactions on pagehide/visibilitychange.

Output is identical (same INP value computed). Savings are largest on
interaction-heavy pages and low-end devices where the per-event sort
compounds.
2026-05-07 15:31:49 +02:00
Stanislaw 71ee000f21 fix: avoid restamping deletedAt + skip Redis DEL for already-soft-deleted slugs
Address Greptile review feedback on #4243.

- Cloud-mode link.updateMany / pixel.updateMany now filter where: { ..., deletedAt: null } so a previously soft-deleted row keeps its original deletion timestamp instead of being restamped with the current time.
- Pre-transaction findMany now selects deletedAt; the Redis invalidation list filters to only live slugs, avoiding harmless but wasted DEL calls for already-soft-deleted entries.

Note: the share.deleteMany cleanup still uses the broad entityId list (not filtered by deletedAt) so that orphan share rows of already-soft-deleted links/pixels are still cleaned up. Filtering the prefetch itself, as Greptile's exact suggestion proposed, would skip those shares while link.deleteMany still hard-deletes the rows, leaving orphan share rows behind. Verified empirically with a 3-scenario reproduction.
2026-05-07 04:14:38 +02:00
Stanislaw a57abbe039 fix: clean up link, pixel, board rows on user/team deletion
deleteUser and deleteTeam left link/pixel/board rows (and their share rows)
in the database after the owner was removed. /q/<slug> and /p/<slug>
also kept serving deleted entries because the routes did not filter
deletedAt and Redis cached lookups for 24h.

- deleteUser: clean up link/pixel/board + shares for the deleted user.
  Cloud mode: soft-delete link/pixel, hard-delete board, only userId-owned.
  Non-cloud: hard-delete everything matching userId or owned teamIds.
- deleteTeam: same cleanup, scoped to teamId.
- /q and /p route handlers: filter deletedAt: null at the call sites
  (not in findLink/findPixel helpers, which would null-deref the
  permission checks at src/permissions/link.ts and pixel.ts).
- Post-transaction Redis invalidation mirrors deleteWebsite.
2026-05-07 03:37:22 +02:00