Tracker & custom events
The tracker is the script embedded on the visitor-facing site (see Installation for the embed). This page covers what it records and the JavaScript API it exposes to page code.
What is recorded automatically
- Pageviews — every page load, plus SPA navigations: the tracker hooks
history.pushState,history.replaceState(a client-side redirect lands on a page too) andpopstate, and counts a page restored from the back/forward cache. A call that leaves the path unchanged (query or hash update) is no navigation, and hash routes (#/pricing) are not told apart: stored paths never carry the fragment. - Clicks and scroll depth — sampled (10 % of page loads by default; see
Settings). These feed the heatmaps. Only clicks the
visitor made count — a script's
element.click()does not — and a click from the keyboard (Enter, Space) is placed at the centre of the element it activated. Every sampled view reports the depth it reached when the visitor leaves, scrolled or not: 0 for a long page read above the fold, 100 for a page shorter than the window. - Page snapshots — only on websites that opted in, and only on visits already sampled for clicks: a serialized rendering of the page, form values and marked elements masked, that the heatmap is drawn over. What is captured, what never is, and the markers are in Pages & heatmaps.
- Session context — referrer, screen size, language, and all five
utm_*parameters — source, medium, campaign, term, content — (extracted onto the session; query strings are dropped from stored paths). The acquisition channel shown on the dashboards is derived server-side from referrer,utm_mediumand ad click ids (Acquisition). When the deployment configures a GeoIP database, the session also carries the visitor's country — derived server-side from the request IP, which is itself never stored.
Events are batched (up to 25) and flushed with sendBeacon to reduce loss when
a page unloads. Delivery is still best-effort: a blocked script or failed network
request can leave gaps. The visitor is identified by a server-side anonymous hash — no
cookie, no localStorage, nothing stored client-side
(Architecture has the details).
Server-side, beacons only count when the browser-reported Origin (or,
failing it, Referer) matches the website's declared domains: a foreign
host is refused (403), a request carrying neither header — no browser sends
a beacon that way — is dropped silently, as are declared bots and
machine-rate sources. Each event is validated on its own: an invalid one is
dropped and counted, never the rest of its batch.
Architecture covers the guard
chain. This keeps other sites' pages out of your data; it does not
authenticate the sender — a script forging a browser's headers is accepted
like any visitor.
Do Not Track and Global Privacy Control are honoured by default: a
browser sending DNT or GPC (navigator.globalPrivacyControl) is not
collected unless the embed opts out with data-do-not-track="false".
Including the tag twice (theme plus tag manager) is harmless: the second copy logs a console warning and does nothing.
Excluded pages. A page matching the tag's data-exclude-paths sends
nothing: no pageview, no click, no scroll depth, no snapshot, and no custom
event while it is on screen. A SPA navigation away from it counts the next
page as usual.
A live website id. The tracker reads its tag's data-website-id as it
goes: changed, what was queued leaves under the previous website and the
page on screen is counted for the new one; removed, nothing more is sent
until it is set again. That is how the dashboard's own tag
(Configuration)
follows sign-out and workspace switches.
Removing or changing the attribute blocks new events synchronously, before the mutation observer runs. Already queued events still flush under the old website. The observer then activates the new website and counts the current page; events during this transition are discarded rather than attributed to the wrong workspace. A tag that initially has no id stays paused and can activate later without loading the script again.
Custom events
Page code reports business events through the API the tracker installs:
window.dmsMarketing.track("signup_submitted", { plan: "pro" });
name— up to 50 characters (the tracker cuts a longer one); required.data— optional object, at most 4 KiB serialized. An oversized payload drops the event server-side (counted, not a 400); the rest of the batch is kept.
Custom events show up in the overview's KPI cards and its Custom events card (counts per name — which CTA gets clicked), and are the second kind of funnel step (Funnels & A/B tests); the funnel builder suggests the names the site already sends. An event that ends a funnel is tagged Goal on the overview.
Experiment variations
variation() answers the arm the backend assigned this visitor and reports
the exposure (Funnels & A/B tests):
window.dmsMarketing?.variation("hero-cta").then((arm) => {
if (arm === "b") {
showAlternateHero();
}
});
Promise-based, so it runs as is in a classic <script> (a top-level
await only works in a module script); the builder's snippet has this shape.
Asynchronous by design — the first call loads the per-visitor assignments
from the backend (nothing is fetched on pages that never ask). A null
answer means "render the control": key unknown or not running,
collection off, DNT. The exposure is only reported for the keys the page
actually consulted, once per page load.
exposure(experiment, variation) is the low-level escape hatch for
integrations that assign on their own; prefer variation(). Either way the
backend only records exposures for a split that is currently running — a
stopped test stops counting, whoever reports the arm.
Guard the call
window.dmsMarketing is installed as a no-op on the paths that collect
nothing (no website id, DNT or GPC, collection paused — the tracker is
then cached a minute and publishes that no-op alone), but is absent when the script
itself never runs — blocked, failed to load, or a type="module" embed,
which defeats document.currentScript. Page code should still guard:
window.dmsMarketing?.track("signup_submitted");
Page snapshots
On a visit sampled for clicks, three seconds after load and in idle time,
the tracker asks GET /api/marketing/snapshot.js?website=&path=&width=
whether the backend wants a capture of this page at this viewport width.
The answer is either empty or the capture script itself, carrying a
one-time token, which waits for the DOM to stop mutating, serializes it,
and posts the document with the token to POST /api/marketing/snapshot —
or, when the serialization overshoots the
1 MiB cap, posts the size it reached instead, which is what makes the
console say the page is too heavy rather than show nothing. Nothing is
asked on websites that have not opted in, from inside a frame, on a hidden
tab, or past three requests per page load (SPA navigations ask again). The
tracker flushes its queued pageview before asking: the upload is only
accepted from the visitor the token was handed to, while their visit sits
on that page. The masking rules and the
data-dms-marketing-mask / data-dms-marketing-block markers are in
Pages & heatmaps; the guards on the
endpoints in Architecture.
Settings reach the tracker with a delay
GET /api/marketing/tracker.js prepends the module's effective settings to
the static file, and the response is cached for one hour
(TRACKER_CACHE_MAX_AGE_SECONDS). A sampling change on the Settings page
reaches returning visitors within that window.