Tracking overview
How the PilotFox tracking script works, what it records and the global object it defines.
The tracking script, s.js, is one small classic script (about 9 KB compressed) that you add to every page. It sets
no cookies.
<script defer src="https://pilotfox.app/api/s.js" data-site="SITE_ID"></script>How it works
- It loads the site's settings from
GET /api/c/SITE_ID, without credentials. The answer says which of the options below are on, whether session replay runs, and whether to respect Global Privacy Control. Browsers cache it for 60 seconds, so a change in Site settings reaches visitors within a minute. If it cannot be loaded (blocked, an error, or no answer within 10 seconds where the browser supports a timeout), the script falls back to safe defaults: the initial pageview, single-page app navigation and query strings, nothing else. - It sends each pageview and event as one request,
POST /api/e, withnavigator.sendBeacon(or, where that is missing or refuses the request,fetchwithkeepaliveand no credentials). The body is JSON sent astext/plain, so the browser sends no CORS preflight, and the server reads no cookie.identifyandsetTraitspost to/api/ithe same way. - The server does the rest: it derives the visitor and the session, looks up location and device, applies your privacy settings and exclusions, filters bots and stores the event. See Concepts.
Everything goes to the directory the script was loaded from. Loaded from https://pilotfox.app/api/s.js, it talks to
https://pilotfox.app/api/...; loaded through your own domain as /_pf/s.js, it talks to
/_pf/....
| Request | What it does |
|---|---|
GET /api/s.js | The script. Cached for an hour. |
GET /api/c/{publicId} | The site's tracker settings. |
POST /api/e | One pageview or event. |
POST /api/i | Identify a user, or set their traits. |
GET /api/r.js | The session replay recorder, loaded only on recorded tabs. |
POST /api/rec/{publicId} | A chunk of a session recording. |
/api/e and /api/i answer 204 whether an event was stored or dropped (by an exclusion or the bot filter, for
example): they never say why. They answer 400 for a malformed body, 404 for an unknown site, 413 for a body that
is too large and 429 when a visitor's address sends too many requests.
What it records
These options are switches in Site settings → Tracking code. Browsers pick up a change within a minute.
| Option | Default | What it records |
|---|---|---|
| Initial pageview | On | A pageview when the script loads. |
| Single-page app navigation | On | A pageview on each History API route change (see Single-page apps). |
| Hash routing | Off | #/path and #!/path changes as pageviews of that path. |
| Query strings | On | The page's query string with each pageview. What is stored of it is a privacy setting. |
| Outbound links | On | Clicks on http and https links to another host name than the page's (another subdomain of your site counts), with the link's address and text (up to 200 characters). |
| Button clicks | Off | Clicks on buttons, submit and button inputs and role="button" elements (or up to three levels inside one), with their tag name, text (up to 100 characters) and id. One per element per second. |
| Form submissions | Off | The form's id, name, action path, method and number of fields, never the values. |
| Copy events | Off | That text was copied, from which kind of element and how many characters, never the text. |
| Input changes | Off | The name (or id) and type of a changed field, never the value. Password and hidden fields are left out. |
| JavaScript errors | Off | Uncaught errors and unhandled promise rejections from scripts of your own origin (a rejection whose reason is not an Error is always sent), at most 25 per page load, the same error once a minute. |
| Web vitals | Off | LCP, CLS, INP, FCP and TTFB, sent once per page view. |
| Payment links and return pages | On | A checkout event with a fresh reference when a link to a Stripe, Dodo Payments or Polar payment page is clicked (the reference is added to the link), and with the payment's id on the page the provider sends the buyer back to. See Revenue. |
Session replay is a separate setting (Site settings → Session replay); see Session replay.
The global object
The script defines window.pilotfox (or the name in the tag's data-namespace):
| Method | What it does |
|---|---|
pageview(url?) | Sends a pageview of the current address, or of url. See Single-page apps. |
event(name, props?) | Sends a custom event. See Events and identify. |
identify(userId, traits?) | Links this browser to your user id, with optional traits. |
setTraits(traits) | Updates the identified user's traits. |
reset() | Forgets the user id (on sign-out). |
getVisitor() | Returns { uid }: the stored user id, or null. |
optOut(), optIn() | Stops tracking in this browser, or resumes it. See Opt-out. |
startReplay(), stopReplay() | Records this tab, or stops recording it. See Session replay. |
loaded | true once the script has run. |
Calls made before the site's settings have arrived wait and run in order once they have (with a
cap on waiting events). Call through
window.pilotfox?. so a page whose script was blocked keeps working:
window.pilotfox?.event("signup", { plan: "pro" });Calls made before the script itself has loaded can be queued; see Calls before the script has loaded.