@pakicetus97/next-analytics
v0.2.1
Published
App Router-only analytics provider for Next.js that posts events to a configurable collect endpoint.
Downloads
112
Maintainers
Readme
@pakicetus97/next-analytics
App Router-only analytics package for Next.js. It sends client-side analytics events to a configurable collect endpoint that you implement in the consumer project.
Installation
pnpm add @pakicetus97/next-analyticsUsage
import { AnalyticsProvider } from '@pakicetus97/next-analytics'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="de">
<body>
<AnalyticsProvider endpoint="/api/collect" />
{children}
</body>
</html>
)
}The endpoint can be a relative path inside the consumer app or a full URL:
<AnalyticsProvider endpoint="https://analytics.example.com/collect" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| endpoint | string | — | URL to POST events to |
| publicKey | string | — | Sent as X-Public-Key header on every request |
| sessionStorageKey | string | "analytics_session_id" | Key used to persist the session ID |
| batch | boolean \| BatchOptions | — | Queue events and flush in batches |
| retry | boolean \| RetryOptions | — | Retry failed requests with exponential backoff |
| throttle | boolean \| ThrottleOptions | — | Limit how often requests are sent |
batch
// defaults: maxSize 10, flushIntervalMs 5000
<AnalyticsProvider endpoint="/api/collect" batch />
// custom
<AnalyticsProvider endpoint="/api/collect" batch={{ maxSize: 20, flushIntervalMs: 10000 }} />Events are collected in a queue and sent together when either maxSize is
reached or flushIntervalMs has elapsed. The queue is always force-flushed on
page unload, route change, and when the tab becomes hidden.
retry
// defaults: attempts 3, delayMs 500 (exponential: 500ms → 1000ms → 2000ms)
<AnalyticsProvider endpoint="/api/collect" retry />
// custom
<AnalyticsProvider endpoint="/api/collect" retry={{ attempts: 5, delayMs: 300 }} />Only retries on network errors, not on HTTP error status codes. Retry is skipped for requests that are sent on page unload (no time to wait).
throttle
// default: minIntervalMs 1000
<AnalyticsProvider endpoint="/api/collect" throttle />
// custom
<AnalyticsProvider endpoint="/api/collect" throttle={{ minIntervalMs: 2000 }} />Drops events that arrive within minIntervalMs of the last send. When batch
is enabled, throttle is ignored — the flush interval already controls request
rate.
Combined example
<AnalyticsProvider
endpoint="/api/collect"
publicKey="pk_live_abc123"
batch={{ flushIntervalMs: 3000 }}
retry
/>Implementing the Collect Endpoint
This package does not include a /api/collect handler. You implement it yourself in your consumer project. Here is everything you need to know.
HTTP Request
POST {endpoint}
Content-Type: application/json
X-Public-Key: <your-key> // only present when publicKey prop is setThe body is either a single event object or an array of event objects (when batch is enabled). Your handler must accept both.
The endpoint can live inside the same Next.js project (e.g. app/api/collect/route.ts) or be a completely separate service — as long as it accepts POST requests with a JSON body.
The client ignores the response body — returning 200 or 204 is enough. Error status codes are not retried automatically (only network-level errors are, when retry is configured).
Security recommendations
- Validate the
X-Public-Keyheader if you use thepublicKeyprop. Reject requests with a missing or invalid key with401. - Validate the
Content-Typeheader and reject anything that is notapplication/json. - Validate the event shape before persisting — check that
type,timestamp, andsessionIdare present and have the expected types. Reject or discard malformed events. - Rate-limit by IP or session to prevent abuse. Since events can arrive in bursts (e.g. on fast navigation), allow short bursts but cap sustained throughput.
- Do not trust
sessionIdas an authentication token — it is generated client-side and can be spoofed. Treat it as an opaque correlation ID only. - Never reflect raw event data back to any client without sanitisation, as it originates from the browser.
Event Shapes
All events share these base fields:
| Field | Type | Description |
|---|---|---|
| type | string | Event type identifier |
| timestamp | string | ISO 8601 UTC (new Date().toISOString()) |
| sessionId | string | Unique per browser-tab session, stored in sessionStorage |
| path | string | Current pathname (e.g. /dashboard) |
page_view
Fired on every client-side route change.
{
"type": "page_view",
"timestamp": "2026-04-08T12:00:00.000Z",
"sessionId": "abc123",
"path": "/dashboard",
"referrer": "google.com",
"device": "desktop"
}| Field | Type | Notes |
|---|---|---|
| referrer | string \| undefined | Hostname only, www. stripped. Omitted if no referrer. |
| device | "mobile" \| "tablet" \| "desktop" | Detected from navigator.userAgent |
scroll
Fired when the user leaves a page (route change or pagehide). Reports the furthest scroll position reached.
{
"type": "scroll",
"timestamp": "2026-04-08T12:00:00.000Z",
"sessionId": "abc123",
"path": "/dashboard",
"maxDepth": 75
}| Field | Type | Notes |
|---|---|---|
| maxDepth | number | Integer 0–100 (percentage of page height scrolled) |
web_vitals
Fired on navigation or when the tab becomes hidden. Only sent when at least one metric was collected.
{
"type": "web_vitals",
"timestamp": "2026-04-08T12:00:00.000Z",
"sessionId": "abc123",
"path": "/dashboard",
"metrics": {
"CLS": 0.05,
"FCP": 1200,
"LCP": 2400,
"INP": 80,
"TTFB": 300
}
}| Metric | Unit | Description |
|---|---|---|
| CLS | score (0–1) | Cumulative Layout Shift |
| FCP | ms | First Contentful Paint |
| FID | ms | First Input Delay |
| INP | ms | Interaction to Next Paint |
| LCP | ms | Largest Contentful Paint |
| TTFB | ms | Time to First Byte |
All metrics fields are optional — only the ones measured in the current session are included.
anchor_navigation
Fired when the URL hash changes (e.g. clicking an in-page anchor link).
{
"type": "anchor_navigation",
"timestamp": "2026-04-08T12:00:00.000Z",
"sessionId": "abc123",
"path": "/docs",
"anchor": "#getting-started"
}| Field | Type | Notes |
|---|---|---|
| anchor | string | The full hash including # |
Batch mode
When batch is enabled, multiple events are sent as a JSON array in a single request:
[
{ "type": "page_view", "timestamp": "...", "sessionId": "...", "path": "/", "device": "desktop" },
{ "type": "scroll", "timestamp": "...", "sessionId": "...", "path": "/", "maxDepth": 42 }
]Your handler must handle both {} and [{}, {}] — see the example above.
Notes
- Next.js App Router only.
- No
/api/collecthandler is included in this package. - The consumer project must implement the collect endpoint itself.
Development
pnpm install
pnpm typecheck
pnpm build
pnpm packDetailed publish steps are documented in PUBLISHING.md.
