Plugins
Analytics

Tracker

How to boot the browser tracker, its methods, its React and server components, its config, and the attributes it reads.

The browser tracker is what captures on your site: it sends pageviews, events, goals and sales to every filled capture slot, behind the gate your consent manager drives. Capture is the guide; this page is its API.

Booting it

A page holds one tracker. Every way to boot it below takes a hold on that same tracker, so rendering two of them together never counts twice: whichever boots first creates it, and its options stick.

FromUse it inBoots from
<AnalyticsScripts />, @10x-media/analytics/sitea server component, usually the root layoutthe config getTrackerConfig resolved on the server
<AnalyticsProvider>, @10x-media/analytics/reacta client component wrapping your treethe same server-resolved config, handed down as a prop
initTracker(), @10x-media/analytics/trackerany page, a static one or one without Reactthe config it fetches from the tracker config endpoint

Server render

app/layout.tsx
import { AnalyticsScripts, getTrackerConfig } from '@10x-media/analytics/site'
import config from '@payload-config'
import { headers } from 'next/headers'
import { getPayload } from 'payload'

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const payload = await getPayload({ config })
  const tracker = await getTrackerConfig(payload, { headers: await headers() })
  return (
    <html lang="en">
      <body>
        {children}
        <AnalyticsScripts config={tracker} />
      </body>
    </html>
  )
}

getTrackerConfig(payload, { headers?, draft? }) resolves the request's TrackerConfig. Pass the incoming headers so a scoped install resolves the request's scope; draft says the page renders in Next's draft mode, and left out it is read from Next's draft mode cookie among the headers (Next.js).

<AnalyticsScripts> renders each ungated slot's vendor snippet and boots the tracker:

PropTypeWhat it does
configTrackerConfigRequired: what getTrackerConfig resolved.
gateGateStateThe decision your own server code already knows, applied at boot (Server render).
noncestringYour CSP nonce, for every script it renders or the tracker injects.
snippetsbooleanfalse renders and injects no vendor script, for a consent manager that loads the vendor SDKs itself.

It takes no loadScript: a function cannot cross from a server component.

React

<AnalyticsProvider> takes the same props as <AnalyticsScripts>, plus loadScript and children. Render it from a client component of your own to boot the tracker from React, or to load the vendor scripts through your framework's own loader.

useAnalytics() answers an AnalyticsApi from any client component, with or without a provider above it, as long as a tracker was booted on the page:

MemberWhat it is
track, trackGoal, purchase, refundThe tracker's methods, below.
setGate(state)Answers the gate. Safe before the tracker boots: the answer is kept for the window and applied at boot.
gateEach slot's GateStatus; a change re-renders the component.
modeEach slot's SlotMode, what it captures in right now; off for every slot before a tracker boots and during the server render. A change re-renders the component.
setExcluded(next), excludedThe tracker's exclude() and excluded, false until the tracker has read the browser's flag.

The tracker is resolved per call, not per render, so a component that renders before the boot effect runs still works. A call to track, trackGoal, purchase, refund or setExcluded that finds neither a provider nor a booted tracker throws; setGate never does.

initTracker

import { initTracker } from '@10x-media/analytics/tracker'

export const tracker = await initTracker()

initTracker({ endpoint?, ...TrackerOptions }) fetches the config from endpoint, DEFAULT_TRACKER_ENDPOINT (/api/_x/config) by default: pass your own when the app moved Payload's routes.api or the plugin's routes.base, or serves under a Next basePath. A failed fetch or a non-ok answer resolves to a tracker whose calls do nothing, so a missing config never breaks the page that awaited it. Its destroy() gives this call's hold back, and the tracker ends once nothing holds it.

createTracker(config, options?) builds a tracker from a TrackerConfig you already have. It is a tracker of its own rather than the page's shared one, so useAnalytics does not reach it; prefer the three above.

Tracker options

TrackerOptions, taken by initTracker, createTracker and the props above:

OptionTypeWhat it does
gateGateStateThe answer the host already has at boot, applied before anything is delivered.
snippetsbooleanfalse injects no vendor script; each vendor slot forwards once its vendor's global exists. Default true.
noncestringThe CSP nonce for the scripts the default loader injects.
loadScriptLoadScript(script: SnippetScript) => Promise<void>: your own script loader, resolving once the script has run.
windowTrackerWindowThe window to run in.

The tracker

MemberWhat it does
track(name, props?, { item? })Sends a custom event to every filled slot. props are flat scalars; item files it under a content item as well.
trackGoal(slug, { value?, currency?, item? })Completes a goal. Without a value, a vendor slot receives the goal's own fixed value from the config.
purchase({ value, currency?, orderId?, items?, props?, goal?, item? })Records a sale against goal, purchase by default. A negative value is dropped.
refund(input)Does nothing in the browser, with a warning in development: refunds are recorded from server code.
setGate(state)Grants or refuses capture slots: true runs a held slot in full, false moves it to its refused level (Consent).
gateEach slot's gate, pending, open or closed.
modeEach slot's SlotMode: full once open, else its level for the phase from capture.withoutConsent. A slot the gate does not hold, closed by name, and a slot the config does not fill read off. Replaced whenever one changes.
exclude(next), excludedTakes this browser out of capture, or puts it back (Excluding your own visits). excluded reads false until a slot runs in full and the tracker reads the browser's flag, or the exclusion link sets it.
page()Sends a pageview for the current address. The tracker calls it itself on boot and on every single-page app navigation.
flush()Sends the open page's engagement now, when it gained anything since the last one.
destroy()Sends the open page's engagement and ends the tracker: every listener and history patch is detached and nothing more is sent. On the page's shared tracker it gives one hold back, and the tracker ends once nothing holds it.

Server code records through other functions of the same names, from @10x-media/analytics: trackServerEvent, purchase and refund.

Types

TypeShape
GateStateboolean | Partial<Record<CaptureSlot, boolean | undefined>>. A boolean answers for every slot capture.gate holds; a record for the slots it names, held or not, and a slot left undefined keeps its gate.
GateStatus'pending' | 'open' | 'closed'
SlotMode'off' | 'cookieless' | 'anonymous' | 'full'; anonymous is native's alone. Exported from @10x-media/analytics, with HeldSlotMode (the three short of full) and ConsentPhase ('pending' | 'refused').
CaptureSlot'global' | 'tenant'
TrackGoalOptions{ value?, currency?, item? }
PurchaseInput{ value, currency?, orderId?, items?, props?, goal?, item? }. orderId keeps a checkout that fires twice counting once, and lets the server's record of the same sale take its place.
RefundInput{ orderId, value, currency?, props? }
RevenueItem{ id, name?, quantity?, price? }, one line of a sale
ItemOptions{ item? }: a signed item ref, as signItemRef writes it

The config

TrackerConfig is what the server resolves for one request and the tracker boots from. It carries only what a browser may see:

FieldWhat it holds
slotsOne TrackerSlotConfig per filled slot: slot, adapterId, kind, client, path (its proxy mount), snippet (the scripts to render or inject), gated (whether it waits for the gate) and, on a held slot that counts without consent in some phase, withoutConsent: { pending, refused }, each 'off', 'cookieless' or 'anonymous', resolved on the server. Absent, as in a config cached from 1.0.0-beta.14, means off in both. Such a slot also carries fullSnippet where its full variant differs (PostHog and GA4), which a page rendered with the slot open uses.
ingestPathWhere the native engine takes beacons, behind the app's base path.
goalsThe scope's goals as a browser may see them: slug, match, value and currency, never the admin-facing name.
autoCaptureThe resolved auto-capture settings.
exclusionWhile the exclusion link is on, its parameter and the SHA-256 of the scope's token, never the token.
skipPresent only when capture.skip turns one of its two off.
drafttrue on a page rendered in draft mode, which the tracker does not count.
adminWhile previews are skipped, the admin's address as SHA-256 digests (AdminDigest), so a page can tell the live preview from a visit without learning where the admin runs.

EXCLUSION_STORAGE_KEY (analytics:exclude) and DEFAULT_EXCLUSION_PARAM (analytics_exclude) are exported beside it.

Attributes

The tracker reads these attributes on click, and on a form that carries them on its submit alone, and @10x-media/analytics/attributes exports their names and helpers that spread them into your markup (Attach goals to your own components):

AttributeConstantWhat it does
data-analytics-goalGOAL_ATTRIBUTECompletes the goal it names. goalAttributes(slug, { value?, currency? }) writes it.
data-analytics-value, data-analytics-currencyVALUE_ATTRIBUTE, CURRENCY_ATTRIBUTEThe amount and currency of that completion.
data-analytics-eventEVENT_ATTRIBUTESends the custom event it names. eventAttributes(name, props?) writes it.
data-analytics-prop-<name>EVENT_PROP_PREFIXOne prop of that event.
data-analytics-itemITEM_ATTRIBUTEMarks a content item: its views and clicks. itemAttributes({ ref, name }) writes it from a signed ref, itemAttributes({ collection, id, name }) from an unsigned one.
data-analytics-item-nameITEM_NAME_ATTRIBUTEThe item's display name, sent to vendor slots only.

The events the tracker sends by itself are named OUTBOUND_EVENT (outbound_link), DOWNLOAD_EVENT (file_download), SCROLL_EVENT (scroll_depth) and ITEM_CLICK_EVENT (item_click).

What the tracker posts to the native engine, field by field, is on Capture; what it keeps in the browser is on Privacy.

On this page