Documentation
Learn how to use the tracking tool
Framework Guides
Two ways to install, and both are fully supported. The package gives you typed imports and nothing to declare, and suits anything with a build step — which every framework here has. The script tag is the better fit if you would rather not add a dependency, want the tracker kept out of your bundle, or have no build step at all. It also starts itself, so there is no code to write. Nuxt is its own case, with a module that handles the install and the start together.
From npm, in a bundler
If your site is built with a bundler, you can install the tracker instead of loading a script tag. npm install @privatrak/tracker gives you the same three functions the script puts on window, as ordinary imports.
import { init, track } from '@privatrak/tracker';
init({
apiKey: 'your-public-key',
apiHost: 'https://api.privatrak.com',
});
track('signup-completed');Nothing starts on its own here. The script tag reads its settings off its own attributes and starts itself. An import has no tag to read, so you call init() once, as early in your app as you can. Give it apiHost as well as apiKey: with no script src to work from, the tracker cannot find the address by itself. Without a key it logs Missing apiKey. Events will not be sent. and stops. From the moment init() runs, page views, clicks and submitted forms are recorded exactly as they are with the script tag — including a page view for the page it runs on, so the first one is not lost by starting late.
init() does nothing when there is no browser, so it is safe to import in code that also renders on a server — but it has to run in the browser for anything to be recorded. Call it wherever your framework does its client-side setup. Calling it twice is harmless: the second call is ignored with a warning. One difference from the served script: the npm build carries the Element Picker with it, so picking elements works without fetching anything extra.
The package brings its own type definitions, so init(), track() and setTraits() are typed for you. There is nothing to add to a .d.ts and no global to describe.
React
Call init() in the file that mounts your app, before you render — src/main.tsx in a Vite project, src/index.tsx under Create React App. Everything after that is automatic.
// src/main.tsx
import { init } from '@privatrak/tracker';
import { createRoot } from 'react-dom/client';
import App from './App';
init({
apiKey: 'your-public-key',
apiHost: 'https://api.privatrak.com',
});
createRoot(document.getElementById('root')!).render(<App />);Next.js
Next.js renders on the server, so init() has to run in a client component. Make a small one that does nothing but start the tracker, and render it once from the root layout.
// app/tracker.tsx
'use client';
import { useEffect } from 'react';
import { init } from '@privatrak/tracker';
export function Tracker() {
useEffect(() => {
init({
apiKey: 'your-public-key',
apiHost: 'https://api.privatrak.com',
});
}, []);
return null;
}With the Pages Router, call init() from _app.tsx inside a useEffect instead. The same component works there.
Nuxt
Nuxt gets a module rather than a script tag. Install @privatrak/nuxt, add it to your modules, and put the settings under a tracker key. Every option of TrackerInitConfig is accepted there, and the module starts the tracker in the browser for you. Give it apiHost explicitly: with no script tag to read, the module has nothing to work the address out from.
export default defineNuxtConfig({
modules: ['@privatrak/nuxt'],
tracker: {
apiKey: 'your-public-key',
apiHost: 'https://api.privatrak.com',
},
});The module also registers a useTracker() composable, auto-imported like Nuxt’s own. It hands back track and setTraits:
<script setup>
const { track, setTraits } = useTracker();
track('button-clicked', { page: 'home' });
setTraits(['plan:pro']);
</script>Vue
Call init() in src/main.ts, before the app is mounted.
// src/main.ts
import { init } from '@privatrak/tracker';
import { createApp } from 'vue';
import App from './App.vue';
init({
apiKey: 'your-public-key',
apiHost: 'https://api.privatrak.com',
});
createApp(App).mount('#app');Angular
Call init() in src/main.ts, before the application bootstraps. To reach the tracker from components, wrap the imported functions in a service rather than importing them in every component.
// src/app/tracker.service.ts
import { Injectable } from '@angular/core';
import { track, setTraits } from '@privatrak/tracker';
import type { DataTrackAttrs } from '@privatrak/tracker';
@Injectable({ providedIn: 'root' })
export class TrackerService {
track(name: string, attrs?: DataTrackAttrs): void {
track(name, attrs);
}
setTraits(traits: string[]): void {
setTraits(traits);
}
}Svelte and SvelteKit
Call init() from the root layout inside onMount, so it runs in the browser rather than while the page is being rendered on the server.
<!-- src/routes/+layout.svelte -->
<script>
import { onMount } from 'svelte';
import { init } from '@privatrak/tracker';
onMount(() => {
init({
apiKey: 'your-public-key',
apiHost: 'https://api.privatrak.com',
});
});
</script>
<slot />With a script tag
Nothing here requires the package. If you would rather not add a dependency, want the tracker out of your bundle, or have no build step at all, load the script instead. It starts itself, reads its settings from its own attributes and works out the API address from its src, so there is no init() call to make. Keep the defer: it lets the browser finish building the page first.
<script
src="https://api.privatrak.com/tracker.js"
data-api-key="your-public-key"
defer
></script>The tag goes in the <head> of your HTML — of every page you want recorded, if your site is made of separate pages. That is index.html at the root of a Vite project, public/index.html under Create React App, index.html for Vue, src/index.html in Angular, and src/app.html in SvelteKit. React never re-renders that file, so the tag stays put for the life of the app. In Next.js there is no HTML file to edit: use the Script component from the root layout with strategy="afterInteractive", which runs it once the page is usable and records the first page view then, or from _app.tsx under the Pages Router.
The tag puts the tracker on window, where TypeScript cannot see it. This is the one case where you have to describe it yourself — the npm package above needs none of this:
export {};
declare global {
interface Window {
tracker: {
init: (config?: Record<string, unknown>) => void;
track: (name: string, attrs?: Record<string, string>) => void;
setTraits: (traits: string[]) => void;
};
}
}From your own server
Events can be posted straight to POST /api/events with an X-API-Key header, which is how you record something that never happens in a browser at all. Use a public key here too, even though the call comes from your server. A public key can only send events in and fetch tracker settings, so it is the one that costs you least if it leaks — and it is the only kind you can create at the moment.
curl -X POST https://api.privatrak.com/api/events \
-H "Content-Type: application/json" \
-H "X-API-Key: your-public-key" \
-d '{
"events": [{
"event_type": "custom",
"data_track": "server-event",
"page_url": "https://example.com/pricing?utm_source=newsletter",
"timestamp": "2026-01-15T10:30:00Z"
}]
}'Wherever one of these examples passes attributes to track(), the same rule holds in every framework: never put an account ID, a user ID, an email address, a username, or a hash of one into an attribute. Stored exactly as sent, a value like that makes one person’s visits linkable across days. See Custom Events.