bugcatch-sdk
v0.7.11
Published
Official JavaScript/TypeScript SDK for BugCatch error tracking
Downloads
180
Maintainers
Readme
bugcatch-sdk
Official JavaScript/TypeScript SDK for BugCatch - lightweight error tracking for web and Node.js applications.
Features
- Automatic capture of uncaught errors and unhandled promise rejections
- Manual
captureExceptionandcaptureMessageAPIs - Structured logs -
captureLog(), batched delivery, optionalconsole.*auto-capture, trace-id correlation - Tracing -
startTransaction()/ spans, waterfall-ready, autohttp.clientspans onfetch/XHR, an Express/ConnecttracingMiddleware(), autodb.query/cache.commandspans viainstrumentTypeOrm()/instrumentIoredis() - Session Replay - opt-in
rrwebDOM recording, server-checked privacy allowlist, configurable text masking - Breadcrumb trail: clicks, navigation, console calls
- User context and custom tags
beforeSendhook to filter or modify events before sending- Works in the browser (modern bundlers + CDN) and Node.js
- Zero runtime dependencies - ~15 KB minified
Installation
npm install bugcatch-sdkQuick Start
import BugCatch from 'bugcatch-sdk';
BugCatch.init({
dsn: 'https://api.bugcatch.app/ingest/<projectId>?key=<sdkKey>',
release: '1.0.0',
environment: 'production',
});The DSN is available on your project page in the BugCatch dashboard.
From this point on, all uncaught errors and unhandled promise rejections are captured automatically.
Manual Captures
// Capture an Error object
try {
await processOrder(order);
} catch (err) {
BugCatch.captureException(err, { orderId: order.id });
}
// Capture a plain message
BugCatch.captureMessage('Quota limit reached', 'warning', { used: 95 });The second argument is "extra" - arbitrary context stored alongside the event - but three keys inside it are special-cased and hoisted to their real fields instead of staying buried under extra: user, tags, and request. This matters most on a concurrent Node server, where the global BugCatch.setUser() isn't safe per-request - passing user here per call (with ip_address, which the ingest API prioritizes over its own request-IP detection) is the reliable way to get per-request identity onto an event:
BugCatch.captureException(err, {
user: { id: req.user.id, ip_address: req.ip },
tags: { route: req.path },
request: { url: req.originalUrl, method: req.method, headers: { origin: req.headers.origin } },
orderId: order.id, // anything else stays under `extra`
});User Context
// After login
BugCatch.setUser({ id: '42', email: '[email protected]', username: 'jane' });
// After logout
BugCatch.clearUser();Tags
BugCatch.setTag('plan', 'pro');
BugCatch.setTag('region', 'eu-west-1');Structured Logs
BugCatch.captureLog('info', 'Checkout completed', { orderId: '4821', amountCents: 1999 });
BugCatch.captureLog('warn', 'Slow query detected', { durationMs: 1200 });
BugCatch.captureLog('error', 'Payment gateway timeout', { gateway: 'stripe', retries: 3 });Log lines are queued and batched, not sent one request per call. They flush
automatically every logsFlushInterval ms (default 2000) or once
logsBatchSize lines are queued (default 20), whichever comes first. Call
BugCatch.flushLogsNow() to force an immediate flush (e.g. before a
short-lived script exits). In the browser, any lines still queued when the
tab closes are sent via navigator.sendBeacon on pagehide.
Levels: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'.
Trace correlation
Give every log line from the same request/trace the same trace_id, so the
BugCatch dashboard can show them together:
// Per-call, safe under concurrency (e.g. inside Express middleware)
BugCatch.captureLog('info', 'Order created', { orderId }, req.headers['x-request-id']);
// Or set it once for everything that follows - simplest in a browser session,
// but a Node server handling concurrent requests should prefer the explicit
// per-call form above instead of this global setter.
BugCatch.setTraceId('req-abc-123');
BugCatch.captureLog('info', 'Step 1 complete');
BugCatch.captureLog('info', 'Step 2 complete');
BugCatch.clearTraceId();Auto-capturing console output
Set autoCaptureLogs: true to forward console.log/info/warn/error/debug/trace
to BugCatch as structured logs, in addition to whatever you send manually:
BugCatch.init({
dsn: '...',
autoCaptureLogs: true,
});
console.log('This now also shows up in the Logs tab');This is independent of autoCaptureBreadcrumbs (which records console.warn/
console.error as breadcrumbs attached to error events, not as searchable
log lines) - you can enable either, both, or neither.
Pino / Winston (Node.js)
There's no bundled transport package - wiring your existing logger into
BugCatch is a few lines using captureLog() directly, so you don't take on a
dependency you don't need.
Pino (legacy synchronous transport - a plain object with write()):
import pino from 'pino';
import BugCatch from 'bugcatch-sdk';
const pinoToBugCatch: Record<number, LogLevel> = {
10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal',
};
const logger = pino({}, {
write(msg: string) {
const { level, msg: message, ...attributes } = JSON.parse(msg);
BugCatch.captureLog(pinoToBugCatch[level] ?? 'info', message, attributes);
},
});Winston (a minimal custom transport - extends the built-in EventEmitter,
no winston-transport dependency needed for this level of integration):
import { EventEmitter } from 'node:events';
import winston from 'winston';
import BugCatch from 'bugcatch-sdk';
class BugCatchTransport extends EventEmitter {
log(info: { level: string; message: string; [key: string]: unknown }, callback: () => void) {
const { level, message, ...attributes } = info;
const mapped = level === 'warning' ? 'warn' : (level as LogLevel);
BugCatch.captureLog(mapped, message, attributes);
callback();
}
}
const logger = winston.createLogger({
transports: [new winston.transports.Console(), new BugCatchTransport()],
});Tracing
Traces are transactions with nested spans - the same shape you'd see as a waterfall in the BugCatch dashboard.
const txn = BugCatch.startTransaction('GET /api/orders/:id', 'http.server');
const dbSpan = txn.startChild('db.query', 'SELECT * FROM orders WHERE id = ?');
const rows = await db.query(sql);
dbSpan.finish();
txn.setHttpStatus(200);
txn.finish(); // queued for delivery - batched, not one request per transactionSpans nest arbitrarily via span.startChild(...). Nothing is sent until
.finish() is called on the transaction - an unfinished transaction is
simply never reported, not a leak.
Express / Connect middleware
import BugCatch from 'bugcatch-sdk';
app.use(BugCatch.tracingMiddleware());
app.get('/api/orders/:id', async (req, res) => {
// Concurrency-safe: this request's own transaction, not global state
const span = req.bugcatchTransaction.startChild('db.query', 'SELECT ...');
const order = await db.query(sql);
span.finish();
res.json(order);
});tracingMiddleware() starts an http.server transaction per request and
finishes it when the response ends. It attaches the transaction to
req.bugcatchTransaction - use that (not global state) in a server handling
concurrent requests.
Auto-instrumented HTTP client spans
If autoTrackRequests: true and a transaction is set as "current" (either by
tracingMiddleware(), or manually via BugCatch.setCurrentTransaction(txn)),
every intercepted fetch/XMLHttpRequest call automatically gets an
http.client child span - no extra code at the call site.
BugCatch.init({ dsn: '...', autoTrackRequests: true });
const txn = BugCatch.startTransaction('checkout flow', 'custom');
BugCatch.setCurrentTransaction(txn);
await fetch('/api/payment'); // auto-recorded as a child span
txn.finish();
setCurrentTransactionis global mutable state - fine for a browser session or a script processing one thing at a time, but a Node server handling concurrent requests should preferreq.bugcatchTransaction(see the middleware above) over relying on this.
Auto-instrumented database / cache spans
instrumentTypeOrm() and instrumentIoredis() patch a single choke point
each. TypeORM's QueryRunner export is an interface, not a class - there's
no .prototype to patch directly, and the concrete class is driver-specific
and unexported - so instrumentTypeOrm() takes the DataSource instance
instead, and discovers the shared query-runner prototype itself. ioredis is
simpler: Redis.prototype.sendCommand() is a real class every command
(including each call inside a pipeline) funnels through, so
instrumentIoredis() takes the class, not an instance. Call both once at
startup, after the DataSource has been initialized:
import BugCatch from 'bugcatch-sdk';
import Redis from 'ioredis';
BugCatch.instrumentTypeOrm(dataSource); // an initialized TypeORM DataSource
BugCatch.instrumentIoredis(Redis);Every query/command run afterward becomes a db.query/cache.command child
span on currentTransaction - no per-call-site code needed. Both are
duck-typed against { createQueryRunner } / { prototype: { sendCommand } },
so neither typeorm nor ioredis is a dependency of this package. Each
returns an unpatch function if you need to undo it.
Same global-mutable-state caveat as
setCurrentTransactionabove - fine for most Node processes, but a server handling concurrent requests should still set the transaction as "current" per-request (e.g. insidetracingMiddleware()or your own request-scoped setup) rather than once globally at startup.
If you'd rather not patch a class at all, wrap the call yourself the same way as before:
const span = req.bugcatchTransaction?.startChild('db.query', 'orders.findOne');
try {
return await orderRepository.findOne({ where: { id } });
} finally {
span?.finish();
}Session Replay
DOM recording via rrweb, for watching what a real user did in the dashboard's replay player. Browser-only, privacy-sensitive - unlike sessions/RUM, off by default; a project opts in deliberately.
BugCatch.init({
dsn: '...',
sessionReplay: true, // start recording immediately on init()
});Or start/stop it manually - e.g. only after a user opts in, or only once you know who they are:
BugCatch.setUser({ id: 'user_123' });
BugCatch.startReplayNow();
// ...
BugCatch.stopReplayNow();Every captureException() call while a recording is active is automatically
tagged with the recording's id, so the dashboard can jump straight from an error
to "what led to this."
Privacy allowlist
A project can restrict recording to specific user.id values (BugCatch dashboard
→ project Settings → Session Replay). When configured, startReplayNow() -
and the sessionReplay: true init option - check the allowlist with the server
before rrweb.record() ever starts, so a user who isn't listed is never
recorded at all, not just dropped after the fact. The check fails closed: a
network error is treated as "not allowed." No allowlist configured means
everyone is recorded (the pre-existing default).
// Decide yourself before attempting to start, e.g. to show a "recording"
// indicator only when it will actually happen:
if (await BugCatch.isReplayAllowed()) {
BugCatch.startReplayNow();
}setUser() must run before the allowlist check has a user id to look up - that's
why sessionReplay: true at init() time only actually starts recording once a
user is set; call startReplayNow() explicitly after setUser() if you need
recording to begin as soon as the user is known.
Masking
maskAllInputs is always on - form inputs are masked unconditionally. It does
not cover rendered text: a table of salaries, an SSN shown as plain text,
etc. are recorded as-is unless matched by replayMaskTextSelector:
BugCatch.init({
dsn: '...',
sessionReplay: true,
replayMaskTextSelector: '[data-sensitive], .salary-cell',
});Passed straight through to rrweb's own maskTextSelector option, so its
selector syntax and matching rules apply here too.
Options
BugCatch.init({
// Required
dsn: string;
// Optional
release?: string; // App version e.g. "1.2.3"
environment?: string; // "production" | "staging" | ...
debug?: boolean; // Print SDK logs to console (default: false)
maxBreadcrumbs?: number; // Max breadcrumbs kept in memory (default: 100)
autoCaptureErrors?: boolean; // Auto-attach global error handlers (default: true)
autoCaptureBreadcrumbs?: boolean; // Auto-capture clicks, nav, console (default: true)
autoCaptureLogs?: boolean; // Forward console.* calls as structured logs (default: false)
logsFlushInterval?: number; // Log batch flush interval, ms (default: 2000)
logsBatchSize?: number; // Flush immediately at this queue size (default: 20)
tracesFlushInterval?: number; // Transaction batch flush interval, ms (default: 2000)
tracesBatchSize?: number; // Flush immediately at this queue size (default: 10)
autoTrackRequests?: boolean; // Auto-track fetch/XHR (metrics + http.client spans) (default: false)
sessionReplay?: boolean; // Record DOM via rrweb for the dashboard's replay player (default: false)
replayFlushInterval?: number; // Replay chunk flush interval, ms (default: 10000)
replayRequestTimeout?: number; // Abort a stuck replay chunk upload after this many ms (default: 15000)
replayMaskTextSelector?: string; // CSS selector for elements whose rendered text is masked in replay
// Drop errors whose message matches any of these
ignoreErrors?: Array<string | RegExp>;
// Drop errors originating from matching script URLs
ignoreUrls?: Array<string | RegExp>;
// Modify or drop an event before it is sent. Return false to discard.
beforeSend?: (event: EventPayload) => EventPayload | false;
});Server Metrics (Node.js)
BugCatchServerReporter periodically reports process memory, CPU and event loop lag to the BugCatch dashboard. Framework-agnostic - wire it into any lifecycle hook.
import { BugCatchServerReporter } from 'bugcatch-sdk';
const reporter = new BugCatchServerReporter({
dsn: process.env.BUGCATCH_DSN!,
});
reporter.start();
// reporter.stop() on shutdownNestJS:
@Injectable()
export class BugCatchMetricsService implements OnModuleInit, OnModuleDestroy {
private readonly reporter = new BugCatchServerReporter({
dsn: process.env.BUGCATCH_DSN!,
});
onModuleInit() { this.reporter.start(); }
onModuleDestroy() { this.reporter.stop(); }
}Options:
new BugCatchServerReporter({
dsn: string; // Required - same DSN used for error capture
reportInterval?: number; // How often to send a snapshot, in ms (default: 30_000)
// Identifies which server/instance a snapshot came from - shown on the
// dashboard and in threshold-alert emails. Useful when a project runs
// multiple instances/pods behind a load balancer.
instanceId?: string; // (default: os.hostname())
debug?: boolean; // Print SDK logs to console (default: false)
});Framework Examples
React
// src/main.tsx
import BugCatch from 'bugcatch-sdk';
BugCatch.init({
dsn: import.meta.env.VITE_BUGCATCH_DSN,
release: import.meta.env.VITE_APP_VERSION,
environment: import.meta.env.MODE,
});Error boundary:
class ErrorBoundary extends React.Component {
componentDidCatch(error: Error) {
BugCatch.captureException(error);
}
render() {
return this.props.children;
}
}Vue
// src/main.ts
import BugCatch from 'bugcatch-sdk';
BugCatch.init({ dsn: import.meta.env.VITE_BUGCATCH_DSN });
app.config.errorHandler = (err) => {
BugCatch.captureException(err);
};Node.js / Express
import BugCatch from 'bugcatch-sdk';
BugCatch.init({
dsn: process.env.BUGCATCH_DSN!,
release: process.env.npm_package_version,
environment: process.env.NODE_ENV,
autoCaptureBreadcrumbs: false, // no DOM in Node.js
});
// Error middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
BugCatch.captureException(err, { path: req.path, method: req.method });
next(err);
});CommonJS
const { BugCatch } = require('bugcatch-sdk');
BugCatch.init({ dsn: process.env.BUGCATCH_DSN });Breadcrumbs
Breadcrumbs are short records of what happened before an error. The SDK captures them automatically when autoCaptureBreadcrumbs: true (default):
| Source | Category | What is recorded |
|--------|----------|-----------------|
| DOM clicks | ui.click | Element tag, text, id/class |
| History navigation | navigation | URL navigated to |
| console.warn / console.error | console | Message text |
Add breadcrumbs manually:
BugCatch.addBreadcrumb({
timestamp: new Date().toISOString(),
type: 'user',
category: 'auth',
message: 'User logged in',
data: { method: 'google-oauth' },
});beforeSend
Use beforeSend to scrub sensitive data or drop specific events:
BugCatch.init({
dsn: '...',
beforeSend(event) {
// Drop network errors
if (event.exception?.values?.[0]?.type === 'NetworkError') return false;
// Scrub email from user context
if (event.user) delete event.user.email;
return event;
},
});SPA / Hot-Reload Cleanup
BugCatch.destroy(); // removes all listeners, flushes queued logs and transactions, resets singletonLicense
ISC
