SDK Usage & Event Enrichment
BugWatch - SDK Usage & Event Enrichmentlink
The other BugWatch folders document the wire API - the exact JSON each ingest endpoint accepts. This folder documents how your application produces that JSON through the BugWatch SDKs: identifying the user, adding tags/context/release, correlating traces, sampling, redaction, and grouping.
Examples use @newinstance/bugwatch (JS/TS - the reference SDK) and newinstance/bugwatch (PHP). The same concepts apply to the mobile SDKs (iOS, Android, React Native, Flutter), which expose a global setUser.
The scope model - global, per-request, per-capturelink
Enrichment lives on a scope, and there are three places to set it:
| Scope | Lifetime | Use it for |
|---|---|---|
| Global | Process-wide, until changed | Single-identity processes - workers, CLIs, a signed-in mobile/desktop user |
| Per-request | One HTTP request; auto-cleared at the end | Concurrent servers - bind the authenticated user to this request only |
| Per-capture | One event | A one-off override on a single captureException / captureLog |
What reaches the wire: the SDK merges these before sending - the per-capture hint wins over the per-request scope, which wins over the global scope. Each event carries exactly one merged
userobject (id,username,ip). There is no separate "per-request user" field on the ingest endpoint; per-request isolation is purely an SDK-side concern.
User identity (global)link
Attach a user to all subsequent events. Pass null to clear (e.g. on logout):
// @newinstance/bugwatch
BugWatch.setUser({ id: "u_123", email: "alice@acme.com", username: "alice" });
// ip is auto-populated by the ingest server if omitted
BugWatch.setUser(null); // on logout// newinstance/bugwatch
BugWatch::setUser(['id' => 'u_123', 'email' => 'alice@acme.com', 'username' => 'alice', 'ip' => $request->ip()]);
BugWatch::setUser(null); // on logoutOnly id, email, username, ip are kept - any other key is dropped before the event leaves your process.
Concurrency warning:
setUsersets a single process-wide user. On a server handling concurrent requests it can attribute one request's events to another user. Use the per-request scope below instead.
Per-request user (concurrency-safe)link
A Node server handles many users on one event loop; a long-running PHP worker services many requests in sequence or as coroutines. A shared "current user" set per request races - whichever request wrote last wins.
The SDK solves this with a per-request scope that is isolated from every other in-flight request and auto-clears when the request ends. Set the user from your auth middleware; any captureException / captureLog during that request is tagged with it.
JS / Node - setRequestUser (plus setRequestTag, setRequestContext) from @newinstance/bugwatch/node, backed by AsyncLocalStorage. Works with the Express, Koa, Fastify and Nest adapters:
import { createClient } from "@newinstance/bugwatch";
import { bugWatchExpressRequestHandler, bugWatchExpressErrorHandler } from "@newinstance/bugwatch/express";
import { setRequestUser, setRequestTag } from "@newinstance/bugwatch/node";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY });
app.use(bugWatchExpressRequestHandler(client)); // 1. open a per-request scope (first)
app.use((req, _res, next) => { // 2. auth middleware - writes THIS request only
if (req.user) setRequestUser({ id: req.user.id, email: req.user.email });
next();
});
app.use(bugWatchExpressErrorHandler(client)); // 3. error handler (last) - user + tags attached
// Concurrent request B has its own scope - its identity is fully isolated.PHP - register BugWatchContextMiddleware (Laravel). It reads $request->user(), scopes it to the current request, and calls flush() + resetScope() on termination:
// app/Http/Kernel.php (Laravel 10/11) or bootstrap/app.php (Laravel 11 middleware() style)
\NewInstance\BugWatch\Laravel\BugWatchContextMiddleware::class,Isolation is automatic per runtime: PHP-FPM (one process per request) needs nothing; Octane/RoadRunner reset between requests via the middleware; Swoole/OpenSwoole store scope in per-coroutine context; queue workers and Artisan commands reset on job/command boundaries.
The request scope wins over the global scope on any overlapping field and clears automatically at request end, so the next request starts clean. Edge runtimes (Hono, Next.js) have no
AsyncLocalStorage- pass the user explicitly per capture instead (below).
Per-capture and withScopelink
Attach identity/tags to one event without touching shared scope - works on any adapter or runtime:
BugWatch.captureException(err, { user: userId ? { id: userId } : undefined, tags: { invoiceId, route: "/api/invoices" } });BugWatch::captureException($e, ['user' => ['id' => $userId], 'tags' => ['route' => $routeName, 'tenant' => $tenantId]]);withScope opens a temporary cloned scope; mutations inside are discarded when it returns:
BugWatch.withScope((scope) => {
scope.setTag("orderId", order.id);
scope.setUser({ id: user.id });
BugWatch.captureException(err); // sees the scoped tag + user
});Tags, context and releaselink
BugWatch.setTag("region", "eu-west-1"); // indexed, searchable
BugWatch.setContext("payment", { provider: "paystack", currency: "NGN" }); // not indexed; on detail view
BugWatch.setRelease("checkout@2.4.1"); // or the `release` init optionBugWatch::setTag('region', 'eu-west-1');
BugWatch::setTags(['tenant' => 't_42', 'version' => '2.4.1']);
BugWatch::setContext('payment', ['provider' => 'paystack', 'amount_ngn' => 5000]);
BugWatch::setRelease('checkout@2.4.1');- Tags - scalar values only, max 50 per event, indexed for filtering. Wire:
tags{}. - Context - arbitrary structured data, not indexed; stored alongside the event. Wire:
context{}(passthrough). - Release - build/version label (version string or git SHA). Wire:
release. Environment is bound to the API key, not sent by the SDK.
Event ID and deduplicationlink
captureException returns an event ID. The server deduplicates events that repeat the same eventId within a 10-minute window - a duplicate increments deduped instead of ingested:
const eventId = BugWatch.captureException(err);Wire: eventId (≤200 chars).
Trace correlation (OpenTelemetry)link
If your app uses OpenTelemetry, the ./otel subpath injects the active span's traceId / spanId into every event automatically:
import { otelTraceContextProvider } from "@newinstance/bugwatch/otel";
BugWatch.init({ projectKey: process.env.BUGWATCH_KEY, traceContextProvider: otelTraceContextProvider() });Or set it manually - per event or globally:
BugWatch.captureException(err, { traceId: "abc123", spanId: "def456" });
BugWatch.setTraceContext(currentTraceId, currentSpanId); // setTraceContext(null, null) to clearWire: traceId (hex, ≤32 chars) and spanId (hex, ≤16 chars) - non-hex characters are stripped server-side.
Samplinglink
Send only a fraction of events (useful for high-volume info/debug logs):
BugWatch.init({ projectKey: process.env.BUGWATCH_KEY, sampleRate: 0.25 }); // 25%; default 1 (100%)Sampling is SDK-side - sampled-out events never reach the wire. For level-based sampling (keep all errors, sample info) use beforeSend.
Redaction and beforeSendlink
The SDK redacts sensitive values before they leave your process (the server redacts again as defence-in-depth). Default keys include password, token, authorization, cookie, secret, apikey, creditcard, cvv, ssn, bvn, nin (case-insensitive). Add your own and/or drop/mutate events:
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY,
sensitiveFields: ["accountNumber", "iban"], // merged with the defaults
beforeSend(event) {
if (event.tags?.url?.includes("/health")) return null; // drop the event
if (event.user) event.user = { id: event.user.id }; // strip email/username
return event;
},
});Both are SDK-side - they shape or suppress the payload before it is sent.
Fingerprinting (grouping)link
Override how events are grouped into issues. PHP exposes setFingerprint; JS accepts a per-capture fingerprint hint:
BugWatch::setFingerprint('payment-gateway-timeout'); // one issue for all
BugWatch::setFingerprint(['checkout', 'GATEWAY_TIMEOUT']); // group by component + codeWire: fingerprint (passthrough grouping hint).
Browser apps - never ship the secretlink
Browser code is public. Never put your project key/secret in client-side code. Your backend mints a short-lived session token; the browser SDK uses that instead:
// Backend (Express) - expose a mint endpoint:
import { bugWatchBrowserSessionHandler } from "@newinstance/bugwatch/express";
app.get("/bugwatch/session", bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY }));
// Browser - point at YOUR endpoint, no projectKey:
import { createClient } from "@newinstance/bugwatch";
import { installBrowserErrorHandlers } from "@newinstance/bugwatch/browser";
const client = createClient({ sessionUrl: "/bugwatch/session" });
installBrowserErrorHandlers(client);The mint endpoint calls POST /api/v1/bugwatch/browser-session (see Server Ingest API); browser events post to /api/v1/bugwatch/ingest/browser with x-bugwatch-session (see Browser Ingest).
What lands on the wirelink
| SDK feature | On the ingest event? | Event field |
|---|---|---|
setUser / setRequestUser / per-capture user | Yes - one merged object | user{id,email,username,ip} |
setTag / per-capture tags | Yes | tags{} (≤50, scalar) |
setContext | Yes | context{} (passthrough) |
setRelease / release | Yes | release (≤200) |
| returned event id | Yes | eventId (≤200, dedup) |
OTel / setTraceContext | Yes | traceId (≤32 hex), spanId (≤16 hex) |
setFingerprint | Yes | fingerprint (passthrough) |
| Sampling | No - sampled out before send | - |
Redaction / beforeSend | Shapes/suppresses the payload | - |
| Per-request vs global | No - resolved to one user before send | - |
For the full per-adapter examples (Koa, Fastify, Hono, Nest, Next.js) and every option, see the SDK READMEs: @newinstance/bugwatch (JS/TS) and newinstance/bugwatch (PHP).