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

@vulcantech/analytics

v0.1.2

Published

Lightweight first-party analytics tracker for Next.js — page views, custom events, form conversions. No cookies, no fingerprinting, <1KB gzipped.

Readme

@vulcantech/analytics

Lightweight first-party analytics for VulcanTech consumer sites — page views, custom events, and form/phone/email conversion tracking. No cookies, no fingerprinting, <1KB gzipped. Events are proxied through the site's own domain to vulcantech-cms.

Also provides CMS-driven third-party tracking tags (CMSTags): GA4 / Google Ads / Meta Pixel IDs are configured in the CMS and injected server-side, so consumer sites get "codeless" tag setup with no hand-rolled gtag/fbq wiring.

Exports:

  • AnalyticsTracker — client component; tracks page views on route change.
  • trackEvent, trackFormView, trackFormSubmit, trackPhoneClick, trackEmailClick, track404, trackPageView — imperative trackers.
  • @vulcantech/analytics/route — App Router proxy handler (POST, OPTIONS) for /api/a.
  • @vulcantech/analytics/tags — CMSTags server component + fetchTrackingTags + the TrackingTags type.

Event shape (AnalyticsEvent)

Every tracker call builds one of these client-side and beacons it to /api/a, which forwards it to the CMS. You rarely construct one by hand, but the field limits below are enforced server-side on ingest — anything over them is silently truncated or dropped, not rejected.

interface AnalyticsEvent {
  type: "pageview" | "event"; // the only two valid values
  url: string;                // pageview: pathname + search. event: pathname only
  ref: string;                // document.referrer on pageviews; "" on custom events
  sid: string;                // session id — see Session identity below
  vw: number;                 // window.innerWidth — the CMS derives device from this
  vh: number;                 // window.innerHeight
  ts: number;                 // Date.now() on the client (clock is not trusted)
  utm?: Record<string, string>;   // pageviews only, and only when present in the URL
  name?: string;              // custom events only — the event name
  props?: Record<string, string | number | boolean>; // custom events only
}

Field notes — all verified against the CMS ingest path (/api/analytics → lib/analytics/ingest):

  • props must be flat primitives. Nested objects and arrays are dropped on ingest, so { items: [...] } or { user: { id } } silently vanish — flatten to string | number | boolean yourself. At most 32 keys survive; keys are truncated to 64 chars, string values to 256, and HTML tags are stripped from both.
  • Length caps: url and ref 2048, name 128, sid 64. Over-length values are truncated, not rejected.
  • vw drives the device breakdown in the CMS dashboard: <768 mobile, <1024 tablet, else desktop. It is not merely diagnostic.
  • ref + utm drive traffic-source classification. Custom events send ref: "", so only pageviews carry attribution — don't expect a source on trackEvent data.
  • Reserved names. Events with name === "tracker_test" or url === "/__analytics_test__" are deliberately discarded by the CMS (they exist for the connection self-test) and will never appear in stats. Don't use those values.
  • type, url, sid, ts are required — an event missing any of them is rejected with a 400 at ingest.

Session identity

sid is a random ID in sessionStorage under __vt_sid — no cookies, so nothing survives a tab close and there is no cross-tab or cross-visit identity. When sessionStorage throws (private mode, blocked storage), the tracker falls back to a fresh random ID per event, which inflates session counts for those visitors. This is by design; the package never fingerprints.

Conversion tracking is automatic — do not double-wire

Importing this package (which AnalyticsTracker does) has two side effects at module load, on the client only:

  1. A delegated document click listener (capture phase, installed once) fires trackPhoneClick / trackEmailClick for every a[href^="tel:"] and a[href^="mailto:"] on the site. Adding your own onClick={() => trackPhoneClick(...)} double-counts the conversion — the listener already caught it. Only call those functions manually for a non-anchor trigger (e.g. a button that dials via JS).
  2. window.__vtTrack is registered with the four conversion trackers. @vulcantech/forms picks it up, so form_view / form_submit conversions track automatically on any site running AnalyticsTracker, with no per-form wiring. Passing onFormView / onFormSubmit props to DynamicForm overrides this rather than adding to it.

Dev-host skip

send() no-ops when the hostname is localhost, 127.0.0.1, 0.0.0.0, ends in .local, or contains "localhost". The tracker functions are still safe to call — they just do nothing — so there is no dev-only guard to write. Test conversions against a deployed URL.

AnalyticsTracker firing rules

Client component ("use client"), renders null, and wraps itself in Suspense (Next 16 requires useSearchParams inside one) — so it is safe to drop straight into a server layout. It fires exactly one trackPageView() on mount and one per pathname or searchParams change. Because it watches search params, /x?page=2 counts as a separate pageview from /x.

Tag config shape (TrackingTags)

fetchTrackingTags() resolves the project's third-party tag IDs; CMSTags renders them. The type carries only public IDs — the CMS strips its internal enabled / updatedAt fields before returning them to a consumer site.

interface TrackingTags {
  ga4Id?: string;       // GA4 measurement ID — CMS validates /^G-[A-Z0-9]{4,14}$/
  googleAdsId?: string; // Google Ads conversion ID — CMS validates /^AW-\d{6,12}$/
  metaPixelId?: string; // Meta Pixel ID — CMS validates /^\d{6,20}$/ (numeric string)
}
  • fetchTrackingTags() returns null, not {}, when all three resolve empty. CMSTags renders null in that case.
  • Format validation is CMS-side only. Those patterns are enforced when a staff user saves the config in the dashboard. Values coming from the NEXT_PUBLIC_* env fallbacks are not validated by this package — a typo'd env ID is injected verbatim.
  • CMSTags is an async React Server Component. Do not add "use client" to a wrapper around it and do not call fetchTrackingTags() from client code — it reads VULCANTECH_API_KEY.
  • What it renders: one shared next/script gtag.js loader with strategy="afterInteractive", keyed to the first present Google ID, plus an inline init that gtag('config', …)s every present Google ID; and, for Meta, the standard inline fbq bootstrap with fbq('track','PageView') and a <noscript> tracking-pixel <img>. Because it uses next/script, mounting it in <body> is correct — it does not need to be in <head>.
  • No consent gating. Tags load as soon as the page hydrates. If the site needs a CMP / GDPR consent gate, CMSTags does not provide one and must not be mounted unconditionally on a site that requires consent.

The enabled toggle does not override env fallbacks

Turning tracking tags off in the CMS makes /api/analytics/config return config: null — which is the same response as "never configured". fetchTrackingTags then fills each field from NEXT_PUBLIC_GA_ID / NEXT_PUBLIC_GOOGLE_ADS_ID / NEXT_PUBLIC_META_PIXEL_ID. So a site with those env vars set keeps firing its tags after the CMS switch is off. To actually disable tags on such a site you must unset the env vars and redeploy. Sites that rely purely on the CMS (no NEXT_PUBLIC_* set) honor the toggle as expected.

Failure mode

Nothing in this package throws, and nothing surfaces an error to your code. There is no error state to render and no catch block worth writing — but that also means a fully broken pipeline looks identical to a working one from the site's side.

  • Trackers (trackEvent and friends) swallow every send failure and return void. They also no-op entirely on SSR and on dev hostnames.
  • The /api/a proxy route returns 503 when VULCANTECH_PROJECT_ID / VULCANTECH_API_KEY are unset and 400 on unparseable JSON — otherwise it always returns 204, including when the CMS was unreachable or rejected the event. A 204 from /api/a is not evidence that anything was ingested.
  • The CMS drops events silently when the request is bot-flagged by user agent, or rate limited — 100 events/min per sid and 600/min per IP. Neither is reported back to the browser.
  • fetchTrackingTags / CMSTags swallow CMS errors and fall through to the env fallbacks, then render null if those are empty too. A page build can never fail on the CMS being down.

Two different API-key scopes are required, and the common misconfiguration is having only one. Getting this wrong is invisible: events vanish while /api/a still answers 204.

| Surface | CMS endpoint | Required scope | |---|---|---| | AnalyticsTracker / trackers, via the /api/a proxy | POST /api/analytics | analytics:write | | CMSTags / fetchTrackingTags | GET /api/analytics/config | analytics:read |

A site doing both needs both scopes on the one key. Verify ingestion in the CMS dashboard (Analytics → realtime), not by watching the browser's network tab for a 204.

Integration

Wire analytics into a consumer site (Next.js App Router):

1. Add the dependency:

npm install @vulcantech/analytics

(Inside the VulcanTech monorepo, use the workspace wiring in MONOREPO.md instead.)

2. Set the env — see Required env below.

3. Mount AnalyticsTracker in the root layout (src/app/layout.tsx) — tracks page views on every route change:

import { AnalyticsTracker } from "@vulcantech/analytics";

// <body>
//   <AnalyticsTracker />
//   {children}
// </body>

4. Re-export the proxy route src/app/api/a/route.ts — sends events to the CMS via the site's own domain (fails silently, never blocks the page):

export { POST, OPTIONS } from "@vulcantech/analytics/route";

5. Mount CMSTags in the root layout (src/app/layout.tsx) — injects the project's GA4 / Google Ads / Meta Pixel tags from the CMS. It is an async server component; render it directly in <body>:

import { CMSTags } from "@vulcantech/analytics/tags";

// <body>
//   <CMSTags />
//   <AnalyticsTracker />
//   {children}
// </body>

CMSTags fetches once per ISR window (revalidate: 60, tag tracking-tags:{projectId}) and renders null when nothing is configured. It replaces any hand-rolled <GoogleAnalytics>, Google Ads gtag Script, and Meta Pixel fbq snippets — remove those from the layout when adopting it.

Required env

VULCANTECH_PROJECT_ID, VULCANTECH_API_KEY (required) · VULCANTECH_CMS_URL (optional, defaults to https://cms.vulcantech.io).

The API key must be granted the analytics:read scope in the CMS for CMSTags to resolve tag IDs (the /api/analytics/config endpoint). Without it (or with tags disabled) the CMS returns null and only the env fallbacks apply.

Event ingestion needs a separate scope — analytics:write — on the same key. See the scope table under Failure mode; a key with only analytics:read drops every event silently.

Env fallbacks (tags)

When the CMS omits a field (unreachable, unconfigured, or key lacks scope), each tag ID falls back to a public env var — CMS value wins when present:

| Tag | CMS field | Env fallback | |---|---|---| | GA4 | ga4Id | NEXT_PUBLIC_GA_ID | | Google Ads | googleAdsId | NEXT_PUBLIC_GOOGLE_ADS_ID | | Meta Pixel | metaPixelId | NEXT_PUBLIC_META_PIXEL_ID |

If all three end up empty, CMSTags renders nothing.

Per-app values

None beyond env — analytics and tags are fully config-driven from the CMS.

Verifying tags are live

  • Use the CMS Integrations → Tracking Tags → Live Status panel: it fetches the deployed site and confirms each enabled tag ID is actually present in the HTML (green "Live" = the whole chain works). "Check again" re-probes.
  • In-browser, verify via the Console, not by eyeballing the Elements DOM:
    Object.keys(window.google_tag_manager)   // includes both G-… and AW-…
    window.dataLayer.filter(a => a[0] === "config")

Notes

  • The tracker skips all sends on localhost/dev hostnames; test conversions against a deployed URL. (Exact host list under Dev-host skip.)
  • CMSTags never throws — tag resolution failures are swallowed so a page build can never break on the CMS being down.
  • ISR invalidation: the CMS purges the tracking-tags:{projectId} tag when the project's analytics config changes. No consumer redeploy is needed for CMS value changes — ISR picks them up within ~60s (or instantly if the site is registered as a revalidation consumer).
  • Google Ads shares the GA4 loader. There is one gtag.js request (keyed to the GA4 ID); the Ads ID rides on it via an inline gtag('config','AW-…'). Don't expect a separate gtag/js?id=AW-… request or a standalone DOM element for it.
  • Ad blockers strip googletagmanager.com / facebook.net client-side, so tags can look absent during manual inspection — verify in a clean browser.