Installation guides
Every guide installs the same tag. Replace pk_live_xxxxxxxx with the key from
Settings → Installation.
<script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script>data-api is optional — it defaults to https://ingest.tracing.tools — but
including it makes the snippet explicit and is what the dashboard copies.
Code frameworks
Section titled “Code frameworks”Paste it before the closing </head> tag of every page. Static-site generators
usually have one layout or partial that every page shares — put it there.
<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script> </head> <body> <!-- … --> </body></html>Use next/script in the root layout. afterInteractive is the right strategy:
the tracker must load on the first paint so the landing hit carries the
campaign, but it does not block rendering.
import type { ReactNode } from "react";import Script from "next/script";
export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body> {children} <Script id="tracing" strategy="afterInteractive" src="https://ingest.tracing.tools/t.js" data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" /> </body> </html> );}Client-side route changes are picked up automatically: the tracker patches
history.pushState and listens for popstate, which is what the App Router
uses. You do not need a usePathname() effect.
For the Pages Router the same <Script> goes in pages/_app.tsx.
Calling the API from a component:
"use client";
declare global { interface Window { tracing?: ((name: string, props?: Record<string, string | number | boolean>) => void) & { identify?: (traits: Record<string, string | number | boolean>) => void; }; }}
export function SignupButton() { return ( <button onClick={() => window.tracing?.("signup_completed", { plan: "pro" })}> Sign up </button> );}Put the tag in index.html — it is a plain static file and the tracker does
not need to be part of the bundle.
<!doctype html><html lang="en"> <head> <script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script> </head> <body><div id="root"></div></body></html>React Router, TanStack Router and Wouter all navigate through
history.pushState, so route changes are tracked with no extra code. A router
that only changes the hash needs data-hash="true" — see
Pageviews & SPAs.
Vue (Vite): the tag goes in index.html, exactly as for React.
Nuxt 3/4: declare it in nuxt.config.ts so it renders on every route,
including statically prerendered ones.
export default defineNuxtConfig({ app: { head: { script: [ { src: "https://ingest.tracing.tools/t.js", defer: true, "data-site": "pk_live_xxxxxxxx", "data-api": "https://ingest.tracing.tools", }, ], }, },});Vue Router uses the History API, so in-app navigation is tracked automatically.
Add the tag to the app shell. %sveltekit.head% is replaced at render time, so
anything you put beside it is on every page.
<!doctype html><html lang="en"> <head> %sveltekit.head% <script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script> </head> <body data-sveltekit-preload-data="hover"> <div>%sveltekit.body%</div> </body></html>SvelteKit’s client router pushes history entries, so route changes are picked
up without a afterNavigate hook.
Put the tag in the shared layout. Astro leaves is:inline-free <script src>
tags with data-* attributes alone when they point at an external origin, so
the attributes survive the build.
---const { title } = Astro.props;---<html lang="en"> <head> <title>{title}</title> <script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script> </head> <body><slot /></body></html>With View Transitions enabled, Astro navigates through the History API, so
pageviews are still tracked. The tracker is loaded once and survives swaps
because it lives on window.
Site builders and CMSs
Section titled “Site builders and CMSs”No plugin is needed. Two options, in order of preference.
Theme header (child theme or a code snippet plugin):
add_action('wp_head', function () { ?> <script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script> <?php}, 1);Enqueue it properly, which lets caching plugins version and defer it correctly:
add_action('wp_enqueue_scripts', function () { wp_enqueue_script( 'tracing', 'https://ingest.tracing.tools/t.js', [], null, ['strategy' => 'defer', 'in_footer' => false] );});
add_filter('script_loader_tag', function ($tag, $handle) { if ($handle !== 'tracing') return $tag; return str_replace( '<script ', '<script data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" ', $tag );}, 10, 2);Exclude the admin area from reports with Settings → General → Excluded paths:
/wp-admin/**/wp-login.phpEdit Online Store → Themes → … → Edit code → layout/theme.liquid and put
the tag directly above </head>.
{{ content_for_header }} <script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script> </head>Shopify’s own checkout runs on checkout.shopify.com and does not execute
theme scripts, so purchases will not appear as pageviews. Send the revenue
from the Order status page script if your plan allows one, as
tracing("purchase", { revenue: total, currency }); see
Tracking revenue.
Tag an add-to-cart from a section:
<button data-tracing-event="add_to_cart" data-tracing-event-product="{{ product.handle }}"> Add to cart</button>Site settings → Custom code → Head code, then publish. Custom code only
runs on the published site, not in the Designer or on *.webflow.io previews
if you have those excluded.
<script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script>For a per-page event, add an Embed element or use the element’s custom
attributes panel to set data-tracing-event on a button.
Project settings → General → Custom code → Start of <head> tag, paste,
then publish.
<script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script>Framer sites navigate client-side through the History API, so page changes are
tracked. Framer also renders an editor preview on a framer.app subdomain; add
it to Excluded paths only if it shares your production key, otherwise give
previews their own site.
Settings → Developer tools → Code injection → Header.
<script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script>Code injection is a Business-plan-or-above feature on Squarespace. On lower plans, use the Google Tag Manager route if GTM is available to you, or upgrade.
Settings → Custom code → Add custom code, then:
- Paste the code snippet here: the tag below
- Add code to pages: All pages
- Place code in: Head
<script defer data-site="pk_live_xxxxxxxx" data-api="https://ingest.tracing.tools" src="https://ingest.tracing.tools/t.js"></script>Wix loads custom head code after its own app shell. Pageviews still arrive; the very first one may be a few hundred milliseconds later than on a hand-written page.
Google Tag Manager
Section titled “Google Tag Manager”-
Tags → New → Tag Configuration → Custom HTML.
-
Paste the snippet. Leave Support document.write unchecked.
<script deferdata-site="pk_live_xxxxxxxx"data-api="https://ingest.tracing.tools"src="https://ingest.tracing.tools/t.js"></script> -
Triggering → Initialization - All Pages. Not All Pages: initialization fires earlier, which keeps the landing hit — and the campaign on it — intact.
-
Set Tag firing options to Once per page.
-
Submit and publish.
To send a custom event from another GTM tag, call the global directly:
<script> window.tracing && window.tracing("form_submitted", { form: {{Form ID}} });</script>Content Security Policy
Section titled “Content Security Policy”The tracker needs to load from the collector and to POST to it. With a strict CSP, add the collector origin to two directives:
script-src 'self' https://ingest.tracing.tools;connect-src 'self' https://ingest.tracing.tools;What each one covers:
| Directive | Why | Requests |
|---|---|---|
script-src |
loading /t.js |
GET https://ingest.tracing.tools/t.js |
connect-src |
the mode probe and every hit | GET /c/:key, POST /e, and navigator.sendBeacon to /e |
The tracker adds no inline script, no styles, no images and no iframes, so
style-src, img-src and frame-src need nothing.
If you proxy the collector behind your own domain, both
directives can stay at 'self' and you need no exception at all.
Verifying the install
Section titled “Verifying the install”- Load a page in a normal tab (not localhost).
- In DevTools → Network, filter for
t.js— it should be200and about 5 KB. - Look for
c/pk_live_…— a200with{"m":"hash"}. - Look for a
POSTto/e— a202. - Open Realtime in the dashboard.
If any of those are missing, work through Opt-out & debugging.