@tailglow/core
v0.4.0
Published
Platform-agnostic analytics core for Tailglow. Used by @tailglow/browser, @tailglow/node, @tailglow/react-native, and other platform packages.
Maintainers
Readme
@tailglow/core
Platform-agnostic analytics core for Tailglow. Used internally by @tailglow/browser.
Supported runtimes
| Runtime | Floor | Notes |
| --------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Node.js | >=22 | Enforced via engines.node. Native fetch and globalThis.crypto required. |
| Bun | Any | Modern JS by default. |
| React Native | >=0.74 (recommended) | globalThis.crypto.getRandomValues is native from RN 0.71. We document 0.74+ as the supported floor. |
| Modern browsers | Chrome 90+, Firefox 90+, Safari 14+, Edge 90+ | Released 2021+. ESM, fetch, crypto, URLSearchParams, Blob all native. |
| Cloudflare Workers / Vercel Edge / Deno | Any recent | Web Crypto + fetch first-class. |
ESM only. No CJS bundle. If your toolchain can't load ESM, upgrade your toolchain.
Direct usage
For environments without a published platform package, you can compose TailglowCore directly. You own the lifecycle (calling flush()/destroy() at the right time) and any auto-collection.
import { TailglowCore } from "@tailglow/core";
const tg = new TailglowCore({
url: "https://ingest.example.com",
key: "tg_ingest_..."
});
tg.track("job_completed", { job_id: "abc" });
process.on("SIGTERM", async () => {
await tg.flush();
await tg.destroy();
});Building a platform package
The intended pattern: compose a TailglowCore and wire platform-specific lifecycle (AppState on RN, process.on on Node, visibilitychange on browser) and any auto-collectors that emit records via core.track(name, props) (preferred) or, for adapter authors, core.capture(prebuiltRecord).
See @tailglow/browser for a reference implementation.
Pipeline order
When a record passes through core.track() or core.capture():
rateLimit gate → stamp → redact → maxRecordBytes check → onBeforeSend → queue → flush → transport- rateLimit: global token-bucket volume gate (when enabled). It runs before the rest of the pipeline, so a record the bucket drops is never stamped, redacted, size-checked, passed to
onBeforeSend, or queued. Drops surface as a collapsedtglow_rate_limitedself-event - stamp: SDK metadata (
event_id,session_id,event_time,user_id,device_id) plus any stickycontextfields - redact: URL token redaction, email redaction, field allow/deny lists
- maxRecordBytes: drop oversized records (default 1MB;
Infinitydisables) and emit atglow_record_droppedself-event - onBeforeSend: consumer hook sees the post-redaction record; return
nullto drop - queue: in-memory buffer
- flush: group records by collection, serialize each batch, POST to
?collection=<slug>(typicallyevents/errors/logs), on permanent failure callonTransportError
onBeforeSend does NOT see pre-redaction values. To get raw payloads, set redact.enabled: false.
Identity
| Mechanism | Source |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| session_id | SessionManager. Renews after sessionTimeout of inactivity. Force-rotate via rotateSession(). The onSessionRotate config hook fires after every rotation with a snapshot of the ended session (platform packages use it for session summaries). |
| user_id | identify(user_id). Pre-identify records still in the buffer OR mid-flight in pending batches are retroactively backfilled in the same session. Clear via unidentify(). |
| device_id | setDeviceId(id). Customer-supplied. Core does not generate or read hardware IDs. |
Sticky sampling resolves in cascade: user_id → device_id → session_id. Without an identifier above session, sampling resets per session, the privacy-preserving default. Caveat: an anonymous user (sampled by session_id) who later calls identify() shifts their sampling key to user_id. The verdict can flip in/out mid-session. Acceptable noise for sampleRate >= 0.1; for tighter rates, identify before any tracking.
Identity changes (logout/login, org switch)
There is no reset() method by design. To clear and rebuild identity:
await tg.flush(); // best-effort drain pending records under the old identity
await tg.destroy(); // remove listeners, stop the queue interval
tg = new TailglowCore({ ...new config() });Records tracked between flush() returning and destroy() are dropped with the old instance. For most telemetry this is acceptable.
Opt-out
optOut() stops tracking: records are dropped at the source (not collected, queued, or sent) until optIn(). It drops any un-sent queue without a final flush, so call await tg.flush() first if you want to deliver what is already queued. isOptedOut() reports status. There is no consent state machine; for a CMP-gated setup, construct the SDK only after the user accepts and destroy() to revoke.
Lifecycle
| Method | Type | Notes |
| ----------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| flush() | async | Force-drain queue. Returns when all batches are acknowledged or have failed. |
| destroy() | async | Final flush + stop queue interval. After this, the instance is inert. |
| Browser hide / Node SIGTERM | sync best-effort | Platform packages handle these. They use sendBeacon (browser) or unawaited transport calls (Node). Delivery is not guaranteed if the runtime exits before the request completes. |
Public API
TailglowCore: the core class.track/identify/setContext/optOut/flush/destroyetc.Transport,Queue,SessionManager: composable building blocks.buildIngestUrl,serializeBatch,groupByCollection,INGEST_CONTENT_TYPE,COLLECTION_FIELD: shared transport primitives.stampRecord,backfillUserId,generateSessionId,generateRecordId,randomBytes: record stamping and identity helpers.redactUrl,redactEmails,redactRecord: PII/token redaction.isSampledIn,hashToUnitInterval: sticky sampling.isBrowser,isNode,isLocalhostOrigin,isDoNotTrack,isGpc: runtime detection helpers.StorageAdapterinterface,deriveStorageKey,saveToStorageAsync,restoreFromStorageAsync,clearStorageAsync: async storage helpers.
Routing & collections
The SDK manages three configurable collections. Defaults shown:
new TailglowCore({
url,
key,
collections: {
events: "events", // default destination for track() + auto-collectors
errors: "errors", // captureException / captureMessage / console.error
logs: "logs" // console wrappers (log/warn/info/debug)
}
});track(name, props) routes the record to the configured events collection and stamps name as the type field:
tg.track("purchase", { amount: 99, currency: "USD" });
// → POST ?collection=events with { type: "purchase", amount: 99, currency: "USD", ... }Use the object form of track() to override the collection per call (rare; useful for high-volume telemetry with its own retention/schema):
tg.track({ type: "audit_event", collection: "audit_log", actor: "..." });
// → POST ?collection=audit_log with { type: "audit_event", actor: "...", ... }The string form (track(name, props)) is the 99% case; the object form is the override. There is no third positional argument — pass type, collection, and props in one object.
Records are grouped by collection at flush time and POSTed in per-collection batches. Tailglow auto-tracks schema versions per collection on the server.
Setting all three collections to the same slug merges everything into one timeline:
new TailglowCore({ url, key, collections: { events: "all", errors: "all", logs: "all" } });Typed event schemas
Augment TailglowEventTypes to declare typed props per event name (the type field). This is the canonical augmentation location. Augmenting platform packages like @tailglow/browser does not work; the schema is re-exported from core, not redeclared.
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.
