Install
One tag, and everything worth knowing about it.
Pasting the tag takes ten seconds. This page is for the hours afterwards: what leaves your site, what never does, and what on your own page can make the numbers wrong.
The tag
<script defer
src="https://panel.raicode.tech/v1/p.js"
data-panel-key="pk_live_…"></script>Swap the key for your own, from the dashboard. There is no second step, no consent library to wire in, and nothing to add to a cookie policy.
defer matters: the tag runs once your document has parsed, so it never enters your first render.Payload
What the tag sends
One small object per page view and per named event, posted as text/plain so that navigator.sendBeacon can still send it from a page that is closing. The keys are one letter because this object travels on your visitors' mobile connection, not on ours.
- KeyWhat it isExample
kYour public key, read off the tag. Public by design.
pk_live_…
nThe event name.
$pageviewfor a page view.purchase
uThe path only, redacted in the browser and redacted again on arrival.
/orders/:id
rThe referrer's host and nothing else. Absent when there was none, or when it was your own site.
news.example.org
vViewport class: m, t or d. Bucketed in the browser, so the real width never travels.
d
pYour event properties. Strings, numbers and booleans only.
{"plan":"pro"}
sHosts of the page's other third-party scripts, once per load. Describes your page, never your visitor.
["www.googletagmanager.com"]
x1 if somebody had already wrapped
history.pushStatebefore we loaded.1
What it never sends
No cookie, no
localStorage, nosessionStorage, no IndexedDB.Nothing at all is written to the visitor's device — which is exactly what removes the consent banner from your site. The build script greps the shipped bytes for all four APIs and fails the build if any of them appears.
No query string and no fragment, except five names.
?token=…,?email=…and#…are precisely where a password-reset token or somebody's address ends up in an analytics tool, and there is no syntax that tells a safe parameter from a dangerous one — so the query string is dropped by shape. The exception isutm_source,utm_medium,utm_campaign,utm_termandutm_content, read by name. A name is not a shape: a URL carrying a reset token and a campaign yields the campaign and not one byte of the token. There is nogclidand nofbclid— a per-click identifier is the one thing this product refuses everywhere — anddata-panel-utm="false"turns even the five off.No page title.
"Order #4471 — Jean Dupont" is a title bar, and it is the most common accidental leak of a customer's customer's name.
No full referrer.
The host answers "where did this traffic come from"; the rest is somebody else's business.
No IP address.
Your visitors' addresses are used server-side to derive a hash that rotates every night and to resolve a country, then discarded. They are written to no table and to no log line.
No screen size.
The viewport is sorted into one of three classes in the browser itself, so the number never travels.
No identifier that outlives the day.
There is nothing this script could store to recognise you tomorrow, because it stores nothing.
Identifying segments are erased from the path before it is sent: /orders/3f2504e0-… becomes /orders/:id, and bare numbers, e-mail addresses and token-shaped strings get the same treatment. The rule is shared with the server, which applies it a second time on arrival. It over-redacts deliberately — /products/iphone-15-pro-max becomes /products/:id, which costs a little reporting detail, where the other direction would cost somebody their privacy.
Options
The attributes
Only data-panel-key is required.
- AttributeDefaultWhat it does
data-panel-keyDefault—
Required. Your public key. It is public by design: it sits in your page source and grants nothing but append-only measurement for one site.
data-panel-hostDefaultthe script's origin
Where events are posted. Only needed if you reverse-proxy
/v1/p.jsthrough your own domain — which is the usual answer to blockers.data-panel-auto="false"Defaulton
Stops the automatic page view and the single-page-app URL watching. You then call
panel.pageview()yourself.data-panel-local="true"Defaultoff
Also counts on
localhost,127.0.0.1,*.local,*.testandfile://. Without it your development machine is never counted.data-panel-diagnostics="false"Defaulton
Stops sending
sandx— the two fields that describe your page rather than your visitor. You lose the diagnosis below along with them.
The JavaScript API
panel('signup', { plan: 'pro', seats: 3 });
panel.pageview();
panel.pageview('/cart');Properties must be strings, numbers or booleans. Nested objects and arrays are dropped rather than flattened: flattening is how a whole user record ends up in four columns because somebody passed the object they already had. Anything that looks like an e-mail address inside a value is replaced before it leaves the page. Names starting with $ are reserved.
If your code calls before the script loads
<script>
window.panel=window.panel||function(){
(panel.q=panel.q||[]).push(arguments)
}
</script>The tag carries defer, so it runs after your document parses. If your own code calls panel(…) before that, add this stub above your other scripts: anything queued is sent in order once the script loads. It is deliberately not in the pasted snippet — the install is one line, and a second line most people do not need is a second thing to get wrong.
Diagnosis
The other scripts on your page.
Almost every "my numbers are wrong" report about a tracking script turns out to be about something else on the page.
So Panel looks, once per page load, and tells you. Two fields only, and they describe your page and never your visitor: the hosts of the other third-party scripts, and a flag if somebody had already wrapped history.pushState before us. These are facts you would read yourself in your own site's source, identical for two people loading the same page, and they contain no identifier. Hosts and never URLs: a third-party script URL routinely carries an account id or a session token.
Both fields switch off with data-panel-diagnostics="false". The diagnosis switches off with them, and that is the one trade-off on this page that is entirely yours to make.
Another script had already wrapped history.pushState
xPanel detects thisThere is no browser event that says "a single-page app just changed page". So every analytics tool wraps the same two
historymethods. Two wrappers on one method is the standard way a single-page count goes wrong: double counts when both fire, missing counts when one forgets to call through. We measure the state before we install ours, because afterwards the answer would always be yes.Two copies of the tag
sPanel detects thisAlmost always one in the site template and one in a tag manager. The second refuses to start, so you are not double-counted — but our own host then shows up in the third-party script list, and one of them should go.
A consent manager holding the tag
sPanel detects thisSome block every third-party script until a click that, more often than not, never comes. Panel does not need that consent: nothing is written to the device and nothing survives the night. So the tag can be allow-listed, or moved out of the managed block entirely. If your page views are a fraction of your server logs, this is the first thing to check.
A tag manager injecting scripts after load
sPanel detects thisThe
slist is a snapshot taken when the first page view is sent, a few milliseconds after your document parses. Anything injected later is not in it. Waiting would delay the page view, and a delayed page view is a lost page view on every visitor who bounces.An A/B testing tool that reloads or redirects
sPanel detects thisA redirect on load manufactures two page views for one visit, and a test that rewrites the URL can manufacture more. The tool shows up in the list; which of your two measurements is right is your call.
An ad blocker, or a filtering DNS resolver
A blocked script sends nothing, and there is no way for us to know it happened. This is an undercount that affects every analytics tool including ours, and it is uneven: larger on a technical audience. Serving
/v1/p.jsfrom your own domain, withdata-panel-host, is the usual answer.A Content-Security-Policy
script-srchas to allow the host serving/v1/p.js, andconnect-srcthe host receiving the events — the same one, unless you setdata-panel-host. If either is missing the script fails silently, and that is deliberate: it never writes to the console, never throws, and never appears in your error tracking. The price of that restraint is that it takes this line of documentation to catch it.A hash router, the #/cart kind
Not followed automatically, and that is a refusal rather than a gap: counting it would mean sending the fragment, which is the half of a URL we drop on principle. Call
panel.pageview('/cart')from your router instead. You decide what leaves your page, and we redact it on the way out anyway.Do Not Track and Global Privacy Control
Honoured, so a small and self-selected slice of your visitors is not counted. Panel cannot follow anyone across sites or across days, so the signal arguably does not apply to us; a product whose whole argument is restraint does not get to claim that exemption.
Your own development machine
localhost,127.0.0.1,*.local,*.testandfile://are not counted, because the first thing anybody does after pasting the tag is reload their dev server twenty times. To test an install, adddata-panel-local="true"— an attribute, not thelocalStorageswitch other tools offer, because Panel writes nothing to a visitor's device. That constraint is the product showing up in the API.Pages the browser prerenders
Chrome loads pages in the background that the visitor may never open. Those are not counted until the page is actually shown.
Bots
Filtered server-side, from the user agent and the request rate, never in the browser: a filter placed in the page is one a headless browser turns off by lying.
Measure your traffic without asking for anything.
One script tag, no consent banner, and a server in the European Union. Your first page view appears within seconds.
No commitment. No qualification call.
The whole install
<script defer
src="https://panel.raicode.tech/v1/p.js"
data-panel-key="pk_live_…"></script>