# Record metrics

Report a numeric measurement your own system produced about an interaction — an eval score, a rating, a latency — as a first-class metric.

`record_metric` / `recordMetric` reports a number your own system already computes about an interaction — an eval score, a customer rating, a latency, a cost — as a first-class Brizz metric. Brizz records it against the session so you can track it over time and slice it alongside everything else it knows about that conversation.

It becomes a real metric, not an event: the value shows up in the session's **Metrics** panel and as a numeric filter on the Sessions page ("every session my judge scored below 0.6"). It does **not** clutter your conversation transcript or your Events page.

It's the typed replacement for hand-rolling this with [custom events](/docs/instrument/custom-events.md): you get a fixed shape (name, value, unit, scale, polarity) instead of a free-form attribute bag, so Brizz can chart and compare the metric without being told how to read it.

Because the metric is anchored by a session id — and optionally carries its own timestamp — it can arrive out-of-band: minutes or days after the turn it describes, even once the original trace has closed.

## Record a metric

Inside a session, the metric attaches to that session automatically.

:::tabs
:::tab[Python]

```python
from brizz import record_metric, start_session

with start_session("session-123"):
    reply = agent.run(prompt)

    score = my_evaluator.score(prompt, reply)
    record_metric("quality_score", score, unit="score", min_value=0, max_value=1, polarity="positive")
```

:::tab[Node.js]

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

await startSession('session-123', async () => {
  const reply = await agent.run(prompt);

  const score = await myEvaluator.score(prompt, reply);
  recordMetric({
    name: 'quality_score',
    value: score,
    unit: 'score',
    minValue: 0,
    maxValue: 1,
    polarity: 'positive',
  });
});
```

:::

`polarity` tells Brizz which direction is good — whether a rising `quality_score` is an improvement (`"positive"`) or a rising `hallucination_rate` is a regression (`"negative"`). `min_value` / `max_value` describe the scale, so a 4 on a 1–5 rating isn't read as a 4 on a 0–1 score.

### Describe a metric once

`unit`, `polarity`, `min_value` and `max_value` describe the *metric*, not one report of it — so Brizz remembers them per metric name. Declare them on any call, and later bare reports inherit them:

```python
record_metric("quality_score", 0.85, unit="score", polarity="positive", min_value=0, max_value=1)
record_metric("quality_score", 0.42)  # same scale and polarity — still renders correctly
```

A descriptor you pass explicitly always wins, so you can re-scale a metric later without an earlier declaration overriding it.

Brizz never *guesses* these. A metric never described anywhere renders as a plain number rather than a gauge — assuming a scale, or which direction is good, would be worse than admitting we don't know. (A `hallucination_rate` of 0.9 assumed to be `positive` would show up bright green.)

## Offline and batch evaluation

Scoring often happens after the fact — a nightly eval job, a human reviewer the next morning. Pass `session_id` to attach the metric from anywhere (no session scope needed), and `timestamp` to say *when the measured thing happened*, so the metric lands on the turn it describes rather than on the evaluation run.

:::tabs
:::tab[Python]

```python
from brizz import record_metric

for session in sessions_to_grade:
    record_metric(
        "quality_score",
        judge.score(session),
        session_id=session.id,
        timestamp=session.ended_at,
        comment="graded by the nightly LLM judge",
        attributes={"evaluator": "gpt-4o", "rubric": "v2"},
    )
```

:::tab[Node.js]

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

for (const session of sessionsToGrade) {
  recordMetric({
    name: 'quality_score',
    value: await judge.score(session),
    sessionId: session.id,
    timestamp: session.endedAt,
    comment: 'graded by the nightly LLM judge',
    attributes: { evaluator: 'gpt-4o', rubric: 'v2' },
  });
}
```

:::

`attributes` are flat labels you can slice the metric by — which evaluator produced it, which rubric version, which variant the user saw.

## Fields

- **`name`** *(required)* — the metric identifier, e.g. `"quality_score"`. Emitted as `brizz.internal.metric.name`.
- **`value`** *(required)* — the measurement. A finite number, or a boolean (recorded as `1` / `0` — handy for pass/fail checks).
- **`unit`** *(optional)* — e.g. `"score"` / `"ms"` / `"usd"`.
- **`comment`** *(optional)* — free text, e.g. the evaluator's rationale.
- **`attributes`** *(optional)* — flat labels, each emitted as `brizz.internal.metric.attribute.<key>`.
- **`polarity`** *(optional)* — `"positive"` if a higher value is better, `"negative"` if a higher value is worse.
- **`min_value` / `minValue`**, **`max_value` / `maxValue`** *(optional)* — the metric's scale.
- **`timestamp`** *(optional)* — when the measured thing happened. Defaults to now.
- **`session_id` / `sessionId`** *(optional)* — target conversation. Defaults to the active session; an explicit value wins.

When called inside a [message](/docs/instrument/message-ids.md) scope, the metric also carries that message id, so it can be attributed to a single turn.

## Validation

Telemetry must never break your code, so bad input is warned about and dropped rather than raised:

- **`name`** is lowercased, then must match `^[a-z][a-z0-9_.-]{0,63}$`. An invalid name emits nothing.
- **`value`** must be a finite number. `NaN`, infinity, and non-numeric values emit nothing.
- **`comment`** is truncated at 4096 characters.
- **`attributes`** are capped at 20 keys. Keys are lowercased and then validated like `name` (so `Complexity` becomes `complexity`; a key that's still invalid, like `has spaces`, is dropped); values are truncated at 256 characters.
- **`polarity`** accepts only `"positive"` / `"negative"`. Anything else is dropped, and the metric still emits without it.
- **A session id is required for the metric to be recorded.** If none can be resolved — no `session_id` argument and no surrounding session scope — the SDK logs a warning and Brizz discards the metric, because there's no conversation to attach it to.

## Known limitation

The `comment` field and any `attributes` values are stored **unmasked** on the metric record — the default log masking rules don't reach them. Don't put personal data in either.

## See also

- [External metrics](/docs/platform/external-metrics.md) — where your metrics show up in the product, and how to slice by them.
- [Custom events](/docs/instrument/custom-events.md) — the free-form escape hatch for anything that isn't a metric.
- [Record feedback](/docs/instrument/record-feedback.md) — capture an end-user's reaction to a reply.
- [Sessions](/docs/instrument/sessions.md) — the conversation a metric attaches to.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — masking configuration.
