<- Back to blog
How-to9 min readUpdated May 1, 2026

How to Track File Downloads with Privacy-Friendly Analytics

Step-by-step guide to tracking PDF, ZIP, and other file downloads using cookieless analytics. HTML snippet, JavaScript handler, gotchas, and FAQs.

track file downloadspdf download trackinganalytics file download eventcookieless download trackingtrack downloads javascriptlead magnet analytics

TL;DR

  • 1.File downloads are first-class conversion events for SaaS, ebooks, and developer tools — track them with the same delegated-listener pattern as outbound clicks.
  • 2.Detect downloads by file extension on the link `href` — `.pdf`, `.zip`, `.csv`, `.dmg`, etc.
  • 3.Send the filename and (optionally) the file size; do not send personal data.
  • 4.For server-rendered downloads (signed URLs, force-download endpoints) you may need a server-side event instead.
  • 5.In Sleek, downloads show up as a `file_download` event you can filter by filename and group by source page.

Why download tracking matters

A file download is almost always a higher-intent action than a pageview. Someone is willing to take a thing off your site and onto their machine. Whether that file is a lead magnet, a release binary, or a CSV export, the download is a real conversion you should be measuring.

For B2B marketers, downloads are the primary way to score a lead before a signup. For developer tools, they tell you which platform your installer audience is actually on. For content sites, they tell you which guides are valuable enough that readers want them offline.

This guide shows you how to set it up in a privacy-friendly way — no cookies, no consent banner, no PII — using Sleek's event API.

Step 1: install the tracking snippet

If Sleek is already installed on your site, skip this. Otherwise, paste the snippet into your `<head>`:

index.html
<script async src="https://getsleek.io/v1.js" data-site="YOUR_SITE_KEY"></script>

Step 2: define which extensions count as downloads

A "download" in analytics terms is any link to a file the user takes off your site. The simplest detection method is by extension — you keep a list of file types that count, and the listener fires when a click goes to a URL ending in one of them.

A reasonable default list: `pdf`, `zip`, `tar`, `gz`, `dmg`, `pkg`, `exe`, `msi`, `csv`, `xlsx`, `docx`, `pptx`, `mp3`, `mp4`, `mov`. Add to this list as your team adds new file types — for example, you might add `epub` if you publish books.

note:Extensions are not perfect — a `force-download` endpoint that serves a PDF from `/download/123` will not match. We cover that case in step 5 with server-side events.

Step 3: write the click handler

Same delegated-listener pattern as outbound clicks. One handler on `document`, one extension check, one tracking call.

downloads.js
const FILE_EXTENSIONS = [
  'pdf', 'zip', 'tar', 'gz', 'dmg', 'pkg', 'exe', 'msi',
  'csv', 'xlsx', 'docx', 'pptx',
  'mp3', 'mp4', 'mov', 'wav',
]

function getExtension(href) {
  try {
    const url = new URL(href, window.location.href)
    const match = url.pathname.match(/\.([a-z0-9]+)$/i)
    return match ? match[1].toLowerCase() : null
  } catch (e) {
    return null
  }
}

function handleDownload(event) {
  const link = event.target.closest('a')
  if (!link || !link.href) return

  const ext = getExtension(link.href)
  if (!ext || !FILE_EXTENSIONS.includes(ext)) return

  const filename = link.href.split('/').pop().split('?')[0]

  window.sleek('track', 'file_download', {
    filename,
    extension: ext,
    source_page: window.location.pathname,
  })
}

document.addEventListener('click', handleDownload)
document.addEventListener('auxclick', handleDownload)

Step 4: handle the `download` attribute

Some links use the HTML `download` attribute to force a save dialog instead of opening the file in the browser. These behave the same way at the click level — your listener still fires — but the URL might not end in the actual filename. For example, `<a href="/api/report" download="Q4-Revenue.pdf">` triggers a download but the `href` is `/api/report`.

For this case, prefer the `download` attribute when it is present:

downloads.js
// Inside handleDownload, before the extension check:
const downloadAttr = link.getAttribute('download')
if (downloadAttr) {
  window.sleek('track', 'file_download', {
    filename: downloadAttr,
    source_page: window.location.pathname,
  })
  return
}

Step 5: server-side downloads

If your downloads are served by a backend route — a signed S3 URL generator, a "gated" download behind an email, or a Stripe-protected file — your client-side listener will only see the click on the trigger button. The actual download happens server-to-browser without ever loading a page you control.

For these you can either fire the event on click (assume the download will succeed) or fire it server-side after the file is delivered. The server-side version is more accurate. Sleek has an HTTP events API for exactly this case:

server-side download event
curl -X POST https://api.getsleek.io/v1/event \
  -H "Content-Type: application/json" \
  -d '{
    "site": "YOUR_SITE_KEY",
    "name": "file_download",
    "url": "/downloads/whitepaper",
    "props": {
      "filename": "2026-state-of-analytics.pdf",
      "size_bytes": 2840193,
      "source": "server"
    }
  }'

Step 6: verify and report

Open Sleek's real-time view in one tab, then trigger a download in another. You should see the `file_download` event within a couple seconds with the right filename. If you do not, double-check that your extension list includes the file type you tested with.

In your dashboard, the most useful default report is downloads grouped by `filename` over the last 30 days. That tells you which assets are pulling weight. The second most useful is downloads grouped by `source_page` — that tells you which pages are converting visitors into downloaders.

Pitfalls to avoid

  • Do not include the user's email or signup form data in download events — keep them anonymous.
  • Do not double-count: if you have a "download" button that triggers a JS download via Blob, do not also have a server event firing for the same action.
  • Do not forget large files: a 200 MB binary download will succeed even when the user navigates away, but only if you used `sendBeacon`. Sleek's SDK does this for you.
  • Do not put query strings in the filename. Strip everything after `?` before sending.
  • Do not track downloads of generic, repetitive assets (favicons, fonts) — restrict your extension list to genuine downloads.

Frequently asked questions

How do I track gated downloads behind an email form?

Track two events: `lead_capture` when the form is submitted, and `file_download` when the file is actually delivered (ideally server-side after the email is sent). The first measures form conversion; the second measures whether the lead actually got their content. The gap between them is your delivery health.

Will Sleek's download tracking work for files hosted on S3 or a CDN?

Yes. The script does not care where the file is served from — it cares about the click. As long as the download link is rendered on a page where the Sleek snippet is loaded, the click is tracked. If you want to measure successful delivery (not just clicks), add a server-side event after the file is sent.

How is this different from Google Analytics enhanced measurement?

GA4 enhanced measurement detects downloads automatically by extension, which is convenient but not customizable. With the script in this guide, you control the extension list, the event name, and the properties — including server-side delivery confirmation, which GA4 cannot do without separate setup.

Can I track downloads without consent in the EU?

Yes, with Sleek. Download events do not include cookies or personal data, so they fall outside the scope of cookie consent. With Google Analytics, you cannot track downloads without consent because GA4 sets cookies regardless of the event.

Why is my download count higher than my form submission count?

Usually because some users download a file multiple times (laptop and desktop, or after refilling a form they accidentally closed). It can also mean someone shared the direct download URL — in which case the second download has no associated form submission. If the gap is large, audit whether your "gated" file URL is actually gated.

How do I report on downloads per blog post?

Send `source_page: window.location.pathname` in your event properties (the snippet above does this). Then in the dashboard, group downloads by `source_page` to see which posts drive the most asset downloads.

Track your own growth loop

Sleek Analytics gives you visitors, sources, pages, devices, and real-time behavior with one lightweight script. No cookies, no GDPR banners.

Related reading