Troubleshooting
Nothing is being counted
Check the mode. Settings → Odometer → How views are counted. If it is Only where asked or Not at all, that is the answer.
Check that the page resolves to an element. Automatic counting needs Craft to have matched a URL
to an element. A template rendered from a route, a category listing, a search results page and the
homepage of most sites are not elements, so nothing is counted on them. Put
{% do craft.odometer.record(entry) %} where you want a specific thing counted.
Check the section scope. Settings → Odometer → Sections.
Check that you are not being ignored. By default admins are not counted, so testing while signed in as yourself shows nothing. Sign out, use a private window, or set Signed-in users to Count everybody while you test.
Check the dedupe window. After the first view, the same browser will not count again for thirty minutes. Set it to 0 while testing.
Check the robot list. curl is on it, and so is wget — a curl request will not be counted no
matter what else is right. Pass a browser user agent when testing from the command line:
curl -A 'Mozilla/5.0 (Macintosh) Chrome/126.0' https://example.test/some-page
Counts stopped a while ago and I only just noticed
Almost always a page cache that went in since the counter did. Craft renders each page once and never hears about the readers who get the cached copy, so the count does not stop dead — it drops to the rate of cache misses, which is why nobody notices for weeks.
Switch to beacon mode.
The beacon is in the page but nothing is counted
Fetch the beacon URL yourself and look at what comes back:
curl -si -A 'Mozilla/5.0 Chrome/126.0' 'https://example.test/actions/odometer/track/beacon?odometerToken=…'
It always answers 200 with a 42-byte GIF, whatever happened — a beacon that returned 403 to a
robot would be telling the robot how to look like a person. So the status tells you nothing; the
count does.
If the count does not move:
- The token is being mangled. It is URL-safe base64; check that nothing between the page and
Craft is rewriting query strings, and that the
&in the URL has not been HTML-escaped into&by something copying it by hand. - The security key changed. Tokens are signed with it, so every token minted under the old key is refused. New page views mint new tokens; old cached pages carry dead ones. Clear the page cache.
- The token expired. Only possible if Token lifetime was changed from 0. On a cached site the token lives as long as the cached HTML does.
- The request is being filtered. Robots, ignored addresses, Do Not Track, signed-in users, the dedupe window — in that order.
The whole site went down and Odometer is in the stack trace
It should not be possible: counting catches its own errors and warns rather than throwing, and the response filter does the same. If you have a trace that says otherwise it is a bug worth reporting — but first switch How views are counted to Not at all, which stops every code path except reading.
An element index throws when I sort by Views
Report it. Sorting by Views works whether or not the Views column is shown, because the join is
added by a handler on ElementQuery::EVENT_BEFORE_PREPARE when the sort names it. If that has been
defeated somehow, the workaround is to add the Views column to the source's columns as well.
popular() returns fewer rows than I asked for
By design: elements that have never been read are not in the ranking at all, because nothing unread
can be the most-read thing. Pass includeUnviewed: true to keep them, at the bottom.
popular() returns the wrong rows
If it is returning the right number of rows in the right order but they are not the top ones, the
ranking has been applied to a query that was already limited. Apply the ranking first, or pass
limit in the options rather than calling .limit() beforehand.
The report screen is slow
The report ranks straight out of the ledger and then asks Craft for the elements behind the ids — one query for the ranking, one per element type for the elements, one for the lifetime numbers and one for all the sparklines. If it is slow, the ledger is probably very large; turn retention down and prune:
php craft odometer/views/prune
Retention only ever deletes day buckets. Lifetime totals are never touched.
Numbers look wrong after a restore or a migration
php craft odometer/views/recount rebuilds every lifetime total from the day buckets. On a site with
retention switched on this replaces lifetime totals with "views since the retention window began",
so it asks before doing it. Set retention to 0 first if you want to be sure.
The dates are a day out
Day buckets are calendar days in the site's time zone (Settings → General → Time Zone), not UTC. If Craft's time zone is not where your readers are, "today" will not match what they would call today. There is no setting for this: Craft has one system time zone and Odometer uses it.
Views appear on the wrong site
Every count is per site. A page read in French counts for the French site. Pass siteId: '*'
wherever a site is accepted to add them up.
Where do I look next
Craft's own logs, filtered to Odometer:
grep odometer storage/logs/web-*.log
Odometer logs at warning level when a view could not be recorded and at error level when the response filter fell over. Both are non-fatal by design, so a problem here is quiet unless you go looking.