@privatrak/tracker
v1.0.0
Published
Cookieless, privacy-first analytics tracker: auto-captures pageviews, clicks and form submissions with no cookies, no localStorage, no PII and no consent banner.
Maintainers
Readme
@privatrak/tracker
The tracking script behind Privatrak, a privacy-first
product analytics service. Add it to a site and it starts recording pageviews,
clicks and form submissions on its own — no manual instrumentation, no track()
calls sprinkled through your components, no tagging plan to maintain. It sets no
cookies, writes nothing to localStorage or sessionStorage, does no browser
fingerprinting, and never reads the values a visitor typed into a form. Visitors
are counted server-side from an irreversible daily hash of IP and User-Agent, so
there is no identifier to store on the device, nothing to ask consent for, and
no cookie banner to show. The browser bundle is a single ~3 KB IIFE.
Using Nuxt? Install @privatrak/nuxt
instead — it wraps this package in a Nuxt module with a useTracker() composable.
Installation
npm install @privatrak/trackerOr skip the bundler entirely and load the hosted script, which Privatrak serves for you:
<script src="https://api.privatrak.com/tracker.js" data-api-key="stk_your_project_key"></script>The script tag route needs nothing built and nothing redeployed when a setting
changes: the options below have data-* attributes too, and the API host is
read from the script's own src. The rest of this README uses the npm form.
Quick start
import { init, track, setTraits } from '@privatrak/tracker';
init({
apiKey: 'stk_your_project_key',
apiHost: 'https://api.privatrak.com',
});
// Everything below is optional — autocapture already records pageviews,
// clicks and form submissions, including single-page-app navigations.
track('checkout-completed', { plan: 'pro' });
setTraits(['paid', 'admin']);Call init() once, as early as you can — from your app's entry module, or a
client-side bootstrap file. It is a no-op during server-side rendering, so it is
safe to import from code that also runs on the server. Your project's API key is
in the Privatrak dashboard under Settings → API Keys; it is a public,
write-only key and is meant to ship in client code.
Elements can be labelled in markup instead of in code, which is how most feature-level analytics end up being defined:
<button data-track="signup-cta" data-track-plan="pro" data-track-source="header">
Sign up
</button>A click on that button is recorded with data_track: "signup-cta" and
data_track_attrs: { plan: "pro", source: "header" }. If the click lands on
something inside a labelled element, the tracker walks up the DOM to find it.
Configuration
Every option is optional except apiKey. Pass them to init(), or set the
matching attribute on the hosted <script> tag. Anything you do not set follows
your project's settings from the dashboard, which the tracker fetches at
startup; values passed to init() always win over the server's.
| Option | Script attribute | Type | Default | Description |
|--------|------------------|------|---------|-------------|
| apiKey | data-api-key | string | — | Project API key. Required; without it nothing is sent. |
| apiHost | data-api-host | string | the page's own origin (the hosted script uses the origin of its src) | Base URL of the Privatrak API, e.g. https://api.privatrak.com. |
| autocapture | data-autocapture | boolean | true | Record clicks and form submissions automatically. |
| autocapturePageviews | data-autocapture | boolean | true | Record pageviews, including SPA navigations. |
| autocaptureElements | — | string[] | a, button, input[type=submit], plus ARIA roles such as [role=button], [role=menuitem], [role=tab] | Selectors treated as interactive click targets. |
| walkDepth | data-walk-depth | number | 3 | How many ancestors to walk up when looking for an interactive element or a data-track label. |
| maxTextLength | data-max-text-length | number | 100 | Element text is truncated to this many characters. |
| flushIntervalMs | data-flush-interval | number | 5000 | How often the queue is sent. |
| maxQueueSize | data-max-queue-size | number | 10 | Queue length that triggers an immediate send. |
| piiUrlParams | data-pii-params | string[] | ['email', 'token', 'key', 'password', 'secret'] | Query parameters whose values are replaced with :redacted before the URL is sent. |
| piiPathPatterns | data-pii-path-patterns | string[] | [] | Regular expressions whose matching path segments are replaced with :redacted. |
| normalizePathIds | data-normalize-path-ids | boolean | true | Collapse ID-shaped path segments to :id, so /orders/4711 and /orders/4712 are one page. |
| excludedUrls | data-excluded-urls | string[] | [] | URL patterns that are not tracked at all. |
| samplingRate | data-sampling-rate | number | 1.0 | Fraction of visitors recorded, from 0 to 1. |
| traits | data-traits | string[] | [] | Session labels attached to every event in the batch — see setTraits(). |
List options are comma-separated in an attribute: data-pii-params="email,token".
The two autocapture switches share one attribute, which takes a list of what to
keep: data-autocapture="pageviews,clicks", or data-autocapture="none" to
record nothing automatically. Add data-manual-init to stop the script
initialising itself, and call window.tracker.init({ ... }) when you are ready.
Project settings, and what happens if they cannot be loaded
Settings come from three places, applied in this order: the values built into
the script, your project's settings fetched from apiHost, and the data-
attributes on the script tag, which win over both.
Nothing is sent until that fetch has succeeded. The project's settings carry
its URL redaction rules, so that one request is what stands between a URL in the
page and a URL in the database. It is retried five times — 500 ms, 1 s, 2 s and
4 s apart — on network errors, timeouts, 408, 429 and any 5xx. A 401 or
403 is not retried: the key is wrong, or the page's origin is not among the
project's allowed domains, and the answer will not change however often it is
asked.
If the settings never arrive, the page load sends nothing at all, and one
console.warn names the request that failed and says why. Sending unredacted
URLs would be worse than sending none. The Tracker Reference goes
through the retry rules and the queue behaviour in full.
The Element Picker
Privatrak's point-and-click element picker is a second file,
tracker-picker.js, and it is not part of this package — your visitors never
download it. It is fetched from apiHost the first time someone signed in to
the Privatrak dashboard activates the picker on your site, which is also the
only time it can run. If that fetch fails — no apiHost was configured, or the
API is not reachable from the page — the tracker logs a warning and carries on
tracking; nothing else is affected.
API
init(config?): void
Starts the tracker: applies the configuration, records the first pageview, and
attaches the delegated click and submit listeners plus the History API
patches that detect SPA navigation. Calling it twice logs a warning and does
nothing the second time. Returns immediately; the first batch leaves once the
project's settings have loaded.
track(name, attrs?): void
Records a custom event. name is the feature label you will see in the
dashboard, attrs an optional flat object of string values, equivalent to
data-track-* attributes in markup.
track('invite-sent', { role: 'editor' });setTraits(traits): void
Labels the current session with developer-chosen strings — ['paid', 'trial'],
['plan:pro'], a feature-flag bucket — which every subsequent event carries and
which the dashboard can filter and break down by. Traits are your own labels,
not personal data: at most 50 per batch, 64 characters each. Call it whenever
what you know about the session changes.
version: string
The package version of the build you imported, baked in at build time. On the
hosted script the same value is at window.tracker.version. Quote it in a bug
report.
Types
TrackerInitConfig (the shape init() takes) and DataTrackAttrs are exported
as types.
What gets captured
Pageview — on load and on every SPA navigation (pushState,
replaceState, popstate). Navigations to the same URL are not recorded twice.
Fields: page_url, referrer, timestamp.
Click — on anything in autocaptureElements, or anything carrying
data-track, via a single delegated listener on document. A click on a
non-interactive child walks up to walkDepth ancestors to find the real target.
Fields: element_tag, element_id, element_class, element_text (truncated),
data_track, data_track_attrs, plus page_url and timestamp.
Form submit — on submit, with the form's own identity and labels. Field
values are never read. Fields: element_tag, element_id, element_class,
data_track, data_track_attrs, page_url, timestamp.
Custom — whatever you pass to track().
Password fields, hidden fields and inputs whose autocomplete starts with cc-
are skipped outright, so they are never even click targets.
Delivery
Events are queued in memory and sent in batches to POST /api/events — every 5
seconds, or as soon as 10 are waiting, or immediately when the tab is hidden.
Normal sends use fetch() with keepalive; the tab-hidden send uses
navigator.sendBeacon() so it survives the page going away. A failed batch is
re-queued for the next cycle, and the queue is capped at 100 events so a broken
connection cannot grow it without bound.
URLs
The tracker sends one absolute URL per event and the API splits it into origin,
path, query and fragment. Origin plus path is the page's identity, so
/search?q=analytics and /search?q=funnels are the same page, counted once,
while the parameters stay individually filterable. Redaction happens here in the
browser, before anything is sent: piiUrlParams, piiPathPatterns and
normalizePathIds rewrite the URL first.
Privacy
This is the whole point of the package, so it is worth being specific.
- No cookies and no storage. Nothing is written to
document.cookie,localStorageorsessionStoragewhile tracking. There is no device identifier, which is why no consent banner is required for it. - No fingerprinting. No canvas, WebGL or AudioContext probing, no navigator enumeration.
- Visitors are counted server-side. The API derives a session ID from
HMAC(IP + User-Agent + project, daily key)and stores only that hash — raw IP and User-Agent are never written down. The key rotates at midnight in the project's timezone, so yesterday's sessions cannot be linked to today's. It is the model Plausible Analytics uses, which CNIL has found acceptable without consent. - Form values are never captured. Not opt-in, not redacted afterwards — the tracker does not read them.
- Element text is truncated to
maxTextLength(100) characters, and sensitive inputs are excluded from tracking entirely. - URLs are cleaned in the browser, before the event leaves the page.
More detail, and the data processing terms, are at privatrak.com/privacy.
Links
- Privatrak — the product this reports into
- How it works · Features · Pricing
@privatrak/nuxt— the Nuxt module built on this package- Contact — questions and bug reports
Versioning and license
This package is versioned independently of the hosted product. Changes are
recorded in CHANGELOG.md, shipped in the package. Released under the MIT
license (LICENSE): it runs on customers' own sites, so it is licensed
separately from the rest of the service.
