Record once, replay anywhere: a time-travel debugger for Python with a visual browser-based player.
Project description
tracesnap
Record once, replay anywhere. A time-travel debugger for Python with a visual browser-based player. Run your code under instrumentation, get a self-contained JSON trace, then scrub through it line-by-line in three views (text, simulator, call graph) — no re-execution.
Status: alpha. Stdlib-only core, plug-and-play integrations for Flask, Django, and FastAPI.
Why
pdb lets you pause once. print() litters your code. Profilers count
nanoseconds but don't show you state. tracesnap captures every line,
every assignment, every branch, and every outbound HTTP call into one
JSON file you can replay, share in a PR, or attach to a bug report.
Install
pip install tracesnap # core + CLI + bundled players
pip install tracesnap[flask] # + Flask integration
pip install tracesnap[django] # + Django middleware
pip install tracesnap[fastapi] # + FastAPI integration
pip install tracesnap[all] # everything above
Requires Python 3.9+. The core has zero runtime dependencies.
Quickstart
Record a script:
tracesnap record examples/sample_complex.py
Open it in your browser:
tracesnap view
That's it. tracesnap view (no args) opens the library — a list of
every recording you've made — and you can pick one to replay. To jump
straight into a specific trace:
tracesnap view <id> # use the id printed by `record`
tracesnap view trace.json # or a saved file
You'll get a three-view player:
- Text view — current step, call stack, full per-variable history.
- Simulator — animated flowchart with loops as cycle boxes, branches with the taken arm tagged ✓, variable chips that flash when changed.
- Call graph — every function as a node, edges labelled with args going in and return values coming back; click a node for per-call details.
All three views consume the same trace and switch via the header buttons.
Library use
Three equally-valid entry points, same underlying engine:
import tracesnap
# 1) Context manager (preferred for explicit scope)
with tracesnap.record(trace_id="demo") as out:
do_stuff()
# out.path -> "trace.json" (when path= is passed)
# out.event_count
# out.trace -> the trace dict in memory
# 2) Decorator (records every call to the wrapped function)
@tracesnap.record(trace_id="checkout")
def checkout(items, coupon):
...
# 3) Imperative (lowest level; what the integrations use under the hood)
tracesnap.start_recording(trace_id="x", source_files=[__file__])
try:
do_stuff()
finally:
trace = tracesnap.stop_recording()
tracesnap.write_trace(trace, "trace.json")
Framework integrations
All three frameworks expose the same @traced decorator. Decorate only
the endpoints you actually want to record — middleware-style blanket
capture is intentionally not offered, because sys.settrace is too
expensive to pay on every request and most endpoints aren't worth
recording. Recording is gated on the TRACESNAP_ENABLED=1 environment
variable, so the decorator is a no-op in production.
Flask
from flask import Flask
from tracesnap.integrations.flask import traced
app = Flask(__name__)
app.config["TRACESNAP"] = {
"output_dir": "traces",
"source_files": [__file__],
}
@app.route("/checkout")
@traced
def checkout():
...
Stack @traced below @app.route(...) (closer to the function).
Django
# settings.py
TRACESNAP = {
"output_dir": "traces",
"source_files": [str(BASE_DIR / "myapp" / "views.py")],
}
# views.py
from tracesnap.integrations.django import traced
@traced
def checkout(request):
...
# DRF ViewSet action
class ProductViewSet(viewsets.ModelViewSet):
@traced
@action(detail=False, methods=["get"], url_path="low-stock")
def low_stock(self, request):
...
For inherited DRF actions (list, retrieve, create, update,
partial_update, destroy), override and call super():
class ProductViewSet(viewsets.ModelViewSet):
@traced
def create(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
FastAPI
from fastapi import FastAPI, Request
from tracesnap.integrations.fastapi import configure, traced
app = FastAPI()
configure(output_dir="traces", source_files=[__file__])
@app.get("/checkout")
@traced
async def checkout(request: Request):
...
Stack @traced below @app.get(...). Declare a request: Request
parameter on the view so the trace can capture method/path (FastAPI only
injects it if asked). Works on both def and async def handlers.
Async note: sys.settrace is per-thread; contextvars are per-task.
For one handler per request (the common case) this works correctly.
Concurrent asyncio.gather(...) of multiple traced sub-tasks within a
single request boundary share the same recording session — not
recommended for production. Document/test your specific use.
CLI reference
tracesnap record PATH [--out FILE] [--id NAME] [--name NAME]
[--redact NAMES] [--no-library] [--kind KIND]
[--structure-out FILE]
tracesnap view [PATH] [--view text|simulator|graph|events|home|record]
[--port PORT] [--no-browser] [--scan-root DIR]
tracesnap list # show the library
tracesnap rename ID NEW_NAME
tracesnap delete ID [-f]
tracesnap --version
record
Runs the given .py under instrumentation, auto-discovers sibling
modules it imports, and saves the trace to the on-disk library (under
~/.tracesnap/ by default). Useful flags:
--out FILE— also write a standalone copy to disk.--id NAME— short identifier stored inside the trace and used as the library id (default:"trace").--name NAME— human-readable display name for the library entry.--redact NAMES— comma-separated extra variable names to redact, on top of the built-in set (password,token,secret,authorization,api_key).--no-library— skip saving to the library.
view
Starts a tiny stdlib http.server (no extra deps), copies the bundled
player HTMLs into a tmpdir, and opens your default browser.
Port behavior:
- Default port is
8765— stable across runs, so bookmarks and open tabs survive a restart. - If
8765is taken, tracesnap falls back to a random free port and prints a notice. --port Npins to your own choice (same fallback applies).--port 0always picks a random free port.
Other useful flags:
--view NAME— start on a specific page (text,simulator,graph,events,home, orrecord). Default:homewhen browsing the library,call_graphwhen a specific trace is given.--no-browser— print the URL but don't auto-open the browser (useful over SSH or in containers).--scan-root DIR— directory the in-browser "New record" page walks when offering scripts to run (default: CWD).
What gets recorded
Every event carries seq, ts, depth, line, parent_seq plus
type-specific fields:
| type | fields |
|---|---|
call |
func, args (each value with repr, type, redacted) |
line |
func |
assign |
var, scope, value, prev, change_index |
branch |
node_id, taken ("if" / "else") |
loop |
node_id, iteration (0-based) |
return |
func, value |
extcall |
kind, verb, target, status, duration_ms, started_ts, ended_ts |
Values are {repr, type, id, truncated, redacted}. repr is capped at
120 chars (truncated: true if hit). Variables named password,
token, secret, authorization, api_key get <redacted> — both as
function args and as locals. Extend the set via redact_names= on any
entry point or --redact on the CLI.
parent_seq links events into a tree:
- Inside a
for/whilebody, every event hasparent_seqpointing at the current iteration'sloopevent. - Inside an
if/elsearm, every event points at thebranchevent. - Top-level events in a function have
parent_seq: null. - A
callevent'sparent_seqis the caller's context at the call site (so a call made inside anifarm points at the branch event in the caller, not at the new frame).
Full spec: docs/trace-format-v0.1.md.
Examples
The examples/ directory has runnable samples:
tracesnap record examples/sample_program.py # straight-line script
tracesnap record examples/sample_complex.py # branches, loops, recursion
tracesnap record examples/sample_pipeline.py # multi-stage data flow
tracesnap record examples/sample_backtrace.py # exception path
python examples/flask_app.py # Flask demo (needs [flask])
python examples/fastapi_app.py # FastAPI demo (needs [fastapi])
python examples/django_app.py # Django demo (needs [django])
For framework demos, install the extra first:
pip install -e .[flask]
python examples/flask_app.py
# then in another terminal:
curl http://127.0.0.1:5050/checkout
tracesnap view # browse the request traces
Known issues / edges
- Recursion — the player keys frames by stack position, so recursive
calls collide. Roadmap item: per-call
frame_id. - Assign attribution is one line late —
sys.settracefires before a line runs; we attribute the diff to the previous line. Documented in the trace-format spec. - Value-change vs assignment — we log value changes, so in-place
mutation like
xs.append(y)doesn't emit. Use rebinding (xs = xs + [y]) to see growth. extcallscope — onlyrequests.Session.sendandurllib.request.urlopenare wrapped.httpx, DB drivers, and stdlibsocketare not (yet).- Async — per-task
contextvarswork; per-taskthreading.settracedoes not (yet). Concurrent traced sub-tasks share the same session.
Development
git clone https://github.com/bangi98/mindplayer
cd mindplayer
pip install -e .[dev]
python -m pytest tests/ # run the test suite
python -m build # build a wheel
The repo directory is still called mindplayer (the original project
name) — the published package is tracesnap.
Roadmap
- SQLite backend for traces > ~10k events (single-trace decision; JSON stays default).
depends_onfield onassignevents → true data-flow graphs in the call-graph view.exceptionevents on unwind.httpx+ DB driverextcallcapture.- Per-task
threading.settracefor parallel-async support.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tracesnap-0.2.0.tar.gz.
File metadata
- Download URL: tracesnap-0.2.0.tar.gz
- Upload date:
- Size: 120.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4d56c328204d820cae213556e253dc19cc7600215ce0a7199643d9f7a866e7d
|
|
| MD5 |
9241e28866a876f068aab5c682d40486
|
|
| BLAKE2b-256 |
55c36ac607429c57cc6d23b9d9ea9e44ce9241a0bbd442a11ec1bfef82dd4d0c
|
Provenance
The following attestation bundles were made for tracesnap-0.2.0.tar.gz:
Publisher:
publish.yml on bangi98/mindplayer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tracesnap-0.2.0.tar.gz -
Subject digest:
e4d56c328204d820cae213556e253dc19cc7600215ce0a7199643d9f7a866e7d - Sigstore transparency entry: 1615509476
- Sigstore integration time:
-
Permalink:
bangi98/mindplayer@38f98b86d1c0bcaaf44097fc5597901c026077f4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bangi98
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@38f98b86d1c0bcaaf44097fc5597901c026077f4 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tracesnap-0.2.0-py3-none-any.whl.
File metadata
- Download URL: tracesnap-0.2.0-py3-none-any.whl
- Upload date:
- Size: 111.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec1d8ad96f0299c7b66a3e4e91799ca4534b4c2c36fcf1aac5c621d1907816c4
|
|
| MD5 |
3dfe968e5151f2002af8a7ba76d19cfe
|
|
| BLAKE2b-256 |
5dd635e1c662e7764ef9238b0aa1700e61b3eef3a00bd1565669eac6b81003f7
|
Provenance
The following attestation bundles were made for tracesnap-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on bangi98/mindplayer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tracesnap-0.2.0-py3-none-any.whl -
Subject digest:
ec1d8ad96f0299c7b66a3e4e91799ca4534b4c2c36fcf1aac5c621d1907816c4 - Sigstore transparency entry: 1615509479
- Sigstore integration time:
-
Permalink:
bangi98/mindplayer@38f98b86d1c0bcaaf44097fc5597901c026077f4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bangi98
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@38f98b86d1c0bcaaf44097fc5597901c026077f4 -
Trigger Event:
workflow_dispatch
-
Statement type: