Repository navigation
Profyle 0.4.0 turns traces into answers: find a slow endpoint, fix it with Claude Code, and verify the fix by replaying the request.
Install
pip install profyle
profyle init
Then start your app with profyle run <command> (for example profyle run uvicorn main:app --reload) and ask Claude Code about a slow endpoint.
Added
profyle run <command>: trace an app without code changes (profyle run uvicorn main:app --reload,flask run,manage.py runserver, ...). Supports FastAPI/Starlette, Flask and Django under any server, and any ASGI app served by uvicorn.- One line per traced request in the console with its main finding, computed in a separate process so requests do not wait for it (
consolesetting). - Generic middlewares
profyle.asgiandprofyle.wsgifor any ASGI/WSGI framework. - Tornado support (#6):
profyle.tornado.instrument(app), andprofyle runfor Tornado's own server and gunicorn's tornado worker (extraprofyle[tornado]). - Unified configuration for every integration: environment variables, code, or
[tool.profyle]inpyproject.toml;profyle doctorshows each value and its source, plus the app that is writing traces. - Claude Code plugin (
claude-plugin/, installable from this repository's marketplace) with thefix-slow-endpointskill: diagnose a slow endpoint from its traces, fix it, replay the request and verify the result. profyle init: sets up Claude Code for a project in one command (.mcp.jsonand thefix-slow-endpointskill, both committable), so installing ispip install profyle+profyle init; the plugin remains an alternative.profyle uninstall: removes whatinitadded and the traces (after asking), so removing Profyle isprofyle uninstall+pip uninstall profyle.- MCP server (
profyle mcp, included; theprofyle[mcp]extra still works) with the toolsdoctor,slowest_endpoints,list_traces,analyze_trace,get_call_details,get_function_source,replay_requestandcompare_traces. - Trace digest: a few-KB summary of a trace (critical path per thread, self time, your own code, I/O wait, repeated calls such as N+1 queries), stored with each trace together with a one-line main finding shown in listings and in the web UI.
- Request recording and replay (
profyle replay): each trace keeps the request that produced it (credentials redacted) and the response status, so it can be sent again to the local app. - Response fingerprints: replays and comparisons report whether the response body is identical, has the same structure, or changed.
- CLI commands
analyze,replayanddoctor. - 100% line and branch coverage, enforced in CI, with a coverage badge in the README.
Changed
- Traces are stored in
<project>/.profyle/profile.db(git-ignored automatically) instead of inside the installed package. SetPROFYLE_DBto choose another file. - Python 3.10 or newer is required. Minimum versions: VizTracer 1.0, FastAPI 0.115, pydantic 2, MCP 2.0, Flask 3.0 (for
profyle[flask]). - The CLI no longer depends on Typer and Rich.
profyle.fastapiandprofyle.flaskare now aliases of the generic ASGI and WSGI middlewares; middleware settings default toNone(unset) so the configuration sources above apply.- The trace viewer no longer sends permissive CORS headers.
- Traces are saved in a background thread after the response is sent: the request no longer waits for the trace to be parsed and written, and an async server's event loop is no longer blocked by it. A request that arrives while the previous trace is still being saved waits for it (up to 5 s; async servers wait in a worker thread) so the saving never shows up in, or slows down, the next trace.
- WSGI apps (Flask...) are traced until the response body has been sent: work done while a generator streams the body is in the trace, and Flask traces get a response fingerprint without a replay. The tracer is stopped between chunks, so a body that is never read cannot leave it running.
profyle analyzewithout an id analyzes the newest trace.capture_secretsandreplay_allow_remoteare regular settings: besides theirPROFYLE_*environment variables they can be set in[tool.profyle], andprofyle doctorshows them with where each value comes from. The trace opened in the viewer is remembered by the viewer instead of the database.- Listing and searching traces is done in the database instead of loading every trace, and the trace tables are created once per process instead of on every request.
- Internal code reorganized for readability:
RequestTrace, a smallerTraceRepository, a typed trace digest, and the analysis split into focused modules (application/analysis/,application/tools/,application/requests/). The code base is formatted withruff format.
Removed
- Unused trace models and the redundant table-creation startup hook of the web viewer.
Fixed
- The duration of a trace missed the time spent in its last call.
- The trace viewer left a database connection open per request.
get_function_sourcecrashed when a trace pointed past the end of a file.- Occasional segmentation faults, from two VizTracer 1.1.1 bugs:
- a thread that exits after the tracer that traced it was freed writes to freed memory (seen when a worker pool shut down, or at exit). Profyle now reuses one tracer instead of creating one per request;
- stopping a tracer clears the call stacks of every thread while another thread may be recording an argument whose
__repr__runs Python code. On Python 3.12+, where every thread is traced, Profyle now stops event delivery and gives threads a moment to finish before it stops the tracer. A__repr__that blocks for longer (for example one that queries a database) can still hit it.
- Windows:
profyle doctorterminated the app it checked (os.kill(pid, 0)ends a process on Windows); it now asks Windows whether the process runs.profyle initwrites its files with LF line endings on every platform. Windows is now tested in CI. - Profyle's console messages (such as "not traced") were swallowed while another request was being traced: VizTracer replaces
print()while it traces. profyle run uvicorn --factoryreported the app asstr; it is now reported as a plain ASGI app.- Concurrent requests corrupted each other's traces (VizTracer: "Overwrite tracer!"). A request that arrives while another one is traced is now served untraced, and the console says so.
- Code run through
loop.run_in_executor(Tornado, plain asyncio) was missing from traces after the first request on Python < 3.12. profyle --helpcrashed with recent Click versions (#8): the CLI no longer uses Typer.PermissionErroron Windows writing a temporary trace file (#4): traces are built in memory, without temporary files.- Dependency ranges were too strict (#5): only minimum versions are declared now.
PROFYLE_ENABLED=falsedid not disable the FastAPI and Flask middlewares.- The disabled Django middleware returned no response, breaking every request.
- The Django middleware read
MIN_DURATIONinstead ofPROFYLE_MIN_DURATION(the old name still works, with a deprecation warning) and ignored environment variables. - The WSGI (Flask) middleware ignored HTTPS requests.
- Importing a middleware opened the trace database even when Profyle was disabled.
min_durationwas documented in milliseconds; it is in microseconds and filters function calls, not requests.- Sync FastAPI/Starlette endpoints and dependencies, Django async views under ASGI and
sync_to_asynccode were missing from traces after the first request on Python < 3.12, because they run in reused worker threads. - The FastAPI middleware duplicated the query string in trace names with some servers.
- The Flask middleware relied on the server-specific
REQUEST_URI.
Full changelog: https://github.com/vpcarlos/profyle/blob/main/CHANGELOG.md