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

@tailglow/browser

v0.4.0

Published

Browser SDK for Tailglow. Auto-collects page views, errors, vitals, and section engagement. Cookie-free by default.

Readme

@tailglow/browser

Browser SDK for Tailglow. Auto-collects page views, section engagement, errors, web vitals, device info, and session summaries. Designed for cookie-free, no-persistent-browser-storage operation by default. Honors DNT and GPC.

Stability

0.x is unstable. Breaking changes may ship in any minor (0.1.00.2.0) release until we declare 1.0. Pin to the current minor (for example ~0.3.x) for patch-only auto-updates.

Supported runtimes

| Runtime | Floor | | ------------------------ | --------------------------------------------- | | Modern browsers | Chrome 90+, Firefox 90+, Safari 14+, Edge 90+ | | Node.js (build/SSR only) | >=22 | | Bun | Any |

ESM only. No CJS bundle.

Install

bun add @tailglow/browser

Usage

import { Tailglow } from "@tailglow/browser";

const tg = new Tailglow({
  url: "https://ingest.tailglow.io",
  key: "tg_ingest_your_key"
});

tg.track("button_click", { button_id: "signup" });
tg.identify("user_123");

Drop-in <script>

The IIFE bundle (dist/tglow.js) self-initializes from data-* attributes and registers window.tglow as a function:

<script
  defer
  src="https://cdn.tailglow.io/tglow.js"
  data-key="tg_ingest_your_key"
  data-url="https://ingest.tailglow.io"
></script>

<script>
  // Optional pre-load stub: works before the SDK loads
  window.tglow =
    window.tglow ||
    function () {
      (window.tglow.q = window.tglow.q || []).push(arguments);
    };
  tglow("track", "signup", { plan: "pro" });
</script>

Declarative event tracking

data-tglow-event="<name>" fires tg.track("<name>", props) on click — routes to the configured events collection with type: "<name>". Other data-tglow-* attributes are stamped as props on the record.

<button data-tglow-event="signup" data-tglow-method="github">Sign up with GitHub</button>

Identity

Browser identity is ephemeral by default. There is no persistent device ID; the only persistent identifier is user_id set via identify() from the customer's existing auth session.

| Layer | Source | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | session_id | In-memory, regenerated after sessionTimeout of inactivity. Force-rotate via rotateSession(). | | user_id | identify(user_id). Pre-identify records still in the buffer or in-flight in the same session are retroactively backfilled. Clear via unidentify(). | | device_id | setDeviceId(id). Opt-in, customer-supplied. The browser SDK does not generate one by design. |

Session summaries

With autoPageViews on, the SDK emits a session_summary record (engaged_ms, pages_viewed, entry_path, exit_path, reason) to the configured events collection on tab hide/unload, on session rotation, and on destroy(). Each emission carries cumulative totals for its session_id — totals only grow within a session, so consume the last record per session. engaged_ms counts only active, visible time (same clock as page_view duration_ms). Disable with autoSessionSummary: false.

Route context on page views

Page views report raw location.pathname, which has unbounded cardinality when paths embed resource IDs. Pass routeContext to stamp your router's route template (and any params) onto every page_view record:

// SvelteKit example
import { page } from "$app/state";

const tg = new Tailglow({
  url: "...",
  key: "tg_ingest_...",
  routeContext: () => ({ route_id: page.route.id, ...page.params })
});

Auto-collected fields (from_path, to_path, duration_ms, nav_type, url) win on collision. A throwing routeContext is ignored — it never breaks navigation tracking.

Sticky sampling cascades: user_iddevice_idsession_id. Without an identifier above session, sampling resets per session. Caveat: when an anon user calls identify() mid-session, the sampling key shifts and the verdict can flip. Acceptable noise for sampleRate >= 0.1; identify before any tracking for tighter rates.

Identity changes (logout/login, org switch)

There is no reset() method. Pattern:

await tg.flush(); // drain pending records under the old identity
await tg.destroy(); // remove listeners
tg = new Tailglow({ ...new config() });

Privacy defaults

| Default | Behavior | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Honor DNT | If navigator.doNotTrack === "1", the SDK is fully disabled. Override with honorDnt: false. | | Honor GPC | If navigator.globalPrivacyControl === true, the SDK is fully disabled. Override with honorGpc: false. | | Skip localhost | The SDK does not track on localhost, 127.0.0.1, 0.0.0.0, or file://. Override with excludeLocalhost: false. | | No browser storage | The default browser path does not read or write cookies, localStorage, sessionStorage, or IndexedDB. | | Redaction | URL token patterns and email-shaped strings are redacted before send (recurses into breadcrumbs, frames, and nested customer context). |

The tglow_ignore developer escape hatch (localStorage.setItem("tglow_ignore", "true")) is not read by default. Pass respectDevOptOut: true to enable it for staging/dev builds.

"Cookie-banner-free" is a technical position, not a legal claim. Verify regional regulatory requirements (EU/UK ICO, CNIL, CCPA) for your specific use case.

Console capture

Default autoConsole: ["error", "warn"]. Only severity-flagged console calls become records on the wire. log / info / debug levels are still wrapped (they feed the breadcrumb buffer attached to the next captured error), but don't emit records by themselves.

To broaden capture, list more levels. To disable entirely, pass [].

// Default: errors and warnings as records, all levels in breadcrumbs
new Tailglow({ url, key });

// Capture everything as records (data-lake mode)
new Tailglow({ url, key, autoConsole: ["log", "warn", "info", "debug", "error"] });

// Disable entirely (no wrapping, no breadcrumbs from console)
new Tailglow({ url, key, autoConsole: [] });

Routing per level when emitted:

  • log / warn / info / debug → configured logs collection with type: "<level>"
  • error(Error)captureException → configured errors collection with type = error class name
  • error(string)captureMessage (level: "error") → configured errors collection with type: "Message"

Breadcrumbs

Every captured error carries a snapshot of the breadcrumb trail: what the user did in the moments before the failure. The SDK produces breadcrumbs automatically from three sources:

  • Navigation (category: "navigation"): the initial page load and every SPA transition, as "/pricing -> /signup" with the nav_type.
  • Clicks (category: "click"): clicks inside tracked sections and outbound link clicks. Crumbs identify targets structurally (section id, tag, href) and never include element text.
  • Console (category: "console"): every wrapped console level, as described above.

Add your own markers with addBreadcrumb({ category: "custom", message: "checkout step 2" }). The buffer is a ring (default 100 entries, breadcrumbBuffer to change) and crumbs only leave the device attached to error records, after redaction.

Testing on localhost

By default the SDK skips tracking on localhost, 127.0.0.1, 0.0.0.0, and file://. To verify the integration end-to-end during development, opt in:

new Tailglow({
  url: "https://ingest.tailglow.io",
  key: "tg_ingest_your_key",
  excludeLocalhost: false, // enable on localhost
  context: { environment: "development" }, // tag dev events
  debug: true // console-log SDK activity
});

Drop-in script equivalent:

<script
  defer
  src="https://cdn.tailglow.io/tglow.js"
  data-key="tg_ingest_..."
  data-url="https://ingest.tailglow.io"
  data-exclude-localhost="false"
  data-environment="development"
  data-debug="true"
></script>

(data-release, data-environment, and data-dist flow into context automatically. For richer context, use tglow("setContext", {...}) after the script loads.)

Tag dev events with a distinct environment so they don't pollute production dashboards. Common pattern:

excludeLocalhost: process.env.NODE_ENV === "production",
context: { environment: process.env.NODE_ENV }

Per-browser kill-switch (e.g. for QA accounts or your personal dev session): set respectDevOptOut: true in your config, then localStorage.setItem("tglow_ignore", "true") in the browser console.

Opt-out

The SDK is cookieless and stores nothing on the device by default, so basic anonymous analytics needs no cookie banner.

tg.optOut(); // stop collecting and sending
tg.optIn(); // resume
tg.isOptedOut(); // check status

optOut() drops the queue without a final flush. Call await tg.flush() first if you need to preserve queued records. For a CMP-gated setup, use init-on-consent: construct the SDK only after the user accepts, and destroy() to revoke.

Lifecycle without auto-collectors

If you want browser-side queue + visibility flush + sendBeacon delivery but not the auto-collectors (Electron renderer, embedded WebView, niche use cases), disable them all:

const tg = new Tailglow({
  url: "...",
  key: "tg_ingest_...",
  autoPageViews: false,
  autoSections: false,
  autoErrors: false,
  autoVitals: false,
  autoDevice: false
});
// You still get visibility/beforeunload flush via sendBeacon and the full track API.

Other runtimes

For Node, Bun, React Native, Electron, pick the platform package (@tailglow/node, @tailglow/react-native, etc., when published) or compose TailglowCore directly from @tailglow/core.

Pipeline

rateLimit gate → stamp → redact → maxRecordBytes check → onBeforeSend → queue → flush → transport

The global rateLimit bucket (when enabled) runs before the rest of the pipeline: a record the bucket drops is never stamped, redacted, size-checked, passed to onBeforeSend, or queued, and the drop surfaces as a collapsed tglow_rate_limited self-event.

onBeforeSend sees the post-redaction record. Return null to drop. Set redact.enabled: false if your hook needs raw payloads.

Transport

All requests go to POST {url}?key={key}&collection={slug} with Content-Type: text/plain. No Authorization header, no custom headers; this is a CORS "simple request" with no preflight.

Failed requests retry on 5xx, 408, and 429 with exponential backoff. Honors the Retry-After header. Permanent failures fire the onTransportError(error, batch) hook if configured. On page hide, the SDK uses navigator.sendBeacon for best-effort delivery.

Collections

Three configurable destinations. Defaults shown:

new Tailglow({
  url,
  key,
  collections: {
    events: "events", // track() + auto-collectors (page_view, click, vital, device, ...)
    errors: "errors", // captureException / captureMessage / console.error
    logs: "logs" // console wrappers (log/warn/info/debug)
  }
});

Records carry a type field that discriminates within the collection (page_view, click, signup, TypeError, etc.). Set all three collections to the same slug to merge into one timeline.

For drop-in <script> users, override via data-collection-events="...", data-collection-errors="...", data-collection-logs="..." on the <script> tag.

Typed event schemas

Augment @tailglow/core's TailglowEventTypes interface (this is the canonical location, and the typing flows through to every platform package automatically). Augmenting @tailglow/browser does not work; the schema is re-exported from core, not redeclared here.

declare module "@tailglow/core" {
  interface TailglowEventTypes {
    signup: { plan: "free" | "pro" };
    purchase: { amount: number; currency: string };
  }
}

tg.track("signup", { plan: "pro" }); // ✓ typed
tg.track("signup", { plan: "wrong" }); // ✗ TS error
tg.track("anything_else", { whatever: true }); // ✓ falls back to Record<string, unknown>

The augmented keys describe the type field of records routed to the configured events collection, not separate collections per key.