@tailglow/browser
v0.4.0
Published
Browser SDK for Tailglow. Auto-collects page views, errors, vitals, and section engagement. Cookie-free by default.
Maintainers
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.0 → 0.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/browserUsage
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_id → device_id → session_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→ configuredlogscollection withtype: "<level>"error(Error)→captureException→ configurederrorscollection withtype= error class nameerror(string)→captureMessage(level:"error") → configurederrorscollection withtype: "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 thenav_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 statusoptOut() 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.
