Tracking
This page is the technical reference for the Warm AI tracking script: how it loads, what it stores in the browser, which events it sends and what each field means. It is written for developers and for anyone checking what they are deploying.
- To get the script onto your site, see Installing the tracker.
- For consent managers and jurisdiction rules, see Cookies & Consent.
- For the formal disclosure of what is collected and why, see Tracker Privacy & Compliance.
Which script you are running
Every customer installs the same script, served per site from:
https://cdn.menoinfra.com/p/YOUR_TRACKING_ID.jsThe file is built per tracking ID. The first line sets window.Pulse, which carries your tracking ID and the settings we hold for your site, and the tracker code follows it. You never edit this file; changes to your settings are picked up the next time the file is served.
If your account uses a custom tracker domain, the same file is served from your own subdomain instead of cdn.menoinfra.com.
Installed Warm AI before mid-2026? Some early customers run an older script, warm.js, from cdn.warmai.uk. It works the same way in outline (a tab-scoped session in sessionStorage, no cookies of its own, no persistent device identifier), but it uses a window.warmai command queue instead of window.pulse and sends events to a different endpoint. Newer features are only added to the standard script, so we recommend switching: replace your existing tag with the snippet in Installing the tracker. If you are not sure which one you have, search your site’s HTML for warm.js or menoinfra.
How the script loads
The install snippet does two things. It creates a small window.pulse function that queues any calls you make before the script has arrived, and it adds an async script tag for your per-site file.
<script>
(function(id){
window.pulse=window.pulse||function(){(pulse.q=pulse.q||[]).push(arguments)};
var s=document.createElement('script');s.async=1;
s.src='https://cdn.menoinfra.com/p/'+id+'.js';
document.head.appendChild(s);
})('YOUR_TRACKING_ID');
</script>When the script runs it:
- Reads your tracking ID from
window.Pulse.id(set by the first line of the per-site file). - Checks for a consent manager on the page and reads its current state.
- Starts or resumes the session and sends the first event.
- Replays any
pulse(...)calls that were queued before it loaded.
Because the tracking ID is built into the file, the script also works as a plain <script src="https://cdn.menoinfra.com/p/YOUR_TRACKING_ID.js" async></script> tag. That form is useful when a consent manager needs to manage the tag directly; see Manual tagging.
What it stores in the browser
The tracking code stores one entry, in sessionStorage:
| Key | Contents | Lifetime |
|---|---|---|
_p_session | { "token": "<uuid>", "lastActivity": <timestamp> } | Ends after 30 minutes without activity, and when the tab is closed |
That is the only thing the tracking code itself writes. It sets no cookies and does not send any persistent device identifier. sessionStorage belongs to a single tab, so a second tab, or the same visitor tomorrow, starts a new session token.
The session token is a random UUID. It contains nothing about the visitor.
Events
The script sends three event types: session_start, page_view and session_end.
Common fields
Every event carries these fields:
| Field | Type | Description |
|---|---|---|
tracking_script_id | string | Your tracking ID |
event_type | string | session_start, page_view or session_end |
session_token | string | The UUID from _p_session |
url | string | Full location.href when the event fired |
path | string | location.pathname |
title | string | document.title |
referrer | string or null | document.referrer, or null when empty |
user_agent | string | navigator.userAgent |
The browser never sends an IP address. Our edge adds the visitor’s IP address and country to the event before it is stored (see Transport).
session_start
Sent when a page loads and there is no live session in _p_session (a new tab, or more than 30 minutes since the last activity). Also sent when a tab becomes visible again after being hidden, with a new session token (see Tab switching).
UTM parameters from the URL are added to session_start only, and only the ones present on the URL:
{
"tracking_script_id": "aeec6fcf-7138-4742-ac4e-0adc0221c737",
"event_type": "session_start",
"session_token": "3f1c9a52-6b0e-4d2a-9c1e-7a4b8e2d5f60",
"url": "https://yoursite.com/pricing?utm_source=google&utm_campaign=brand",
"path": "/pricing",
"title": "Pricing | YourSite",
"referrer": "https://www.google.com/",
"user_agent": "Mozilla/5.0 ...",
"utm_source": "google",
"utm_campaign": "brand"
}The five UTM keys read are utm_source, utm_medium, utm_campaign, utm_term and utm_content. Ad click identifiers such as gclid are not sent as separate fields; they arrive as part of url and are read from it on our side.
page_view
Sent in two cases:
- A full page load inside a live session. For example, the visitor clicks a normal link to another page on your site. The session token is reused and the event carries only the common fields.
- A client-side navigation in a single-page app (
history.pushState,history.replaceStateor the back and forward buttons) that changes the path. Changes to the query string or hash alone are ignored. These events add timing fields for the page being left:
{
"event_type": "page_view",
"duration_seconds": 47,
"active_seconds": 32,
"scroll_depth": 0
}| Field | Meaning |
|---|---|
duration_seconds | Wall-clock seconds spent on the previous path |
active_seconds | Active seconds counted so far in this tab’s session (see Active time) |
scroll_depth | Always 0 on single-page-app navigations at present. Scroll depth is reported on session_end |
session_end
Sent when the tab is hidden (the visitor switches tab, minimises the window or locks the phone) or the page is unloaded.
{
"event_type": "session_end",
"duration_seconds": 183,
"active_seconds": 94,
"scroll_depth": 100
}| Field | Meaning |
|---|---|
duration_seconds | Wall-clock seconds on the current page |
active_seconds | Active seconds counted so far in this tab’s session |
scroll_depth | Furthest scroll position reached on the current page, as a percentage from 0 to 100 (100 if the page does not scroll) |
Tab switching
Hiding a tab sends session_end. When the tab becomes visible again, the script creates a new session token and sends session_start. On our side, a session_start that arrives from the same IP address and browser within 30 minutes of an earlier session on the same site that is still open is added to that session rather than counted as a new visit. This is why one visitor’s tab switches do not normally show up as many separate sessions.
Active time and page time
The script measures two kinds of time:
- Page time (
duration_seconds): wall-clock time from arriving on a page until leaving it or hiding the tab. It includes time the tab sat in the background or the visitor was idle. - Active time (
active_seconds): time during which the tab was visible and the visitor clicked, moved the mouse, pressed a key or touched the screen within the previous 30 seconds. It is checked every 5 seconds.
Active time is the better engagement signal. A pricing page left open in a background tab for 20 minutes has a large page time and almost no active time. A visitor reading every paragraph has the two close together.
Consent
The script reads consent from your consent manager if it finds one. Cookiebot, OneTrust, CookieYes, Transcend, Framer’s built-in banner, HubSpot, Klaro, Google Consent Mode v2 and IAB TCF v2 are detected automatically.
- Consent denied: the script sends nothing.
- Consent manager found, visitor has not chosen yet: the script holds up to 50 events and sends them if the visitor grants consent.
- Consent granted: events are sent as normal.
How consent affects person-level identification, and what happens for visitors in different countries, is covered in Installing the tracker and Cookies & Consent.
The pulse command API
Once the script has loaded, window.pulse accepts commands. Calls made before that are queued by the install snippet and replayed in order, so you can call pulse(...) straight after the snippet.
| Call | What it does |
|---|---|
pulse('consent', 'grant') | Marks analytics consent as granted and sends any held events |
pulse('consent', 'revoke') | Marks consent as denied and discards any held events |
pulse('consent', 'status') | Returns { state, source, queued }. Only useful after the script has loaded, because a queued call has no return value |
pulse('track', name, data) | Sends a page_view carrying custom_event: name plus the fields in data |
window.Pulse exposes the same consent controls as methods: Pulse.giveCookieConsent(), Pulse.revokeCookieConsent(), Pulse.consentStatus() and Pulse.track(name, data).
Use the manual consent calls only if you have no consent manager. If one is installed, let the tracker read it. A manual grant enables the session events above. Where person-level identification needs consent, it waits for a signal from a consent manager, and a manual grant does not unlock it.
pulse('track', ...) is not a custom events feature yet. The event is currently stored as an extra page view, and the name and properties you pass are not shown in the dashboard.
Transport
Events are sent with navigator.sendBeacon, which survives the page being closed. Browsers without it fall back to fetch with keepalive: true.
All events go to https://t.menoinfra.com/api/track. That edge proxy adds the visitor’s IP address and country and forwards the event to our ingestion service, which:
- rejects events from a domain the tracking ID was not issued for (subdomains and
localhostare allowed; see Installing the tracker); - rejects events while tracking for the site is paused;
- runs company identification once a session has ended.
If your site sets a Content Security Policy, allow cdn.menoinfra.com in script-src and t.menoinfra.com in connect-src.
Troubleshooting
If sessions are not appearing in the dashboard a minute after you visit your own site:
- Is the script loading? Open devtools, go to Network and filter for
menoinfra.p/YOUR_TRACKING_ID.jsshould return 200. No request at all means the snippet is not on the page or is not running. - Are events being sent? Filter for
api/track. If the script loaded but nothing is posted, check your Content Security Policy and whether a consent manager is holding the events. - Is it the right tracking ID? The ID must be the one issued for this domain. Events from another domain are rejected.
- Check consent. In the console, run
Pulse.consentStatus(). Astateofdeniedmeans nothing is sent;pendingmeans events are being held until the visitor accepts.
Platform-specific problems (smart quotes, WP Rocket, Cloudflare Rocket Loader, caching) are covered in install troubleshooting. Still stuck? Email support@getwarmai.com with your site URL and tracking ID.