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

@growth-labs/analytics

v0.16.1

Published

On Cloudflare Workers, publicly cacheable middleware-injected HTML is deterministic across consent, country, visitor/session, attribution, and auth state. Its serialized config contains deployment and route constants only; browser-readable consent and eph

Readme

@growth-labs/analytics

Public HTML invariance

On Cloudflare Workers, publicly cacheable middleware-injected HTML is deterministic across consent, country, visitor/session, attribution, and auth state. Its serialized config contains deployment and route constants only; browser-readable consent and ephemeral session identity are hydrated after delivery. A consented runtime Cloudflare SSR navigation necessarily sets visitor/session cookies and therefore receives private, no-store on Cache-Control, CDN-Cache-Control, and Cloudflare-CDN-Cache-Control. That private representation carries only the non-identifying isNewSession and sessionPageCount lifecycle signals needed to preserve session_start and immediate multi-page session_engaged; it still carries no visitor ID, session ID, attribution, country, or consent decision. Astro prerender always keeps false/1 lifecycle defaults so build-time state cannot become a static artifact. Synthetic/request-specific disabled HTML receives the same full triplet even when no cookie is set and no downstream Vary header exists. Every event-route outcome—including malformed JSON, schema failure, denied/synthetic events, and accepted writes—returns 204 with the full private/no-store triplet.

Denied or unresolved Worker requests emit no analytics page view, Web Vitals, or Cloudflare Web Analytics activity. Static builds place a configured public CWA token in deterministic bootstrap state without loading the beacon, and the client creates the beacon script only after analytics consent. Cache-invariant Cloudflare SSR and static HTML both assign exactly one page view to the client; legacy/non-Worker SSR can retain server ownership through the explicit serverPageView flag. Astro prerender is detected explicitly; it does not depend on a build request exposing request.cf. If an older or nonstandard static artifact has no analytics data node, consent's deterministic <meta name="growth-labs-consent" content="…"> marker preserves the configured cookie identity and the same fail-closed browser gate.

WAE-primary analytics with auto-injected behavioral tracking, attribution, and conversion event routing. Requires Cloudflare Workers Paid plan ($5/mo) for WAE access. No D1 fallback for behavioral events.

Config

import analytics from '@growth-labs/analytics'

analytics({
  siteId: 'fedweek',                            // Cookie prefix + WAE index
  waeBinding: 'ANALYTICS',                      // WAE binding in wrangler.toml
  d1Binding: 'SITE_DB',                         // D1 for conversion events
  cookiePrefix: 'fedweek',                      // → fedweek_vid, fedweek_sid, fedweek_attr
  session: { timeoutMinutes: 30, engagedThresholdSeconds: 10 },
  scroll: { thresholds: [25, 50, 75, 90] },
  media: { thresholds: [10, 25, 50, 75, 100] },
  ecommerce: { enabled: true, currencyCode: 'USD' },
  webVitals: { enabled: true },
  cloudflareWebAnalytics: {
    enabled: true,
    token: 'public-beacon-token',
    tokenBinding: 'CF_WEB_ANALYTICS_TOKEN',
    // consentGated defaults to false: cookieless CWA loads unconditionally.
  },
  ga4: {
    measurementId: 'G-XXXXXXXX',                // public GA4 id (client + server)
    apiSecretBinding: 'GA4_MP_API_SECRET',      // server Measurement Protocol secret
    clientTag: true,                            // consent-gated browser gtag.js tag
  },
  socialSharing: {
    enabled: true,
    defaultProperty: 'fronts',
    serviceTokenBinding: 'SHARE_LINK_SERVICE_TOKEN',
    shortLinkBaseUrl: 'https://go.fronts.co',
    campaigns: ['fronts__membership-push__2026q2'],
  },
})

What It Injects

Middleware (server-side, every request):

  1. Resolves consent before creating visitor/session/attribution state
  2. Reads/creates visitor ID cookie ({cookiePrefix}_vid) only when granted
  3. Manages session and attribution cookies only when granted
  4. Owns visitor/session/attribution state; cache-invariant page views use the client route
  5. Injects deterministic public route/deployment config with no request identifiers; consented lifecycle flags appear only on a state-setting private response
  6. Bot filtering (private/no-store disabled HTML for known bot user-agents)

Routes:

  • POST /api/analytics/event — beacon receiver for client-side events
  • GET /api/consent/status — injected by @growth-labs/consent; private, server-authoritative no-decision geography used by direct browser analytics
  • Optional social routes when socialSharing.enabled is true: /admin/share, /api/share-links, /api/internal/share-links/service, /api/social-spend/import, /go/[shareLinkId]

The internal service route is for trusted automation such as Foundry. It uses Authorization: Bearer <token> from socialSharing.serviceTokenBinding, not a browser session, and writes idempotent gl_share_links rows from caller-supplied UTM-tagged destination URLs.

Client script (auto-injected):

  • page_view on cache-invariant Cloudflare and static pages (see "Page-view ownership" below)
  • Scroll depth tracking (fires at 25/50/75/90%)
  • Media progress tracking (native audio/video + Vidstack, including dynamic elements)
  • Core Web Vitals (LCP, INP, CLS)
  • Outbound link clicks, file downloads, 404 detection
  • Form tracking (start, submit, abandon)
  • E-commerce events (product_viewed, add_to_cart, checkout_success)
  • Search events (search query + result count)
  • window.glAnalytics.track(eventName, data), .identify(userId), and .reset() public API

Cloudflare Web Analytics (optional, cookieless — unconditional by default):

  • When cloudflareWebAnalytics.enabled is true, the external static.cloudflareinsights.com/beacon.min.js script is injected on every page — SSR, cache-invariant Cloudflare SSR, Astro prerender, and the schema-missing fallback — regardless of the consent decision. CWA is privacy-first RUM that sets no cookies, so it is not consent-gated. The beacon is an external tag with no inline configuration, so a hash-based CSP stays stable. The injection dedupes if the HTML already carries a beacon.
  • SSR middleware reads the public RUM token from the runtime binding named by cloudflareWebAnalytics.tokenBinding (default CF_WEB_ANALYTICS_TOKEN) and falls back to cloudflareWebAnalytics.token. Static/prerendered HTML uses the explicit token. If neither token source is set, no beacon loads.
  • Set cloudflareWebAnalytics.consentGated: true to opt back into the deferred path: static/prerendered HTML then carries only the public token in an inert <template>, and the browser creates the beacon element only after analytics consent is granted (or the private /api/consent/status route confirms a non-required region). Use this only where policy requires even cookieless RUM to wait for consent.

GA4 (Google Analytics 4) (optional):

  • Client tag (ga4.clientTag: true): a consent-gated browser gtag.js tag. Nothing loads until the @growth-labs/consent analytics-category consent is granted. On grant, the client tracker injects the external www.googletagmanager.com/gtag/js?id=<measurementId> loader and calls gtag('config', …) with anonymize defaults (anonymize_ip: true, allow_google_signals: false, allow_ad_personalization_signals: false). A later revocation flips window['ga-disable-<measurementId>'] = true and pushes a denied consent update, so no further hits leave the browser. The tag is external-script-only — no inline gtag bootstrap — so hash-CSP sites only allowlist the origins below (no per-render inline hash to maintain). The first-party WAE layer stays primary; GA4 is a parallel tag and never gates, replaces, or double-counts WAE.
  • Server Measurement Protocol (ga4.measurementId + ga4.apiSecretBinding): unchanged. Server-emitted conversion events (which never reach a client tag) are POSTed straight to GA4's Measurement Protocol; dormant until the api secret binding resolves. Both layers share the same public ga4.measurementId.

Content Security Policy — sources to allowlist

These tags load external scripts and endpoints, so a site with an enforced CSP must declare the following sources (see @growth-labs/conformance SecurityHeadersConfig — add them to the exact-origin scriptSources / connectSources / imgSources allowlists, matching how existing origins are declared). All are exact HTTPS origins; no wildcards for script hosts.

| Tag | Directive | Source | | --- | --- | --- | | CWA beacon | script-src | https://static.cloudflareinsights.com | | CWA beacon (RUM upload) | connect-src | https://cloudflareinsights.com | | GA4 gtag.js loader | script-src | https://www.googletagmanager.com | | GA4 collect / config fetch | connect-src | https://www.google-analytics.com https://*.google-analytics.com https://*.analytics.google.com | | GA4 pixel fallback | img-src | https://www.google-analytics.com https://*.google-analytics.com |

Both GA4 and CWA tags are external <script src> tags with no inline bootstrap, so sites on Astro-native hash-based CSP do not need any per-render inline hash for them — allowlisting the script origins above is sufficient.

Page-view ownership (dedup rule)

page_view is emitted by exactly one side per pageview — never both:

  • Cache-invariant Cloudflare SSR and STATIC / prerendered pages: deterministic config carries serverPageView: false. After consent, the client owns and emits exactly one page_view; the server does not emit a duplicate. A static page with no analytics config self-bootstraps from the DOM, the consent package's deterministic meta descriptor, and cookies.
  • Legacy/non-Worker SSR: after a consented server write, config carries serverPageView: true and suppresses the client page view. An unresolved first visit carries false, allowing a newly written non-required helper to resolve before the browser emits its one page view.

The explicit serverPageView flag is route/build state, not request state. A public data node contains no consent decision, country, visitor/session identifier, session lifecycle signal, or attribution and is byte-stable for a fixed route and deployment. A consented Cloudflare response is already private because it sets state cookies; its data node may carry isNewSession and sessionPageCount so the client preserves the existing WAE session lifecycle without exposing either identifier. Prerendered/static data always keeps the deterministic false/1 defaults. The node is inert HTML-safe JSON, so a strict CSP does not need a nonce or unsafe-inline.

stripAnalyticsConfigDataScript remains exported for compatibility with older consumers, but public-cache safety no longer depends on calling it:

import { stripAnalyticsConfigDataScript } from '@growth-labs/analytics/client-config'

const legacySanitizedHtml = stripAnalyticsConfigDataScript(renderedHtml)

Both paths use deterministic consent metadata: consentManaged + consentCookieName in analytics config and the matching growth-labs-consent meta marker as the static fallback. The browser reads that configured decision cookie and its ${cookieName}_required helper; it does not hardcode gl_consent. The readable helper may hydrate the first-party client, but it is not authority: SSR and event-route writes recompute consent from the trusted request, while Cloudflare Web Analytics and Zaraz require an explicit decision or a private /api/consent/status response confirming non-required geography. Missing, malformed, or unavailable trusted state fails closed. Every granted navigation produces one page view through its assigned owner.

For a client page_view, the /api/analytics/event route re-derives the WAE source dimensions server-side from the posted navigation url + referrer (the beacon's request url is the analytics endpoint and carries no source signal), using the same parseTouchPoint precedence the middleware uses — read-only for the blobs, so beacons never write an attribution touch. The resulting WAE page_view is equivalent to the server middleware's for the same navigation (blob1='page_view', blob2=siteId from the route env/config, blob6=pagePath, blob16=contentSlug, blob17=contentType, blob8–11 server-derived source dims), so client and server page-views are indistinguishable downstream.

Event Routing

| Event type | Destination | Examples | |------------|-------------|---------| | Behavioral | WAE only | page_view, scroll_50, page_exit | | Conversion | WAE + D1 | newsletter_subscribed, checkout_success, form_submit | | System | WAE only | session_start, session_engaged | | Vital | WAE only | web_vital_lcp, web_vital_inp, web_vital_cls |

WAE Schema

20 blobs + 12 doubles per data point. Key fields:

  • blob1: eventName, blob2: siteId, blob3: visitorId, blob5: pageUrl
  • blob8-10: UTM params, blob11: derivedSource, blob12: country
  • blob13: deviceType, blob16: contentSlug, blob18: eventCategory
  • double1: timestamp, double2: eventValue / page-exit loaded seconds, double3: web vital value, double5: active/watch/listen seconds

Server-side helpers should use the exported WAE builders/writers instead of ad hoc schemas. writeAuthEvent() is available for non-Astro auth Workers; buildWAEDataPoint() remains the low-level schema builder for package-owned event emitters.

Utility Exports

@growth-labs/analytics/utils re-exports the core event helpers plus the v0.3 WAE infrastructure primitives:

  • writeAuthEvent() for best-effort auth success rows from Worker-only auth flows.
  • trackServerEvent() for Astro/Worker server routes that need the same WAE + D1 attribution path as /api/analytics/event.
  • appendCheckoutAttribution() for LemonSqueezy checkout links with visitor/session/first/latest/content custom data and discount code propagation.
  • Social attribution helpers: buildSocialShareUrl(), parseSocialUtmContent(), extractShareIdFromUtmContent(), classifySocialShare(), parsePlatformClickEvidence(), and buildPlatformTrackingFields().
  • queryWAE(), fetchWAE(), and assertValidWaeDataset() for Cloudflare Analytics Engine reads.
  • buildQueryContext(), resolveDataSourceContext(), fetchSiteD1(), finalizeDataSourceStatus(), and pickDataForStatus() for analytics-platform-style live/demo/error data-source handling.
  • cachedFetchWAE() / waeCacheKey() for KV-backed dashboard query caching.
  • validateCloudflareCredentials(), probeWaeDataset(), and probeSiteD1() for setup probes with injectable fetch/query hooks.
  • compactNumber(), formatDate(), formatDuration(), formatPercent(), and formatPercentChange() for dashboard display formatting.
  • encryptSecret() / decryptSecret() using Web Crypto AES-GCM.
  • resolveWindow() / windowSqlFragment() for closed UTC reporting windows that exclude partial today, plus rolling sub-day windows ({ minutes: N }, { hours: N }) anchored to now() for short-interval scheduled digests.
  • @growth-labs/analytics/client/helpers for consent-aware browser instrumentation of search, newsletter conversion, share/related links, client errors, HTTP error mapping, and redirect attribution.

Content Meta Helper

inferAnalyticsContentMeta(pathname, opts?) (exported from the package root and from @growth-labs/analytics/content-meta) is a pure, framework-agnostic helper that maps a URL pathname to { contentSlug, contentType, sourcePath } | null. Channel layouts call it once and render the result as three <meta> tags in <head> so every site emits identical gl-content-* semantics. The SSR analytics middleware (extractPageContentContext) reads those exact tag names back out of the rendered HTML to attribute page-view and content events; this helper is the single source of truth for the path → content mapping. It agrees with the middleware's inferPathContent fallback (sharing DEFAULT_COLLECTION_CONTENT_TYPES) for the canonical single-segment content path shape (/<collection>/<slug>); when a meta tag is emitted the middleware reads it verbatim, so multi-segment slugs are still attributed correctly even though the helper's slug match is more permissive than the fallback's.

Article-route pages that actually carry a video or podcast must pass contentTypeOverride to preserve fronts' automatic video/podcast reclassification, or they will be typed article:

---
import { inferAnalyticsContentMeta } from '@growth-labs/analytics/content-meta'
const meta = inferAnalyticsContentMeta(Astro.url.pathname, {
  contentTypeOverride: item?.video ? 'video' : item?.audio ? 'podcast' : undefined,
})
---
{meta && (
  <>
    <meta name="gl-content-slug" content={meta.contentSlug} />
    <meta name="gl-content-type" content={meta.contentType} />
    <meta name="gl-source-path" content={meta.sourcePath} />
  </>
)}

D1 Tables (prefixed gl_)

  • gl_conversion_events — conversion event log with e-commerce fields
  • gl_analytics_visitors — visitor summary (session count, first/last seen)
  • gl_content_progress — legacy table retained by migrations; high-volume reading/watching progress now writes to WAE only
  • gl_identity_links — anonymous visitor to authenticated identity bridge
  • gl_conversion_attribution_touches — bounded touch history materialized only when a conversion is written
  • gl_social_authors — public, non-PII author slug/id metadata
  • gl_social_handles — social account metadata with brand-account flag
  • gl_share_links — generated social links and canonical UTM fields
  • gl_social_spend_daily — daily spend/ad-object metadata mapped to share links

Social Attribution

Social is measured through the normal attribution pipeline, not a separate analytics layer. The link builder emits canonical UTMs:

  • utm_source: platform, e.g. x, instagram, facebook
  • utm_medium: social or paid_social
  • utm_campaign: property__initiative__period
  • utm_content: sl__{property}__{platform}__{handle_id}__{author_id}__{share_id}
  • utm_term: placement, e.g. feed, story, bio, ad

The /admin/share route is for an admin operator. Writers do not log in; the operator explicitly selects or type-to-creates the human author, and the generated gl_share_links.created_by records the operator separately from the marketing author_id. Author Social batch links may omit a posting handle when the operator is minting a link for the selected author to post from their own account; those rows use handle_id = 'na' in the legacy NOT NULL column and leave handle_id_v2 null.

Wrangler Bindings

[analytics_engine_datasets]
binding = "ANALYTICS"

[[d1_databases]]
binding = "SITE_DB"
database_id = "..."

Runtime Behavior

Server entrypoints side-effect import virtual:growth-labs/analytics/config, so package state seeds itself in the Worker that actually handles the request.

Bindings resolve from the standard Cloudflare surfaces:

  • cloudflare:workers for env bindings
  • Astro.locals.cfContext.waitUntil() for background writes

The package no longer reads legacy locals.runtime; consumers should not patch @growth-labs/analytics/state or add package-specific runtime-state middleware.

Consent Integration

Optional peer dependency on @growth-labs/consent. If installed:

  • Consent middleware publishes a deterministic managed/cookie-name descriptor.
  • Missing trusted state, unresolved, or denied consent → zero page views, browser events, Web Vitals, CWA loads, tracking cookies, or attribution.
  • The readable helper never authorizes server writes or direct CWA/Zaraz loads.
  • Granted consent → full tracking (cookies, attribution, all events).

If consent package NOT installed → tracking runs unconditionally.

Client-Side Event API

// gl:* CustomEvent pattern for loose coupling
document.dispatchEvent(new CustomEvent('gl:track', {
  detail: { event: 'product_viewed', data: { productId: '123' } }
}))

// Or direct API
window.glAnalytics.track('custom_event', { label: 'something' })
window.glAnalytics.identify('user_123')
window.glAnalytics.reset()

Key Patterns

  • Virtual module: virtual:growth-labs/analytics/config
  • Runtime config self-seeds in middleware and routes via the virtual module
  • gl:* CustomEvent pattern on document for loose coupling between client-side packages. Package-local gl:* events are not automatically forwarded; use window.glAnalytics.track(...) when an event should be written to WAE.
  • Package-owned WAE writes use the shared schema helpers; auth Workers use writeAuthEvent()
  • Bot filtering runs before any tracking
  • gl_ prefix on all D1 table names