integrity value in your snippet decides which bytes your visitors’ browsers will run. You can check that it matches the published release with one command, and rebuild the file from source to check the release itself. If the file at the tracker URL ever changes, browsers refuse to run it and your pages keep working.
Tracker supply chain explains why the tracker is pinned this way and what each control protects against.
1
Hash the file the server sends
integrity attribute uses, without the sha384- prefix. -binary makes openssl dgst output raw bytes instead of hex, and -A keeps the base64 on one line. The file is 10,447 bytes: curl -s https://t.lumenstat.com/v/1.0.0/l.js | wc -c prints 10447.2
Compare it with releases.json
Every release is recorded in Your output must equal the
packages/tracker/releases.json when it is built:packages/tracker/releases.json
integrity value after sha384-. The same hash appears in two other places, each produced independently:- The Setup page. The dashboard hashes the tracker file it fetches from the server each time Setup loads, and puts that hash in your snippet. It does not copy it from
releases.json. - The GitHub release notes for the tag
tracker-v1.0.0, written by the release workflow fromreleases.jsonafter it rebuilds the file.
3
Check what your site serves
Confirm that your deployed pages pin the same hash:For a site that injects the tag from JavaScript, such as Next.js with
next/script, inspect the rendered <script> element in the browser’s developer tools instead.4
Rebuild it from source (optional)
This checks the release itself: that the published file is exactly what the source in the repository produces. You need git and Node.js 22 or later.The same check runs in CI on every push to
sync-umami.sh clones Umami at the tag v3.5.0 and applies the patches in patches/umami. build-tracker.mjs --check builds the tracker from that source with the collect host and endpoint recorded in packages/tracker/package.json, compares the result with packages/tracker/releases/1.0.0.js byte for byte, and writes nothing. A match prints:main and every pull request, in the release workflow before anything is published, and in the deploy script before every deploy.What the browser does if the bytes change
- Because of
crossorigin="anonymous", the browser fetches the file in CORS mode.t.lumenstat.comanswers tracker requests withAccess-Control-Allow-Origin: *, so the browser can read the bytes and check them. Without the attribute, a browser cannot check a cross-origin file and blocks it. - The browser computes the SHA-384 hash of the bytes it received and compares it with the hash in
integrity. - If they differ, the browser does not run the file, fires an
errorevent on the<script>element and logs an integrity error in the console.
Cache-Control: public, max-age=31536000, immutable, and a fix ships as a new version at a new URL with a new hash. Upgrading means changing src and integrity together, on purpose. Tracker releases lists every version.
Verify the npm package
The first npm publish of
@lumenstat/tracker is pending (Milestone 1). These checks apply once it is published.dist/l.js is copied from releases/<version>.js when the package is packed, so it has the same hash as the hosted file:
npm audit signatures checks the registry signatures and provenance attestations of the packages installed in your project. Self-host the tracker covers serving that file from your own domain.
Limits
- The browser enforces the hash you give it. If you copy a hash from a tampered source, the browser enforces the wrong one. Compare at least two independent sources: the bytes the server sends and
releases.jsonin the repository. - Integrity covers the tracker file only. It says nothing about the collect endpoint or the dashboard.
- A new version is new code. Upgrading accepts a different file. Review what changed in
patches/umamiand the release notes before you change your snippet. - Browsers without Subresource Integrity support run the file unchecked. All current major browsers support it.