# Import OpenTelemetry files

Upload traces your agents already exported with OpenTelemetry, without adding the EvalRouter SDK. Each trace is stored, unchanged or rejected on its own, so one bad trace or file never drops the others.

## Import from the browser

1. Open [New source](/traces/sources/new), choose **OpenTelemetry file** under **Import existing traces** and name the source. A workspace admin creates sources; members can import into them.
2. Drop files or a folder on the drop zone, or click it and choose **Files…** or **Folder…**. EvalRouter reads folders recursively and groups spans by `traceId` across every file you choose.
3. Watch the counts of stored, unchanged and rejected traces. Rejected traces are listed with a code and a reason. **Retry rejected** sends only those traces again.
4. Open **View traces** to inspect the imported traces in the source.

The browser reads at most 64 MiB at a time. Import larger exports with the CLI.

## What a file can contain

- The protobuf JSON form of an OTLP `ExportTraceServiceRequest`, or JSON Lines of them, which is what the OpenTelemetry Collector file exporter writes.
- `.json` and `.jsonl` files, plain or gzip-compressed (`.gz`). Other files in a folder are skipped and counted.
- zstd and zip files and the binary protobuf format aren't supported. Each such file is listed as not imported, and the other files continue. Decompress or unzip it, then import the folder.

EvalRouter keeps agent, model and tool spans. It recognizes them from the OpenTelemetry GenAI conventions (`gen_ai.operation.name`), OpenInference (`openinference.span.kind`) or `evalrouter.kind`. Other spans, such as HTTP or database spans, are dropped. A trace with no agent, model or tool span is rejected.

Credentials are removed before storage, along with your workspace's redaction fields. Imported traces expire after your workspace's retention period, counted from the import, not from when the trace was recorded.

## Keep a trace's files together

Import every file that holds part of a trace in the same import. An import completes each trace. If a later import sends new spans for that trace, it is rejected with `trace_already_complete`. Sending the same spans again is safe: the trace comes back `unchanged`.

## Outcomes and limits

| Outcome | Meaning |
| --- | --- |
| `stored` | The trace is new, or gained spans, and is now complete. |
| `unchanged` | Every span was already stored with the same content. |
| `rejected` | Nothing was stored for this trace. The code says why. |

Each stored or unchanged trace also returns its `started_at`: when the trace itself started, the time Traces shows and filters by, not the import time. It is `null` for a rejected trace.

| Code | Why |
| --- | --- |
| `otlp_trace_invalid` | The trace isn't valid OTLP JSON, for example a span ends before it starts, an ID isn't hexadecimal, or no span is an agent, model or tool span. |
| `trace_already_complete` | An earlier import completed this trace, and this one adds new spans. |
| `trace_span_conflict` | A span ID was sent again with different content. |
| `trace_span_too_large` | A span is larger than 128 KiB after redaction. |
| `trace_size_limit` | The trace is larger than 16 MiB, or than one CLI request. |
| `trace_span_limit` | The trace has more than 10,000 spans. |
| `browser_limit` | The trace doesn't fit one browser request. Import it with the CLI. |
| `request_failed` | The browser's request failed. Use **Retry rejected**. |

Each request carries at most 20 traces and 10,000 spans. The browser and the CLI split an import into requests for you. The browser keeps each request to about 4 MiB and the CLI to about 15 MiB. The API accepts up to 16 MiB per request and 256 attributes per span.

## Import with the CLI

`traces import` and the SDK import methods arrive in the first CLI and SDK release after 0.5.3. Sign in with `evalrouter login`, then import files, globs or folders:

```sh
evalrouter traces import ./otel-export --source SOURCE_ID --report import-report.json
```

The CLI prints progress after each request, then the counts and every rejected trace and unreadable file. `--report` also writes the per-trace outcomes and file counts as JSON. It exits with status 0 when everything was stored or unchanged, 1 when a trace was rejected or a file couldn't be read, and 2 when the input is invalid, such as a missing path. Running the same import again is safe.

For a scheduled job, a workspace admin creates a source and saves its ID and ingest key to an ignored env file. The job imports with that key, which can only write traces to that source. Never commit the key.

```sh
evalrouter traces connect --name nightly-otel-export --framework otlp --env-file .env.local
set -a; . ./.env.local; set +a
EVALROUTER_API_KEY="$EVALROUTER_TRACE_KEY" evalrouter traces import ./otel-export --source "$EVALROUTER_TRACE_CONNECTION_ID"
```

## Import from code

The Python and TypeScript SDKs send one request of OTLP JSON, within the limits above:

```python
result = client.traces.import_otlp(source_id, {"otlp": export_trace_service_request})
for trace in result.traces:
    print(trace.trace_id, trace.outcome, trace.code)
```

```typescript
const result = await client.traces.importOtlp(sourceId, { otlp: exportTraceServiceRequest });
```

In Python, `client.traces.import_otlp_file(source_id, ["./otel-export"])` reads files and folders the way the CLI does and returns the same report.

## Score imported traces

Enabled trace evaluators grade new traces every five minutes, including the **Turn used tools** check, which tells you whether a given turn called the tools you name. Create the evaluator before you import: traces imported before an evaluator exists aren't scored automatically.
