@puremetrix/tracker
v0.3.1
Published
Official frontend package for PureMetrix cookieless analytics: init/track, consent mode, beforeSend, and adapters for React, Next.js, Vue, Nuxt, SvelteKit, Astro and Remix. Fastest setup: npx puremetrix@latest init
Maintainers
Readme
@puremetrix/tracker
Official frontend package for PureMetrix — cookieless, privacy-first web analytics from Germany.
Typed init / track helpers, manual pageviews, a consent mode, a beforeSend filter and ready-made adapters for React, Next.js, Remix / React Router, Vue, Nuxt, Svelte / SvelteKit and Astro. It works with PureMetrix Cloud out of the box; a same-origin proxy is optional.
Fastest setup: one command
npx puremetrix@latest initRun it in your project folder. The puremetrix CLI detects your framework, signs you in through the browser and adds the tracking code — or this package with --mode package. It shows a diff first and npx puremetrix@latest remove undoes it.
Prefer to wire it up by hand? Read on.
Install
npm install @puremetrix/tracker # or: pnpm add / yarn add / bun addQuick start
import { init, track } from "@puremetrix/tracker";
init({ domain: "example.com", publicKey: "pk_..." });
// Custom event (use the same name as a goal in the dashboard)
track("Signup", { props: { plan: "starter" } });domain and publicKey come from Site settings → General in the PureMetrix app. The public key is meant to be public — never put a private/server key in frontend code. key is accepted as an alias of publicKey.
That's it: pageviews (including client-side route changes in SPAs), engagement time and your events are sent to https://collect.puremetrix.org. init() is a no-op during SSR and ignores repeated calls.
Framework adapters
| Framework | Import | Usage |
|---|---|---|
| React (Vite, CRA, …) | @puremetrix/tracker/react | <PureMetrixProvider domain publicKey> (or <PureMetrix … />) |
| Next.js | @puremetrix/tracker/next | Same provider, rendered via next/script |
| Remix / React Router | @puremetrix/tracker/remix | Same as /react |
| Vue 3 | @puremetrix/tracker/vue | app.use(createPureMetrix({ … })) |
| Nuxt 3/4 | @puremetrix/tracker/nuxt | Nuxt module |
| Svelte / SvelteKit | @puremetrix/tracker/sveltekit (or /svelte) | injectPureMetrix({ … }) |
| Astro | @puremetrix/tracker/astro | Astro integration |
All adapters take the same options as init() and expose usePureMetrix() (or $puremetrix) with track, trackPageview, grantConsent, revokeConsent, optOut, optIn, isOptedOut. Route changes are tracked automatically in every framework — no router hook needed.
React (Vite, Create React App, …)
import { PureMetrixProvider, usePureMetrix } from "@puremetrix/tracker/react";
createRoot(document.getElementById("root")!).render(
<PureMetrixProvider domain="example.com" publicKey="pk_...">
<App />
</PureMetrixProvider>,
);
function SignupButton() {
const { track } = usePureMetrix();
return <button onClick={() => track("Signup")}>Sign up</button>;
}/react has no dependency on Next.js. Use publicKey, not key: React never passes a prop called key to a component.
Next.js (App Router and Pages Router)
// app/layout.tsx
import { PureMetrixProvider } from "@puremetrix/tracker/next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<PureMetrixProvider domain="example.com" publicKey="pk_...">
{children}
</PureMetrixProvider>
</body>
</html>
);
}Extra props: strategy (afterInteractive by default) and nonce for a strict CSP. @puremetrix/tracker/react also works in Next.js (it loads the script after hydration); /next uses next/script.
Remix / React Router
// app/root.tsx
import { PureMetrixProvider } from "@puremetrix/tracker/remix";
export default function App() {
return (
<PureMetrixProvider domain="example.com" publicKey="pk_...">
<Outlet />
</PureMetrixProvider>
);
}Vue 3
import { createApp } from "vue";
import { createPureMetrix } from "@puremetrix/tracker/vue";
createApp(App).use(createPureMetrix({ domain: "example.com", publicKey: "pk_..." })).mount("#app");In components: const { track } = usePureMetrix() (from @puremetrix/tracker/vue) or $puremetrix.track("Signup") in templates.
Nuxt 3/4
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@puremetrix/tracker/nuxt"],
puremetrix: { domain: "example.com", publicKey: "pk_..." },
});Runs on the client only and provides useNuxtApp().$puremetrix. Values can be overridden per environment with NUXT_PUBLIC_PUREMETRIX_DOMAIN and NUXT_PUBLIC_PUREMETRIX_PUBLIC_KEY. Function options (beforeSend, function props) can't be serialised into Nuxt's runtime config — set them with window.puremetrixConfig in a client plugin instead.
Svelte / SvelteKit
// src/hooks.client.ts
import { injectPureMetrix } from "@puremetrix/tracker/sveltekit";
injectPureMetrix({ domain: "example.com", publicKey: "pk_..." });Plain Svelte: call injectPureMetrix() once in your entry file or in onMount.
Astro
// astro.config.mjs
import { defineConfig } from "astro/config";
import puremetrix from "@puremetrix/tracker/astro";
export default defineConfig({
integrations: [puremetrix({ domain: "example.com", publicKey: "pk_..." })],
});Adds the tracker to every page; view transitions count as pageviews.
Custom events and revenue
import { track } from "@puremetrix/tracker";
track("Signup");
track("Download", { props: { format: "pdf", pages: 12 } });
track("Purchase", { revenue: { currency: "EUR", amount: 29.9 } });
// Background events that should not count as interaction (bounce rate stays as is)
track("Video autoplay", { interactive: false });
// Know when it was sent (or why it was skipped)
track("Signup", { callback: (result) => console.log(result) }); // { status: 202 } | { ignored: "opt-out" } | { error }Events sent before the tracker is ready are queued and delivered once it starts. Property values can be strings, numbers or booleans (stored as text).
Manual pageviews
import { init, trackPageview } from "@puremetrix/tracker";
init({ domain: "example.com", publicKey: "pk_...", manual: true });
trackPageview(); // current URL
trackPageview({ url: "https://example.com/checkout/step-2", referrer: "https://example.com/cart" });manual: true turns off automatic pageviews, so you decide what counts (for example virtual steps or pages behind a modal). A manual pageview behaves like an automatic one: it closes the previous page's engagement time and starts a new one. Without manual, trackPageview() for the URL that was just counted is ignored, so you can't double count by accident.
Privacy controls
Filter or redact data: beforeSend and excludeSearch
init({
domain: "example.com",
publicKey: "pk_...",
excludeSearch: true, // drop query parameters (keeps utm_* and ref)
beforeSend(event) {
if (event.url.includes("/admin")) return null; // don't send at all
return { ...event, url: event.url.replace(/\/users\/\d+/, "/users/:id") };
},
});beforeSend sees every event (name, url, referrer, props, revenue) and returns it — changed or not — or null to drop it. If your function throws, the event is dropped rather than sent unfiltered.
Global properties
init({ domain: "example.com", publicKey: "pk_...", props: { app_version: "2.4.0" } });
// or computed per event:
init({ domain: "example.com", publicKey: "pk_...", props: (eventName) => ({ logged_in: String(isLoggedIn()) }) });Added to every pageview and custom event; properties passed to track() win.
Consent mode
PureMetrix is cookieless and doesn't store anything on the visitor's device, so many sites run it without a consent banner. Please check this with your own legal advice. If you want to (or must) wait for consent:
import { init, grantConsent, revokeConsent } from "@puremetrix/tracker";
init({ domain: "example.com", publicKey: "pk_...", consent: "pending" });
// in your cookie banner callback:
grantConsent(); // starts tracking now and counts the current page
revokeConsent(); // stops againWhile consent is pending nothing is sent and nothing is buffered. Cookie banners usually report the stored choice on every page load, so call grantConsent() from that callback. Without the npm package the same works with window.puremetrix("consent", "granted") / "denied" and data-consent="pending" on the script tag.
Cookie banner recipes (add PureMetrix as a service in the "statistics" category):
// Cookiebot
window.addEventListener("CookiebotOnConsentReady", () =>
Cookiebot.consent.statistics ? grantConsent() : revokeConsent());
// Usercentrics (Admin → Implementation → Data layer & events → window event "ucEvent")
window.addEventListener("ucEvent", (e) => {
if (e.detail?.event === "consent_status") e.detail["PureMetrix"] ? grantConsent() : revokeConsent();
});Borlabs Cookie and Real Cookie Banner (WordPress): create a PureMetrix service and paste this as its opt-in code — it also works before the tracker has loaded:
<script>window.puremetrix=window.puremetrix||function(){(window.puremetrix.q=window.puremetrix.q||[]).push(arguments)};puremetrix("consent","granted");</script>Exclude your own visits
import { optOut, optIn, isOptedOut } from "@puremetrix/tracker";
optOut(); // this browser is no longer counted (localStorage flag)Do Not Track and Global Privacy Control are always respected, automated browsers (Selenium, Puppeteer, Cypress) are skipped, and localhost is ignored unless you set includeLocalhost: true.
Debugging
debug: true logs to the console why something was not sent (localhost, opt-out, no consent, beforeSend, Do Not Track …). It's on automatically on localhost — that's where you'd otherwise wonder why nothing arrives.
Loading modes
Loader (default). Adds the small async collect script (under 3 KB gzip) from collect.puremetrix.org. Your CSP needs script-src and connect-src for that host; pass nonce if you use nonces.
Bundled. init({ …, bundled: true }) runs the tracker from your own JavaScript bundle (loaded lazily as a separate chunk). No external script: only connect-src is needed and ad blockers that block third-party scripts don't apply.
Same-origin proxy. Route script and events through your own domain (fewest ad-blocker drops):
init({ domain: "example.com", publicKey: "pk_...", proxy: true }); // /px/event + /px/js
init({ domain: "example.com", publicKey: "pk_...", proxy: "/stats" }); // /stats/event + /stats/jsProxy recipes: Vercel, nginx, Cloudflare and more. Self-hosted collector: host: "https://collect.example.com".
Upgrading from 0.1.x: defaults changed from
/px/event+/px/jsto the PureMetrix Cloud URLs. If you run a proxy and relied on the old defaults, addproxy: true. Explicitapi/scriptSrckeep working unchanged.
All init options
| Option | Type | Default | What it does |
|---|---|---|---|
| domain | string | (required) | Site domain as registered in PureMetrix |
| publicKey / key | string | (required) | Public site key (pk_…) |
| host | string | https://collect.puremetrix.org | Collect host (self-hosting) |
| proxy | boolean \| string | false | Same-origin proxy: true = /px/*, string = base path |
| api | string | from host/proxy | Explicit event URL |
| scriptSrc | string | from host/proxy | Explicit script URL (base path with scriptFile) |
| scriptFile | string | — | File appended to the script base, e.g. script.hash.js |
| bundled | boolean | false | Run the tracker from your bundle, no external script |
| nonce | string | — | CSP nonce for the script tag |
| manual | boolean | false | No automatic pageviews; use trackPageview() |
| hash | boolean | false | Count #/route changes as pageviews |
| outbound | boolean | false | Outbound link clicks (incl. middle click) |
| downloads | boolean | false | File download clicks (incl. middle click) |
| downloadExtensions | string[] | see below | Replace the list of download file types |
| forms | boolean | false | Form submissions |
| scroll | boolean | false | Scroll depth (50 % / 90 %) |
| events | boolean | false | CSS class events (puremetrix-event-name=Signup) |
| is404 | boolean | false | Send a 404 event for this page |
| includeLocalhost | boolean | false | Also track on localhost / file:// |
| props | object \| (name) => object | — | Global properties |
| beforeSend | (event) => event \| null | — | Inspect, change or drop events |
| excludeSearch | boolean | false | Drop query strings (keeps utm_*, ref) |
| consent | "pending" | — | Wait for grantConsent() |
| debug | boolean | on for localhost | Console logging of skipped events |
Default download types: DEFAULT_DOWNLOAD_EXTENSIONS (pdf, xls(x), doc(x), ppt(x), zip, rar, 7z, gz, csv, txt, mp3, mp4, mov, avi, wmv, exe, dmg). To add one: downloadExtensions: [...DEFAULT_DOWNLOAD_EXTENSIONS, "epub"].
Exports
init, track, trackPageview, grantConsent, revokeConsent, optOut, optIn, isOptedOut, resolveEndpoints, DEFAULT_HOST, DEFAULT_DOWNLOAD_EXTENSIONS and all TypeScript types. ESM and CommonJS builds (the Nuxt module and Astro integration are ESM-only), sideEffects: false, SSR-safe.
When a script tag is enough
Marketing sites and WordPress can skip this package and use the HTML snippet or the WordPress plugin:
<script>window.puremetrix=window.puremetrix||function(){(window.puremetrix.q=window.puremetrix.q||[]).push(arguments)}</script>
<script async data-domain="example.com" data-key="pk_..." src="https://collect.puremetrix.org/js/script.js"></script>Use async, not defer: the tracker never has to wait for your page, and an unreachable collector can never delay rendering. The first line is a tiny queue so window.puremetrix() calls never fail before the script has loaded. Every option above has a data-* twin (data-consent="pending", data-exclude-search, data-debug, data-download-extensions="pdf,epub", data-props='{"plan":"pro"}', data-before-send="myFilter"), see the tracking script reference.
Browser support
Modern evergreen browsers (ES2018). During SSR every function is a no-op.
Docs
License
MIT © Lydia Rebers
