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

@adpharm/silo-analytics

v2.2.1

Published

Custom Segment wrapper with automatic Time on Page tracking

Readme

@adpharm/silo-analytics

A drop-in replacement for @segment/analytics-next that adds automatic Time on Page tracking with built-in guardrails for SPAs (React Router) and SSR environments.

📖 Documentation: silo.docs.adpharm.digital

Features

  • Zero-Config Tracking: Automatically tracks Page Time Spent events.
  • SPA Ready: Smartly handles internal route changes by listening to Segment page events + browser back/forward via popstate.
  • Tab & Visibility Aware: Pauses/resumes tracking when users switch tabs or minimize the browser; honors the GA4 three-condition rule (visible AND focused AND in pageshow→pagehide).
  • Bounded-Exponential Heartbeat: Periodic dispatches on a 30→60→120→240→480→600s schedule cap data loss on browser crash / mobile tab-kill to ≤ one schedule-step.
  • Scroll Depth: Each beacon carries scroll_depth_max_px (deterministic peak scrollY) plus, when the page is scrollable, est_scroll_depth_max_pct + est_scroll_depth_pct. The est_ prefix encodes lossiness — see Scroll Depth section.
  • Consent-Gated: Client-side consent gate — when the visitor denies the analytics category (or sends GPC), silo collects nothing (no events, beacons, or ubid). Per-writeKey opt-in / opt-out regime. @segment/analytics-consent-tools still gates third-party device-mode pixels.
  • Data Quality Guardrails:
    • 5s Threshold: Ignores "bounces" (sessions under 5 seconds).
    • 15m Cap: Prevents skewed data from "zombie tabs" by capping events at 900 seconds.
    • Idle Detection: Engagement timer pauses after 30s of zero user input (GA4 "AFK reading" weakness).
  • Reliable Delivery: pagehide + visibilitychange + Safari-only beforeunload; failed beacons persist to localStorage and replay on next page load (50-entry cap).

Installation

Remove the standard Segment package and replace it with this wrapper:

bun remove @segment/analytics-next
bun add @adpharm/silo-analytics

@segment/analytics-next is a peer dependency — removing your direct install is fine, the package manager keeps it present transitively. You import everything from @adpharm/silo-analytics instead (it re-exports the full analytics-next surface unchanged).

Usage

The minimum setup — one call, plugin auto-registered with sensible defaults:

import { AnalyticsBrowser } from "@adpharm/silo-analytics";

export const analytics = AnalyticsBrowser.load({
  writeKey: "your-write-key",
  env: window.env.PUBLIC_APP_ENV, // optional, namespaces storage keys per env
});

Pass env if your environments share an apex domain (e.g. app.example.com and staging.example.com). Without it, all envs read the same cookie key and you'll see anonymousIds bleed across deployments. Single-env or different-apex setups don't need it.

Defaults applied internally:

  • cdnURLhttps://cdn.silo.adpharm.digital
  • timeOnPage.apiHosthttps://event-gateway.silo.adpharm.digital/v1/t
  • timeOnPage.writeKey → inherits from the top-level writeKey
  • Storage keys → ajs_user_id_${writeKey}_${env} and ajs_user_traits_${writeKey}_${env} (no suffix if env is omitted)

Overrides

Pass anything you want to customize. timeOnPage is Partial<> — only override the fields you care about.

const analytics = AnalyticsBrowser.load({
  writeKey: "your-write-key",
  env: "production",
  cdnURL: "https://your-cdn.example.com", // override
  timeOnPage: {
    minSeconds: 10, // partial override; apiHost + writeKey still default
    maxSeconds: 1800, // cap events at 30m instead of the 900s default
    idleSeconds: 60, // pause engagement after 60s of no input (0 disables)
    eventName: "Engagement", // beacon event name (default "Page Time Spent")
    heartbeatSchedule: [60, 120, 240], // custom backoff (false disables)
    scrollTracking: false, // opt out of scroll-depth fields
  },
  user: { cookie: { key: "custom_cookie_key" } }, // full override
});

Scroll Depth

Every beacon (heartbeat, internal_nav, terminal) carries up to three scroll fields:

| Field | Type | Always emitted? | |---|---|---| | scroll_depth_max_px | integer | yes (when scrollTracking !== false) | | est_scroll_depth_max_pct | integer | only when page is real-scrollable | | est_scroll_depth_pct | integer | only when page is real-scrollable |

Convention: est_ prefix means "denominator can shift." The pct fields are computed as scrollY / (scrollHeight - viewportHeight). Both terms can change mid-session — lazy images, font swap, ad injection, infinite scroll. The est_ prefix encodes that lossiness in the field name so analysts can't accidentally build KPIs on the noisy signal without seeing the warning.

The _px field doesn't carry the prefix because scrollY is a deterministic browser-API integer. Trust it. Use it for absolute thresholds ("user scrolled past 2400px").

Reset boundaries. Scroll state resets when page_view_id rolls — i.e. SPA navigation (analytics.page() or popstate) and bfcache restore. Tab-switch-back does NOT reset (same page-view from the server's perspective).

Reliability across rendering modes (rough estimates, not measured):

  • Static / classic MPA: _px ~95% reliable, _pct ~90%.
  • SSR with client-routing (Next App Router, Remix): same as MPA for first page, ~80% on subsequent SPA navs — requires analytics.page() on route change.
  • Client-only SPA: _px ~85%, _pct ~70% (denominator unstable longer post-load).
  • Infinite-scroll feeds: _px reliable, _pct near-meaningless — the prefix is doing its job here.

To disable entirely: timeOnPage: { scrollTracking: false }. All three fields are then suppressed on every beacon.

Server-Side Deduplication

Every beacon carries three identifiers so your server can collapse retries that the in-process dedup flag can't catch (e.g. a sendBeacon that the browser silently re-delivers, or a heartbeat racing its successor):

| Field | Location | Meaning | |---|---|---| | messageId | top-level | UUID, unique per beacon — the dedup key for "is this the same physical send?" | | properties.page_view_id | per beacon | UUID, stable for one view of one URL; rolls on SPA nav (analytics.page() / popstate) and bfcache restore, not on tab-switch-back | | properties.sequence | per beacon | integer counter, increments per beacon within a page_view_id |

Collapse on messageId for exact-retry dedup; group by (page_view_id, sequence) to reconstruct a single page-view's beacon stream in order.

The full beacon shape is exported as the SiloBeacon / SiloBeaconProperties / SiloBeaconContext TypeScript types — import them into your server-side ingestion code so the wire contract stays in sync with the library.

Consent

Consent is a client-side gate: when the visitor's CMP denies the analytics category (or GPC is on), silo sends nothing — no events, beacons, or ubid. Per-writeKey opt-in / opt-out regime; @segment/analytics-consent-tools still gates third-party device-mode pixels. Wire it once with consent.getCategories (+ onConsentChanged for live updates).

The full guide is the single source of truth — how it works, precedence, opt-in/out, map, offlineFallbackMode, ubid, and CMP recipes (OneTrust / TCF / custom): Consent docs.

Remote Settings

On boot, load() fetches the CDN settings JSON for your writeKey itself, hands it to analytics-next (which then skips its own fetch — request count unchanged), and reads a custom silo block out of it. Features can be toggled and tuned per writeKey from the settings document, with no redeploy of the consuming site:

{
  "integrations": { "...": "standard Segment fields" },
  "silo": {
    "timeOnPage": { "enabled": true, "idleSeconds": 60 },
    "consent": { "mode": "opt-out" }
  }
}

Precedence: plugin defaults < code config < remote settings (remote wins). The block is partial — omitted fields fall through to code config. If the settings fetch fails, the library loads with pure code config and lets analytics-next fetch the settings itself — remote unreachable behaves exactly like a library without remote settings.

The block's shape is exported as the SiloRemoteSettings / SiloRemoteTimeOnPage / SiloRemoteConsent types. Full option tables: Remote Settings docs.