# SDK Usage & Event Enrichment

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-capture

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 `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)

Attach a user to **all subsequent events**. Pass `null` to clear (e.g. on logout):

```ts
// @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
```

```php
// 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)

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, Hapi, Nest and Next.js adapters (Hapi opens its request scope in an onRequest extension). `enterContext(ctx)` is the non-callback variant for runtimes where wrapping in `runWithContext` is awkward. Every adapter also accepts a `getUser` option resolved lazily at capture time, which usually replaces this middleware pattern; an explicit `setRequestUser` still wins. `setRequestUser` returns false when called outside a request scope, and it never writes the global scope. Inside one request the last write wins; async work started in a handler keeps that request identity until its async tree resolves. Non-framework code (queues, workers) gets the same isolation from `installAsyncScope` + `runWithContext`:

```ts
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](/php): register `BugWatchContextMiddleware` (Laravel). It reads `$request->user()`, scopes it to the current request, and calls `flush()` + `resetScope()` on termination:

```php
// 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. On the Node runtime, `withBugWatchRouteHandler` opens a per-request scope, so `setRequestUser` works inside Next.js route handlers too; only true edge runtimes (Cloudflare Workers, Vercel Edge) lack `AsyncLocalStorage` and need the per-capture hint below.


## Per-capture and `withScope`

Attach identity/tags to **one event** without touching shared scope. This works on any adapter or runtime:

```ts
BugWatch.captureException(err, { user: userId ? { id: userId } : undefined, tags: { invoiceId, route: "/api/invoices" } });
```

```php
BugWatch::captureException($e, ['user' => ['id' => $userId], 'tags' => ['route' => $routeName, 'tenant' => $tenantId]]);
```

`withScope` opens a **temporary cloned scope**; mutations inside are discarded when it returns. It captures nothing by itself: call a capture method inside the callback:

```ts
BugWatch.withScope((scope) => {
  scope.setTag("orderId", order.id);
  scope.setUser({ id: user.id });
  BugWatch.captureException(err); // sees the scoped tag + user
});
```


## Tags, context and release

```ts
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
```

```php
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 grouped by name, not indexed; shown on the event detail. Wire: `contexts{}` (passthrough, redacted like everything else).
- **Breadcrumbs**: `addBreadcrumb({ category, type, level, message, data })` keeps a bounded trail (last 50; per-request inside an adapter scope) rendered as a timeline on the occurrence. Wire: `breadcrumbs[]`.
- **Release**: build/version label (version string or git SHA). Wire: `release`. Environment is bound to the API key, not sent by the SDK.
- **Cause chains**: automatic in both SDKs: JS `new Error(msg, { cause })` / `AggregateError` and PHP `getPrevious()` chains ship a structured `causes[]` array (each cause with its own type, message and, in JS, stacktrace; depth 3).


## Event ID and deduplication

`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`:

```ts
const eventId = BugWatch.captureException(err);
```

Wire: `eventId` (≤200 chars).


## Trace correlation and distributed tracing

Both SDKs join W3C traces automatically: every JS server adapter and the PHP Laravel middleware read the inbound `traceparent` header, and a request without one gets a synthesized trace id (JS), so errors and logs always share the request's trace. Outbound, `wrapFetch` (JS) injects `traceparent` for you, and `BugWatch.traceHeaders()` (both SDKs) returns the header for any other client. Both SDKs can also create spans (`withSpan`/`startSpan`) that appear on the trace waterfall; see the JS and PHP pages under [Install an SDK](/install-an-sdk) for the span API.

If your app already uses OpenTelemetry, the JS `./otel` subpath injects the active OTel span's `traceId` / `spanId` into every event instead:

```ts
import { otelTraceContextProvider } from "@newinstance/bugwatch/otel";
BugWatch.init({ projectKey: process.env.BUGWATCH_KEY, traceContextProvider: otelTraceContextProvider() });
```

Or set it manually, per event or globally:

```ts
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.


## Sampling

Send only a fraction of events (useful for high-volume info/debug logs):

```ts
BugWatch.init({ projectKey: process.env.BUGWATCH_KEY, sampleRate: 0.25 }); // 25%; default 1 (100%)
```

Sampling is **SDK-side** and runs **before** `beforeSend`; sampled-out events never reach the wire. For level-based sampling (keep all errors, sample info) use `beforeSend`.


## Redaction and `beforeSend`

The SDK redacts sensitive values **before they leave your process** (the server redacts again as defence-in-depth). Default keys include `password`, `passwd`, `pwd`, `token`, `accesstoken`, `refreshtoken`, `idtoken`, `auth`, `authorization`, `cookie`, `setcookie`, `secret`, `clientsecret`, `privatekey`, `apikey`, `sessionid`, `creditcard`, `cardnumber`, `cvv`, `pin`, `ssn`, `bvn`, `nin` (case-insensitive). Matched values become `[REDACTED]`; redaction runs on a deep clone and never mutates your objects. Add your own and/or drop/mutate events:

```ts
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)

Override how events are grouped into issues. PHP exposes `setFingerprint` (the JS SDK has no fingerprint control today; its per-capture hint accepts `level`, `tags`, `user`, `traceId`, `spanId` only):

```php
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 secret

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:

```ts
// Backend (Express) - expose a mint endpoint:
import { bugWatchBrowserSessionHandler } from "@newinstance/bugwatch/express";
app.get("/bugwatch/session", bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY }));

// Browser - one call, no projectKey (v0.2.0+):
import { initBrowser } from "@newinstance/bugwatch";
const client = initBrowser({ sessionUrl: "/bugwatch/session" });
```

`initBrowser` handles the SSR guard, the singleton and handler installation, resolves a relative `sessionUrl` against the page origin, and never throws: any failure degrades to a working no-op client plus one console warning, so the reporter can never crash the page.

The mint endpoint calls `POST /api/v1/bugwatch/browser-session` (see [Server Ingest API](/server-ingest-api)); browser events post to `/api/v1/bugwatch/ingest/browser` with `x-bugwatch-session` (see [Browser Ingest](/browser-ingest)).


## What lands on the wire

| 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 | `contexts{}` (passthrough) |
| `addBreadcrumb` | Yes | `breadcrumbs[]` (last 50) |
| cause chains (automatic) | Yes | `causes[]` (depth 3) |
| `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 | None |
| Redaction / `beforeSend` | Shapes/suppresses the payload | None |
| Per-request vs global | **No**, resolved to one `user` before send | None |

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).
