Send Traces
Give Voxli your agent’s traces, so each conversation shows what your agent did in every turn.
Traces reach Voxli in two ways: your agent sends them with an OpenTelemetry exporter, or Voxli reads them from a connected Langfuse or LangSmith. Either way, Voxli matches traces to a chat by its id, so the chat id in your traces must equal the externalId you import the chat with.
Sending traces with OpenTelemetry
Point an OTLP/HTTP exporter at POST /traces, with your API key as Authorization: Bearer <key>.
import jsonimport osfrom opentelemetry import tracefrom opentelemetry.exporter.otlp.proto.http import Compressionfrom opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporterfrom opentelemetry.sdk.resources import Resourcefrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import BatchSpanProcessorapi_key = os.getenv("VOXLI_API_KEY")base_url = os.getenv("VOXLI_API_URL", "https://api.voxli.io")exporter = OTLPSpanExporter(endpoint=f"{base_url}/traces",headers={"Authorization": f"Bearer {api_key}"},compression=Compression.Gzip,)provider = TracerProvider(resource=Resource.create({"service.name": "support-agent"}))provider.add_span_processor(BatchSpanProcessor(exporter))trace.set_tracer_provider(provider)tracer = trace.get_tracer("support-agent")def messages(role, text):return json.dumps([{"role": role, "parts": [{"type": "text", "content": text}]}])def check_order(order):with tracer.start_as_current_span("execute_tool check_order") as span:span.set_attribute("gen_ai.operation.name", "execute_tool")span.set_attribute("gen_ai.tool.name", "check_order")span.set_attribute("gen_ai.tool.call.arguments", json.dumps({"order": order}))result = {"status": "shipped", "eta": "Friday"}span.set_attribute("gen_ai.tool.call.result", json.dumps(result))return result# One trace per turn. Its root span names the chat and holds what was said.def answer(chat_id, user_message):with tracer.start_as_current_span("invoke_agent support-agent") as span:span.set_attribute("gen_ai.operation.name", "invoke_agent")span.set_attribute("gen_ai.conversation.id", chat_id)span.set_attribute("gen_ai.input.messages", messages("user", user_message))order = check_order("NS-1042")reply = f"NS-1042 has {order['status']} and should arrive on {order['eta']}."span.set_attribute("gen_ai.output.messages", messages("assistant", reply))return replyanswer("chat-8f2c1a", "Where is my order NS-1042?") # your agent's own id for the chatprovider.shutdown() # sends what is left before the process exits
- Body - an OTLP trace export request as protobuf (
application/x-protobuf, the exporter’s default) or JSON (application/json), optionally compressed withgzipordeflate. - Size - at most 10 MB per request, before and after decompression. A larger request gets
413. - Path - an exporter given only a base URL, such as
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.voxli.io/traces, appends/v1/traces. Voxli takes traces at/traces/v1/tracestoo. - Response - an OTLP export response. Spans without valid trace and span ids are rejected and counted in its partial success; the rest are kept. A body that isn’t an OTLP trace request gets
400.
Naming the chat
Each span belongs to the chat named in its gen_ai.conversation.id attribute, or in session.id when that is missing. A span that names no chat takes the id from another span of its trace, or from its resource. Naming the chat on the root span of each turn is enough.
Spans of one trace don’t have to arrive together. A span whose trace hasn’t named a chat yet is held until a span of that trace does. Voxli deletes held spans after 7 days if their trace never names a chat.
What Voxli reads from a trace
Voxli treats each trace as one turn of the chat. The user’s message and the reply come from the trace’s input and output, usually set on its root span: gen_ai.input.messages and gen_ai.output.messages, or the same values in OpenInference, Langfuse, OpenLLMetry or Vercel AI SDK attributes. Tool calls, retrievals and guardrails come from the spans inside the turn.
Voxli merges new spans in the background, shortly after they arrive.
Reading traces from Langfuse or LangSmith
If your agent already sends traces to Langfuse or LangSmith, connect it in Settings > Integrations instead. Conversations shows how. Voxli then reads a chat’s traces by its id:
- Langfuse - the session with that id.
- LangSmith - the thread with that id: runs whose metadata has it as
session_id,thread_idorconversation_id, in any project of the workspace.
Voxli reads them at two moments:
- After each import. About a minute after you import a chat, Voxli reads its traces from every connection, from up to 30 days before the chat started. Importing the chat again reads them again, so a longer chat gets its newer turns.
- When Voxli AI looks the chat up by its id and Voxli has no traces for it.
A connection alone doesn’t bring chats into Voxli; Voxli reads it only for a chat id it has. If your agent sends its traces to Voxli with OpenTelemetry, Voxli doesn’t also read that chat from your connections after an import.
How traces and transcripts combine
- With an imported transcript, the transcript stays as you sent it. The traces attach to it and show how each turn was produced.
- Without one, Voxli builds the conversation from the traces, and the app marks it From traces. Traces record what your agent’s code did, which can differ from what the user saw. Importing the transcript later replaces the built one, and the traces stay attached.
Traces with no user message or reply in any turn don’t make a conversation.
Traces of a Voxli test don’t make one either, since the test already shows the chat. Voxli recognizes them when the chat id is the test’s id (what Voxli sends your agent as test_result_id), or when a span carries that id as voxli_test_id or test_result_id, in an attribute or in its metadata. The traces are still kept.
What’s next
- Import Conversations - send the transcript the user saw.
- Send traces (OTLP/HTTP) -
POST /tracesin the API reference. - Conversations - what your team sees in the app, and asking Voxli AI about a chat.