AbsoluteJS

Beacon

@absolutejs/beaconv0.7.0-beta.15betaObservability

A ~2 KB gzipped browser error and breadcrumb SDK that ships envelopes to your own @absolutejs/errors ingest endpoint.

A tiny, zero-dependency browser SDK that captures uncaught errors and unhandled rejections, records breadcrumbs, batches events and POSTs an envelope to the @absolutejs/errors /ingest endpoint via navigator.sendBeacon or fetch keepalive. It stays around 2 KB gzipped by keeping Effect and Schema validation server-side, while a compile-time assertion contract-locks the envelope shape to the ingest endpoint — change either side and the build breaks.

#Installation

BASH
bun add @absolutejs/beacon

#Capabilities

Overview

Tiny, zero-dependency browser SDK for the AbsoluteJS observability stack. 2 KB gzipped.

Captures uncaught errors + unhandled rejections, records breadcrumbs (console / click / fetch / navigation), batches, and POSTs an envelope to @absolutejs/errors/ingest via navigator.sendBeacon / fetch keepalive.

Why it's not Effect-native (on purpose)

A browser SDK loads on every page for every user, so bytes are the dominant cost. Measured: an Effect-native client is 108 KB gz; this is 2 KB gz. The client has no trust boundary — it's a dumb producer of telemetry — so the Effect/Schema rigor lives server-side in @absolutejs/errors/ingest, which validates the untrusted POST body.

You lose nothing on type safety: the envelope is contract-locked to the ingest endpoint's accepted shape by a compile-time assertion (the type spans the wire; the runtime machinery does not). Change the shape on either side and the build breaks.

Quick start

Or hold an instance instead of the global:

Preserve pre-hydration layout shifts

If Beacon itself is loaded lazily, install its zero-configuration early buffer from the synchronous application entry before framework hydration:

Show 1 more

The full Beacon instance consumes this buffer automatically when disruptive layout-shift signals are enabled. It retains the entry's original performance time, observation time, source rectangles, viewport and document state, plus resize and privacy-safe interaction timing. This avoids misclassifying a buffered startup entry using only state observed after hydration. The early entry point does not initialize Beacon, send telemetry, or import the main SDK.

What it does

Auto-capture — window.onerror + unhandledrejection (toggle via instrument).

Breadcrumbs — console.error/warn, clicks, fetch (skipping its own

ingest endpoint), and SPA navigations, in a ring buffer attached to each event.

Show 10 more

Batching — buffers up to maxBatch (default 30) / flushIntervalMs

(default 5s); flushes reliably on pagehide / tab-hidden via sendBeacon.

Context — setTags, setUser, per-call tags/extra, a per-session id.

Stable semantic grouping — pass groupingKey for synthetic or provider

failures whose stack or wording can move between releases. The matching @absolutejs/errors ingest service validates and hashes it server-side; clients never choose raw fingerprints. Beacon's built-in signals always add a stable grouping key derived from the normalized route and semantic target, excluding durations, counts, release ids, entity UUIDs, and collector stacks.

Cause chains — preserves nested Error.cause stacks and diagnostic fields

in extra.errorCauses, including database driver error codes and details.

Sampling + redaction — sampleRate, a beforeSend(event) hook

(return null to drop), and default credential/context redaction after the hook so host customization cannot accidentally reintroduce secrets. URL query/hash values, secret-bearing fields, bearer/JWT values, and breadcrumb text are sanitized. Set redact: false only when a trusted boundary replaces it.

Noise filtering — known browser-host/scanner failures such as CefSharp's

API

SSR-safe: imported in a non-DOM environment, createBeacon returns a no-op.

Semantic grouping keys require @absolutejs/errors 0.7.3 or newer at the ingest boundary.

Noise policy (@absolutejs/beacon/policy)

An issue board is only worth opening if everything on it is real. Every application that ships Beacon ends up writing the same beforeSend filter — and most of what goes in it is not application-specific at all: a stale chunk import after a deploy, a theme extension rewriting inline styles before hydration, a read that failed while the tab was hidden, a 5xx the server already captured with a stack. What differs is a handful of endpoints and hostnames.

Built in, each switchable and individually exported: hidden-tab read failures, 5xx the server already captured, stale releases the app prompts for itself, stale chunk imports, theme-extension hydration mismatches, and third-party deprecation reports. rules takes application-specific predicates.

reliabilitySignalPreset turns off the four detectors another system usually already owns — browser policy reports (a report-to endpoint is canonical), document discards, latency (a performance console), stale releases (a service worker prompt) — and puts the behavioural detectors behind a consent flag. Everything it does not name keeps its Beacon default.

Show 1 more

This entry point imports nothing from the SDK at runtime: boot-time policy is read by the synchronous page graph, and pulling Beacon's runtime in there makes the bundler merge the whole SDK back into it, defeating lazy startup.

Automatic error capture

window.onerror and unhandledrejection handlers are installed by initBeacon and can be toggled via the instrument option.

Breadcrumb trail

console.error/warn calls, clicks, fetch requests (skipping its own ingest endpoint) and SPA navigations are kept in a ring buffer attached to each event.

Reliable batching

Events buffer up to maxBatch (default 30) or flushIntervalMs (default 5s) and flush reliably on pagehide and tab-hidden via sendBeacon.

Context and sampling

setTags, setUser, per-call tags and extra, and a per-session id enrich every event; sampleRate and a beforeSend hook drop or redact before sending.

Session replay seam

getReplayId() stamps each event with the active session-replay id so @absolutejs/replay can cross-link an error to its exact DOM recording.

SSR safe

Imported in a non-DOM environment, createBeacon returns a no-op — safe to ship in server-rendered code paths.

Outcomes

What you can build

Overview

Tiny, zero-dependency browser SDK for the AbsoluteJS observability stack. 2 KB gzipped.

Why it's not Effect-native (on purpose)

A browser SDK loads on every page for every user, so bytes are the dominant cost. Measured: an Effect-native client is 108 KB gz; this is 2 KB gz. The client has no trust boundary — it's a dumb producer of telemetry — so the Effect/Schema rigor lives server-side in @absolutejs/errors/ingest, which validates the untrusted POST body.

Install

Zero runtime dependencies.

Hardening checklist

Production guidance

Noise policy (@absolutejs/beacon/policy)An issue board is only worth opening if everything on it is real. Every application that ships Beacon ends up writing the same beforeSend filter — and most of what goes in it is not application-specific at all: a stale chunk import after a deploy, a theme extension rewriting inline styles before hydration, a read that failed while the tab was hidden, a 5xx the server already captured with a stack. What differs is a handful of endpoints and hostnames.

Follow in order

Troubleshooting path

1
Why it's not Effect-native (on purpose)
A browser SDK loads on every page for every user, so bytes are the dominant cost. Measured: an Effect-native client is 108 KB gz; this is 2 KB gz. The client has no trust boundary — it's a dumb producer of telemetry — so the Effect/Schema rigor lives server-side in @absolutejs/errors/ingest, which validates the untrusted POST body.
2
What it does
Auto-capture — window.onerror + unhandledrejection (toggle via instrument).

#Quick start

Partial snippet

Working example for Quick start.

TS
import { initBeacon, captureException } from "@absolutejs/beacon";

initBeacon({
  project: "web",
  endpoint: "https://api.example.com/ingest",
  release: import.meta.env.VITE_RELEASE,
  environment: "production",
  // Optional: detect a tab that stayed open across a deployment. The endpoint
  // may return either `{ commit: "..." }` or `{ release: "..." }`.
  releaseProbe: {
    endpoint: "/version",
    onStale: () => window.location.reload(),
  },
});

// Uncaught errors + unhandled rejections are captured automatically.
// Manual capture anywhere:
try {
  await checkout();
} catch (e) {
  captureException(e, { tags: { component: "billing" } });
}

#Quick start 2

Partial snippet

Or hold an instance instead of the global:

TS
import { createBeacon } from "@absolutejs/beacon";
const beacon = createBeacon({ project: "web" });
beacon.setUser({ id: currentUserId });
beacon.captureMessage("checkout started", "info");

#Preserve pre-hydration layout shifts

Partial snippet

If Beacon itself is loaded lazily, install its zero-configuration early buffer from the synchronous application entry before framework hydration:

TS
import { installEarlyLayoutShiftBuffer } from "@absolutejs/beacon/early";

installEarlyLayoutShiftBuffer();

#API

Partial snippet

Working example for API.

TS
createBeacon(options) => Beacon
initBeacon(options)   => Beacon   // also sets the global singleton
getBeacon()           => Beacon | undefined

// Beacon:
captureException(error, { groupingKey?, level?, traceId?, spanId?, tags?, extra? })
captureMessage(message, level?, { groupingKey?, traceId?, spanId?, tags?, extra? })
addBreadcrumb({ message, type?, data? })
setTags(tags) · setUser(user | null)
observeCapability(name, promise) => Promise
flush() => Promise<void>          // buffered events out now
close() => Promise<void>          // remove listeners + final flush

// Typed names for event.tags.signal in beforeSend policies (all built-ins):
BEACON_SIGNAL.FETCH_FAILED
BEACON_SIGNAL.SLOW_RESPONSE
BEACON_SIGNAL.HTTP_5XX
BEACON_SIGNAL.HTTP_RESPONSE_FAILURE
BEACON_TRACE_HEADER // "x-absolute-trace-id"

// Global helpers (no-op until initBeacon): captureException, captureMessage,
// addBreadcrumb, observeCapability

#Quick Start

Partial snippet

Initialize the global singleton once; auto-capture takes over, and the global helpers work anywhere after that.

TS
import { captureException, initBeacon } from '@absolutejs/beacon';

initBeacon({
	endpoint: 'https://api.example.com/ingest',
	environment: 'production',
	project: 'web',
	release: import.meta.env.VITE_RELEASE
});

// Uncaught errors + unhandled rejections are captured automatically.
// Manual capture anywhere:
try {
	await checkout();
} catch (e) {
	captureException(e, { tags: { component: 'billing' } });
}

#Instance API

Partial snippet

Hold an instance instead of the global when you need explicit lifecycle control.

TS
import { createBeacon } from '@absolutejs/beacon';

const beacon = createBeacon({ project: 'web' });
beacon.setUser({ id: currentUserId });
beacon.captureMessage('checkout started', 'info');

// Buffered events out now:
await beacon.flush();
// Remove listeners + final flush:
await beacon.close();
Bytes over machinery
Beacon is deliberately not Effect-native: an Effect client measures around 108 KB gzipped versus about 2 KB here. The browser is a dumb producer of telemetry; validation rigor lives server-side in @absolutejs/errors/ingest.

Current package surface

What ships today

@absolutejs/beaconv0.7.0-beta.15 · betaObservabilitynpmSource
6entry points

Import surface · click to copy