[invisit](https://invisit.dev) · [Docs](https://invisit.dev/docs) · [llms.txt](https://invisit.dev/llms.txt) · [llms-full.txt](https://invisit.dev/llms-full.txt)

# How to track outbound link clicks and file downloads: 8 tools tested

> Which outbound link clicks and file downloads GA4, Plausible, Matomo, PostHog, Umami, Fathom, Simple Analytics and invisit record, tested in Chrome.

- Author: Miraç Berk
- Published: 2026-11-03
- Page: https://invisit.dev/blog/outbound-link-tracking
- Tags: events, outbound-links, file-downloads, google-analytics

![A soft 3D browser window with a link arrow leaving through its edge and a file icon dropping into a tray, in violet dithered light and shadows.](https://invisit.dev/_astro/cover.BUbfAzJH.png)

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](#how-invisit-records-outbound-links-and-downloads).

## 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](https://support.google.com/analytics/answer/9216061), Plausible's [outbound links](https://plausible.io/docs/outbound-link-click-tracking), [file downloads](https://plausible.io/docs/file-downloads-tracking) and [plans](https://plausible.io/docs/subscription-plans), Matomo's [tracking guide](https://developer.matomo.org/guides/tracking-javascript-guide) and [what counts as a hit](https://matomo.org/faq/general/what-is-a-hit/), [PostHog autocapture](https://posthog.com/docs/product-analytics/autocapture) and [pricing](https://posthog.com/pricing), Umami's [outbound links guide](https://docs.umami.is/docs/track-outbound-links) and [pricing](https://umami.is/pricing), Fathom's [external clicks guide](https://usefathom.com/docs/events/external-clicks) and [pricing](https://usefathom.com/pricing), Simple Analytics' [automated events](https://docs.simpleanalytics.com/automated-events) and [pricing](https://www.simpleanalytics.com/pricing), and invisit's [options](https://invisit.dev/docs/web/installation.md#options) and [billing](https://invisit.dev/docs/guides/billing.md).

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](https://support.google.com/analytics/answer/9216061) 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](https://developer.matomo.org/guides/tracking-javascript-guide) 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](https://plausible.io/docs/file-downloads-tracking), 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 | Email            | 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](/blog/spa-page-views)).

## 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:

```js title="outbound.js"
// 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:

```js title="open-demo.js"
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`:

```html title="partner-link.html"
<a href="/go/acme" data-invisit-event="affiliate_click" data-invisit-prop-partner="acme">
  Try Acme
</a>
```

Plausible documents [a script for cloaked links](https://plausible.io/docs/custom-automatic-link-tracking) 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](https://developer.matomo.org/guides/tracking-javascript-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:

```js title="export.js"
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](https://support.google.com/analytics/answer/10071811), 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](/blog/cross-domain-tracking). 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 `click` and `file_download`. Link URL, Link domain, Link text, Outbound, File name and File extension are [predefined dimensions](https://support.google.com/analytics/answer/9143382), 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](https://support.google.com/analytics/answer/14240153), and it uses up quota. GA4 keeps no link text for outbound clicks: our captured hits carried `link_id`, `link_classes`, `link_url`, `link_domain` and `outbound`, and Google lists `link_text` for downloads only.
- **Plausible:** the Goals tab, under `Outbound Link: Click` and `File Download`; a property filter on `url` groups 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](https://posthog.com/docs/web-analytics/dashboard)).
- **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_download` sits on the Events page with a breakdown by `url`.

## 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](/blog/bounce-rate)). The Outbound tab is never shown on a [public dashboard](/blog/public-analytics-dashboard). To turn either off:

```html title="index.html"
<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`, `.apk` and `.epub` included.
- [ ] Search your code for `window.open(` and `location.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.
