PilotFoxDocs

Install the script

Add the script tag to every page, set its attributes, allow it in a Content Security Policy and verify the installation.

The script tag

Put this tag in the <head> of every page, with your site's id in place of SITE_ID. Site settings → Tracking code shows the tag with the id filled in.

<script defer src="https://pilotfox.app/api/s.js" data-site="SITE_ID"></script>
  • defer lets the browser download the script early and run it once the page has been parsed.
  • Keep it a classic <script src>, not type="module": the script finds its own tag (and so its data-site) through document.currentScript, which module scripts do not set.
  • Load it at the start of the page, not later (after a click, after a route change, or with a "lazy" loading strategy). A late script records the first pageview late and for the address of that moment, and misses errors thrown before it ran.
  • If a page optimizer strips data-* attributes, put the id in the URL instead: https://pilotfox.app/api/s.js?site=SITE_ID.
  • One tag per page. A second tag with the same namespace is ignored. A second tag with another data-namespace runs separately, under its own name.

In a single-page app, put the tag in the root layout or document template once: route changes are counted for you (see Single-page apps). For example, in a Next.js App Router layout:

app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <script defer src="https://pilotfox.app/api/s.js" data-site="SITE_ID"></script>
      </head>
      <body>{children}</body>
    </html>
  );
}

Use a plain <script> tag as above rather than next/script with lazyOnload, which would load it late.

Install with an AI assistant

Site settings → Tracking code → Install with an AI assistant, and the last step of adding a site, have a Copy AI prompt button. Paste the prompt into Claude Code, Cursor or another coding assistant working in your site's codebase. It describes the tag with your site's id, single-page apps, identify for signed-in users, and how to link payments to visits for each payment provider. It contains no keys or secrets.

Script attributes

AttributeDefaultMeaning
data-site(required)Your site's public id. Or ?site=SITE_ID on the script URL.
data-namespacepilotfoxThe name of the global object (window.pilotfox): ASCII letters, digits, _ and $, not starting with a digit. Any other value falls back to pilotfox. It also prefixes the browser storage keys.
data-debounce300Milliseconds to wait after a route change before counting it as a pageview. A negative or non-numeric value falls back to 300.
data-skipnoneA JSON array of path patterns that are never recorded, for example ["/admin/**"]. Invalid JSON is ignored.
data-masknoneA JSON array of path patterns that hide part of a path, for example ["/orders/*"] records /orders/123 as /orders/*. Invalid JSON is ignored.

Patterns match the page's path (with hash routing, the route's path):

  • A plain pattern matches the whole path, case-sensitively. * matches within one path segment and ** across any number of segments: /admin/** matches /admin/users/7 but not /admin itself (list both if you need both). A path that matches is recorded as the pattern itself (in data-mask) or not at all (in data-skip).
  • A pattern that starts with re: is a regular expression, matched anywhere in the path. In data-mask, each match is replaced with *.
  • In data-mask, the first pattern that matches applies.
<script
  defer
  src="https://pilotfox.app/api/s.js"
  data-site="SITE_ID"
  data-skip='["/admin/**"]'
  data-mask='["/orders/*", "re:[0-9a-f]{8}-[0-9a-f-]{27}"]'
></script>

With this tag, /admin/users/7 sends no pageview, no event and no recording (identify() and setTraits() calls, which carry no page address, are still sent), /orders/123 is recorded as /orders/*, and /invoices/0b9d8f3e-1c2a-4b5d-9e8f-123456789abc/pdf as /invoices/*/pdf.

The URL fragment (#...) is never sent, except as a route with hash routing.

Content Security Policy

If your site sends a Content Security Policy, allow PilotFox in two directives:

DirectiveAllowFor
script-srchttps://pilotfox.appThe script, and the session replay recorder (/api/r.js) when session replay is on.
connect-srchttps://pilotfox.appThe settings request, events, identify and recordings.
Content-Security-Policy: script-src 'self' https://pilotfox.app; connect-src 'self' https://pilotfox.app
  • With a nonce, give the tag the nonce: the replay recorder the script adds gets the same nonce, so script-src 'nonce-...' 'strict-dynamic' works without listing PilotFox.
  • If connect-src (or default-src, its fallback) does not allow PilotFox, the browser blocks the settings request and every event: nothing is recorded.
  • Served from your own domain, the script and its requests need only 'self'.

Verify the installation

Verify installation (Site settings → Tracking code, and the last step of adding a site) loads your site's home page, over HTTPS and then over HTTP if HTTPS does not answer, and looks for a PilotFox tag with your site's id: a <script> whose src ends in /s.js (so a tag served from your own domain counts) with your id in data-site or ?site=. It reads the first 512 KB of the HTML your server sends (following up to 3 redirects, for 8 seconds at most per attempt): a tag that JavaScript adds to the page later is not seen. The result is one of:

Resultreason in the APIMeaning
InstalledThe home page has the tag with this site's id.
Not installedscript_not_foundThe page loaded, without the tag.
Wrong site idwrong_siteThe page has a PilotFox tag for another site.
Couldn't checkunreachable, timeout, blockedThe page could not be loaded (no answer or an error status), took longer than 8 seconds, or the domain points to a private or non-public address.

The same check is available over the API (reference), with the site:read permission and up to 10 checks a minute:

curl -X POST -H "Authorization: Bearer $PILOTFOX_API_KEY" \
  -H "content-type: application/json" -d '{}' \
  https://pilotfox.app/api/v1/sites/SITE_ID/verify-install
{ "data": { "installed": true, "url": "https://example.com/", "status": 200 } }

When the tag is not found, installed is false and reason says why, as in the table above. A check that could not load the page is still a 200 answer.

Then visit your site in another tab and open the site's dashboard: the Online figure and the Realtime view show the visit as it arrives.

Troubleshooting

Open your site with the browser's developer tools on the Network tab and filter on pilotfox.app (or your proxy path):

What you seeLikely cause
No request to /api/s.jsThe tag is missing from this page, or an ad blocker blocked it: serve it from your own domain.
/api/s.js loads, nothing elseThe tag is type="module" or injected without its data-site; or a Content Security Policy blocks connect-src (the console says so).
/api/c/SITE_ID answers 404The site id is wrong, or the site was deleted.
/api/c/... loads, no POST /api/eThis browser is opted out (?pilotfox_ignore=true or optOut() earlier: run pilotfox.optIn()), sends Global Privacy Control, the page matches data-skip, or Initial pageview is off.
POST /api/e answers 204, nothing in the reportsThe event was dropped on the server: Allowed hostnames, an exclusion (your own IP address, for example) or the bot filter. See Opt-out and excluding traffic.
POST /api/e answers 429Too many requests from one address in a short time.

On this page