Tracker
The Sumimi tracker is a single dependency-free JavaScript file. It has no build step, no
npm package and no bundler; the file in the repository is byte-for-byte the file that gets
served. It sets no cookies, uses no localStorage, does no fingerprinting, and
honours Do Not Track and Global Privacy Control.
The script tag
This is all you need:
<script defer
src="https://your-sumimi-host/sumimi.js"
data-website-id="3f2a1b4c-5d6e-7f80-9a1b-2c3d4e5f6071"></script>
Put it in the <head> of every page you want to count. A page view is sent
automatically when the page finishes loading. The defer attribute means it
never blocks rendering, and the tracker never throws into your page: every network call
is wrapped so a failure is silent.
The server serves the tracker from its own origin at /sumimi.js, so by
default the tracker and the API are the same host and no CORS configuration is needed.
From TypeScript
There is a typed package on JSR (and on npm): it injects the script and gives the calls types.
# deno
deno add jsr:@ljh/sumimi
# npm
npm install @sumimi/tracker
import { init } from "@sumimi/tracker";
// The calls made before the script has loaded are queued.
const sumimi = init({ website: "<website-uuid>" });
sumimi.track("signup", { plan: "pro" });
sumimi.identify({ plan: "pro" });
Attributes
| Attribute | Required | Meaning |
|---|---|---|
data-website-id |
yes, unless data-website is used |
The website id (a UUID) that the page view belongs to. Shown when you create a website in the dashboard. |
data-website |
alternative to the id | The website domain instead of its id. Useful when you want one snippet across several sites with different domains. |
data-api-url |
no | Base URL of the API, when the tracker file itself is hosted somewhere else. Defaults to the origin the script was loaded from. |
Deployment options
1. Served by your own Sumimi host (recommended)
The default and the simplest. The server serves the tracker at
/sumimi.js from the same origin as the API, so there is no cross-origin
request at all.
<script defer src="https://analytics.example.com/sumimi.js"
data-website-id="3f2a1b4c-5d6e-7f80-9a1b-2c3d4e5f6071"></script>
2. Self-hosted on your own site
If you would rather not make a request to another origin at all — because of a strict
Content Security Policy, a firewall, or a preference for keeping the tracker on the same
domain — copy sumimi.js into your own site and point
data-api-url at the Sumimi server.
# copy it next to your other static assets
cp sumimi.js /var/www/html/assets/sumimi.js
<script defer src="/assets/sumimi.js"
data-api-url="https://analytics.example.com"
data-website-id="3f2a1b4c-5d6e-7f80-9a1b-2c3d4e5f6071"></script>
The file is self-contained and has no dependencies, so serving it from anywhere static
works. data-api-url must include the scheme and host but no trailing slash.
3. On a CDN
A CDN gives the tracker a stable URL close to your visitors. Upload the file to your CDN
as-is and point data-api-url back at your server.
<script defer src="https://cdn.example.com/sumimi.js"
data-api-url="https://analytics.example.com"
data-website-id="3f2a1b4c-5d6e-7f80-9a1b-2c3d4e5f6071"></script>
The dashboard frontend works the same way, but in the other direction: it is loaded by
your own browser, and by default it comes from a CDN
(https://cdn.jsdelivr.net/gh/sumimi-dev/sumimi@main/js) — the backend serves
only a tiny HTML shell and ships no frontend code. Set SUMIMI_APP_URL=/app to
have the backend serve it from its own js/ directory instead. Either way, the
tracker above is always served by your own server.
4. Several websites, one snippet
Give each site a domain in the dashboard and use
data-website instead of data-website-id. The same snippet then
works on all of them:
<script defer src="https://analytics.example.com/sumimi.js"
data-website="blog.example.com"></script>
Keep in mind that data-website is public in the page source, so it is not a
secret. The same is true of data-website-id — they are identifiers, not
credentials, and the collection endpoints are public by design.
Custom events
The tracker exposes a small global API, window.sumimi. (It also aliases
itself as window.umami so existing snippets keep working.)
// a page view is sent automatically; this is for the rest
sumimi.track('signup', { plan: 'free' });
sumimi.identify({ plan: 'free' });
sumimi.pageview();
Do not put personal data in event properties
Custom event properties are free-form data you supply, and they are
stored verbatim. If you send an email address, a user id or a phone number, you have
created personal data in your database and every privacy guarantee Sumimi makes stops
applying to it. The tracker never attaches anything sensitive on its own — keep it
that way. Prefer a coarse flag ({ "plan": "free" }) over an identifier.
sumimi.identify() attaches data to the current session only. It is not a
cross-session identifier, and the session id it belongs to rotates — see the
privacy page.
The web vitals
The tracker measures the Core Web Vitals with PerformanceObserver:
lcp from largest-contentful-paint, fcp from
paint, cls from layout-shift (the worst
session window, as the specification defines it), inp from
event with a durationThreshold, and ttfb from
the navigation timing.
The metrics are only known once the page has been painted, so the page view is sent
when the document becomes hidden or the page is closed — with
navigator.sendBeacon, so it survives the unload. A page view is never
sent twice.
Single page applications are supported: the tracker patches
history.pushState and history.replaceState and listens for
popstate, so every route change is recorded as its own page view.
How a hit is sent
- One request per event, to
POST /api/send. navigator.sendBeaconis used first, withfetchandkeepaliveas the fallback, so a hit survives the visitor navigating away.- The request body is JSON:
{"type":"event","payload":{…}}. - Credentials are omitted (
credentials: 'omit'), so no cookie is ever attached to the request either. - A failure is swallowed. Your page keeps working whether or not analytics does.
What the tracker sends
| Field | Example | Notes |
|---|---|---|
website | 3f2a… | The id or domain you configured. |
url | /posts/hello?utm_source=hn | The full URL: the path and the query string. The server splits the two apart. |
hostname | blog.example.com | The visitor's host. |
title | Hello | The document title. |
referrer | https://news.example.com/item?id=1 | The full referring URL. The server splits it into its host, its path and its query. |
language | en-GB | navigator.language. |
screen | 1920x1080 | Screen dimensions, not the viewport. |
lcp | 1500.5 | Largest Contentful Paint, in milliseconds. |
inp | 120 | Interaction to Next Paint, in milliseconds. |
cls | 0.05 | Cumulative Layout Shift, as a ratio. |
fcp | 800 | First Contentful Paint, in milliseconds. |
ttfb | 123.5 | Time to First Byte, in milliseconds. |
name | signup | Only for custom events. |
tag | cta | Only for custom events. |
data | {"plan":"free"} | Only for custom events and identify. User-supplied — see the warning above. |
The browser family, browser version, operating system, OS version and device class are
derived server-side from the User-Agent header. Nothing is
fingerprinted in the browser: no canvas, no audio, no font list, no hardware probing.
Privacy behaviour you can rely on
- No cookies. The tracker never sets one, and the collection request is
sent with
credentials: 'omit'. - No localStorage. Nothing is written to the visitor's storage, so nothing survives between visits on the same browser.
- DNT and GPC are honoured. If
navigator.doNotTrackis"1"or"yes", ornavigator.globalPrivacyControlistrue, the tracker sends nothing at all and does not even load the API. - No IP address. The tracker does not send one, and the server does not store one. A rotating, non-reversible id derived from a coarse IP prefix is used instead.
- The query string is sent, because the server parses the UTM parameters and the ad click ids out of it. Nothing is fingerprinted and no identifier is derived from it.
Full details, including what the server does with what it receives, are on the privacy and data-protection page.
Verifying the installation
- Open your site in a private window with the developer tools open.
- Look at the Network tab and filter for
/api/send. There should be onePOSTper page view. - Check the Application tab: Cookies and Local Storage should both be empty.
- Reload your dashboard — the page view should be there within a couple of seconds.
If nothing arrives, check that the website id is correct and that the domain matches. The
server rejects hits for websites it does not know with a
400 unknown website: … response.
Existing Umami snippets
The API is Umami v2 compatible, so the standard Umami tracker and the official Umami dashboard work against Sumimi unchanged. Sumimi is an independent project that implements the Umami v2 HTTP API; it uses no Umami assets.
The tracker file is original work, Apache-2.0 licensed together with the rest of Sumimi. All
licences are recorded in assets/CREDITS.md.