Skip to content

fix(lib): stop re-subscribing global error listeners on every render#1338

Open
abhay-codes07 wants to merge 1 commit into
supermemoryai:mainfrom
abhay-codes07:fix/error-tracking-listener-churn
Open

fix(lib): stop re-subscribing global error listeners on every render#1338
abhay-codes07 wants to merge 1 commit into
supermemoryai:mainfrom
abhay-codes07:fix/error-tracking-listener-churn

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

Problem

ErrorTrackingProvider is mounted at the root of the app (app/layout.tsx) and installs global window "error" and "unhandledrejection" listeners in an effect keyed on trackError:

const { trackError } = useErrorTracking()

useEffect(() => {
  const handleError = (event: ErrorEvent) => trackError(event.error, { ... })
  const handleUnhandledRejection = (event: PromiseRejectionEvent) => trackError(event.reason, { ... })
  window.addEventListener("error", handleError)
  window.addEventListener("unhandledrejection", handleUnhandledRejection)
  return () => { /* remove both */ }
}, [trackError])

useErrorTracking returns a fresh trackError on every render (it is a plain closure over posthog, session and pathname, not memoized), so the dependency changes every render. That tears down and re-adds both global listeners on every render of a provider that wraps the entire app, including on each navigation and session refresh. It is wasted work on a hot path, and it leaves a small window on each cycle where a synchronously thrown error can land between the removeEventListener and the following addEventListener.

Fix

Keep the latest trackError in a ref and register the listeners once for the provider's lifetime, dispatching through the ref:

const trackErrorRef = useRef(trackError)
useEffect(() => { trackErrorRef.current = trackError }, [trackError])

useEffect(() => {
  const handleError = (event: ErrorEvent) => trackErrorRef.current(event.error, { ... })
  const handleUnhandledRejection = (event: PromiseRejectionEvent) => trackErrorRef.current(event.reason, { ... })
  window.addEventListener("error", handleError)
  window.addEventListener("unhandledrejection", handleUnhandledRejection)
  return () => { /* remove both */ }
}, [])

The listeners are now added once and removed on unmount, and each report still reads the current posthog / session / pathname context through the ref. No functional change to what gets tracked, just no more churn.

ErrorTrackingProvider wraps the whole app and keyed its window "error" and
"unhandledrejection" listener effect on trackError. trackError is rebuilt on
every render because it closes over posthog, session and pathname, so the two
global listeners were torn down and re-added on every render of the provider,
including on each navigation and session tick. Hold the latest trackError in a
ref and register the listeners once for the provider's lifetime, dispatching
through the ref so reports still use current posthog/session/pathname context.
Copilot AI review requested due to automatic review settings July 22, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants