Optional tracker modules
Load optional tracker modules for time on page and scroll depth, automatic outbound link, download, form and 404 events, Web Vitals and aggregate click maps.
Modules are small separate files the tracker loads on demand, from the same
origin as the tracker (or data-api). Choose
them with data-modules:
<script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID"
data-modules="engage,auto,vitals,clicks,ask"></script>
| Module | Default | What it adds |
|---|---|---|
engage |
On | Time on page (visible time only) and scroll depth |
auto |
Off | Outbound links, file downloads, mailto:/tel: links, form submits and 404s as events. Loads pa.auto.js (about 1.4 KB) |
vitals |
Off | Web Vitals: LCP, INP, CLS, FCP, TTFB. Loads pa.vitals.js (about 1.5 KB) |
clicks |
Off | Aggregate click counts per element for click maps. Loads pa.clicks.js (about 1.3 KB) |
ask |
Off | Cookieless on-site surveys: a small card with your question. Loads pa.ask.js (about 7.5 KB) |
The Default column is what the tracker loads when the script tag has no
data-modules attribute. The snippet in Site settings → Tracking (and
in the setup page and the API) lists all five, so every report has data
from the start and your first survey works without touching the snippet
again. Untick a module there to leave it
out.
Warning: data-modules replaces the default. data-modules="auto"
turns engage off. Write data-modules="engage,auto" to keep both.
engage: time on page and scroll depth#
Built into pa.js (no extra file). It measures:
- Engaged time: only while the tab is visible. Switching tabs or minimizing pauses the clock.
- Scroll depth: the deepest point reached, as a percentage of the scrollable height, stored in 10% bands.
It sends an engagement hit when the page is hidden or closed, and before
the next SPA pageview, if at least one second of visible time has passed.
It also sends one at the visitor's first input on the page (see the
interaction flag). The server
counts at most 4 hours of engaged time per hit. Engagement hits aren't
pageviews or events: they don't count toward your
usage.
Without engage, time on page, visit duration and the "under 10 seconds"
part of bounce rate can't be measured.
auto: automatic events#
Loads pa.auto.js and tracks these events. They are custom events, so
they count toward your usage. The module has no
options: it sends all six or none.
| Event name | When | Properties |
|---|---|---|
Outbound Link |
A click on an http(s) link to another host (a subdomain counts as another host) |
domain (without www.), url (origin and path, no query) |
File Download |
A click on a link whose path ends in a file extension from the list below, on any host | file (file name), host |
Email Link |
A click on a mailto: link |
to_domain (the part after @, never the address) |
Phone Link |
A click on a tel: link |
none (the number isn't sent) |
Form Submit |
Any form is submitted | form: the form's id, else its name, else its action |
404 |
When the page loads, its <title> contains "404" or "not found" (any case), or the page has <meta name="privatus-404"> |
path |
File extensions counted as downloads: pdf, zip, gz, tgz, rar,
7z, dmg, exe, msi, pkg, deb, rpm, apk, ipa, csv, xls,
xlsx, doc, docx, ppt, pptx, odt, ods, txt, rtf, epub,
mobi, mp3, mp4, mov, avi, wav, webm, iso, json, xml,
svg. A link to a file on another host is a File Download, not an
Outbound Link.
Links and forms with a data-privatus-event
attribute are skipped by auto, so you never count one click twice.
The 404 check runs once, when the module loads. It doesn't run again on SPA navigations.
For a reliable 404 event on pages whose title doesn't say "not found", add the meta tag to your 404 template:
<meta name="privatus-404" content="true">
Create goals of type Outbound click or File download to turn these into conversions.
vitals: Web Vitals#
Loads pa.vitals.js, which uses PerformanceObserver to measure:
| Metric | Unit |
|---|---|
LCP Largest Contentful Paint |
milliseconds |
INP Interaction to Next Paint (the slowest interaction) |
milliseconds |
CLS Cumulative Layout Shift |
unitless, 3 decimals |
FCP First Contentful Paint |
milliseconds |
TTFB Time to First Byte |
milliseconds |
Values are sent once per page load, one hit per metric, when the page is
first hidden. In a single-page app that means once for the whole session
on the page, not once per route. Browsers without PerformanceObserver
send nothing, and a metric the browser doesn't support is left out.
Web Vitals hits are sampled on the server at the site's sample rate (Site settings → Tracking → Web Vitals sampling, 0 to 100%, 100% by default, where the Free plan allows up to 10%) and never count toward usage. Results are on the Performance page.
clicks: click maps#
Loads pa.clicks.js, which counts clicks per element for aggregate
click maps. For each click it sends one name:
- When the clicked element or one around it has a name,
data-privatus-click="buy-pro", it sends[data-privatus-click="buy-pro"]. A name may have letters, digits,.,_,:and-, up to 100 characters. Any other value is not sent. - Otherwise a short CSS selector (up to 4 levels, e.g.
nav.main > ul > li > a.pricing) of the nearest link, button, input, select, label,summary,[role=button]or element withdata-privatus-click, or of the clicked element itself when none of those contains it. Each level is the tag name plus the id, or else up to two classes, and the selector stops at the first ancestor with an id. Ids and classes that look generated are left out: three or more digits in a row, or eight or more capital letters or digits in a row (the shape of a record id or a token). So are classes containing:.
The server cuts names at 512 characters. It never sends mouse movement, coordinates, the text of the element or anything typed. Clicks are throttled to one per 300 ms.
When you open a page from its click map (Show on the page), pa.js
also loads pa.overlay.js, which draws the click map on the page. It is
never loaded for visitors, and nothing is sent while it is open.
Click hits don't count toward your usage.
ask: on-site surveys#
Loads pa.ask.js, which shows a
cookieless survey card on your own pages.
Once per page load it asks GET /api/surveys what to show, sending the site
id, the page URL and the dnt and gpc flags when the browser sets them.
The URL is the same minimized one a pageview reports: the canonical link
when data-canonical is on, your data-mask replacements applied and the
query string reduced to campaign parameters and your data-params
allowlist. It asks nothing at all on a page data-exclude leaves out, or
when the visitor has opted out. It doesn't ask
again on SPA route changes.
We answer with at most one survey, and only what the card needs to draw itself: its public id, the trigger, the questions, the position, the thank-you message and the card's button labels. Your targeting rules stay on the server.
When the card is shown, when it is closed with something already answered,
and when it is sent, the module posts to /api/survey with the survey's
public id, which of the three happened and the answers given. Both requests
use credentials: 'omit', so no cookie of yours is ever sent with them.
It stores nothing in the browser: no cookie, no localStorage, no
sessionStorage and no id of any kind. Asking at most once a day is
remembered on our side, under a daily hash that is never stored (see
Surveys).
Styles are set through the CSSOM (element.style), which a Content
Security Policy doesn't restrict, so no style-src exception is needed.
The card follows prefers-color-scheme and prefers-reduced-motion.
What the request costs#
The snippet we give you includes ask, so a survey you create later
shows up without a change to your pages. Take ask out of data-modules
if you never want the request: a site without it makes no extra request
at all.
When it is on, the one GET /api/surveys per page load is answered from
Redis and makes no database query: the site's collecting surveys and
their targeting rules are cached, and so is the one lookup a plan with a
monthly response allowance needs. A typical answer is a few hundred bytes,
or 11 bytes when there is nothing to show. The request is sent after the
page's own work, because the tracker and its modules load with defer, so
it never holds up rendering.
Pausing a survey, changing it or changing plan takes effect at once: the cache entry is rebuilt on the next request rather than waiting to expire.
Survey responses don't count toward your usage.