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

@streamscloud/analytics-tracker

v0.1.0

Published

Anonymous web analytics tracker for StreamsCloud consumer hosts

Readme

@streamscloud/analytics-tracker

Anonymous web analytics tracker for StreamsCloud consumer surfaces. Collects post views and card clicks, qualifies them client-side, and ships them in batches to a StreamsCloud ingest endpoint.

  • Anonymous by construction — no cookies, no Web Storage, no device ids, nothing derived from identity. The only identifier is a random session UUID held in a JS variable: it dies on page reload and survives client-side navigation.
  • Never breaks the host page — no method ever throws into the caller, there are zero runtime dependencies, and losing an event is always preferred over interfering with the visitor's page.
  • Explicit wiring only — nothing auto-initializes and there is no global instance. You create a tracker, you pass it where it is needed, you dispose of it when the surface unmounts.

Install

npm install @streamscloud/analytics-tracker

Quick start

import { AnalyticsTracker } from '@streamscloud/analytics-tracker';

const tracker = new AnalyticsTracker({
  endpoint: 'https://<your-streamscloud-host>/events',
  platform: 'WEBSITE', // 'CHANNEL' | 'WEBSITE' | 'WEB_EMBED'
});

// Low-level API — for hosts with their own UI and their own view logic:
tracker.track({ eventType: 'VIEW', entityId: postId });
tracker.track({ eventType: 'CTA_CLICK', entityId: postId, ctaId: cardId });
tracker.track({ eventType: 'PRODUCT_CLICK', entityId: postId, ctaId: productCardId });

// When the surface that owns this tracker unmounts (e.g. an SPA route teardown):
tracker.dispose();

All ids (entityId, ctaId) must be UUIDs — the ingest endpoint rejects anything else. entityId is always the post id.

Wiring a UI through raw signals

If your UI can report raw signals — visibility changes, playback position, clicks — you don't need your own view logic. createPostSignals returns a sink whose built-in sensors decide when a post counts as viewed and emit the events for you:

import { AnalyticsTracker, createPostSignals } from '@streamscloud/analytics-tracker';

const tracker = new AnalyticsTracker({ endpoint, platform: 'CHANNEL' });
const signals = createPostSignals(tracker);

// From an IntersectionObserver callback (report threshold crossings).
// rootBounds is null in cross-origin iframes — guard it, or coverage-based
// qualification silently turns off there (ratio-based still works):
signals.visibilityChanged(post, {
  ratio: entry.intersectionRatio,
  viewportCoverage: entry.rootBounds ? entry.intersectionRect.height / entry.rootBounds.height : 0,
});

// From a video element's timeupdate (raw position samples, ~4/s):
signals.playbackTime(post, { seconds: video.currentTime, mediaId });

// From click handlers:
signals.clicked(post, { kind: 'cta', id: ctaCardId });

where post is { id: string; type: PostType }id is the post's UUID and type is one of 'ARTICLE' | 'ARTICLES_COLLECTION' | 'GALLERY' | 'PRODUCTS_COLLECTION' | 'SHORT_VIDEO' | 'VIDEO'.

The sink is consumed by shape (structural typing): a UI library can declare the same interface locally and accept it as an optional prop without depending on this package.

When does a view count?

The sensors apply these rules, once per (session, post):

  • The card must be fully visible, or cover ≥ 75 % of the viewport height, for an accumulated ≥ 1 second. The coverage rule exists for cards taller than the viewport (their intersection ratio can never reach 1) but applies to every card. Scrolling away pauses the clock; scrolling back resumes it. A qualified view never re-fires.
  • Video posts (VIDEO, SHORT_VIDEO) additionally require ≥ 3 seconds of accumulated real playback: missing samples mean paused, a forward jump beyond ~2 s is a seek and a negative delta is a loop wrap — neither counts toward the 3 s, neither disqualifies.
  • Surfaces where the post is on screen by definition (an opened post page, the active item of a player) must report one fully-visible sample on activation — e.g. signals.visibilityChanged(post, { ratio: 1, viewportCoverage: 1 }).

Clicks are simpler: { kind: 'cta' | 'product', id } maps to a click event; a plain card tap ({ kind: 'card' }) produces no event.

Delivery semantics

  • Batching: events are stamped at event time (occurredAt, ISO-8601 UTC) and flushed every ~10 seconds or 20 events, whichever comes first. When the page is hidden or unloaded, pending events go out through navigator.sendBeacon.
  • Dedup: VIEW fires at most once per (session, post) — the sensors and the tracker core both enforce it. Clicks are never deduplicated: two taps are two events.
  • Retry: a batch is sealed at first send and retried verbatim under the same batch id on 429/5xx/network failure, honoring Retry-After (clamped to 1 s – 1 h), with exponential backoff otherwise. The retry queue is bounded (oldest batches drop first), a batch older than 24 hours is abandoned, and a 400 response drops the batch permanently. The server deduplicates whole batches by id, so an ambiguous delivery (e.g. a beacon plus a retry) never double-counts.
  • A session is one page load. The retry queue lives in memory only — closing the tab loses whatever the final beacon couldn't carry. That is by design.

Content-Security-Policy

The tracker POSTs to your ingest endpoint with Content-Type: text/plain (no CORS preflight). If the embedding page sets a CSP, its connect-src must allow the ingest host.

API

| Export | Description | | --- | --- | | new AnalyticsTracker(config) | An isolated tracker instance with its own session. Multiple instances on one page are legal. config: { endpoint: string; platform: 'CHANNEL' \| 'WEBSITE' \| 'WEB_EMBED' }. | | tracker.track(input) | Enqueue one event. input is a discriminated union: { eventType: 'VIEW', entityId } or { eventType: 'CTA_CLICK' \| 'PRODUCT_CLICK', entityId, ctaId }. Never throws. | | tracker.dispose() | Last-chance beacon flush, then stops timers and removes listeners so the instance can be collected. Further track calls are ignored. | | createPostSignals(tracker) | The raw-signal sink described above. Never throws. |

Exported types: TrackerConfig, TrackInput, WebPlatform, PostType, PostSignals, PostSignalRef, VisibilitySample, PlaybackSample, ClickTarget.

Development

npm run check   # typecheck + lint + format check
npm test        # vitest
npm run build   # dist/ (ESM + CJS + declarations)

Release

Always via the npm scripts, never raw npm publish. Both scripts require NPM_TOKEN in the environment (.npmrc reads it), a clean working tree, and run check, test and build first.

npm version patch --no-git-tag-version   # or minor / major — then commit the bump
npm run publish:prod                     # publishes package.json version with the `latest` tag
npm run publish:dev                      # publishes `X.Y.Z-<timestamp>` with the `next` tag, restores package.json
npm run pack                             # builds and produces a local tarball for inspection

Standard workflow: bump version → commit → npm run publish:prod → push → PR → merge → git tag @streamscloud/analytics-tracker@{version}. Consumers install explicit versions, never @latest.