Better I18NBetter I18N

Analytics

Read content view analytics — counts, language breakdown, country stats, time series

The analytics namespace lets you read back the view data tracked by @better-i18n/content SDK's useTrackView() hook.

View counts

Get aggregate view counts per content entry:

const views = await admin.analytics.views('blog-posts')
// {
//   views: {
//     "better-auth-localization-guide": 42,
//     "digital-nomad-guide": 128
//   },
//   period: "30d",
//   cachedAt: "2026-05-16T15:23:10.514Z"
// }

Single entry

const entry = await admin.analytics.views('blog-posts', 'digital-nomad-guide')
// { views: 128, period: "30d", cachedAt: "..." }

With period

const weekly = await admin.analytics.views('blog-posts', { period: '7d' })
const daily = await admin.analytics.views('blog-posts', 'my-post', { period: '24h' })

Available periods: 24h, 7d, 30d (default), 90d.

Full stats breakdown

Get a dashboard-grade analytics breakdown with 5 parallel queries:

const stats = await admin.analytics.stats('blog-posts', { period: '30d' })

Response:

{
  "overview": {
    "totalViews": 256,
    "uniqueEntries": 12
  },
  "viewsByEntry": [
    { "slug": "digital-nomad-guide", "views": 128 },
    { "slug": "better-auth-localization-guide", "views": 42 }
  ],
  "viewsByLanguage": [
    { "language": "en", "views": 180 },
    { "language": "de", "views": 40 },
    { "language": "tr", "views": 36 }
  ],
  "viewsByCountry": [
    { "country": "US", "views": 120 },
    { "country": "DE", "views": 40 },
    { "country": "TR", "views": 36 }
  ],
  "viewsOverTime": [
    { "timestamp": "2026-05-01 00:00:00", "views": 8 },
    { "timestamp": "2026-05-02 00:00:00", "views": 12 }
  ],
  "period": "30d",
  "cachedAt": "2026-05-16T15:23:13.249Z"
}

Stats for a single entry

const entryStats = await admin.analytics.stats('blog-posts', {
  period: '7d',
  entrySlug: 'digital-nomad-guide'
})

Time series granularity

PeriodBucket size
24h1 hour
7d1 day
30d1 day
90d1 day

Caching

All analytics responses are cached in KV with period-dependent TTL:

PeriodCache TTL
24h2 minutes
7d5 minutes
30d10 minutes
90d10 minutes
app/api/popular/route.ts
import { createAdminClient } from '@better-i18n/admin'

const admin = createAdminClient({
  apiKey: process.env.BETTER_I18N_API_KEY!,
  projectId: 'nomadvibe/packervibe'
})

export async function GET() {
  const stats = await admin.analytics.stats('blog-posts', { period: '7d' })

  const popular = stats.viewsByEntry.slice(0, 5).map(entry => ({
    slug: entry.slug,
    views: entry.views,
  }))

  return Response.json(popular, {
    headers: { 'Cache-Control': 'public, max-age=300' }
  })
}

On this page