Metadata-Version: 2.4
Name: traceact
Version: 1.4.0
Summary: X-ray vision for your code. Lightweight action-level tracing for Python.
Author: Mohammed Shehu
License-Expression: MIT
Project-URL: Homepage, https://github.com/traceact/traceact
Project-URL: Repository, https://github.com/traceact/traceact
Project-URL: Documentation, https://github.com/traceact/traceact/blob/main/USAGE.md
Project-URL: Changelog, https://github.com/traceact/traceact/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/traceact/traceact/issues
Project-URL: Author, https://mohammedshehu.com
Keywords: tracing,observability,debugging,devtools,ai-agents
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Debuggers
Classifier: Topic :: System :: Logging
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: pytest-randomly>=3.15; extra == "dev"
Requires-Dist: flask>=3.0; extra == "dev"
Requires-Dist: starlette>=0.36; extra == "dev"
Requires-Dist: django>=4.2; extra == "dev"
Requires-Dist: requests>=2.31; extra == "dev"
Requires-Dist: httpx2>=2.9; extra == "dev"
Requires-Dist: langchain-core>=0.2; extra == "dev"
Requires-Dist: celery>=5.3; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: shiplock>=0.1.0; extra == "dev"
Dynamic: license-file

# TraceAct

[![PyPI version](https://img.shields.io/pypi/v/traceact.svg)](https://pypi.org/project/traceact/)
[![Python versions](https://img.shields.io/pypi/pyversions/traceact.svg)](https://pypi.org/project/traceact/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

X-ray vision for Python code.

TraceAct is a lightweight Python package for action-level tracing. It records the full story of what happens when a function runs — every step taken, resource touched, event recorded, and failure encountered — so you or your agent can understand what happened.

## Install

```bash
pip install traceact
```

## Quick start

```python
from traceact import traced_action, configure, JsonlSink

configure(
    project="my-app",
    sinks=[JsonlSink("data/traces.jsonl")],
)

@traced_action(action="note.create", kind="app", actor="user")
def create_note(title, body):
    return {"note_id": "note_123"}

create_note("Hello", "World")
```

That call appends one JSON object to `data/traces.jsonl` the instant it finishes:

```json
{
  "trace_id": "trc_ccc9be1639a8",
  "root_trace_id": "trc_ccc9be1639a8",
  "parent_trace_id": null,
  "project": "my-app",
  "action": "note.create",
  "kind": "app",
  "actor": "user",
  "status": "completed",
  "started_at": "2026-08-10T22:39:00.739Z",
  "ended_at": "2026-08-10T22:39:00.740Z",
  "duration_ms": 0.361,
  "steps": [],
  "events": [],
  "touches": [],
  "errors": []
}
```

Writes are immediate by default, so the viewer (next) shows traces as your app runs. Without any `configure()` at all, traces print to stdout instead of vanishing.

## The viewer

TraceAct includes a local web viewer. No extra install — it ships with the package.

```bash
traceact view data/traces.jsonl
```

This starts a server at `http://127.0.0.1:8765` and opens your browser. The viewer tails the file live: traces appear as your app writes them.

Three views of a trace: the **log** (live table), the **map** (events and resources as connected nodes, played as a replay), and the **timeline** (events as bars on the trace's own clock, with wall-clock, summed event time, overlap saved, max concurrency, and the longest event measured above the chart). Retried operations recorded with `attempt=1, 2, ...` render as one sequence in the inspector and one `×N` node on the map — details in [USAGE.md's Timeline](https://github.com/traceact/traceact/blob/main/USAGE.md#timeline).

### See it in the map

`--map` opens the browser straight onto the animated trace map for the newest trace, instead of the log. Save this as `demo.py`:

```python
import time
from traceact import ActionTrace, configure, JsonlSink

configure(project="quickstart", sinks=[JsonlSink("demo_traces.jsonl")])

with ActionTrace.start(action="order.checkout", kind="app", actor="user") as trace:
    trace.step("Validated cart")
    trace.event(kind="db", operation="select", target="inventory")
    time.sleep(0.05)
    trace.step("Reserved stock")
    trace.event(kind="http", operation="POST", target="payments-api")
    time.sleep(0.05)
    trace.step("Charged card")
    trace.event(kind="db", operation="insert", target="orders")
    trace.output({"order_id": "ord_789"})
```

Then, one line:

```bash
python3 demo.py && traceact view demo_traces.jsonl --map
```

No account, no config file, no auth — the viewer has none by default. Full write-up (with what each part of the record means): [USAGE.md's Quickstart](https://github.com/traceact/traceact/blob/main/USAGE.md#quickstart).

### Source types

| What you pass | What happens |
|---|---|
| A `.jsonl` file | Tails that file live |
| A folder | Merges all `.jsonl` files inside (e.g. per-process shards) |
| A SQLite database (`SqliteSink` output) | Reads the `traces` table and tails new rows live — detected by file content, any extension works |
| Nothing | Opens empty; use the in-app modal to add a source |

### CLI flags

```bash
traceact view [SOURCE] [--port N] [--host HOST] [--no-browser] [--new]
traceact show [SOURCE] ...   # identical alias of view
```

| Flag | Default | Effect |
|---|---|---|
| `--port N` | `8765` | Port to serve on |
| `--host HOST` | `127.0.0.1` | Interface to bind (localhost only by default) |
| `--no-browser` | off | Start the server without opening a browser tab |
| `--new` | off | Force a fresh instance even if one is already running |
| `--base-path PATH` | *(none)* | Mount at a subpath for reverse-proxy deployments |
| `--require-token` | off | Require a random token on every API request — keeps other OS users on a shared machine out |
| `--map` | off | Open straight onto the trace map for SOURCE's newest trace, instead of the log |
| `--focus-hook URL` | *(none)* | Show a Focus control on every trace; clicking it POSTs the full trace record to URL. A non-loopback URL auto-enables `--require-token` |

### Port selection

The viewer auto-increments the port if the requested one is taken. If you ask for `8765` and it's in use, it tries `8766`, `8767`, and so on up to 20 times before giving up. Pass `--port` to start from a different base.

### Single-instance behaviour

Running `traceact view` a second time reuses an already-running viewer rather than starting a second server. The new source (if given) is added to the running viewer and a browser tab is opened on it. This means you can call `traceact view path/to/new-file.jsonl` from multiple terminal tabs during a session and they all feed into one viewer.

Pass `--new` to bypass this and force a second independent instance.

### macOS launcher

Double-click `launch.command` in the repo root to open TraceAct from Finder without a terminal. It checks for a running instance first, then creates a `.venv/`, installs or upgrades `traceact`, and opens the browser.

### Adding sources in the app

The source modal (click the source name in the header) lets you:

- **Choose file / Choose folder** — opens a native macOS picker; returns the filesystem path for live tailing
- **Drag and drop** a `.jsonl` file — saved as a static snapshot in `~/.traceact/imports/`
- **Type a path** — collapsible fallback for pasting an absolute path

### Health checks

```bash
traceact doctor [SOURCE]
```

Checks Python version, whether the optional rates package is installed (for cost estimates), that `~/.traceact` is writable, whether a viewer is already running, and (if `SOURCE` is given) that the file or folder parses as valid trace data. Useful for ruling out setup problems before debugging your own code. The same checks are also available from the viewer itself — Settings > **Run diagnostics**. See [USAGE.md](https://github.com/traceact/traceact/blob/main/USAGE.md#viewing-traces) for full output and exit-code details.

## Manual tracing

```python
from traceact import ActionTrace

with ActionTrace.start(action="note.create", kind="app") as trace:
    trace.input({"title": "Hello"})
    trace.step("Validated input")
    trace.event(kind="db", operation="insert", target="notes")
    trace.output({"note_id": "note_123"})
```

## Concepts

| Concept | Meaning |
|---|---|
| `Trace` | The full record of one action (function call) |
| `Step` | A human-readable timeline marker within a trace |
| `Event` | A structured operation: db, http, file, model, job, etc. |
| `Touch` | A resource involved in the trace (auto-derived from events) |
| `Sink` | Where completed traces are written (`JsonlSink`, `ConsoleSink`, `SqliteSink`, `HttpSink`, `OtlpSink`, wrapped by `AsyncSink`) |

### Design principle: observable by choice, never forced blind

TraceAct exists to give you X-ray vision for your code. That means nothing TraceAct does itself should take that vision away.

Wherever TraceAct might skip, drop, or truncate data, there's a signal for it. Events truncated by a budget limit set the `budget_hit` flag. Records dropped by `AsyncSink` under backpressure increment `AsyncSink.dropped`. A failure inside a sampled-out trace is recorded anyway (with `always_trace_errors`, on by default) and marked `sampled_out` so you know its detail wasn't captured. The one deliberate silence is a sampled-out *success* — that's what sampling is for — and it's opt-in through `sample_rate`.

The design choice is always: **silent by default, observable by choice**. You decide whether to log, alert on, or ignore those signals. TraceAct never makes that decision for you.

## Tracing AI agents

One agent turn is a model call plus tool calls, and telling those apart is the point of tracing an agent. TraceAct ships a `"tool"` event kind, explicit parenting for callback-style frameworks, and a LangChain adapter:

```python
from traceact.integrations.langchain import TraceActCallbackHandler

handler = TraceActCallbackHandler()
chain.invoke(inputs, config={"callbacks": [handler]})
```

Chains, model calls, tool runs, and retrievers each become traces with the right parent links and one shared correlation ID per run. Model events carry the provider langchain-core reports (`ls_provider`), so adapter-recorded calls get viewer cost estimates like hand-recorded ones. Prompt text isn't recorded unless you opt in, and opted-in content still passes through redaction. The adapter imports `langchain-core` only when you import it — `import traceact` stays zero-dependency.

Captured values are guarded twice: field-name redaction (`password`, `api_key`, …) plus default-on content scanning that catches credential formats (AWS keys, `sk-` tokens, JWTs, PEM blocks) wherever they appear — even in a field named `location` or mid-sentence in free text. `traceact doctor --scan` runs the same registry over trace files already on disk.

With the optional [rates](https://pypi.org/project/rates/) package installed (`pip install rates`), the viewer also prices model calls: a model event recorded with a provider and token counts — `trace.model(operation="completion", target="claude-sonnet-5", provider="anthropic", tokens_in=800, tokens_out=200)` — shows an estimated cost in the inspector, and each trace shows the sum across its calls. Estimates are computed at display time from a dated price snapshot; nothing is written into the trace records. Full detail: [USAGE.md's Cost estimates](https://github.com/traceact/traceact/blob/main/USAGE.md#cost-estimates).

A traced call ending via `asyncio.CancelledError` or `KeyboardInterrupt` records with `status: "cancelled"` (exception captured, always re-raised) rather than as a failure — and a cancelled `@traced_action` coroutine is recorded at all, where it previously vanished. Failures themselves can carry caller-declared codes (`errors={TimeoutError: "timeout"}` on the decorator) and be queried by them, nested fields included: `TraceLog(...).filter(**{"errors.code": "timeout"})`. Details: [USAGE.md's Errors](https://github.com/traceact/traceact/blob/main/USAGE.md#errors).

For long-running work, opt-in in-flight streaming (`TraceConfig(stream_progress=True)`) shows a `running` row that fills in as the trace progresses — and a process that crashes mid-trace leaves its last snapshot on disk as evidence instead of losing the trace entirely.

## Background jobs and queues

A queue boundary breaks ambient context: the worker runs in a different process with a fresh, empty context, so there's nothing for it to inherit. TraceAct sends the context across as job data instead — `inject_context()` on the producer, the reserved `traceact_context` kwarg on the worker:

```python
from traceact import inject_context, traced_action

# Producer
export_report.delay(user_id=42, traceact_context=inject_context())

# Worker
@shared_task(name="export_report")
@traced_action(action="report.export", kind="job", actor="worker")
def export_report(user_id: int):
    ...
```

The decorator consumes the kwarg — your function never sees it — and links the job's trace to the enqueuing trace via `upstream_trace_id` and `correlation_id`. Works with Celery, RQ, or any queue that carries a dict. `trace.queue()` records the publish and consume events on either side.

## Wiring into a web app

If your app has its own UI, add a backend route to launch or connect to the viewer, then call it from a button:

```python
# FastAPI — launch_or_connect is blocking, so use run_in_executor
import asyncio
from traceact.viewer.instance import launch_or_connect

@router.get("/api/launch-viewer")
async def launch_viewer():
    loop = asyncio.get_event_loop()
    url = await loop.run_in_executor(None, launch_or_connect,
                                     "data/traces/traces.jsonl")
    return {"url": url}
```

```javascript
// Frontend button
document.getElementById("btn-viewer").addEventListener("click", async () => {
    const btn = document.getElementById("btn-viewer");
    btn.disabled = true;
    try {
        const { url } = await fetch("/api/launch-viewer").then(r => r.json());
        window.open(url, "_blank", "noopener");
    } finally {
        btn.disabled = false;
    }
});
```

`launch_or_connect` checks for a running viewer first (via `~/.traceact/viewer.json` + a health probe). If one is found, it adds your source to it and returns the URL immediately — no new process. If nothing is running, it spawns the viewer as a background subprocess and waits up to 3 s for it to be ready.

## Requirements

Python 3.10+. No runtime dependencies.

## Development

```bash
pip install -e ".[dev]"
pytest
```

## Full reference

See [ARCHITECTURE.md](https://github.com/traceact/traceact/blob/main/ARCHITECTURE.md) for the recording-pipeline and viewer diagrams with per-component contracts.

See [USAGE.md](https://github.com/traceact/traceact/blob/main/USAGE.md) for complete API documentation: all decorator and context manager parameters, helper methods (`trace.db`, `trace.http`, `trace.file`, `trace.model`, `trace.tool`, `trace.queue`), input capture, queue and background job tracing, parent/child traces, sinks, budget configuration, the trace record schema, test isolation, and the full viewer server API.

## License

MIT

---

Built by [Mo Shehu](https://mohammedshehu.com).
