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

AttributeRequiredMeaning
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.sendBeacon is used first, with fetch and keepalive as 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

FieldExampleNotes
website3f2a…The id or domain you configured.
url/posts/hello?utm_source=hnThe full URL: the path and the query string. The server splits the two apart.
hostnameblog.example.comThe visitor's host.
titleHelloThe document title.
referrerhttps://news.example.com/item?id=1The full referring URL. The server splits it into its host, its path and its query.
languageen-GBnavigator.language.
screen1920x1080Screen dimensions, not the viewport.
lcp1500.5Largest Contentful Paint, in milliseconds.
inp120Interaction to Next Paint, in milliseconds.
cls0.05Cumulative Layout Shift, as a ratio.
fcp800First Contentful Paint, in milliseconds.
ttfb123.5Time to First Byte, in milliseconds.
namesignupOnly for custom events.
tagctaOnly 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.doNotTrack is "1" or "yes", or navigator.globalPrivacyControl is true, 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

  1. Open your site in a private window with the developer tools open.
  2. Look at the Network tab and filter for /api/send. There should be one POST per page view.
  3. Check the Application tab: Cookies and Local Storage should both be empty.
  4. 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.