What should I trace in a browser automation agent?
Rius traces a browser automation agent best as one root span per run, a generation for each model call, a tool span for each browser action, and a session and user set on every span. The trace is then a tree you can read from the task down to the click that failed. Rius computes cost from the model and token counts on the generations, so you never send a cost value.
How it works
A trace is one end-to-end run, and it has exactly one root span. The root’s duration is the run’s latency, and a failed root means a failed run. Map a browser agent onto that tree like this:
- Root span per run. Wrap the function that handles one task in
observewithkind=SpanKind.AGENT. Everything the run does nests under it, and the waterfall highlights the path from the root to a failing span. - Generations for model calls. Each call that decides the next action is a generation, a span of kind
LLMthat carries the model, token usage and optionally the messages. If you use an auto-instrumented client library, these spans are created for you. If you trace a call by hand, pass token usage withset_usage(), or the span has no tokens or cost. - Tool spans for browser actions. Navigate, click, type, extract and screenshot are tool calls. Wrap each in
observewithkind=SpanKind.TOOL. Unnamed, the span is calledexecute_toolplus the function name, such asexecute_tool click, so name your functions after the action. - Spans for other steps. Planning or page-parsing stages that are not tool calls stay as the default
CHAINkind. A search over a knowledge base is aRETRIEVERspan. - Session and user. The session groups the traces of one job or conversation. The user is the person the run served.
Use SpanKind.TOOL for browser actions rather than a bare observe. A bare observe makes a CHAIN span, which the Tool kind filter and the Tool Loop alert never see. Tool Loop is the alert that catches an agent repeating the same action, so you want it to see your click and navigate calls.
Browser tools often return a lot of data, such as page text or an encoded screenshot. observe records arguments and the return value as content, truncated at 32 KB. Set capture_output=False on a tool whose output you do not want stored. Names, timings, kinds and errors still flow.
Set it up
- Install and initialize the SDK once, as the Tracing your code page and the quickstart show. Use
rius.init(api_key="ri_...", service_name="browser-agent"). - Wrap each browser action as a tool, and the run as an agent. The tool functions below are yours, so replace the names and bodies with your own.
- Scope the user and session around each run, as in the code below.
import rius
from rius import SpanKind
rius.init(api_key="ri_...", service_name="browser-agent")
@rius.observe(kind=SpanKind.TOOL)
def navigate(url: str) -> str:
...
@rius.observe(kind=SpanKind.TOOL)
def click(selector: str) -> str:
...
@rius.observe(kind=SpanKind.TOOL, capture_output=False)
def screenshot() -> str:
...
@rius.observe(kind=SpanKind.AGENT)
def run_task(task: str) -> str:
# model calls here are generations; the loop calls navigate, click, screenshot
...
opaque_user_id = "user-8f3a" # your own opaque id for the person
job_id = "job-1234" # your own job or conversation id
with rius.user(opaque_user_id):
with rius.session(job_id):
run_task("Find the cheapest flight and fill in the form")- Choose the session id deliberately. Use your own job or conversation id so a task that spans several runs stays one session. A trace with no session id is grouped as its own single-trace session, so if the session count equals the trace count, nothing is setting
session.id. - Choose the user id deliberately. Pass an opaque id rather than an email, because the value is stored on every span and shown in the console. A span with no user is anonymous.
If a process handles exactly one job, such as a CLI or batch run, you can set RIUS_SESSION_ID once instead of scoping each call. Do not do that in a server that handles many users, because their traces would merge into one session. There is no environment variable for the user, so scope it per request. In TypeScript the same scopes are withSession and withUser.
If you replace an auto-instrumented model call with a manual generation, turn that integration off with an explicit instruments list in rius.init, or the call is counted twice.
What you get
- A trace list where each row is one browser run, with its duration, status and cost.
- A waterfall that shows the model call that chose an action, the tool span that performed it, and the step that failed, with the error type recorded on the span.
- Cost per run from the model and token counts on your generations, as a list-price estimate in US dollars.
- The Spans tab filtered to the Tool kind, so you can see which browser actions are slow or failing.
- Sessions that tie together the runs of one job, and a Users view of what happened to each person your agent served.
The Concepts page defines traces, spans, generations, sessions and users in full. For the wider picture of watching agents in production, see Monitor AI agents in production.
When something else fits
- If one process serves a single job, a process-wide
RIUS_SESSION_IDis simpler than scoping every call. - If an action is a short helper you do not need to see on its own, leave it inside its parent span rather than adding a span for it.
- If you need prompt management, evals or a playground, Rius does not offer them. It traces, prices and alerts on agent runs.