# TypeScript/Node.js SDK

Instrument AI libraries in Node.js and emit sessions/events.

The Brizz Node.js SDK instruments your AI libraries from a single `Brizz.initialize()` call. One setup covers ESM, CommonJS, bundlers and `tsx`.

## Installation

```bash
npm install @brizz/sdk
```

## Initialization

Call `Brizz.initialize()` in your entry file and hand it the AI libraries you want captured via `instrumentModules`. Passing the actual imported module is what lets Brizz hook it, so it works the same under ESM, CommonJS, bundlers (Next.js, Webpack) and `tsx`.

```typescript
import { Brizz } from '@brizz/sdk';
import OpenAI from 'openai';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  appName: 'my-ai-app',
  instrumentModules: { openAI: OpenAI },
});

const openai = new OpenAI();
```

In CommonJS, the same call works with `require` instead of `import`.

Run your app the way you already do — `node app.js`, `tsx app.ts`, or your framework's own start command. There are no launch flags to add.

## Sessions

Sessions group related operations into a single user journey.

### Using `startSession` (Recommended)

The `startSession` function creates a session span and provides a `Session` object:

```typescript
import { startSession } from '@brizz/sdk';

// All LLM calls within the callback are automatically linked to the session
const result = await startSession('session-123', async (session) => {
  // Add custom properties (optional)
  session.updateProperties({ user_id: 'user-42', plan: 'premium' });

  // Your AI logic
  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: userQuery }],
  });

  return response;
});
```

For manual input/output tracking in special scenarios (multi-agent flows, structured data), see [Sessions](/docs/instrument/sessions.md#manual-inputoutput-tracking).

### Per-Turn Context

Pass a second argument to `setInput` / `setOutput` to attach metadata to a single turn. Each turn's bag is rendered under a collapsible **Context** panel on that message in the dashboard.

```typescript
await startSession('session-123', async (session) => {
  session.setInput('Why is my bill high?', { selected_invoice: 'INV-9182' });
  const response = await openai.chat.completions.create({ ... });
  session.setOutput(response.choices[0].message.content, {
    message_id: 'msg-42',
    sources: ['doc-abc'],
  });
});
```

### Using `getActiveSession`

Retrieve the current session from anywhere within a `startSession` scope, without passing it as a parameter:

```typescript
import { getActiveSession, startSession } from '@brizz/sdk';

function logStep(stepName: string) {
  const session = getActiveSession();
  session?.updateProperties({ last_step: stepName });
}

await startSession('session-123', async (session) => {
  logStep('start'); // Accesses the session automatically
  const response = await openai.chat.completions.create({ ... });
});
```

Returns `undefined` when called outside a session scope.

### Session Title Generation

Wrap LLM-based title generation so it doesn't appear in the conversation:

```typescript
import { startSession, startSessionTitle } from '@brizz/sdk';

await startSession('session-123', async (session) => {
  const response = await openai.chat.completions.create({...});

  await startSessionTitle(async (title) => {
    const t = await openai.chat.completions.create({ model: 'gpt-4', messages: [...] });
    title.setTitle(t.choices[0].message.content);
  });
});
```

You can also use `startSession('id', callback, undefined, { mode: 'title' })` to mark an entire session scope as title generation.

### Muting internal calls

Some LLM calls — classification, routing, guardrail checks, internal summarization — aren't part of the conversation your user had. Wrap them so their content is left out of the captured conversation. The call still runs and its latency/tokens/cost are recorded; only the user prompt, assistant reply, and tool calls are dropped.

```typescript
import { callWithMute } from '@brizz/sdk';

// Keep an internal call out of the conversation (everything).
await callWithMute({}, () => agent.run('Summarize this conversation for internal logging.'));

// Hide the text but keep the tool calls.
await callWithMute({ tools: false }, () => agent.run('…a prompt you’d rather not store…'));
```

See [Mute messages](/docs/instrument/mute.md) for one-sided muting, muting tool calls on their own, async, and reuse.

### Using `withSessionId`

For simpler cases where you just need to tag traces with a session ID:

```typescript
import { withSessionId } from '@brizz/sdk';

async function runAgent(userId: string, query: string) {
  // All LLM calls here are part of the session
  await openai.chat.completions.create({ ... });
}

// Create a wrapped version of your function
const runWithSession = withSessionId('session-123', runAgent);

// Execute it
await runWithSession('user-42', 'Hello world');
```

## Vercel AI SDK

If you are using the Vercel AI SDK, you must enable the `experimental_telemetry` flag in your function calls for Brizz to capture them.

```typescript
import { generateText } from 'ai';

const result = await generateText({
  model: openai('gpt-4'),
  prompt: 'Hello',
  experimental_telemetry: { isEnabled: true }, // Required!
});
```

See the [Vercel AI quickstart](/docs/sdks/vercel-ai.md) for install (including the extra package `ai@7` needs), tool approvals, and a complete example.

## Google GenAI (Gemini)

`@google/genai` is ESM-only, and a Node ES module namespace is read-only — it can't be patched in place, and `instrumentModules` doesn't apply. Build your client from the class `instrumentGoogleGenAI` returns (after `Brizz.initialize()`):

```typescript
import { instrumentGoogleGenAI } from '@brizz/sdk/google-genai';
import * as genai from '@google/genai';

const GoogleGenAI = instrumentGoogleGenAI(genai);
const ai = new GoogleGenAI({ vertexai: true, project, location });
```

See the [Google GenAI quickstart](/docs/sdks/google-genai.md) for install, tool calling, and a complete example.

## Custom Events

Emit custom events to track business logic.

```typescript
import { emitEvent } from '@brizz/sdk';

emitEvent('user.signup', {
  plan: 'pro',
  source: 'referral'
});
```

### Attaching a session ID

If you know the session ID but aren't inside a `startSession` / `withSessionId` scope, use `emitEventWithSessionId` to stamp it on a single event:

```typescript
import { emitEventWithSessionId } from '@brizz/sdk';

emitEventWithSessionId('session-123', 'button.click', {
  source: 'sidebar',
});
```

## Configuration

| Option | Description |
|--------|-------------|
| `apiKey` | Your Brizz API Key (Required) |
| `appName` | Name of your application |
| `environment` | Deployment environment (e.g., `production`) |
| `masking` | Configuration for PII masking |

### Reporting as several services

`appName` names the whole process. When one process runs several agents that should show up as separate services in Brizz, scope the name around each one instead:

```typescript
import { callWithServiceName, withServiceName, setServiceName } from '@brizz/sdk';

// Everything traced inside the callback reports as 'checkout-agent',
// including auto-instrumented LLM calls
await callWithServiceName('checkout-agent', async () => {
  return openai.chat.completions.create({ ... });
});

// Pre-wrap a handler
const handleSupport = withServiceName('support-agent', supportHandler);

// When the enclosing span already exists — an MCP tool or HTTP handler
// opens its span before your code runs
setServiceName('billing-agent');
```

Sessions and events created outside any of these keep the configured `appName`.

### PII Masking

```typescript
Brizz.initialize({
  apiKey: '...',
  masking: {
    spanMasking: {
      rules: [
        {
          attributePattern: 'gen_ai\\.(prompt|completion)',
          mode: 'partial',
          patterns: ['sk-[a-zA-Z0-9]{48}'], // Mask API keys
        },
      ],
    },
  },
});
```

### Dropping Spans

Filter spans before they leave the SDK. Return `false` to drop, `true` to keep. Useful for stripping noisy paths (health checks, internal tooling) or excluding telemetry for specific end-users.

```typescript
Brizz.initialize({
  apiKey: '...',
  beforeSendSpan: (span) => {
    // OpenInference (LangChain JS, LangGraph JS) packs per-call
    // `config.metadata` into a JSON-stringified `metadata` attribute.
    const raw = span.attributes['metadata'];
    if (typeof raw !== 'string') return true;
    try {
      return JSON.parse(raw).customer_id !== 'cust_42';
    } catch {
      return true;
    }
  },
});
```

Tag the call site so the filter has something to match on:

```typescript
await llm.invoke([new HumanMessage('Hello')], {
  metadata: { customer_id: 'cust_42' },
});
```

Both sync and async filters are supported. Exceptions are caught — the span passes through.

To modify attribute values without dropping the span, use [PII Masking](#pii-masking) instead.

## Complete example

A single file you can copy, set `BRIZZ_API_KEY` + `OPENAI_API_KEY`, and run with `tsx app.ts`.

```typescript
// app.ts
import { Brizz, startSession, emitEvent } from '@brizz/sdk';
import OpenAI from 'openai';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-ai-app',
  environment: process.env.NODE_ENV ?? 'development',
  instrumentModules: { openAI: OpenAI },
  masking: {
    spanMasking: {
      rules: [
        // Mask the literal OpenAI API key pattern inside prompt/completion attributes.
        { attributePattern: 'gen_ai\\.(prompt|completion)', mode: 'partial', patterns: ['sk-[a-zA-Z0-9]{48}'] },
      ],
    },
  },
  beforeSendSpan: (span) => {
    // Drop telemetry for an opted-out customer; tagged via metadata at the call site.
    const raw = span.attributes['metadata'];
    if (typeof raw !== 'string') return true;
    try { return JSON.parse(raw).customer_id !== 'cust_optout'; } catch { return true; }
  },
});

const openai = new OpenAI();

async function runTurn(sessionId: string, userId: string, userMessage: string) {
  return await startSession(sessionId, async (session) => {
    // Session-wide properties — every span in the session inherits them.
    session.updateProperties({ user_id: userId, plan: 'enterprise' });
    session.setInput(userMessage, { selected_invoice: 'INV-9182' });

    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: userMessage }],
    });
    const reply = response.choices[0].message.content ?? '';

    session.setOutput(reply, { message_id: response.id, sources: ['doc-abc'] });

    // User feedback — map "feedback.positive" / "feedback.negative" to system
    // events in Org Settings -> Event so it powers filters and the Overview chart.
    emitEvent(
      'feedback.positive',
      { category: 'helpfulness' },
      { comment: 'Exactly what I needed!', context: 'billing-assistant' },
    );
    return reply;
  });
}

console.log(await runTurn('session-123', 'user-42', 'Why is my bill high?'));
```

**Gotchas**

- Put `Brizz.initialize()` at the top of your entrypoint, before your app makes any AI call, and list every AI library you use in `instrumentModules` — handing Brizz the imported module is what makes instrumentation independent of import order.
- [LangChain JS](/docs/sdks/langchain-js.md) and [Vercel AI](/docs/sdks/vercel-ai.md) need framework-specific setup — see their pages.

## See also

- [Sessions](/docs/instrument/sessions.md) — capture and enrich sessions in depth.
- [Custom events](/docs/instrument/custom-events.md) — emit business events and feedback from Node.
- [Record metrics](/docs/instrument/record-metric.md) — report your own eval scores and measurements with `recordMetric`.
- [Identify users](/docs/instrument/identify-users.md) — attach user properties so analytics roll up by person.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — masking configuration.
- [Mute messages](/docs/instrument/mute.md) — keep internal or unrelated calls out of the captured conversation.
