track_links() (and track_forms()) attach click listeners to the specific DOM elements matching your selector at the moment you call them - not via event delegation - so any element added to the page afterward (a React re-render, AJAX-loaded content, a modal opened later) was never wrapped with a listener and will not fire an event on click.
Why this happens
Older analytics helper methods like track_links() were written for static, server-rendered pages, where every link exists in the DOM before your script runs once at page load. Modern apps mount and unmount elements constantly, so a one-time, selector-based binding misses everything created afterward - and because there is no error, it just silently tracks nothing for those elements, which looks like "clicks are not tracked" when the real problem is timing.
Fix it
- Stop relying on track_links()/track_forms() for dynamic UI; attach a delegated event listener at a stable ancestor (for example
document.body) usingaddEventListener. - Inside the delegated handler, check
event.target.closest("[data-track]")(or your chosen selector) and callmixpanel.track("Link Clicked", {...})manually with whatever properties you need from the matched element. - In component frameworks, prefer attaching the tracking call directly in the component's onClick handler rather than any global selector-based binder - this ties tracking to the same lifecycle as the element.
- If you do re-run track_links() after content changes, scope it carefully - re-running it against elements it already bound can double-bind listeners and produce duplicate events.
How to verify it worked
In the browser console, click a dynamically added element and confirm a mixpanel.track() call fires (watch the Network tab for the request, or a temporary console.log inside the handler). Then check Mixpanel Live View for the event with the expected properties attached.