Version 1.6.0

October 25, 2026

This release makes the counter answer "how many times was this file downloaded?" rather than "how many HTTP requests did it receive?", and catches the automated traffic that borrows a real browser's User-Agent. If your numbers have looked too good to be true, this is why, and they should settle after updating.

Changed

  • One download now counts once, however many requests it takes. A single click on a managed download link often reaches the plugin as several requests: a browser's PDF viewer opens the file and then pulls pages on demand, a media player seeks, a download manager probes with a HEAD and then fetches the file over several connections at once, a person double-clicks. Every one of those used to count as a download. Now a HEAD or OPTIONS request is served but not counted, a Range request that picks the file up part-way through is served but not counted, and repeat hits on the same file from the same visitor within a new Duplicate window (60 seconds by default, on both routes) count as the one download they were. The window lives in Craft's cache under a one-way hash, so it adds no database rows and keeps nothing about the visitor. Set it to 0 to count every request as before.
  • The distilled crawler list no longer blocks. The documentation has always said the thousand-odd distilled third-party tokens are only ever counted separately, never used to refuse a download, because a list that broad is more likely to mis-match than the hand-picked names. In practice, under Block with a 403, they did block. They now behave as documented: a match is counted on the crawler side and served. Hand-picked names, your own Extra crawler user agents, and the generic bot/crawl/spider giveaways still block.

Added

  • Scripts wearing a browser's User-Agent are now caught. The traffic that inflates download numbers most rarely announces itself: email security tools that open every link before it reaches the reader, proxies and antivirus sandboxes that re-fetch a file to inspect it, scrapers that set a Chrome User-Agent to blend in. Every current browser sends a set of Fetch Metadata headers (Sec-Fetch-Mode and friends) with every request to a secure site, and page scripts can't remove them. A request that claims to be a browser version known to send them, and arrives with none, wasn't sent by that browser. It's now counted on the crawler side. The check only runs over HTTPS, and only when the User-Agent names a browser and version known to send the headers (Chromium 80+, Firefox 90+, Safari 17+, any browser on iOS 17+), so it can only catch a User-Agent that is lying, never doubt one that isn't. Like the distilled list, it's a soft signal: counted separately, never blocked.
  • Tests for the classifier. The request classifier, the request-shape check and the fingerprint are pure functions, and now have a PHPUnit suite (composer test) covering named crawlers, the token lists, prefetch headers, the Fetch Metadata check across browsers and versions, and Range parsing.

For developers

  • RequestSignals::classify() takes the firm and soft token lists as separate arguments, and gains a new SIGNAL_SUSPECT result for soft matches. Use RequestSignals::isCrawler($signal) to test for either crawler signal.
  • Crawlers::classificationTokens(), which merged the two lists, is gone. Read Settings::normalizedCrawlerUserAgents() and Crawlers::tokens() instead.
  • Downloads::increment() accepts an optional client fingerprint as its fifth argument, which enables the duplicate window for that call. Calls without one (the console, imports, your own code) count every time, as before.

Version 1.5.0

September 10, 2026

This release is about trusting your download numbers. It shows the real human count first, instead of a total quietly padded by bots, and it gets a lot better at telling the two apart.

Changed

  • Your stats now lead with people, not bots. A download total has always counted real visitors and automated crawlers together — search engines, AI bots, social-media link scanners and the like. That made a big number easy to read as "this many people downloaded it" when a large slice was never a person at all. Now, wherever a figure appears — the Downloads list, each file's own page, saved reports and the Top Downloads dashboard widget — the People figure comes first, and the lists sort by it. The all-in total and the crawler count still sit right beside it, so nothing is hidden; the honest number just leads.

Added

  • Much sharper bot detection, built in. The plugin already knew the big-name crawlers; now it also ships a list of around a thousand lesser-known ones, so they're caught automatically with nothing for you to set up. The list is drawn from three well-respected, community-maintained crawler databases and then trimmed hard — anything already detected, plain everyday words, and the signatures of real browsers and phone apps are all left out. The result is a small, careful list that only ever catches more bots, and won't mistake a genuine visitor for one.
  • A new Crawlers page to keep that list fresh. Under Download Tracker → Crawlers, admins can see how many crawlers are in the list and when it was last updated, and press Refresh and apply now to pull the latest versions of those databases and put them to work straight away. Prefer automation? The same job runs from the command line, so you can schedule it.
  • Two more crawlers known by name. Google's "GoogleOther" and Meta's web indexer are now recognised directly. Both announce themselves, but weren't on the built-in list before, so their visits had been counting as people.
  • A read-only reporting API. Set a connection code under Settings → Reporting API and an outside tool — a dashboard, a spreadsheet, a scheduled report — can read your headline download stats: a total for a chosen period, the most-downloaded files, and a day-by-day series. It only ever reads, only ever returns summary numbers (counts and file names, never individual downloads), and every request is signed and short-lived. Leave the code blank to keep it switched off.

A note on safety: detected crawlers are still only ever counted separately from people, never blocked — so even a wrong guess can only nudge a number, never break someone's download.

Version 1.4.0

July 23, 2026

Added

  • Craft 4 support. The plugin now installs on Craft 4.0 and later as well as Craft 5, from the same release - there's no separate Craft 4 branch or version line to choose between. Nothing in the plugin needed changing to get there: every Craft API it uses exists in Craft 4 with an identical signature, and so does every control-panel Twig macro, block and filter its templates rely on, checked against 4.0.6 and 4.18.5 rather than assumed. PHP 8.2 stays the floor; the code parses under 8.0, but 8.0 and 8.1 are both end-of-life.

    This matters most for the Link Vault import. Link Vault's schema is identical on its Craft 4 and Craft 5 releases, so the importer works as-is, and the people who most need it are the ones still on Craft 4: they can import and drop Link Vault first, rather than carrying it through a Craft 4 to 5 upgrade.

Version 1.3.0

July 23, 2026

Added

  • A legend under the day-by-day chart, naming which colour is people and which is crawlers. The totals above the chart had always split the two, but nothing connected those words to the stacked bars, leaving the split to be guessed at. It only appears while crawlers are counted separately, since there's no split to explain otherwise.

Improved

  • Every day in the chart now answers a hover with its date and numbers, not just the days tall enough to aim at. Each day gets a full-height, invisible hit area over its bar, and highlights while hovered, so a day reads the same whether it saw 400 downloads, one, or none - previously only days with a download carried a tooltip at all, and on a 365-day range a quiet day's bar was too small to reliably hit.

Version 1.2.1

July 19, 2026

Fixed

  • Managed download links now work on Craft 5.9 and later. The signed link from craft.downloadTracker.url() carried its token in a token query parameter, which collides with Craft's own reserved route-token parameter. Craft 5.9 started rejecting any request whose token doesn't resolve to a real Craft token with a 400 Bad Request, thrown before the plugin's controller ran - so every managed download returned a 400. The token now travels in a dlt parameter instead, sidestepping Craft entirely. The zero-touch click beacon was never affected; only managed url() links were. If you embed managed links in statically-cached pages, clear those caches after updating so the new links are baked in.

Version 1.2.0

July 17, 2026

Added

  • Import from Link Vault, for sites moving off Masuga's Link Vault plugin. It folds Link Vault's row-per-download log into this plugin's per-file counters, keeping the running totals, the day-by-day history and each file's last-downloaded date. Run it from Download Tracker → Import (admins only, and only shown while Link Vault is still installed) or with php craft download-tracker/import/link-vault.
  • The Import page shows what will happen before you commit to anything: how many downloads it will import, how many files they belong to, and what it will skip. --dryRun does the same on the command line.
  • The import runs as a queue job, so progress shows in the control panel's queue indicator and a large history won't time out. It's safe to re-run: it resumes from where it stopped rather than counting anything twice, so you can import, leave Link Vault running a while longer, and top up before uninstalling.

Fixed

  • The per-file detail page's date range collapsed to a single day, so the chart, the day-by-day table and the "in this range" totals only ever showed one day's downloads - the day the range started - instead of the whole window. The same bug made craft.downloadTracker.daily() return one day rather than the range asked for. Both affect 1.1.0, and are most visible on a file with a long history, since a site with only recent downloads showed an empty range instead.

Notes

  • Run the import before uninstalling Link Vault - it reads Link Vault's tables directly, and can't recover the history once they're gone.
  • A count per file can't hold everything a row per download could. Imports keep the totals and daily history, and drop the per-user, per-IP and custom-field detail. Blocked leech attempts and downloads deleted in Link Vault are skipped. Link Vault never recorded a user agent, so imported downloads can't be split into people and crawlers, and all count as people.

Version 1.1.0

July 16, 2026

Added

  • Crawler downloads setting, controlling what happens when a search engine, AI crawler, link unfurler or monitoring tool downloads a file:
    • Count separately (new default) - counted toward the total, and toward a crawler total of their own, so you can read your human numbers off the split.
    • Don't count - the file is served, nothing is recorded.
    • Block with a 403 - the served-download route refuses outright and streams nothing. Only the served route can block; the beacon has no file to withhold.
  • Crawler detection covers the well-known search, AI, social and monitoring crawlers by name, with a catch-all for the long tail, and an Extra crawler user agents setting for anything else you see in your own logs.
  • Per-file detail page: click a file in the Downloads list for its totals, a day-by-day chart and table over the last 30, 90 or 365 days, and a CSV export of that history.
  • Twig API: craft.downloadTracker.userTotal(), .crawlerTotal(), and .daily(file, days) for a file's day-by-day history.
  • beforeTrackDownload events now carry isCrawler, so a handler can decide about crawler hits itself without touching the setting.

Changed

  • count is now the total: people and crawlers together. Under the new default, crawler downloads are included in it, and the human figure is count - crawlerCount. Every existing list, report, widget and Twig call keeps working unchanged, but a site that previously discarded bot hits will see its totals rise once crawlers start counting. Installs that had ignorePrefetchAndBots on keep their existing behaviour automatically - see below - so this only affects new installs and anyone who opts in.
  • The served-download route now recognises prefetch and crawler requests, which it previously ignored entirely: it counted every request carrying a valid token, whatever sent it. A browser prefetch of a download link is therefore no longer counted twice (once when speculatively fetched, once when clicked).
  • Browser prefetch and prerender requests are always served and never counted, whatever Crawler downloads is set to. A prefetch is a real browser getting ready for a real click - neither a crawler to turn away nor a download to count.
  • lastDownloaded tracks when count last moved, so a counted crawler hit updates it too.
  • The Downloads CSV export gained User downloads and Crawler downloads columns. They're appended after the existing ones, so anything reading that file by column position is unaffected.

Deprecated

  • ignorePrefetchAndBots, replaced by crawlerMode. It's still honoured, from both project config and config/download-tracker.php: true maps to crawlerMode: 'ignore' and false to 'separate', so upgrading doesn't change how an existing site counts. An explicit crawlerMode always wins. Support for the old setting will be removed in 2.0.

Version 1.0.4

July 8, 2026

Fixed

  • The served-download route now returns a plain 404 when a request arrives with a missing or invalid token, instead of throwing a BadRequestHttpException. The route is anonymous and its URL is easy to guess, so bots and stale links probe it constantly with no token at all. Each of those probes was raising an application exception, which filled up error trackers with noise that wasn't a fault in the site. Nothing about the token check has changed, and no file was ever served without a valid token. The rejection reason is still written to the Craft logs, so a genuinely broken signed link can still be traced.

Version 1.0.3

July 8, 2026

Changed

  • Now we're in the plugin store, I thought it was time for a decent logo. Redrew the plugin icon as a proper coloured badge (a download arrow over rising count bars) so it stands on its own next to the other listings, and updated the control-panel mask icon to match.

Version 1.0.2

July 3, 2026

Fixed

  • Settings page is now viewable (read-only) for admins in environments where allowAdminChanges is off, instead of returning a 403. Saving is still blocked in those environments.
  • The Tracked extensions, Tracked path prefixes, and Excluded hosts textareas no longer render their values run together on a single line - each value now appears on its own line again.

Changed

  • Redrew the control-panel icon with a more balanced arrow-and-tray glyph so it no longer looks squished in the sidebar.

Version 1.0.1

July 3, 2026

Changed

  • Adopted the standard Coysh Digital commercial license.
  • Refreshed the plugin icon and updated the README.

Version 1.0.0

July 3, 2026

Added

  • Initial release.
  • Atomic per-file download counter (one row per file) plus a bounded per-day rollup - no element-per-download or row-per-download bloat.
  • Zero-touch JS beacon that auto-detects download links and counts them without any template changes. Works with full-page static caches (e.g. Blitz) because counting happens on a non-cached /actions/... request.
  • Optional served-download route (craft.downloadTracker.url(asset)) that streams or redirects to the file after counting - for gated, private, remote, or forced ("Save as…") downloads, with a signed, tamper-proof link.
  • Twig API: craft.downloadTracker.url(), .total(), .record(), .top().
  • Control panel: a sortable/searchable Downloads list with CSV export, saved reports, and a Top Downloads dashboard widget.
  • Automatic garbage collection of the daily rollup beyond a configurable retention window.