> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentopfor.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Trace-aware testing

> Let the judge see tool calls and retrievals, not just the reply.

Plug opfor into your observability stack (Langfuse or Netra) and it unlocks two capabilities:

* **Grounded attack generation** — opfor reads real production traces before generating attacks, so they mirror actual user flows, tool calls, and data instead of being generic.
* **Judge enrichment** — opfor injects a trace ID into each target request, fetches the recorded trace afterward, and passes every tool call, retrieval, and span to the judge. This catches PII that leaks into a tool call but never reaches the user, and agents that fetch unauthorized data but render a clean reply.

<Info>Available for **agent** targets via the [CLI](/cli/overview) and [SDK](/sdk/overview). Add the `telemetry` block below to your agent config file.</Info>

<Note>
  **Ingestion delay:** observability platforms process spans asynchronously. Opfor polls for the trace after all turns complete, so on multi-turn attacks some spans may not have arrived yet. Tune `traceFetchInitialDelayMs`, `traceFetchMaxAttempts`, and `traceFetchRetryDelayMs` at the cost of a longer scan. Grounded attack generation reads historic traces and isn't affected.
</Note>

## Setup

<Tabs>
  <Tab title="Langfuse">
    ```json theme={null}
    "telemetry": {
      "provider": "langfuse",
      "langfuse": {
        "baseUrl": "https://cloud.langfuse.com",
        "traceSelection": { "lookbackHours": 24 }
      },
      "propagation": { "headers": { "X-Langfuse-Trace-Id": "{{traceId}}" } },
      "enrichJudgeFromTrace": true
    }
    ```

    ```bash theme={null}
    export LANGFUSE_PUBLIC_KEY=pk-lf-...
    export LANGFUSE_SECRET_KEY=sk-lf-...
    ```

    Use `langfuse.publicKeyEnv` / `langfuse.secretKeyEnv` for custom env var names.
  </Tab>

  <Tab title="Netra">
    ```json theme={null}
    "telemetry": {
      "provider": "netra",
      "netra": {
        "baseUrl": "http://localhost:3000",
        "traceSelection": { "lookbackHours": 24 }
      },
      "propagation": {
        "traceIdBodyField": "trace_id",
        "traceIdStrategy": "per-attack"
      },
      "enrichJudgeFromTrace": true
    }
    ```

    ```bash theme={null}
    export NETRA_API_KEY=NE_...
    ```

    Use `netra.apiKeyEnv` for a custom env var name. `propagation.traceIdBodyField` must match a field your agent reads from the request body and forwards to the Netra SDK as the active OTel trace ID — without that wiring, judge enrichment won't correlate.
  </Tab>
</Tabs>

## Config reference

### Top-level

| Field                                    | Description                                                             |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| `telemetry.provider`                     | `"langfuse"`, `"netra"`, or `"none"` — required                         |
| `telemetry.enrichJudgeFromTrace`         | Fetch the recorded trace after each attack and pass spans to the judge  |
| `telemetry.traceFetchInitialDelayMs`     | Initial delay before first trace fetch (ingestion lag). Default 1000 ms |
| `telemetry.traceFetchMaxAttempts`        | Max polling attempts per trace. Default 8                               |
| `telemetry.traceFetchRetryDelayMs`       | Backoff between polling attempts. Default 1500 ms                       |
| `telemetry.enrichJudgeTraceJsonMaxChars` | Max JSON chars of merged trace passed to the judge. Default 40000       |

### Propagation

| Field                                    | Description                                                                        |
| ---------------------------------------- | ---------------------------------------------------------------------------------- |
| `telemetry.propagation.headers`          | HTTP headers set on each target request. Values support `{{traceId}}`, `{{runId}}` |
| `telemetry.propagation.traceIdBodyField` | Top-level JSON body field to inject the trace ID into (e.g. `"trace_id"`)          |
| `telemetry.propagation.traceIdStrategy`  | `"per-attack"` (default) or `"per-run"`                                            |
| `telemetry.propagation.traceIdPrefix`    | Prefix for generated trace IDs                                                     |

Header values support `${VAR}` substitution (e.g. `"Authorization": "Bearer ${TARGET_TOKEN}"`).

<AccordionGroup>
  <Accordion title="Langfuse options">
    | Field                                               | Description                                                                                     |
    | --------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
    | `telemetry.langfuse.baseUrl`                        | API base URL. Default `https://cloud.langfuse.com`                                              |
    | `telemetry.langfuse.baseUrlEnv`                     | Env var that overrides `baseUrl` at runtime                                                     |
    | `telemetry.langfuse.publicKeyEnv`                   | Env var holding the public key. Default `LANGFUSE_PUBLIC_KEY`                                   |
    | `telemetry.langfuse.secretKeyEnv`                   | Env var holding the secret key. Default `LANGFUSE_SECRET_KEY`                                   |
    | `telemetry.langfuse.traceCurationListJsonMaxChars`  | Max chars of list JSON sent to the curator LLM. Default 28000                                   |
    | `telemetry.langfuse.traceSummarySourceJsonMaxChars` | Max chars of hydrated JSON sent to the summarizer LLM. Default 100000                           |
    | `telemetry.langfuse.traceSummaryForAttackMaxChars`  | Max chars of trace summary passed into attack generation. Default 26000                         |
    | `telemetry.langfuse.traceDetailFields`              | `fields` param for `GET /api/public/traces/{id}`. Default `core,io,observations,scores,metrics` |
    | `telemetry.langfuse.observationV2Fields`            | `fields` for `/v2/observations` when full hydration is needed                                   |
    | `telemetry.langfuse.observationV2MaxPages`          | Max cursor pages per trace for v2 observations. Default 15                                      |

    **`traceSelection`**

    | Field                                 | Description                                                                           |
    | ------------------------------------- | ------------------------------------------------------------------------------------- |
    | `lookbackHours`                       | Fetch traces from the last N hours                                                    |
    | `fromTimestamp` / `toTimestamp`       | ISO 8601 bounds                                                                       |
    | `setupTraceIds`                       | Explicit trace IDs to always include (skips curation)                                 |
    | `userId` / `sessionId`                | Filter by Langfuse user / session                                                     |
    | `name`                                | Filter by trace name (e.g. `"POST /chat"`)                                            |
    | `tags`                                | Filter to traces including **all** listed tags                                        |
    | `environment`                         | Filter by environment string (or array)                                               |
    | `version` / `release`                 | Filter by trace version / release                                                     |
    | `orderBy`                             | Sort order, e.g. `"timestamp.desc"`                                                   |
    | `filter`                              | Advanced Langfuse filter conditions (JSON array); takes precedence over direct params |
    | `observationName` / `observationType` | Pre-filter by observation name + type (`GENERATION`/`SPAN`/`EVENT`)                   |
    | `listLimit`                           | Max rows per page. Default 100                                                        |
    | `listMaxPages`                        | List pages to fetch before curation. Default 1                                        |
    | `fields`                              | Field groups in list responses (e.g. `"core,io,scores"`)                              |
  </Accordion>

  <Accordion title="Netra options">
    | Field                                            | Description                                                             |
    | ------------------------------------------------ | ----------------------------------------------------------------------- |
    | `telemetry.netra.baseUrl`                        | API base URL (e.g. `http://localhost:3000`)                             |
    | `telemetry.netra.baseUrlEnv`                     | Env var that overrides `baseUrl` at runtime                             |
    | `telemetry.netra.apiKeyEnv`                      | Env var holding the API key. Default `NETRA_API_KEY`                    |
    | `telemetry.netra.traceCurationListJsonMaxChars`  | Max chars of list JSON sent to the curator LLM. Default 28000           |
    | `telemetry.netra.traceSummarySourceJsonMaxChars` | Max chars of hydrated JSON sent to the summarizer LLM. Default 100000   |
    | `telemetry.netra.traceSummaryForAttackMaxChars`  | Max chars of trace summary passed into attack generation. Default 26000 |

    **`traceSelection`**

    | Field                  | Description                                           |
    | ---------------------- | ----------------------------------------------------- |
    | `lookbackHours`        | Fetch traces from the last N hours                    |
    | `fromTime` / `toTime`  | ISO 8601 bounds                                       |
    | `setupTraceIds`        | Explicit trace IDs to always include (skips curation) |
    | `sessionId` / `userId` | Filter by session / user                              |
    | `environment`          | Filter by environment string                          |
    | `listLimit`            | Max rows per page (1–100). Default 50                 |
    | `listMaxPages`         | List pages to fetch before curation. Default 1        |
  </Accordion>
</AccordionGroup>
