One line on your site. It sets no cookies, reads nothing out of the page, and if the collector is unreachable the host page is completely unaffected.
<script src="https://loghound.example.com/b.js?v=1" defer></script>
Put it anywhere — <head> is earliest, before </body> works too. That is the whole installation, and it is the snippet the panel’s settings page hands you with your own URL already in it.
defer, and the collector URL looks after itself
defer never blocks rendering and starts the clocks at parse time rather than after the last image has loaded. async works too and is marginally earlier, at the cost of a non-deterministic start point. The collector URL is derived from the script’s own src, so there is no second URL to keep in sync.
The beacon is plane 3, and it does two things and refuses to do anything else:
- It measures how long a visitor was actually there. Not “the page was open” — three separate clocks, never conflated.
- It is the execution plane. Whether JavaScript ran at all, whether the engine matches the version the User-Agent claims, and whether a human plausibly drove the pointer. The signal codes are the full list.
Loghound still works — it still does everything on the transport and behavioural planes — but headless automation is inferred rather than proven, and time on site falls back to the weak log-derived number every other log analyser reports. The two things this project exists to do both need the beacon.
Figure 1 — the beacon path. The collector never touches Solr. Two reasons: a Solr write per beacon would put an indexing round trip in front of every visitor on every page, several times per pageview; and the Solr credentials never enter the request path of the most exposed file in the project.
b.js ships as commented source, because there is no build step anywhere in this project: roughly 33 KB on disk and about 12 KB over the wire once it is gzipped. Readable source that a site owner can audit before embedding it is the deliberate trade.
The shipped vhost examples set the beacon’s cache and CORS headers but deliberately do not touch compression, because on both Debian-family Apache and stock nginx it is a global setting and a per-vhost override surprises people. So confirm it rather than assume it:
curl -sI -H 'Accept-Encoding: gzip' https://loghound.example.com/b.js | grep -i content-encoding
No Content-Encoding: gzip in that output means every visitor is downloading 33 KB instead of 12 KB. Turn compression on for application/javascript in your server’s global configuration.
?v= mattersb.js is served with max-age=604800, immutable, so the only thing that reaches a returning visitor after an upgrade is a new URL. The panel’s settings page builds the snippet with the beacon file’s own modification time in that slot, which means there is nothing to remember to bump. If you hand-write the tag instead, bump the number yourself when you upgrade.
The shipped vhosts also set Access-Control-Allow-Origin: * and Timing-Allow-Origin: * on it, because the beacon is embedded on other origins by design.
- Every option it reads — nine of them, with defaults and whether your installation stores what each one sends.
- Identity and signed-in — real WordPress and Drupal code for attaching who the visitor is.
- Search terms — collecting what people looked for, by naming parameters rather than hoovering up query strings.
- A site on another server — same snippet, no log access required, and an honest account of the limits.
- Content-Security-Policy — two directives, and the one people forget.