npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pakicetus97/next-analytics

v0.2.3

Published

App Router-only analytics provider for Next.js that posts events to a configurable collect endpoint.

Readme

@pakicetus97/next-analytics

Most analytics solutions are either too heavy, too expensive, or require you to hand your data to a third party. This package takes a different approach: it is a thin client-side layer you drop into your Next.js app that collects a handful of meaningful signals — page views, scroll depth, web vitals, and anchor navigation — and sends them to an endpoint you own and control. No external dashboard, no tracking script from a CDN, no vendor lock-in.

The idea is to keep things simple. You configure the <AnalyticsProvider> once in your root layout, point it at a route in your own project, and you are done. The package intentionally stays on the coarse side: it tells you which pages were visited, on what kind of device, roughly how far users scrolled, and how fast the page felt. It does not track mouse movements, clicks on individual elements, form inputs, or anything else that would require a more invasive approach.

If you want a quick, self-hosted overview of how your app is being used without setting up a full analytics platform, this package is for you. The scope is deliberately narrow for now, but the package will be extended as new use cases come up.


App Router-only analytics package for Next.js. It sends client-side analytics events to a configurable collect endpoint — all it needs is something that accepts a POST request with a JSON body. That endpoint can live inside the same consumer project (e.g. app/api/collect/route.ts) or in a completely separate, centralised analytics API that receives events from multiple websites at once. Either way, the package does not care: you own the infrastructure, you decide where the data lands.

Because the endpoint is called directly from the browser it is publicly reachable by definition. If you expose it on the open internet, make sure to follow the security recommendations in the Implementing the Collect Endpoint section below.

Installation

pnpm add @pakicetus97/next-analytics

Usage

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 set

The 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

Because the collect endpoint is called directly from the browser, it is publicly reachable by definition — anyone can POST to it. The following measures help you keep the noise out and make sure only your own app can write meaningful data into your backend.

  • Validate the X-Public-Key header if you use the publicKey prop. Reject requests with a missing or invalid key with 401. Enforce a strict format on the key itself (e.g. a fixed prefix + hex string of a known length) so that arbitrary strings cannot be used to probe the endpoint.
  • Accept the key via Authorization: Bearer as well — some infrastructure strips custom headers. Supporting both Authorization: Bearer <key> and X-Public-Key: <key> makes the integration more robust.
  • Validate the Content-Type header and reject anything that is not application/json.
  • Enforce a body-size limit (e.g. 512 KB) and return 413 for oversized requests. Without this, a single malformed request can tie up your handler.
  • Validate the event shape before persisting — check that type, timestamp, and sessionId are present and have the expected types, and enforce a maximum length on all string fields. Reject or discard malformed events. A schema validation library (e.g. Zod) is well suited for this.
  • Cap the number of events per batch request (e.g. 50). Combined with the body-size limit this prevents a single request from flooding your database.
  • Rate-limit per client or origin to prevent abuse. Since events can arrive in bursts on fast navigation, allow short bursts but cap sustained throughput. Return 429 with a Retry-After header so clients back off gracefully.
  • Restrict allowed origins if your endpoint only serves known domains. An optional per-client origin whitelist lets you return 403 for unexpected origins without breaking legitimate traffic.
  • Do not trust sessionId as 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/collect handler is included in this package.
  • The consumer project must implement the collect endpoint itself.

Development

pnpm install
pnpm typecheck
pnpm build
pnpm pack

Detailed publish steps are documented in PUBLISHING.md.