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

@whatworks/analytics

v3.3.0

Published

Analytics components for Next.js with cookie consent.

Readme

NextJS Analytics

 

Analytics components for Next.js (App Router) with built-in cookie consent. Most vendor snippets assume a full page reload on every navigation, so when you drop them straight into layout.tsx the initial page view fires but client-side route changes are never tracked — you end up with a single hit per session. This package wraps the tracking scripts most projects need — Google Analytics, Google Tag Manager, Facebook Pixel, Microsoft Clarity, LinkedIn Insight Tag, and PostHog — in components that load via next/script and re-fire page views off usePathname as the user navigates, all gated through a shared consent provider that can optionally require consent by geolocation (EEA / UK / CH).

Each vendor lives in its own subpath import, so you only ever bundle the tags you actually use.

Contents

Installation

pnpm add @whatworks/analytics

Peer deps: next >= 13.4, react >= 18.2.

Usage

1. Add the consent API route

The provider needs an endpoint that returns whether the current request requires consent. A default handler is exported that uses Vercel's x-vercel-ip-country header to flag EEA / UK / CH visitors.

// app/api/consent/route.ts
export { GET } from '@whatworks/analytics/api/consent'

If you aren't on Vercel, write your own handler that returns { requiresConsent: boolean }.

basePath: under a Next basePath, pass the prefixed path to <Analytics consentApiPath> (e.g. consentApiPath="/my-app/api/consent") — a browser fetch is not auto-prefixed.

2. Compose <Analytics> in your root layout

<Analytics> is a provider shell: it wires up cookie consent and renders whatever tags you pass as children. Import each tag from its vendor subpath and give it the IDs it needs.

// app/layout.tsx
import { Analytics, CookieBanner } from '@whatworks/analytics'
import { GoogleAnalytics, GoogleTagManager } from '@whatworks/analytics/google'
import { MicrosoftClarity } from '@whatworks/analytics/clarity'
import { FacebookPixel } from '@whatworks/analytics/facebook'
import { LinkedInInsightTag } from '@whatworks/analytics/linkedin'
import { PostHog } from '@whatworks/analytics/posthog'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Analytics>
          <GoogleAnalytics gaId="G-XXXX" />
          <GoogleTagManager gtmId="GTM-XXXX" />
          <FacebookPixel pixelId="0000000000" />
          <MicrosoftClarity clarityId="xxxxxxxx" />
          <LinkedInInsightTag partnerId="1234567" />
          <PostHog apiKey="phc_XXXX" />
          <CookieBanner />
        </Analytics>
        {children}
      </body>
    </html>
  )
}

Render only the tags you need — each is independent. Pass IDs explicitly as props; there are no environment-variable fallbacks (read your own process.env.NEXT_PUBLIC_* at the call site if you want them). <CookieBanner> must live inside <Analytics> so it can read the consent context (it portals to the bottom of the page regardless of where it sits in the tree).

3. Enabling and disabling tags

Every tag accepts an enabled prop. It defaults to process.env.NODE_ENV === 'production', so tags stay inert in next dev and run on production and preview builds. Set it explicitly to override:

<PostHog apiKey="phc_XXXX" enabled />              {/* force on — e.g. to test in dev */}
<GoogleAnalytics gaId="G-XXXX" enabled={false} />  {/* force off */}

<Analytics enabled> sets the default for every child tag; a tag's own prop still wins:

<Analytics enabled={flags.analytics}>
  <GoogleAnalytics gaId="G-XXXX" /> {/* inherits flags.analytics */}
  <PostHog apiKey="phc_XXXX" enabled /> {/* overrides → always on */}
</Analytics>

This replaces the old top-level NODE_ENV gate: the provider itself always mounts (so consent context is available), and the production-only default is now resolved per tag.

PostHog

PostHog ships as a posthog-js npm module rather than an inline snippet. It is not a dependency (or peer dependency) of this package — it is loaded via a runtime dynamic import() and its types are kept out of the public API, so nothing is pulled in unless you import @whatworks/analytics/posthog. If you use <PostHog>, install posthog-js yourself:

pnpm add posthog-js

Pass apiKey and it loads lazily, consent-gated, behind the same provider as every other tag — so the SDK only downloads once consent is granted. Sensible defaults are applied — PostHog EU Cloud host, session replay off, identified_only profiles, opt-out until consent is granted — and can be overridden via apiHost / options:

import { PostHog } from '@whatworks/analytics/posthog'
;<PostHog apiKey="phc_XXXX" apiHost="https://eu.i.posthog.com" />

Send custom events from anywhere in the app with capture(). It routes to the same instance the provider manages and no-ops until PostHog has loaded and consent is granted, so it is always safe to call:

import { capture } from '@whatworks/analytics/posthog'

capture('table_company_opened', { companyId, companyName })

For anything beyond capture()identify(), feature flags, group analytics — import posthog-js directly (you already have it installed).

Custom analytics scripts

To add a script the package doesn't ship, render it as a child of <Analytics>. Children mount inside the shared CookieBannerProvider, so a 'use client' component can call useCookieBanner() for consent without mounting its own provider:

'use client'
import { useCookieBanner } from '@whatworks/analytics'

function MyTag() {
  const { consentStatus, shouldLoadScripts } = useCookieBanner()
  // load your script / apply consent...
  return null
}

// in layout.tsx
;<Analytics>
  <MyTag />
</Analytics>

Consent strategies

<Analytics> accepts a consentStrategy prop, defaulting to load-scripts-then-revoke-consent-after-geolocation-check — good for most marketing sites:

<Analytics consentStrategy="require-consent-before-loading-scripts">
  <GoogleAnalytics gaId="G-XXXX" />
</Analytics>

<Analytics> is a server component. To compose tags from a 'use client' file, render CookieBannerProvider directly instead — it is the same provider <Analytics> wraps, minus the inherited enabled default (so pass enabled to each tag yourself):

'use client'
import { CookieBannerProvider, CookieBanner } from '@whatworks/analytics'
import { GoogleAnalytics } from '@whatworks/analytics/google'
;<CookieBannerProvider
  consentApiPath="/api/consent"
  consentStrategy="require-consent-before-loading-scripts"
>
  <GoogleAnalytics gaId="G-XXXX" enabled />
  <CookieBanner />
</CookieBannerProvider>

| Strategy | Scripts load | Default consent | Banner shown | | ---------------------------------------------------------- | ------------------ | ----------------------- | -------------------------- | | load-scripts-always-grant-consent | immediately | granted | never | | load-scripts-revoke-consent-immediately | immediately | denied | if geolocation requires it | | load-scripts-then-revoke-consent-after-geolocation-check | immediately | granted until geo check | if geolocation requires it | | require-consent-before-loading-scripts | only after consent | denied | if geolocation requires it |

Subpaths and components

| Import | Exports | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | | @whatworks/analytics | Analytics, CookieBanner, CookieBannerProvider, CookieBannerPortal, useCookieBanner, ConsentStrategy | | @whatworks/analytics/google | GoogleAnalytics, GoogleTagManager | | @whatworks/analytics/linkedin | LinkedInInsightTag | | @whatworks/analytics/clarity | MicrosoftClarity | | @whatworks/analytics/facebook | FacebookPixel | | @whatworks/analytics/posthog | PostHog, capture | | @whatworks/analytics/api/consent | GET (consent route handler) |

All exports are named, and every tag's Props type is exported alongside it (e.g. GoogleAnalyticsProps).

  • <Analytics> — provider shell. Sets up consent and the inherited enabled default, then renders the tags you pass as children. Props: enabled?, consentStrategy?, consentApiPath?.
  • <CookieBanner> — default banner UI with title, description, acceptText, rejectText props. Only visible when the provider decides it should be.
  • <CookieBannerProvider> / useCookieBanner() — consent context. Exposes consentStatus, shouldLoadScripts, shouldShowBanner, accept(), reject().
  • <GoogleAnalytics gaId> (/google) — GA4 config script. Renders the shared gtag / Consent Mode bootstrap internally.
  • <GoogleTagManager gtmId> (/google) — GTM container script plus the <noscript> ns.html fallback (see note below). Renders the shared bootstrap internally; deduped automatically when GA is also present.
  • <FacebookPixel pixelId> (/facebook) — Meta Pixel with SPA route tracking and noscript fallback.
  • <MicrosoftClarity clarityId> (/clarity) — Clarity with consent v2 signalling. You must disable cookies in the Clarity dashboard for GDPR compliance — see Clarity's docs.
  • <LinkedInInsightTag partnerId> (/linkedin) — LinkedIn Insight Tag, consent-gated. Loads only once consent is granted (LinkedIn has no native consent API).
  • <PostHog apiKey> (/posthog) — PostHog product analytics, consent-gated. Lazy-loads posthog-js (which you install yourself; not a dependency of this package), opts out until consent is granted. Pair with capture() for events.

Every tag also accepts an enabled prop — see Enabling and disabling tags. The shared gtag bootstrap is an internal detail of the Google tags and is no longer exported.

Notes

  • If your GTM container includes a GA4 Configuration tag for the same property as gaId, page_view events will be double-counted. Pick one side.
  • The default CookieBanner renders into a portal and links to /privacy-policy — pass a custom description to override.
  • All tag components are 'use client'; <Analytics> itself is a server component, so compose it from a server layout. To use the tags from a 'use client' file, render CookieBannerProvider directly (see Consent strategies).
  • <noscript> fallbacks reflect the server-rendered consent posture only. A client with JS disabled never runs the geolocation check or banner, so the GTM/Facebook/LinkedIn <noscript> tags appear only under grant-by-default strategies (load-scripts-always-grant-consent, load-scripts-then-revoke-consent-after-geolocation-check) and cannot honor a region-based revoke. To gate the no-JS path by region, seed requiresConsent from request headers at SSR rather than relying on the client fetch.