Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
Version History
- "Enable analytics by default — active as soon as `@intlayer/analytics` is installed"v9.3.38/22/2026
- "Init doc — @intlayer/analytics package, provider/node-level tracking, A/B testing, dashboard"v9.0.07/8/2026
If you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
Intlayer Analytics Documentation
@intlayer/analytics is an optional companion package that tells you which content is actually shown to your visitors — which page, in which locale, and which specific piece of translated content — so you can understand your audience and run A/B tests on content.
Table of Contents
What it tracks
@intlayer/analytics batches three kinds of anonymous events:
Open the table in a modal to view all data content clearly
| Event | Captured where | What it tells you |
|---|---|---|
page_view | Provider level (the Intlayer provider) | Which page and locale a session viewed, on first load, route change, or locale switch. |
content_exposure | Node level (useIntlayer / interpreter plugins) | Which dictionary key / key path was actually resolved and displayed — and, when part of an experiment, which variant. |
conversion | Wherever you call useConversion() | A goal reached (signup, click, purchase…) attributed to the A/B variant the session was exposed to. |
Events are collected in memory and sent as a single batched request roughly every 20 seconds — never on every keystroke or render — so analytics never impacts first render time or adds a request per interaction.
How it powers A/B testing on content
Intlayer already lets you declare content Variants (e.g. a hero-banner dictionary with a default and a black_friday variant). @intlayer/analytics closes the loop:
useExperiment(experimentKey, variants)deterministically assigns each anonymous session to a variant — a pure function of the session id and the experiment key, so the assignment is stable across the session and requires no server round-trip before first render (no flicker, no layout shift).- Every
content_exposureevent carries thevariantthat was shown. useConversion()lets you attribute a goal (e.g."cta_click") to that variant.- The dashboard's experiment results endpoint compares conversion rates per variant, including statistical significance (a z-test).
Installation
@intlayer/analytics is an optional dependency of every framework package (react-intlayer, next-intlayer, vue-intlayer, …), so most projects already have it. Install it explicitly if your setup skips optional dependencies (npm install --no-optional, NODE_ENV=production installs of some package managers, …):
Copy the code to the clipboard
Installing the package is all it takes to turn analytics on: analytics.enabled defaults to true, and Intlayer resolves it to false whenever the package cannot be found in your project. If you don't install it, every integration point resolves to a no-op — see Zero-cost when not installed below.
Configuration
Analytics needs no configuration to start: it is enabled by default and reuses the existing editor configuration block for its endpoint and project key.
Copy the code to the clipboard
import type { IntlayerConfig } from "intlayer";
const config: IntlayerConfig = {
editor: {
backendURL: "https://back.intlayer.org", // Also used as the analytics ingestion endpoint
clientId: "your-client-id", // Also used as the analytics project key
clientSecret: "your-client-secret",
},
};
export default config;
editor.backendURL— the base URL analytics events are sent to (POST {backendURL}/api/analytics/events).editor.clientId— the public project key. It identifies the project when the SDK requests an ingest token, and acts as an enable switch: analytics stays fully disabled (and tree-shaken, see below) untilclientIdis configured.editor.clientSecret— never used by analytics, and never sent to the browser. It is a server-only credential; see How events are authenticated.
If you self-host Intlayer, analytics automatically points at your own instance since it shares editor.backendURL.
Calling the API from the browser
The same token backs a small credential-free client, so a static site or SPA can read its CMS content at runtime with no server, no server action, and no secret in the bundle:
Copy the code to the clipboard
It authenticates itself from editor.clientId — the exchange, caching and renewal are handled internally. The scopes bound what it can reach: published dictionary content and analytics ingestion. Anything else (pushing dictionaries, reading a project, spending AI credits) needs a real credential, and therefore a server or a signed-in user.
Opting out
The optional analytics block tunes — or turns off — the collection:
Copy the code to the clipboard
import type { IntlayerConfig } from "intlayer";
const config: IntlayerConfig = {
analytics: {
enabled: false, // Default: true — opts the whole integration out of the bundle
flushInterval: 20_000, // Milliseconds between two batched flushes
sampleRate: 1, // Fraction of sessions to record, from 0 (none) to 1 (all)
},
};
export default config;
Uninstalling @intlayer/analytics has the same effect as enabled: false. See the Configuration reference for the full field list.
Usage
Automatic provider-level tracking
No code changes are required. Once @intlayer/analytics is installed and editor.clientId is configured, the Intlayer provider you already mount automatically:
- initializes the analytics client on mount,
- records a
page_viewon initial load, - records a
page_viewon every locale change, - starts the ~20s flush loop and flushes any remaining events on unmount / tab close (via
navigator.sendBeacon, falling back tofetch(..., { keepalive: true })).
The entry point differs per framework — but in every case it is the same one you already use to set Intlayer up, so there is nothing extra to add:
IntlayerProvider mounts the analytics provider internally.
Copy the code to the clipboard
next-intlayer re-exports React's IntlayerProvider, so analytics is wired the same way.
Copy the code to the clipboard
The intlayer plugin registers the analytics hooks on the root component's lifecycle.
Copy the code to the clipboard
With Nuxt, nuxt-intlayer installs the plugin for you — nothing to do.
setupIntlayer() starts analytics from the component that sets Intlayer up.
Copy the code to the clipboard
IntlayerProvider mounts the analytics provider internally.
Copy the code to the clipboard
IntlayerProvider lazily mounts the analytics provider, so the chunk stays off the critical path.
Copy the code to the clipboard
provideIntlayer() already includes provideIntlayerAnalytics().
Copy the code to the clipboard
Use provideIntlayerAnalytics() on its own only if you manage providers individually.
Automatic node-level tracking
Every time useIntlayer resolves a piece of content for display, the interpreter reports a content_exposure event for that exact dictionaryKey + key path + locale — again, no code changes required. Repeated exposures of the same node within a flush window are coalesced into a single event with a count, so a list re-rendering 50 times doesn't send 50 events.
Tracking conversions for A/B tests
useConversion() returns a callback that attributes a goal to the variant a session saw. It is exported from every framework package, with the same signature:
Copy the code to the clipboard
Copy the code to the clipboard
useConversionis a client hook — mark the component"use client".
Copy the code to the clipboard
Copy the code to the clipboard
Copy the code to the clipboard
Copy the code to the clipboard
Copy the code to the clipboard
Resolving a variant client-side
useExperiment() assigns the session to a variant and records the exposure that becomes the denominator of the conversion rate. Gate the variant-aware subtree on isAssigned so no visitor sees the control flash before the assignment resolves:
variant is a plain string.
Copy the code to the clipboard
variant is a plain string. Assignment happens in the browser, so the component must be a client component.
Copy the code to the clipboard
variant and isAssigned are Refs.
Copy the code to the clipboard
variant and isAssigned are stores — read them with the $ prefix.
Copy the code to the clipboard
variant is a plain string.
Copy the code to the clipboard
variant and isAssigned are Accessors — call them to read the value.
Copy the code to the clipboard
variant and isAssigned are Signals — call them to read the value.
Copy the code to the clipboard
Weights are optional — pass one per variant to skew the split, e.g. useExperiment("homepage-hero", ["default", "black_friday"], [9, 1]).
The child then reads the Variant of the dictionary that matches:
Copy the code to the clipboard
Reading the variant in a child is what makes this work outside React: in Vue, Svelte, Solid, and Angular the selector passed to useIntlayer is captured when the component sets up, so the read has to happen in a component that only mounts once the variant is known.
If the experiment covers a whole page rather than a single dictionary, hoist the variant onto the provider instead — see Ambient variant. Every useIntlayer below then resolves against it with no call-site change.
If you need the raw assignment outside of a component, reach for the client directly:
Copy the code to the clipboard
getVariantonly assigns — it does not record the exposure. PreferuseExperiment(), otherwise the conversion rate has no denominator.
Privacy & performance
- Anonymous by design: sessions are identified by a rotating id; the backend only ever stores a SHA-256 hash of that id — never the raw id, never an IP address.
- Location is coarse: only a country code, derived from CDN geolocation headers (
cf-ipcountry,x-vercel-ip-country, …) — no IP is read or stored. - URLs exclude search params by default, so query strings are never captured.
- Sampling:
sampleRatelets you keep only a fraction of content-exposure events on high-traffic apps. - Batched: one request roughly every 20 seconds (
flushInterval), or earlier if the buffer fills up (maxBufferSize) — never one request per event.
Zero-cost when not installed
@intlayer/analytics follows the exact same optional-dependency pattern as @intlayer/editor:
- every integration point loads the package via a dynamic
import()wrapped intry/catch— an app that never installs@intlayer/analyticsnever pays a bundle-size or runtime cost, and never sees an error; - a compile-time env var (
INTLAYER_ANALYTICS_ENABLED), automatically set to'false'whenever the package is not installed,analytics.enabledisfalse, oreditor.clientIdis not configured, lets bundlers dead-code-eliminate the whole integration; - analytics is disabled inside the Intlayer editor/CMS preview iframe, so editor sessions are never counted as real traffic.
Dashboard: Analytics page
Once your project has collected events, the Analytics page in the Intlayer dashboard (visible in the sidebar once a project is selected) shows:
- Active users — distinct visitors over the selected rolling window (7 / 30 / 90 days).
- Users today and users over the last 7 days.
- Page views over the selected window.
- An evolution graph of daily distinct visitors.
- Locales and Location breakdown tabs, ranking your audience by locale and by country.
Backend API reference
All read endpoints require authentication; the token exchange and ingestion are public.
Open the table in a modal to view all data content clearly
| Method | Endpoint | Description |
|---|---|---|
POST | /api/public/token | Exchange the public clientId for a short-lived, scoped browser token. |
POST | /api/analytics/events | Ingest a batch of events (public, analytics:ingest scope). |
GET | /api/analytics/overview | Page/locale totals for the authenticated project. |
GET | /api/analytics/audience?days=30 | Distinct visitors, page views, daily series, locale + country breakdowns. |
GET | /api/analytics/content-stats | Per-content exposure totals, grouped by dictionary key / key path / locale. |
GET | /api/analytics/experiments/:experimentKey | Per-variant conversion rates and statistical significance for an A/B experiment. |
You can also call these programmatically with the CMS SDK:
Copy the code to the clipboard
Server-side only.createIntlayerCMS()authenticates withclientId+clientSecret, and the secret is never available in the browser — this snippet would issue unauthenticated requests if it ran there. Keep it in a route handler, server action, or script.
