Analytics
इस पेज पर
- What gets tracked
- Configure
- Install
- Quick start (Next.js)
- Wrap your app
- Track views on content pages
- See the data
- Why a public API key is safe
- Transport: never block your page
- SSR & build-time safety
- Graceful degradation
- Other frameworks
- Property validation
- Why is my API key safe?
- Proxy to avoid ad blockers
- Next steps
The @better-i18n/content SDK is a lightweight tracking client for Better i18n Content. Drop it into any frontend, call track() or useTrackView(), and your dashboard fills with views, languages, models, and countries — automatically.
What gets tracked #
By default, only what you explicitly track. There's no auto-pageview, no fingerprinting, no cookies.
Every track() call sends one event with:
- Project + organization context (resolved from your API key)
- Event name (e.g.
content.view) - Custom properties you pass (
entryId,language,framework, ...) - Country code (from Cloudflare's edge metadata — never IP)
- Timestamp
See Data Model for the full schema.
Configure #
You need two values from the dashboard:
| Value | Where to find it |
|---|---|
| Project ID | Settings → General → Project ID (format: your-org/your-project) |
| API Key | Settings → API Keys → Public Key (starts with bi_pub_) |
Set them in your .env.local:
NEXT_PUBLIC_BETTER_I18N_PROJECT_ID=your-org/your-project
NEXT_PUBLIC_BETTER_I18N_KEY=bi_pub_xxxxxInstall #
npm install @better-i18n/content'use client'
import { ContentProvider } from '@better-i18n/content/adapters/nextjs'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ContentProvider
config={{
projectId: process.env.NEXT_PUBLIC_BETTER_I18N_PROJECT_ID!,
apiKey: process.env.NEXT_PUBLIC_BETTER_I18N_KEY!,
}}
>
{children}
</ContentProvider>
)
}Track views on content pages #
'use client'
import { useTrackView } from '@better-i18n/content/adapters/nextjs'
export default function BlogPost({ post }: { post: Post }) {
useTrackView('content.view', {
entryId: post.id,
contentModel: 'blog',
entrySlug: post.slug,
language: post.locale,
})
return <article>{post.body}</article>
}See the data #
Open your project in the dashboard — within seconds, the Content Analytics section fills in. Total views, top entries, language breakdown, country map.
Why a public API key is safe #
The SDK uses your bi_pub_* key — designed to be embedded in client-side code. Track events are write-only:
- Can't read content data
- Can't read other projects' analytics
- Server-side rate-limited per IP and per project
This is the same model PostHog, Mixpanel, and Vercel Analytics use. Stolen keys can spam your events, but they can't exfiltrate anything.
See Why is my API key safe? below for the threat model.
Transport: never block your page #
The SDK tries three transports in order and silently drops failures:
sendBeacon → fetch(keepalive) → fetch
│ │ │
│ └─ < 51.2 KB body └─ regular POST
└─ fire-and-forget at page unloadsendBeacon survives navigation and tab close — events never get lost when users leave. fetch keepalive handles the rest under the body limit. Plain fetch is the last resort.
All three are wrapped in try/catch. A failed event is dropped silently — never thrown to your app.
SSR & build-time safety #
The SDK has three guards:
isBuildTime()— DetectsNEXT_PHASE=phase-production-build,GATSBY_BUILD_STAGE,ASTRO_BUILD. Calls during build log a warning and skip.isBrowser()— Calls from a server (Node.js, edge runtime) skip unlessoptions.allowServer: true.- Synchronous init —
ContentProvidercreates the tracker synchronously during the first render. The tracker itself is SSR-safe:track()checksisBrowser()internally and becomes a no-op on the server.
You can call track() in any render path — it's safe.
Graceful degradation #
If useTrackView() or useContent() is called outside of a ContentProvider, the SDK logs a one-time warning and silently disables tracking — your app never crashes.
[better-i18n/content] useContent called outside ContentProvider. Tracking will be disabled.This matches the behavior of PostHog, Segment, and other production analytics SDKs.
Other frameworks #
import { ContentProvider, useTrackView } from '@better-i18n/content/adapters/react'
<ContentProvider config={{ projectId, apiKey }}>
<App />
</ContentProvider>Note: Expo uses fetch (no sendBeacon on React Native).
import { provideContent, useTrack } from '@better-i18n/content/adapters/vue'
// In your root component
provideContent({ projectId, apiKey })
// In any child component
const track = useTrack()
track('content.view', { entryId: 'abc' })Or via window.betterContent.track(...) after init().
Property validation #
Properties must be primitive: string, number, boolean, or null. Nested objects and arrays are rejected:
- Development (
NODE_ENV !== 'production'): throwsError("Invalid property...") - Production: silently strips the invalid property and warns in debug mode
This catches bugs early without breaking your app in prod.
// ✅ OK
track('content.view', { entryId: 'abc', count: 1, featured: true })
// ❌ Throws in dev, stripped in prod
track('content.view', { entryId: 'abc', author: { id: '1' } })Why is my API key safe? #
| Concern | Mitigation |
|---|---|
| Key extracted from JS bundle | Key is write-only — can't read content or analytics |
| Spam events from stolen key | Server-side rate limit (per IP, per project) + CF WAF |
| Cross-project access | Key scoped to a single projectId at validation time |
| Sensitive PII in properties | You control payload — never log emails, only IDs |
Phase 1 mitigations: KV-cached key validation (1h TTL), CF Workers CPU/memory limits, server-side validation rejects malformed payloads.
Proxy to avoid ad blockers #
Ad blockers may block requests to content.better-i18n.com. Proxy the track endpoint through your own domain so the browser only sees first-party requests.
Next.js rewrites
The simplest option for Next.js apps. Add a rewrite rule in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/api/ca/:path*',
destination: 'https://content.better-i18n.com/:path*',
},
]
},
}
export default nextConfigThen point the SDK to your proxy:
<ContentProvider
config={{
projectId: process.env.NEXT_PUBLIC_BETTER_I18N_PROJECT_ID!,
apiKey: process.env.NEXT_PUBLIC_BETTER_I18N_KEY!,
analytics: {
endpoint: '/api/ca/v1/track',
},
}}
>Next.js middleware
If rewrites cause issues on your host, use a middleware (Next.js 16: proxy.js, 15 and earlier: middleware.js):
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/ca/')) {
const url = new URL(
request.nextUrl.pathname.replace('/api/ca', ''),
'https://content.better-i18n.com',
)
url.search = request.nextUrl.search
return NextResponse.rewrite(url)
}
}
export const config = {
matcher: '/api/ca/:path*',
}Cloudflare Worker
Zero egress fees and 100K free requests/day. Deploy a Worker on your domain:
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url)
if (!url.pathname.startsWith('/api/ca/')) {
return new Response('Not found', { status: 404 })
}
const target = new URL(
url.pathname.replace('/api/ca', ''),
'https://content.better-i18n.com',
)
target.search = url.search
const headers = new Headers(request.headers)
headers.set('host', 'content.better-i18n.com')
return fetch(target.toString(), {
method: request.method,
headers,
body: request.body,
})
},
}Add a route in your wrangler.toml matching /api/ca/* on your domain.
Nginx / Caddy
location /api/ca/ {
proxy_pass https://content.better-i18n.com/;
proxy_set_header Host content.better-i18n.com;
proxy_ssl_server_name on;
}Next steps #
- API Reference — full surface area:
track(),useTrackView(), options - Data Model — blob/double mapping, what's stored where
Better I18N