Analytics
Properties, setup in one command, automatic pageviews, track() and identify(), events and goals, one install with experiments.
An analytics property is a stream of pageviews and custom events, like a site in Google Analytics. It lives next to your experiments: the same config file, the same SDK or script, the same CLI and key, and the same storage choice (SplitHub Cloud or your own database). A property has no variants and no verdicts; its reports are counts. How to read them: Reports.
Properties
Each property has a generated id like sh-x1h78w. Events go only to a property named by its id, never to a guess, so a page without a property sends no analytics at all.
A property comes from one of two places:
The config file. Self-hosted projects keep their properties here; cloud projects may too.
// splithub.config.ts properties: { 'sh-x1h78w': { name: 'Marketing site' }, },Field Default Meaning name— shown in the dashboard and the CLI, up to 80 characters description— a note for your team statusrunningrunningorpausedgoalsevery goal goal keys counted as conversions in this property The dashboard (Analytics → New property). Its id is on the property's page. It needs no push and no change to the file.
Set it up
One command picks the project's property (or creates one) and adds it to the provider in your code:
npx splithub analytics connect # or: Analytics → Set up in the npx splithub menu
npx splithub analytics connect --name "Marketing site" # create one with this name
npx splithub analytics connect sh-x1h78w # use this id
It finds the one <SplitHubProvider> or <SplitHubNextProvider> in your code and adds property="sh-…". When there is none, or several, it prints the snippet to add yourself. In cloud mode the property is created in the cloud at once, so the next deploy collects: no push needed. To only write the property into the file, use npx splithub analytics create --name "Marketing site".
Never push just to add analytics. A push uploads the whole file, and an experiment the cloud knows from an earlier push that the file lacks would be removed (push --dry-run lists what would change).
React and Vite
<SplitHubProvider config={config} property="sh-x1h78w">
<App />
</SplitHubProvider>
Details: React.
Next.js
<SplitHubNextProvider config={config} property="sh-x1h78w" visitorId={sh.visitorId ?? undefined} remoteOverrides={sh.overrides}>
{children}
</SplitHubNextProvider>
Details: Next.js.
Any site, no build step
<script src="https://go.splithub.io/sh.js" data-key="pk_live_…" data-property="sh-x1h78w" async></script>
Details: Drop-in script.
Pageviews
With a property, a pageview is recorded on load and on every client-side navigation: history.pushState, replaceState, back and forward, and hash routes. A new path, query or hash is a new page. You do not call anything for a single-page app. Each page also reports time on page and scroll depth, which the reports turn into visit duration and engagement.
Turn the automatic navigation tracking off with trackNavigation: false on the provider (or createClient) and report pages yourself with usePageview(path) or client.pageview(path).
Events
track is the one call for events, in every install:
const track = useTrack(); // React and Next.js
track('Video Played', { id: 3 });
track('checkout', { revenue: 49 });
splithub.track('Video Played', { id: 3 }); // sh.js
- Names: letters, digits, spaces and
_ . : / -, up to 64 characters. Any name is recorded; there is nothing to declare first. - Props: a flat object of up to 4 KB of JSON. Larger props keep
revenueand the plain values that fit, with a console warning. revenueis the one prop with a built-in meaning: it becomes the revenue of a goal.- Only what your code passes to
track()is sent. Fire events on actions, never in a render loop or on every scroll: each one is stored and billed.
Identify
After sign-in, tie the visitor to your own user id:
const identify = useIdentify();
identify(user.id, { plan: 'pro' }); // after sign-in
identify(null); // on sign-out
splithub.identify(id, traits) does the same from sh.js. The id is 1 to 128 characters; traits are flat text, number and true/false values up to 4 KB. The browser remembers the id and sends it again once per session. Reports then count users next to visitors, and you can break any report down by a trait (--by trait --key plan). Use an internal id, never an email or a name.
Events and goals
Every event is recorded. A goal is an event you marked as one:
- a goal in
goalsof the config (see Goals and revenue), - the goal switch on an event in the dashboard (Settings → Events), or
- naming the event as an experiment's goal:
Video Playedbecomes the goalvideo-played.
Goals count as conversions in every property unless the property's goals list narrows them. A goal's value is what one conversion is worth when the event carries no revenue prop; event: 'Video Played' maps a goal key to an event name that is not a valid key.
One install for analytics and experiments
The provider (or sh.js) with a property collects pageviews and runs experiments. Each track() sends one event row and each page one page row. Experiments whose goal is that event or that page count their conversions from those same rows, so nothing extra is sent or billed.
Keep one install per property. If sh.js and the SDK both run on a page for the same property, the first one to send a page collects pageviews, time on page and identify; the other sends only its own track() calls.
Development
A development build (NODE_ENV=development) sends nothing to SplitHub Cloud, so your own visits never reach the reports. The console says so once, and ?sh_debug=1 logs each row held back. Check tracking on a deploy or with vite preview, then list the latest raw rows:
npx splithub analytics events sh-x1h78w
Allowed domains
Allowed domains are set per property, like per experiment. An empty list accepts every origin.
npx splithub domains set property sh-x1h78w example.com # subdomains included
npx splithub domains clear property sh-x1h78w # any origin again
npx splithub domains # every list
Pageviews and events from another origin are dropped; a batch with nothing allowed answers 403. Add localhost when you test against the cloud from it.
Pause, archive, remove
npx splithub analytics pause sh-x1h78w # stop collecting; resume with: analytics resume sh-x1h78w
npx splithub analytics archive sh-x1h78w # stop collecting and leave the list, numbers kept
npx splithub analytics restore sh-x1h78w # back in the list, paused
npx splithub analytics remove sh-x1h78w # out of the config
Pause and resume take effect in the cloud within a minute; events of a paused property are dropped and not billed. Archive and restore are SplitHub Cloud features, the same as Archive in the dashboard. remove takes a property out of the file (push next); removing a property made in the dashboard deletes it with its numbers.
What is billed
Every pageview and every custom event of a running property counts like one exposure against the project's allowance. Time on page, scroll depth and identify are not billed, and neither are the goal conversions experiments count from your events. See Plans and limits.
Privacy
- Bots are never counted: known crawlers are turned away at the edge and bot user agents never reach analytics.
- Keep personal data out of props and traits: no emails, names or ids from other systems.
- The visitor id is a random first-party cookie (
sh_vid); identify links it to your user id only when you call it.
Self-hosted
With storage: { mode: 'self' } the rows go to your own database through the ingest handler. The same commands answer raw counts for a range of UTC days: pageviews, visitors, visits, identified users and events by name.
npx splithub analytics sh-x1h78w --days 7
npx splithub analytics sh-x1h78w --from 2026-10-01 --to 2026-10-31
Reports with pages, sources, channels, visits, conversions, filters, --by and --compare, plus funnels, paths and retention, come with SplitHub Cloud.