Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@unkcode/sentinel

Privacy-safe browser telemetry for Sentinel, implemented as a thin TypeScript wrapper around the official OpenTelemetry JavaScript SDK.

import { Sentinel } from "@unkcode/sentinel";

const sentinel = Sentinel.init({
  endpoint: "https://ingest.example.com",
  publicKey: "sip_pub_0000000000000000000000000000000000000000000",
  serviceName: "gofip-frontend",
  release: "1.0.0",
  rum: { enabled: true },
});

sentinel.info("checkout loaded", { cartSize: 2 });
await sentinel.startActiveSpan("checkout.submit", async span => {
  span.setAttribute("checkout.method", "card");
});

Sentinel configures official trace, log, and metric providers, batch processors/readers, OTLP/HTTP exporters, W3C Trace Context, Zone context, and fetch instrumentation. The public credential is sent only as an Authorization: Bearer exporter header.

Configuration

  • endpoint, publicKey, and serviceName are required.
  • release, environment, and tracesSampleRate set resource/sampling data.
  • tracePropagationTargets allows additional cross-origin W3C propagation. Official fetch instrumentation always propagates to same-origin requests.
  • instrumentFetch and captureErrors default to true.
  • beforeSend synchronously rewrites or drops Sentinel-owned drafts after initial sanitization; its output is sanitized again.
  • diagnostics defaults to false; enabled diagnostics are content-free and rate-limited.
  • rum.enabled defaults to false; enabling it captures semantic browser RUM.
  • replay.enabled defaults to false. Replay also requires rum.enabled and a fresh enabled policy from Ingest for the exact public credential and Origin.

Session replay recorder

Replay is opt-in with rum: { enabled: true }, replay: { enabled: true }. The SDK reads <endpoint>/v1/rum/replay/policy with the public credential and records only while its enabled v1 policy is fresh. The first semantic JavaScript error, network error, rage click, or dead click promotes a bounded in-memory rrweb checkpoint buffer. Chunks go only to <endpoint>/v1/rum/replay/chunks; a 202 response confirms persistence. There is no continuous replay upload.

Add data-sentinel-private to the root of every sensitive subtree before recording begins. Its contents are blocked, including descendants and later mutations. The recorder retains visible page text, masks form values and strips source attributes. Use data-sentinel-mask or an explicit maskTextSelector for sensitive page text; data-sentinel-private still blocks entire subtrees. It retains a closed set of resolved layout properties (sizes, spacing, flex/grid layout, typography and colors) as generated inline styles, including updates when an element's attributes change. Source CSS, selectors, classes, resource URLs, generated text, images, custom fonts, frames, canvas, media, scripts, unsafe DOM surfaces, plugins, cookies and browser storage values remain excluded. This restores layout and text, not a pixel-perfect copy. Stylesheet-only changes are reflected at the next checkpoint. Existing recordings without layout cannot recover it. The original request and replay limits still apply, so sufficiently large snapshots stop capture locally. Mark dynamic private roots before attaching them to the document. The replay viewer itself must carry this marker before recorder rollout. Changing this marker on an existing element stops replay and discards local buffers; it cannot retract chunks that Ingest has already accepted.

Visual capture

Enable CSS and public resources explicitly:

replay: {
  enabled: true,
  visual: {
    enabled: true,
    resourceOrigins: ["https://your-store.example", "https://your-cdn.example"],
  },
  // Optional: mask additional sensitive page text.
  maskTextSelector: "[data-sentinel-mask]",
}

Visual mode retains class/id and a closed set of visual attributes, sanitized source CSS, SVG shapes, empty form controls, mouse/scroll/resize events and stylesheet/attribute changes. Relative resources are resolved against their original stylesheet/document base. CSS generated text, scripts, private roots, form values, media, frames and shadow DOM stay excluded. Public CSS and class/id values must not contain private application data. Visible page text is captured by default, including text added/updated during scroll. Values in input, textarea, select/option and contenteditable surfaces remain excluded regardless of public selectors. data-sentinel-private still blocks an entire subtree. maskTextSelector defaults to [data-sentinel-mask]; set it to "*" to restore full page-text masking. unmaskTextSelector can reveal selected non-control text. Masked text uses a constant "*", independent of its original length.

The isolated player separately needs the matching REPLAY_RESOURCE_ORIGINS list. Origins are exact (no wildcards); only configured public HTTP loopback/HTTPS resources are admitted at capture, and production playback accepts HTTPS only. Credential-bearing and signed/token query URLs are excluded; common image resize/version and public font query parameters are accepted. Image-optimizer url/src query references must recursively satisfy the same allowlist. CORS must permit anonymous image, stylesheet and font loads. No resources are stored in Sentinel: changing/removing them can change old playback.

Source CSS supersedes computed styles in visual mode; computed layout is the fallback when the browser cannot read a stylesheet. Constructed/adopted stylesheets are not supported yet. Chunks are gzip-compressed once before upload when CompressionStream is available, and retries reuse the same bytes. Both wire and decoded server policy limits still apply; identity fallback is bounded by the wire cap. CSS capture and changed text policy need new recordings and an updated consumed SDK release. Previously removed text/images cannot be restored from old chunks. If playback has only structure/colors, replay: { enabled: true } alone is layout mode: enable visual.enabled and configure both SDK and player resource origins to include the recorded site and its image/font/CDN hosts.

An unpromoted prebuffer is discarded on hard navigation. A promoted replay with at least one acknowledged chunk can continue after a reload in the same tab and Sentinel session: sessionStorage contains only bounded replay identity/count metadata, never rrweb events, snapshots or chunks. Each new document obtains a fresh policy and page ID with its own FullSnapshot. A navigation before the first valid 202 cannot continue that replay: the next document starts a fresh buffer and needs a new semantic trigger. Page exit cannot guarantee delivery of in-flight chunks. Later pages count toward continuation only after their own first chunk is acknowledged. The SDK stops replay independently when policy, privacy, upload or size limits fail; semantic RUM and OpenTelemetry remain active. sentinel.replayStatus() exposes only a fixed local state, with no replay or session identifiers. With RUM-8, a 429 quota_exceeded stops only Replay with state quota-exceeded and is not retried. Operational 429 responses still use the existing bounded retry policy. Daily quotas share usage across all credentials and Ingest replicas in the same tenant/application/environment.

Semantic browser RUM

Opt in with rum: { enabled: true }. The browser sends page_view, click, rage_click, dead_click, scroll_depth, javascript_error, and network_error events as schema v1 JSON to <endpoint>/v1/rum/events, using the same public key in an Authorization header. It complements the existing OpenTelemetry traces, logs, and metrics; it does not use OTLP.

The SDK creates a random tab-scoped session ID in sessionStorage, reuses it across reloads, and rotates it after 30 minutes of inactivity. If storage is blocked, it continues with an in-memory session. Events are batched at 20 or every 5 seconds in a 200-event memory queue; overflow drops the oldest event. Transient failures get one retry. Page lifecycle delivery uses authorized fetch with keepalive and is best effort. A successful Ingest response in RUM-2A confirms admission, not durable storage.

rage_click represents at least three rapid, nearby interaction attempts on one stable target. An attempt can be a native click or a short, completed pointer press without a click (for example, on a disabled button). A press that produces a click counts once. The serialized click_count field is a legacy name for the number of attempts. click and dead_click retain their existing browser-click behavior. Historical rage events were detected from clicks only, so detection coverage changes with this SDK version.

RUM records pathname-only routes, normalized interaction positions, bounded semantic target fields (tag, role, approved data-testid), and safe event metadata. For small interactive controls, it also automatically captures the control's own safe aria-label as target.label, or bounded visible control text as target.text when no safe label exists. Each human-readable field is at most 80 UTF-8 bytes. Add data-sentinel-private to a control or ancestor to omit these human-readable fields within the eight-element ancestor lookup; click-like events can still carry safe tag, role, and test ID metadata. Input, textarea, select, password, and contenteditable values or contents are never captured as target text. Rage targets on non-interactive elements carry only safe semantic identifiers; the SDK does not read arbitrary page text from them. It never reads form values, arbitrary page text, HTML, cookies, request bodies, headers, or storage contents other than its own session metadata. JavaScript error messages are generic to avoid leaking application data. No user identity or fingerprinting is included in semantic events. Replay snapshots are a separate, explicitly enabled signal. Ingest derives tenant, application, and environment scope from the public credential.

javascript_error comes from automatic window.error and unhandledrejection observation. Calling captureException() explicitly continues to emit OTel telemetry without claiming a browser RUM error.

Trace and span IDs are attached only when a valid OpenTelemetry context is active at event creation. Ordinary DOM listeners may run in their registration context, so interaction correlation is best effort.

Fetch pathnames are retained while queries, fragments, user information, sensitive headers, and bodies are excluded. Sentinel owns the browser global OpenTelemetry setup in 0.1.x and fails deterministically if incompatible global tracing or existing fetch wrapping is detected.

Optional entrypoints

@unkcode/sentinel/react exports SentinelErrorBoundary. React is an optional peer dependency. @unkcode/sentinel/web-vitals exports instrumentWebVitals, which records supported Web Vitals through an existing Sentinel instance.

Call flush() for an explicit best-effort export and shutdown() to disable Sentinel-owned listeners, instrumentation, providers, and globals.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages