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:

ScopeLifetimeUse it for
GlobalProcess-wide, until changedSingle-identity processes - workers, CLIs, a signed-in mobile/desktop user
Per-requestOne HTTP request; auto-cleared at the endConcurrent servers - bind the authenticated user to this request only
Per-captureOne eventA 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 user object (id, email, 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 logout

Only id, email, username, ip are kept - any other key is dropped before the event leaves your process.

Concurrency warning: setUser sets 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 option
BugWatch::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 clear

Wire: 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 + code

Wire: 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 featureOn the ingest event?Event field
setUser / setRequestUser / per-capture userYes - one merged objectuser{id,email,username,ip}
setTag / per-capture tagsYestags{} (≤50, scalar)
setContextYescontext{} (passthrough)
setRelease / releaseYesrelease (≤200)
returned event idYeseventId (≤200, dedup)
OTel / setTraceContextYestraceId (≤32 hex), spanId (≤16 hex)
setFingerprintYesfingerprint (passthrough)
SamplingNo - sampled out before send-
Redaction / beforeSendShapes/suppresses the payload-
Per-request vs globalNo - 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).