To track outbound link clicks and file downloads, turn on your analytics tool’s automatic link tracking: enhanced measurement in GA4, the Outbound links and File downloads options in Plausible, enableLinkTracking in Matomo (already in its default snippet) or the extra auto-events.js script in Simple Analytics. Umami and Fathom have no automatic option, so you add the click listener from their docs, and PostHog’s autocapture records every link click without telling downloads apart.
Automatic tracking only sees clicks on real <a href> links, and the tools disagree on everything around them. On 3 November 2026 we clicked 27 kinds of link and button on one test page with each of eight tools in headless Chrome 155. None recorded a link opened with window.open() or a link behind a /go/ redirect on your own domain as outbound, and only Matomo counted a file built in JavaScript as a download. invisit, the analytics tool we build, is one of the eight; how it decides is near the end.
Which analytics tools track outbound clicks and downloads automatically?
GA4, Plausible, Matomo, Simple Analytics and invisit record both without code. PostHog records every link click and flags the ones that leave your site. Umami and Fathom need code. As of November 2026:
| Tool | Outbound clicks | File downloads | Where you switch it | Billed |
|---|---|---|---|---|
| GA4 | Automatic: event click with outbound: true |
Automatic: event file_download |
Admin, Data streams, Enhanced measurement | No |
| Plausible | Automatic: goal Outbound Link: Click |
Automatic: goal File Download |
Site settings, Tracking; init() in the npm package |
Yes, as custom events |
| Matomo | Automatic: Outlinks report | Automatic: Downloads report | enableLinkTracking in the snippet |
Matomo Cloud: one hit each |
| PostHog | Autocapture: $autocapture with a destination URL |
No: a file link is an ordinary click | The autocapture option, on by default |
Yes, every autocaptured event |
| Umami | Manual: attributes or the script in its docs | Manual | Your code | Umami Cloud: event plus each property |
| Fathom | Manual: fathom.trackEvent() in a click listener |
Manual | Your code | Yes, in the pageview allowance |
| Simple Analytics | With auto-events.js |
With auto-events.js |
data-collect on that script |
Paid plans only, as data points |
| invisit | Automatic: event outbound_link |
Automatic: event file_download |
outboundLinks, fileDownloads, on by default |
Yes, as custom events |
Sources: GA4 enhanced measurement, Plausible’s outbound links, file downloads and plans, Matomo’s tracking guide and what counts as a hit, PostHog autocapture and pricing, Umami’s outbound links guide and pricing, Fathom’s external clicks guide and pricing, Simple Analytics’ automated events and pricing, and invisit’s options and billing.
Umami’s suggested script stores the URL as a property, so each click costs two Umami Cloud events.
What counts as an outbound link or a download in each tool?
Most tools call a link outbound when its host differs from the page’s, which makes every subdomain outbound; GA4 and invisit are the exceptions. Only Simple Analytics records mailto: and tel: links on purpose. Downloads are a list of file extensions matched against the end of the link, and the lists differ widely:
| Tool | Link to another subdomain | mailto: and tel: |
Download extensions | Change the list |
|---|---|---|---|---|
| GA4 | Internal | Ignored | 29 | No |
| Plausible | Outbound | Ignored | 26 | fileExtensions replaces it |
| Matomo | Outbound, unless in setDomains |
Ignored | 86, plus download attributes |
set, add and removeDownloadExtensions |
| PostHog | Outbound | Plain clicks | None | Not applicable |
| Umami’s docs script | Outbound | Recorded as outbound | None | Not applicable |
| Fathom’s docs script | Outbound | Recorded, domain empty | None | Not applicable |
| Simple Analytics | Outbound | Own email_ and phone_ |
7 | data-extensions |
| invisit | Internal | Ignored | 30 | No |
GA4’s documented pattern is pdf|xlsx?|docx?|txt|rtf|csv|exe|key|pp(s|t|tx)|7z|pkg|rar|gz|zip|avi|mov|mp4|mpe?g|wmv|midi?|mp3|wav|wma, with no .dmg or .apk. Plausible’s 26 add .dmg but not .apk. Simple Analytics defaults to pdf, csv, docx, xlsx, zip, doc, xls. Matomo’s 86 include .png, .jpg, .js and .xml, so a link to a full-size image is a download.
The test turned up details the docs leave out. Plausible’s tracker (npm package 0.4.7, published 28 October 2026) compares extensions case-sensitively and keeps the #fragment, so it skipped /files/Price-List.PDF?v=3 and /files/manual.pdf#page=4; GA4, Matomo, Simple Analytics and invisit counted both. A PDF on a CDN at another domain became an outbound click in Plausible, as its docs warn, a download in Matomo, Simple Analytics and invisit, and two events at once in GA4.
Which clicks did each tool record in our test?
Each row is one fresh load of a page on qa.example.com and one click, driven by Playwright in headless Chrome 155. Every tool ran its current script (Plausible as its npm package 0.4.7, Matomo 5.14.2, posthog-js 1.445.1, Simple Analytics v12) with its requests answered on our machine, so nothing reached the vendors; invisit’s went to our development collector. Umami and Fathom ran the scripts from their docs. “Outbound” and “Download” mean the tool sent that event, “Click” a generic click event, “No” nothing.
| What the visitor did | GA4 | Plausible | Matomo | PostHog | Umami | Fathom | Simple Analytics | invisit |
|---|---|---|---|---|---|---|---|---|
| Clicked an external link | Outbound | Outbound | Outbound | Outbound | Outbound | Outbound | Outbound | Outbound |
Clicked a link to docs. subdomain |
No | Outbound | Outbound | Outbound | Outbound | Outbound | Outbound | No |
Clicked a mailto: link |
No | No | No | Click | Outbound | Outbound | No | |
Clicked /files/report.pdf |
Download | Download | Download | Click | No | No | Download | Download |
Clicked Price-List.PDF?v=3 |
Download | No | Download | Click | No | No | Download | Download |
Clicked manual.pdf#page=4 |
Download | No | Download | Click | No | No | Download | Download |
| Clicked a PDF on another domain | Both | Outbound | Download | Outbound | Outbound | Outbound | Download | Download |
Clicked a .dmg |
No | Download | Download | Click | No | No | No | Download |
Clicked an .apk |
No | No | Download | Click | No | No | No | Download |
Clicked /export?format=csv with download |
No | No | Download | Click | No | No | No | No |
Clicked /go/partner (redirects) |
No | No | No | Click | No | No | No | No |
| Middle-clicked an external link | Outbound | Outbound | Outbound | No | No | No | No | Outbound |
| Right-clicked an external link | No | No | Outbound | No | No | No | No | No |
| Pressed Enter on a focused link | Outbound | Outbound | Outbound | Outbound | Outbound | Outbound | Outbound | Outbound |
Button calling window.open() |
No | No | No | Click | No | No | No | No |
| Exported a CSV built as a Blob | No | No | Download | Click | No | No | No | No |
| Clicked a link rendered after load | Outbound | Outbound | Outbound | Outbound | No | No | No | Outbound |
invisit’s subdomain cell comes from a rerun the same day, after invisit began treating the site’s own hosts as internal; the first run recorded it as outbound, and the rerun also sent nothing for a link to the www. host.
Every tool recorded Ctrl+click, Shift+click and target="_blank" links, and treated a button that set location.href like the window.open() row. Simple Analytics’ auto-events.js and the Umami and Fathom doc scripts go through the page’s links once, when it loads, so they missed the link our page rendered a second later, the way a single-page app renders most links (more on single-page apps).
Why are outbound click events lost when the page unloads?
A request started in a click handler is cancelled when the browser leaves the page, unless it is sent with navigator.sendBeacon() or fetch() with keepalive: true. On a fast connection the request is usually out before the next page arrives, which is why this bug passes testing on localhost and can lose clicks on slow networks.
To reproduce it, we gave an external link a click handler that sent one request, and held that request inside the browser for 300 ms before letting it out, standing in for a slow connection to the analytics host. Three runs gave the same result:
| Sent with | Arrived, no delay | Arrived, 300 ms delay |
|---|---|---|
fetch() |
Yes | No |
fetch() with keepalive: true |
Yes | Yes |
navigator.sendBeacon() |
Yes | Yes |
XMLHttpRequest |
Yes | No |
new Image().src |
Yes | No |
With the same 300 ms delay on their own requests, the outbound events of invisit, Plausible, Umami, Matomo, Fathom and Simple Analytics all arrived. GA4 and PostHog queue the click and send it in a batch as the page is hidden, beyond our delay’s reach; both arrived without it. What differed was the visitor’s wait. Umami’s tracker cancels a same-tab click on a data-umami-event link, waits for its request, then navigates, so with the delay the next page started 374 ms after the click. Matomo sends a beacon, then holds the page in a loop for up to 100 ms before it unloads (its matomo.js source): over 14 same-tab clicks its navigations started a median 169 ms after the click, against 64 to 78 ms for the other tools and 63 ms with invisit’s link tracking off.
If you write your own, send the event without touching the click:
// Sends one event for each click on a link that leaves the site or downloads a file.
const ENDPOINT = '/api/events' // your collector
const DOWNLOAD = /\.(pdf|zip|csv|xlsx?|docx?|pptx?|dmg|exe|msi|apk)$/i
function onLinkClick(event) {
if (event.type === 'auxclick' && event.button !== 1) return // middle button only
const link = event.target.closest?.('a[href]')
if (!link || !/^https?:$/.test(link.protocol)) return // skips mailto:, tel:, blob:
const download = DOWNLOAD.test(link.pathname)
if (!download && link.hostname === location.hostname) return
const body = JSON.stringify({
name: download ? 'file_download' : 'outbound_link',
url: link.href,
page: location.pathname,
})
// keepalive lets the request outlive the page; the click is never delayed.
fetch(ENDPOINT, { method: 'POST', body, keepalive: true }).catch(() => {})
}
for (const type of ['click', 'auxclick']) {
document.addEventListener(type, onLinkClick, { capture: true, passive: true })
}We ran this script through the same 27 cases: it recorded the same rows as invisit’s built-in tracking, kept the query string, and lost nothing with the 300 ms delay. Avoid the older pattern of event.preventDefault() followed by a timed redirect, which slows every click and, unless it checks for them, breaks Ctrl+click and target="_blank".
Why are links opened with window.open() not tracked?
Automatic link tracking reads the href of the <a> that was clicked, so a button that calls window.open(url) or sets location.href gives it nothing to read. None of the eight tools recorded either as outbound; PostHog logged a click on the button with no destination. Send the event in the handler that opens the link:
document.querySelector('#open-demo').addEventListener('click', () => {
const url = 'https://demo.example.org/'
window.invisit.track('outbound_link', { url })
window.open(url, '_blank')
})In invisit, an outbound_link event with a url property lands in the same Outbound report as the automatic ones; we checked on our development stack. In other tools, reuse the event name and property their automatic events use, so the rows end up in one report.
Do middle-clicks, Ctrl+click and right-clicks count as outbound clicks?
Ctrl+click, Cmd+click and Shift+click fire an ordinary click event, so every tool counts them. A middle-click fires auxclick instead: GA4, Plausible, Matomo and invisit listen for it, while PostHog’s autocapture, Simple Analytics and the Umami and Fathom doc scripts do not, so middle-clicked links are missing from their numbers.
Matomo goes the other way. enableLinkTracking() with no argument also listens for mousedown, mouseup and contextmenu, so a right-click on a link counts as an outlink even when the visitor only copies the address; the comment in matomo.js says as much. _paq.push(['enableLinkTracking', false]) stops that, and in our test it also stopped counting middle-clicks. Choosing “Open link in new tab” from the context menu fires no click at all, so no tool can tell it apart from a visitor who changed their mind.
How do you track affiliate links and redirect wrappers?
A cloaked link such as /go/partner points at your own domain and redirects on the server, so no tool counts it as outbound: it is an internal link until the redirect, by which time your page is gone. Mark those links yourself. In invisit an attribute is enough, and our test recorded affiliate_click with partner: acme:
<a href="/go/acme" data-invisit-event="affiliate_click" data-invisit-prop-partner="acme">
Try Acme
</a>Plausible documents a script for cloaked links that sends Cloaked Link: Click for any link containing /go/, and Matomo counts any link with the class matomo_link as an outlink (tracking guide).
Links through a third-party redirect, such as an affiliate network’s click tracker, do count as outbound, but to the wrapper’s domain. GA4, Plausible, Matomo, PostHog, Umami and Simple Analytics keep the full URL, so the destination and your affiliate tag survive in the query string. invisit keeps only the origin and path: https://go.skimresources.com/?id=...&url=... arrives as https://go.skimresources.com/ and an Amazon link loses its ?tag=. Fathom’s doc script keeps only the second-level name, so docs.qa.example.com and cdn.example.net both became External link clicked: example. Use an attribute or a track() call when the query matters.
How do you track downloads generated in JavaScript?
Export buttons that build a file in the browser, and download links without a file extension such as /export?format=csv, slip past every extension list. Only Matomo recorded either as a download, because it counts any link with a download attribute; it logged our Blob export as a download of blob:http://qa.example.com:8797/ab1eb79d-..., an address that means nothing in a report. Send the event where the file is made:
function downloadCsv(csv) {
const link = document.createElement('a')
link.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
link.download = 'report.csv'
document.body.appendChild(link)
link.click()
link.remove()
window.invisit.track('file_download', { url: `${location.origin}/exports/report.csv` })
}For a server endpoint without an extension, put data-invisit-event="file_download" and a data-invisit-prop-url on the link instead. Plausible’s docs suggest a custom event class for these, and Matomo has the matomo_download class.
Why do subdomains show up as outbound links?
Plausible, Matomo, PostHog, Simple Analytics and the Umami and Fathom doc scripts compare the link’s host with the page’s, so a link from example.com to docs.example.com counts as leaving. GA4 sent no outbound event for links from qa.example.com to docs.qa.example.com or www.qa.example.com in our test, and skips any domain you list under Configure your domains, up to 100 conditions.
In Matomo, _paq.push(['setDomains', ['*.example.com']]) marks every subdomain as your site; our test confirmed it. Plausible and Simple Analytics document no setting for this as of November 2026. invisit needs none: since November 3, 2026 it treats www and the bare domain, the same host over http://, a host above or below the page’s, and anything under one of the site’s allowed domains as internal, and we confirmed it in our cross-domain test. Sibling subdomains such as app. and docs. count as one site once the apex domain is listed.
Where do outbound clicks and downloads show up in each tool?
- GA4: Reports, Engagement, Events counts
clickandfile_download. Link URL, Link domain, Link text, Outbound, File name and File extension are predefined dimensions, so you need no custom dimensions: build an Exploration with Event name and Link URL, filtered to Outbound =true. Google calls registering a custom dimension for an already predefined parameter not a best practice, and it uses up quota. GA4 keeps no link text for outbound clicks: our captured hits carriedlink_id,link_classes,link_url,link_domainandoutbound, and Google listslink_textfor downloads only. - Plausible: the Goals tab, under
Outbound Link: ClickandFile Download; a property filter onurlgroups them by domain. - Matomo: Behaviour, Outlinks (clicks and unique clicks per domain) and Behaviour, Downloads.
- PostHog: the Outbound clicks list in the web analytics Paths tile (dashboard docs).
- Umami, Fathom and Simple Analytics: the Events page. Simple Analytics names each event after the host or file, such as
outbound_www_wikipedia_org. - invisit: the Outbound tab of the Pages card, by domain and then by link, with clicks and visits.
file_downloadsits on the Events page with a breakdown byurl.
How invisit records outbound links and downloads
invisit records both by default as custom events, outbound_link and file_download, each with one property: url, the link’s origin and path with the query string and fragment removed. A link is outbound when its origin differs from the page’s, and a download when its path ends in one of 30 extensions in any letter case: pdf zip rar 7z gz tgz tar dmg exe msi pkg deb rpm apk iso csv xls xlsx doc docx ppt pptx txt rtf epub mp3 wav mp4 mov avi. Downloads are checked first and on any host, and only http: and https: links count.
The tracker listens for clicks and middle-clicks without delaying or cancelling them, and sends the event at once with fetch and keepalive. Both events are billed as custom events, and a visit with one is never a bounce (how bounce rate is counted). The Outbound tab is never shown on a public dashboard. To turn either off:
<script
async
src="https://api.invisit.dev/invisit.js"
data-site="<site id>"
data-collector="https://api.invisit.dev"
data-outbound-links="false"
data-file-downloads="false"
></script>With the npm package, pass outboundLinks: false or fileDownloads: false to init(). What invisit lacks: you cannot change the extension list or keep the query string, so affiliate tags and redirect destinations need the attribute shown above.
Check your outbound tracking in five minutes
Open DevTools, tick Preserve log in the Network panel, filter by your analytics host, then:
- Click an external link, then middle-click it. Both should send an event; a request marked as canceled is the unload bug.
- Click a link to your own subdomain and decide whether you want it counted as leaving.
- Click your most important download, your export button and every kind of
/go/link. - Compare the tool’s extension list with the files you host,
.dmg,.apkand.epubincluded. - Search your code for
window.open(andlocation.href =and add an event next to each one that leaves the site. - If you pay per event, multiply last month’s outbound clicks and downloads by your price before you switch them on.
