@loglab/telemetry
v0.2.1
Published
Lightweight, dependency-free browser telemetry SDK with console, network and error capture plus an embeddable feedback widget.
Maintainers
Readme
@loglab/telemetry
Browser telemetry SDK with a built-in feedback widget. Captures console output,
fetch/XMLHttpRequest activity and unhandled errors in the background, keeps a
rolling ten-minute window of them in memory, and attaches that window to every
bug report the widget sends.
No dependencies. React bindings ship in the same package and are optional.
The published runtime bundles are minified, mangled and shipped without source maps. Type declarations remain available so the public API stays type-safe. This is packaging hardening, not encryption: browser code can always be inspected by a determined user.
Install
pnpm add @loglab/telemetryQuick start
import { Telemetry } from "@loglab/telemetry";
Telemetry.init({
apiKey: "demo",
endpoint: "/api/events",
widget: true,
});That is the whole integration. From this point on:
- console calls, network requests and unhandled errors are recorded and batched
to
endpoint; - a discreet launcher is pinned to the side of the viewport;
- a bug report sent through the widget carries the diagnostics window with it.
Custom events and programmatic opening:
Telemetry.capture("checkout_started", { plan: "pro" });
Telemetry.feedback.open(); // type picker
Telemetry.feedback.open("bug"); // straight to the bug formTelemetry is a shared per-page client. For isolated instances (tests, multiple
roots) use createTelemetry().
Configuration
| Option | Default | Purpose |
| -------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------- |
| apiKey | — | Sent in the batch envelope and as x-api-key. |
| endpoint | — | Where batches are posted. Never instrumented itself. |
| environment, release | — | Passed through in context. |
| widget | false | true for defaults, or WidgetOptions. |
| console | true | false to disable, or { levels, maxArgs, maxArgLength }. Default levels: log, info, warn, error. |
| network | true | false to disable, or { fetch, xhr, ignoreUrls, captureHeaders }. |
| errors | true | error and unhandledrejection listeners. |
| privacy | see below | { redactKeys, redactPatterns, stripQuery, maxDepth } (maxDepth defaults to 4). |
| limits | 10 min window | { maxLogs, maxErrors, maxRequests, maxBytesPerBuffer, windowMs }. |
| sampleRate | 1 | Ratio of non-feedback events forwarded. Feedback and errors are never sampled. |
| flushInterval | 5000 | Idle delay before an accumulated batch is sent (minimum 250). |
| maxBatchSize | 25 | Reaching it flushes immediately. |
| maxQueuedBatches, maxRetries | 10, 3 | Backpressure and retry bounds. |
| flushOnHidden | true | Flush on pagehide/visibilitychange, via sendBeacon when possible. |
| user | — | Arbitrary identity object added to context. |
| beforeSend | — | Last chance to mutate or drop a batch (null drops it). |
| transport | HTTP | Replace delivery entirely. |
| debug | false | Log SDK internals through the pristine console. |
Widget appearance
The popup and launcher can be branded without replacing the feedback flow:
Telemetry.init({
apiKey: "demo",
endpoint: "/api/events",
widget: {
side: "right",
theme: {
accentColor: "#6d5dfc",
surfaceColor: "#18181b",
cornerStyle: "round",
panelWidth: "comfortable",
shadow: "soft",
},
copy: {
launcher: "Tell us what you think",
title: "How can we improve?",
},
},
});Colors use six-digit hex values. cornerStyle accepts square, soft or
round; panelWidth accepts compact, comfortable or wide; shadow
accepts none, soft or strong.
Public API
Telemetry.init(config) // idempotent, no-op without a DOM
Telemetry.capture(name, properties?) // application event
Telemetry.captureException(error, options?) // handled error
Telemetry.getDiagnostics() // { logs, errors, requests }
Telemetry.bufferUsage() // counts + approximate bytes
Telemetry.subscribe(observer) // returns an unsubscribe function
Telemetry.buildBugReport(message, email?) // payload without sending it
Telemetry.feedback.open(type?) / close() / isOpen() / submit(submission)
Telemetry.flush() // resolves once queued batches drain
Telemetry.shutdown() // restores every patched globalReact
The React entry point has no 'use client' directive of its own — put it on your
own component so the rest of the package stays importable from server code.
"use client";
import {
TelemetryProvider,
FeedbackPanel,
useDiagnostics,
useTelemetry,
} from "@loglab/telemetry/react";
export function Providers({ children }) {
return (
<TelemetryProvider
config={{ apiKey: "demo", endpoint: "/api/events", widget: true }}
>
{children}
</TelemetryProvider>
);
}useTelemetry()— the client from context, or the shared one.useDiagnostics(throttleMs?)— live view of the buffers, coalesced renders.FeedbackPanel— the widget's own card rendered inline. It mounts the same factory the modal uses, so an embedded panel and the launcher can never drift.
What reaches your endpoint
One POST per batch, content-type: application/json:
{
"sdk": { "name": "@loglab/telemetry", "version": "0.2.1" },
"apiKey": "demo",
"sentAt": 1756000512345,
"session": { "id": "…", "startedAt": 1756000500000 },
"context": {
"page": { "url": "…", "path": "/schedule" },
"browser": { "…": "…" },
},
"events": [
{
"type": "console",
"level": "warn",
"args": ["slot 09:30 is full"],
"timestamp": 1756000512000,
},
{
"type": "request",
"method": "DELETE",
"url": "…",
"status": 500,
"duration": 412,
"success": false,
"kind": "xmlhttprequest",
"timestamp": 1756000512100,
},
{
"type": "error",
"message": "…",
"kind": "exception",
"handled": false,
"timestamp": 1756000512200,
},
],
}A bug report is a single self-contained event:
{
"type": "bug_report",
"message": "Saving the appointment does nothing",
"timestamp": 1756000512345,
"page": { "url": "…", "path": "/schedule" },
"browser": {
"userAgent": "…",
"language": "en-US",
"viewport": { "width": 1512, "height": 860 },
},
"logs": [],
"errors": [],
"requests": [],
}Ideas and comments send { "type": "feedback", "feedbackType": "idea" | "comment", … } without the diagnostics window.
Feedback bypasses the queue so the form learns whether delivery succeeded and can keep the user's text on failure.
Privacy
Defaults, enforced at capture time — before anything is buffered, let alone sent:
- object keys, query parameters and header names are normalized (lowercased,
separators stripped) and redacted when they contain any fragment from
DEFAULT_SENSITIVE_KEYS—authorization,cookie,password,passwd,passphrase,secret,token,apikey,accesskey,privatekey,credential,sessionid,creditcard,cardnumber,cvv,ssn,otp,jwt. SoX-API-Key,api_key,accessTokenandrefresh_tokenare all replaced with[REDACTED]; - token-shaped values (bearer tokens, JWTs, long hex/base64 blobs) are scrubbed out of strings and stack traces;
document.cookieis never read, input values are never captured, request and response bodies are never captured;- headers are only captured with
network.captureHeaders: true, and still pass through redaction.
Add your own rules with privacy.redactKeys and privacy.redactPatterns, or drop
query strings entirely with privacy.stripQuery: true.
Behaviour guarantees
- Patched globals delegate to the originals: return values, thrown errors, rejected promises, headers and bodies are untouched, and DevTools output is unchanged. Capture failures are swallowed, never surfaced to your code.
- Instrumentation is installed once per page and is reversible via
shutdown(). - Buffers are bounded by record count and by approximate byte size, then pruned
lazily to
windowMs; nothing polls while the page is idle. - Circular structures, getters that throw and huge payloads are all safe to log.
Development
pnpm build # tsup: ESM + CJS + .d.ts
pnpm test # vitest (jsdom)
pnpm typecheck