Docs
Export session telemetry you can preview first
RepoOps runs a local OpenTelemetry receiver that turns any tool's model-call spans into session records, and an exporter that sends your captured sessions to an OTLP backend you name. Nothing leaves the machine until you set the flag and press the button, and the tab shows what would leave before it does.
For: the engineer who runs an AI gateway or an OTel-instrumented tool and wants its sessions in RepoOps, and the lead who wants those sessions in the observability stack the team already runs
What it does, and why it helps
The receiver is POST /v1/traces on the app's own port. It accepts OTLP over HTTP in both encodings, JSON and protobuf, and keeps a span only when it describes a model call: a gen_ai.* operation, an OpenInference span kind, a model name or token usage. An HTTP trace from a gateway carries none of those and is dropped. What survives is written to ~/.repoops/signal/otlp-sessions.jsonl as the same session record every other capture source produces, with an empty prompt list, and a sidecar remembers what the last non-empty batch delivered so the tab can tell a quiet emitter from one that is sending spans that never become sessions.
The exporter runs the other way. It maps the sessions in your window to one chat span per session with an execute_tool child per tool call, carries counts, model, cost, file paths and outcome, never a prompt, and runs every string through the secret scrubber before the bytes leave. What would be shared shows that payload as counts before you press Export now. Every export appends a ledger row with the host, the counts and how many values were scrubbed, and Pipeline health is derived from that ledger. The same tab also shows a per-tool cost and acceptance table, the inferred accept and reject rate for edits an agent proposed, and a fine-tuning dataset download.
The pain. Every coding tool keeps its own transcript files where its launcher decided, and a gateway in front of them sees HTTP requests, not model calls. When you point one at a receiver and the table stays empty, you cannot tell a broken connection from a working one that is sending the wrong spans.
The point of view. Count what arrived, not what connected. Show what would leave before it leaves, as counts a reader can check, and keep prompt content off the span in every mode rather than trusting a collector setting downstream.
What gets easier. Diagnosing a source. Live status says one of three things: no spans yet, spans arriving with none of them AI calls, or the last session received with its tool, model and time. Diagnosing an export is the ledger row and the health pill, with the reason printed beside it.
When it helps. A LiteLLM or Portkey gateway, a Kong 3.13 or later gateway with the ai-proxy plugin, an OpenLLMetry-instrumented app, or Claude Code with its own telemetry on, all pointed at one local endpoint. Or a team that already reads Langfuse, Arize Phoenix, Portkey, Datadog, Honeycomb or Grafana and wants RepoOps sessions there too.
Its limits. Nothing exports on a schedule; each export is a press of Export now or Export to targets, or a POST to the route. An exported span cannot be recalled. Named targets take a Team license; the plain env endpoint does not. A gateway with a transport and no gen_ai attributes connects and contributes nothing. The accept and reject rate is inferred by matching proposed text against the landed file, and ghost-text completions reach it only through the VS Code extension.
Understand it in 30 seconds
Read the narration
- 0:00 You pointed a gateway at the local receiver.
- 0:02 Nothing shows up yet.
- 0:06 A span is kept only when it describes a model call.
- 0:09 HTTP traces are dropped.
- 0:13 The tab says which: spans with no AI calls, or nothing.
- 0:17 The preview shows what would leave; prompts never ride a span.
- 0:23 Export when the preview reads right.
- 0:25 The ledger keeps each receipt, stats only.
Synthetic example. Read the guide
Where to find it
Where to find it
- Desktop:
localhost:4000, then Attribution in the sidebar, then Signal under All tools, in the Sessions, transcripts and traces group. - Hosted: desktop only.
- Keyboard: ⌘ K, then type “Signal”.
When to use it
A gateway is connected and the table is still empty
Situation. You set OTEL_EXPORTER_OTLP_PROTOCOL=http/json and OTEL_EXPORTER_OTLP_ENDPOINT on a gateway, traffic is flowing through it, and Received over the wire still reads Nothing received yet.
What you do. Open the Signal tab and read Live status under Connect a source. Then read the API gateways cards, which say for each of the four whether it has a transport and whether its spans carry gen_ai attributes.
What you see. Live status reads Spans are arriving, and none of them are AI calls, with the span count of the last batch and 0 of them became a session. The OTLP receiver card still counts the sessions already stored.
What it establishes. The connection works and the gateway's default tracing describes HTTP requests. Turn on its GenAI instrumentation (the recipe on the card) or accept that this gateway cannot feed the receiver. Nothing was fabricated to fill the table.
Sending a repository's sessions to Langfuse
Situation. The team reads Langfuse. You hold a Team license, and you want the last 30 days of one repository's sessions there without the prompts.
What you do. Add a langfuse row to REPOOPS_EXPORT_TARGETS in the data directory's .env with the base URL and the two keys, restart, pick the Repo and Window (days), read What would be shared, then press Export to targets.
What you see. The Export targets table lists the row with its host, Auth configured and Status enabled, and the pill reads Team. Recent exports gains a row with the record count, span count, the scrubbed count and 1/1 ok. Pipeline health reads ok with the reason exporting.
What it establishes. The spans are in Langfuse under service.name repoops and vcs.repository.name set to the repository id. The ledger holds the host and the counts, never the payload. A later warn on the health row means no successful export in 3 days, which is expected on a feature with no schedule.
Before you start
- Supported versions
- RepoOps desktop v0.3.1, the release this guide was read against. A bare Sensor install can host the same receiver with repoops sensor --watch --otlp-receive, bound to 127.0.0.1.
- Where it runs
- Local: Attribution, then Signal under All tools. Hosted: /team/signal, under All tools on Attribution, renders the desktop-only stub. It says the OTLP pipeline is per machine and never streamed to the team database, and points at Telemetry and App LLM calls for what the cloud does aggregate.
- Permissions
- The receiver takes no key. Export now and Export to targets run from the tab on this machine. Export to targets also needs the install's license state to be active or grace; otherwise the route answers skipped team-feature and the targets stay inert.
- Connections
- For the receiver, an emitter that can POST OTLP over HTTP to an endpoint you name. For export, an OTLP endpoint reachable from this machine and, for a vendor, its auth header or keys in the data directory's .env. Private, link-local and cloud-metadata IP literals are refused unless REPOOPS_EXPORT_ALLOW_PRIVATE=1.
- Plan
- Receiving and the plain env export have no plan gate. Export to your observability stack (Langfuse, Phoenix, Portkey, or any OTLP backend) is a Team row in the pricing capability map, and the route checks the license state.
Configure it
- Point a tool at the receiver.
Under Connect a source, 1. Receiver endpoint shows the exact URL with this server's port, and Copy URL copies it. 2. Set these on the emitting tool gives the two variables, protocol http/json and the endpoint set to the receiver base; the emitter appends /v1/traces itself. For Claude Code as a source also set CLAUDE_CODE_ENABLE_TELEMETRY=1 on its process. RepoOps never reads those variables.
- Read Live status before anything else.
It has three states. No spans received yet means nothing has arrived. Spans are arriving, and none of them are AI calls means the transport works and the spans carry no gen_ai or OpenInference attributes. Last session received names the tool, the model, the repo when attributed, and the time.
- Pick the Repo and the Window (days).
The window offers 7, 30 and 90 days; the server clamps any value to 90 and defaults to 30. With no repo chosen the per-tool table and the suggestion outcomes ask you to pick one, and the preview scans every tracked repository.
- Read What would be shared.
Five cards: Sessions, Spans, Tool spans, Prompt content and Secrets redacted. Prompt content reads NEVER in every mode. Secrets redacted counts the values the scrubber masked, by type. A cold scan that outruns its budget shows Export preview is not available yet and fills in on the next refresh.
- Arm the plain export and press Export now.
REPOOPS_SIGNAL_EXPORT=1 and OTEL_EXPORTER_OTLP_ENDPOINT in the data directory's .env, or REPOOPS_SIGNAL_OTLP_ENDPOINTS for a fan-out list, plus OTEL_EXPORTER_OTLP_HEADERS when the backend wants a key. With the flag unset the tab says Export skipped: set REPOOPS_SIGNAL_EXPORT=1 + OTEL_EXPORTER_OTLP_ENDPOINT to enable, and nothing is sent.
- For a team stack, add named targets and press Export to targets.
REPOOPS_EXPORT_TARGETS holds a JSON array of rows with type langfuse, phoenix, portkey or otlp; REPOOPS_EXPORT_TARGETS_FILE names a file merged after it. A row with enabled false is inert. The Export to targets button appears only when the install is Team entitled.
- Read Pipeline health and Recent exports after each export.
Health is derived from the ledger per target: fail when the most recent export to it failed, warn when no success in 3 days, ok when exporting, idle when configured and never used. Recent exports lists the last 50 rows with When, Surface, Repo, Targets, Records, Spans, Scrubbed and Result.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
REPOOPS_SIGNAL_EXPORT | the data directory's .env | unset until you have read the preview; 1 to arm | Off by default. Without it POST /api/signal/export answers skipped disabled and the receiver still works. |
OTEL_EXPORTER_OTLP_ENDPOINT | the data directory's .env | the backend's base URL | The exporter appends /v1/traces. Required for the plain export when the fan-out list is unset. |
REPOOPS_SIGNAL_OTLP_ENDPOINTS | the data directory's .env | a comma-separated list when one payload should reach several backends | Overrides the single endpoint. One endpoint failing does not stop the others. |
OTEL_EXPORTER_OTLP_HEADERS | the data directory's .env | k=v pairs the backend requires | Sent on every export POST. The status route reports only has_headers; the values are never displayed. |
REPOOPS_SIGNAL_SERVICE_NAME | the data directory's .env | repoops (the default) | The service.name resource attribute on exported spans. Attribution never maps repoops itself to a tracked repository. |
REPOOPS_SIGNAL_CAPTURE_MODE | the data directory's .env | metadata (the default) | content is accepted and shown on the mode pill, but the span mapper places no prompt on a span in either mode; the preview's Prompt content card reads NEVER both ways. |
REPOOPS_EXPORT_TARGETS | the data directory's .env | one row per backend, enabled false for one you are not ready to send to | Types langfuse (public_key and secret_key), phoenix (default http://localhost:6006, optional api_key), portkey (default https://api.portkey.ai, api_key) and otlp (endpoint plus raw headers). Honored only on a Team entitled install. |
REPOOPS_EXPORT_ALLOW_PRIVATE | the data directory's .env | unset unless your collector sits on a LAN address | Off by default; private, link-local and cloud-metadata IP literals are refused before any POST. Loopback and ordinary hostnames are unaffected. |
REPOOPS_SIGNAL_RETENTION_DAYS | the data directory's .env | 90 (the default) | Both signal stores prune records older than the window, the receiver on every ingest and the ledger when it passes the compaction size. 0 keeps everything. Undated records are never dropped. |
REPOOPS_SIGNAL_LEDGER_COMPACT_MB | the data directory's .env | 32 (the default) | The ledger is append-only, so it compacts once it crosses this size instead of rewriting on every append. |
REPOOPS_SIGNAL_STORE, REPOOPS_SIGNAL_EXPORT_LEDGER | the data directory's .env | unset | Move the two JSONL files. The ledger follows the store's directory unless named on its own. |
REPOOPS_SIGNAL_PREVIEW_BUDGET_MS | the data directory's .env | 2000 (the default in code; the .env.example line shows 15000) | How long a cold preview scan may run before the route answers a timeout and finishes in the background. The memo lasts five minutes. |
Window (days) | the Signal tab, top controls | 30 | Every read on the tab and every export uses it. The server clamps it to 90. |
Redaction mode, Max sessions | the Signal tab, Fine-tuning dataset export | metadata (safe), 500 | The dataset download is separate from the span export. metadata drops prompt and assistant text; content keeps it after the scrubber. The limit caps at 2000. |
What you should see
A source that emits model-call spans
Configuration. LiteLLM with its otel callback, or Claude Code with CLAUDE_CODE_ENABLE_TELEMETRY=1, both with protocol http/json and the endpoint set to the receiver base.
Expect. Live status reads Last session received with the tool, the model and the time. The OTLP receiver card counts sessions. Received over the wire lists Tool, Session, Model, Tokens, Cost, tool calls with accept and reject pills when captured, and Repo with the attribution method in parentheses, or unattributed.
Verify. POST /v1/traces answers repoops.received above 0 and repoops.accepted above 0 for the batch. The Coding-specific view table shows the tool once a repo is picked.
A source that connects and contributes nothing
Configuration. Gravitee or Apigee pointed at the receiver with default tracing, or Kong without its GenAI plugin.
Expect. Live status reads Spans are arriving, and none of them are AI calls, with the last batch's span count and 0 of them became a session. The stored count does not move.
Verify. The receiver's response for the batch shows repoops.received above 0 and repoops.accepted 0. The API gateways card for that gateway reads no GenAI attributes.
An export that the guard or the backend refused
Configuration. REPOOPS_SIGNAL_EXPORT=1 with an endpoint on a private address, or a backend that answers a redirect or a non-2xx.
Expect. The tab reads Export failed with the reason: blocked export target, a redirect that was not followed so the payload and auth header were not forwarded, or the HTTP status. The ledger row reads 0/1 ok and health reads fail with the reason most recent export to this target failed.
Verify. Recent exports shows the row with the host only. Nothing was sent to the refused address; the guard runs before the POST.
Data and cost
- What is captured
- Received: a session record per conversation or trace id with tool, model, provider, token counts, cost, files touched, tool calls as name and count, and an empty prompt list, at ~/.repoops/signal/otlp-sessions.jsonl, plus last-ingest.json with the last batch's span and session counts. Exported: per session a chat span with operation, provider, model, conversation id, token counts, cost, client tool, surface, capture tiers, files changed and their paths, suggestion outcome, the OpenInference aliases, and any context snapshot hashes; per tool call an execute_tool child with name, count and paths. Suggestion outcomes: file path, tool, outcome, band and character counts in the repository brain's suggestion-outcomes/<yyyy-mm>.jsonl.
- Who can see it
- Local. The hosted /team/signal page is the desktop-only stub. On this machine the receiver store is also read by the Agent traces per-tool strip through syncSignalTraces. A vendor sees whatever you export to it; the tab shows its host and never its header values.
- How long it is kept
- Both signal stores keep 90 days by default: the receiver prunes on every ingest and never drops the batch it is writing, the ledger compacts past 32 MB. Reads of either file are capped at the newest 256 MB. No retention rule was found for the suggestion-outcomes files.
- What leaves the machine
- Nothing by default. With the flag and an endpoint, or an enabled Team target, one POST per endpoint per press with a 10 second timeout, redirects not followed, private addresses refused. The receiver itself makes no outbound call. The fine-tuning dataset is a browser download from this machine.
- What it costs
- No model calls anywhere on this tab. Export is bounded by the window (at most 90 days) and by one press. The preview is cached for five minutes per repo, window and exporter config.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| Received over the wire reads Nothing received yet. | The emitter has not POSTed, or it is sending protobuf with a JSON content type, or it is pointed at the wrong port. | Copy the URL from 1. Receiver endpoint, which carries this server's port. Send one batch and read Live status. |
| Live status reads Spans are arriving, and none of them are AI calls. | The spans carry no gen_ai.* or OpenInference attributes; the transport is fine. | Turn on the source's GenAI instrumentation (the recipe on its card), or read the API gateways cards to see whether it can emit them at all. |
| A received row reads unattributed under Repo. | The span carried no vcs.repository.name, service.name or file path that maps to a tracked repository. | Set vcs.repository.name on the emitter's resource to the tracked repository id, or accept the row as cross-tool. |
| Export now says Export skipped. | REPOOPS_SIGNAL_EXPORT is not 1, or no endpoint is set. | Set both in the data directory's .env and restart; the message names which one is missing. |
| Export targets shows the pill Team feature and no button. | The license state is not active or grace, so the targets are inert. | Connect in the Continuity tab. The plain env export still works without a license. |
| Export failed with blocked export target. | The endpoint is a private, link-local or cloud-metadata IP literal. | Use the hostname, or set REPOOPS_EXPORT_ALLOW_PRIVATE=1 only when the collector is on your LAN. |
| Pipeline health reads warn with no successful export in 3d. | No export has succeeded in the staleness window; there is no schedule to keep it fresh. | Press Export now or Export to targets, or read it as the honest state of a manual pipeline. |
| Export preview is not available yet. | The cold scan ran past its budget (2 seconds by default) or the request failed. | Refresh after a moment; the scan finishes in the background and the memo serves the next read. |
| Suggestion outcomes reads No correlated suggestion outcomes yet. | No Claude Code session in the window proposed an edit that could be matched against a landed file, or no repo is picked. | Pick a repo and widen the window. Ghost-text accept and reject arrives only through the VS Code extension and lands in Received over the wire. |
- Disable
- Unset REPOOPS_SIGNAL_EXPORT, or set enabled false on each target row, or remove the rows. To stop receiving, stop the emitter; the route stays registered while the app runs.
- Roll back
- Not provided. An exported span lives in the destination once the POST succeeds, and RepoOps has no call that recalls it. The ledger row is the record that it went.
- Revoke access
- Vendor keys live in the data directory's .env (OTEL_EXPORTER_OTLP_HEADERS, or the keys inside REPOOPS_EXPORT_TARGETS) or the file REPOOPS_EXPORT_TARGETS_FILE names. Rotate them at the vendor and edit the file; RepoOps never displays them. Disconnecting the license makes every named target inert without touching the rows.
- Delete
- Not provided. No route deletes a received session, a ledger row or a suggestion outcome. The files are ~/.repoops/signal/otlp-sessions.jsonl, last-ingest.json and export-ledger.jsonl beside it, and the repository brain's suggestion-outcomes/<yyyy-mm>.jsonl. Retention prunes the two signal stores at 90 days.
Related tasks
Maintenance evidence
- Feature id
signal(spine leafsignal)- Owner
- Signal Layer program (WS4, WS4-A to WS4-E; Integrations Phase 1 and Phase 3; Both Ends WS9 gateway interop). Guide: LDG-0717.
- Supported product version
- RepoOps v0.3.1
- Last verified
- 2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source public/signal.html, public/lib/arrival-state.js and public/lib/gateway-recipes.js; defaults read from lib/otel/*.mjs, lib/routes/signal.mjs, lib/routes/otlp.mjs and .env.example. Not checked on a running instance.
- Example fixtures
- No fixture file; the payload shapes are inline in lib/otel/receiver.test.mjs, lib/otel/exporter.test.mjs, lib/otel/export-egress.test.mjs, lib/otel/export-ledger.test.mjs, lib/otel/export-targets.test.mjs, lib/otel/arrival-count.test.mjs, lib/otel/attribute.test.mjs, lib/otel/protobuf-otlp.test.mjs, lib/otel/semconv.test.mjs, lib/suggestion-outcome.test.mjs, lib/signal-preview-cache.test.mjs, public/lib/arrival-state.test.mjs and public/lib/gateway-recipes.test.mjs.
- Source references
lib/routes/otlp.mjs,lib/routes/signal.mjs,lib/otel/receiver.mjs,lib/otel/semconv.mjs,lib/otel/exporter.mjs,lib/otel/export-targets.mjs,lib/otel/export-ledger.mjs,lib/otel/signal-retention.mjs,lib/otel/attribute.mjs,lib/tier.mjs,lib/suggestion-outcome.mjs,lib/routes/export.mjs,public/signal.html,public/lib/arrival-state.js,public/lib/gateway-recipes.js- Documentation review
- Independent review requested on the slice pull request; not yet recorded.
- Video review
- Narrated story rendered and published 2026-09-26 (render cd328bc45eaa, LDG-1014) with the breadcrumb Attribution, which lists the feature under Moved here, checked against main at 7aab4cd82 with LDG-1014 part 1. Six frames, the captions and the transcript were reviewed by the authoring agent, not an independent reviewer; the audio was not listened to by a person. Narration is the provisional Windows voice until LDG-0721.
Last updated