traceact 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {traceact-0.4.0/traceact.egg-info → traceact-0.6.0}/PKG-INFO +7 -2
  2. {traceact-0.4.0 → traceact-0.6.0}/README.md +1 -1
  3. {traceact-0.4.0 → traceact-0.6.0}/USAGE.md +217 -6
  4. {traceact-0.4.0 → traceact-0.6.0}/pyproject.toml +17 -1
  5. traceact-0.6.0/tests/test_propagation.py +873 -0
  6. {traceact-0.4.0 → traceact-0.6.0}/tests/test_redaction.py +1 -1
  7. {traceact-0.4.0 → traceact-0.6.0}/tests/test_tracelog.py +259 -0
  8. traceact-0.6.0/tests/test_viewer_query.py +436 -0
  9. {traceact-0.4.0 → traceact-0.6.0}/traceact/__init__.py +31 -1
  10. {traceact-0.4.0 → traceact-0.6.0}/traceact/config.py +12 -0
  11. {traceact-0.4.0 → traceact-0.6.0}/traceact/log.py +157 -9
  12. traceact-0.6.0/traceact/middleware.py +225 -0
  13. traceact-0.6.0/traceact/propagation.py +267 -0
  14. {traceact-0.4.0 → traceact-0.6.0}/traceact/redaction.py +12 -0
  15. {traceact-0.4.0 → traceact-0.6.0}/traceact/sinks.py +57 -16
  16. {traceact-0.4.0 → traceact-0.6.0}/traceact/trace.py +37 -0
  17. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/server.py +112 -1
  18. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/static/app.js +197 -5
  19. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/static/index.html +18 -11
  20. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/static/styles.css +55 -0
  21. {traceact-0.4.0 → traceact-0.6.0/traceact.egg-info}/PKG-INFO +7 -2
  22. {traceact-0.4.0 → traceact-0.6.0}/traceact.egg-info/SOURCES.txt +4 -0
  23. traceact-0.6.0/traceact.egg-info/requires.txt +9 -0
  24. traceact-0.4.0/traceact.egg-info/requires.txt +0 -4
  25. {traceact-0.4.0 → traceact-0.6.0}/LICENSE +0 -0
  26. {traceact-0.4.0 → traceact-0.6.0}/MANIFEST.in +0 -0
  27. {traceact-0.4.0 → traceact-0.6.0}/setup.cfg +0 -0
  28. {traceact-0.4.0 → traceact-0.6.0}/tests/test_async_sink.py +0 -0
  29. {traceact-0.4.0 → traceact-0.6.0}/tests/test_decorators.py +0 -0
  30. {traceact-0.4.0 → traceact-0.6.0}/tests/test_doctor.py +0 -0
  31. {traceact-0.4.0 → traceact-0.6.0}/tests/test_http_sink.py +0 -0
  32. {traceact-0.4.0 → traceact-0.6.0}/tests/test_otlp_sink.py +0 -0
  33. {traceact-0.4.0 → traceact-0.6.0}/tests/test_reader.py +0 -0
  34. {traceact-0.4.0 → traceact-0.6.0}/tests/test_sinks.py +0 -0
  35. {traceact-0.4.0 → traceact-0.6.0}/tests/test_sqlite_sink.py +0 -0
  36. {traceact-0.4.0 → traceact-0.6.0}/traceact/budget.py +0 -0
  37. {traceact-0.4.0 → traceact-0.6.0}/traceact/context.py +0 -0
  38. {traceact-0.4.0 → traceact-0.6.0}/traceact/decorators.py +0 -0
  39. {traceact-0.4.0 → traceact-0.6.0}/traceact/helpers.py +0 -0
  40. {traceact-0.4.0 → traceact-0.6.0}/traceact/ids.py +0 -0
  41. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/__init__.py +0 -0
  42. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/cli.py +0 -0
  43. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/doctor.py +0 -0
  44. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/instance.py +0 -0
  45. {traceact-0.4.0 → traceact-0.6.0}/traceact/viewer/reader.py +0 -0
  46. {traceact-0.4.0 → traceact-0.6.0}/traceact.egg-info/dependency_links.txt +0 -0
  47. {traceact-0.4.0 → traceact-0.6.0}/traceact.egg-info/entry_points.txt +0 -0
  48. {traceact-0.4.0 → traceact-0.6.0}/traceact.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: traceact
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: X-ray vision for your code. Lightweight action-level tracing for Python.
5
5
  Author: Mohammed Shehu
6
6
  License-Expression: MIT
@@ -26,6 +26,11 @@ License-File: LICENSE
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: pytest>=7.0; extra == "dev"
28
28
  Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
29
+ Requires-Dist: pytest-randomly>=3.15; extra == "dev"
30
+ Requires-Dist: flask>=3.0; extra == "dev"
31
+ Requires-Dist: starlette>=0.36; extra == "dev"
32
+ Requires-Dist: django>=4.2; extra == "dev"
33
+ Requires-Dist: requests>=2.31; extra == "dev"
29
34
  Dynamic: license-file
30
35
 
31
36
  # TraceAct
@@ -123,7 +128,7 @@ Double-click `launch.command` in the repo root to open TraceAct from Finder with
123
128
 
124
129
  The source modal (click the source name in the header) lets you:
125
130
 
126
- - **Choose file / Choose folder** — opens a native macOS picker; returns the real filesystem path for live tailing
131
+ - **Choose file / Choose folder** — opens a native macOS picker; returns the filesystem path for live tailing
127
132
  - **Drag and drop** a `.jsonl` file — saved as a static snapshot in `~/.traceact/imports/`
128
133
  - **Type a path** — collapsible fallback for pasting an absolute path
129
134
 
@@ -93,7 +93,7 @@ Double-click `launch.command` in the repo root to open TraceAct from Finder with
93
93
 
94
94
  The source modal (click the source name in the header) lets you:
95
95
 
96
- - **Choose file / Choose folder** — opens a native macOS picker; returns the real filesystem path for live tailing
96
+ - **Choose file / Choose folder** — opens a native macOS picker; returns the filesystem path for live tailing
97
97
  - **Drag and drop** a `.jsonl` file — saved as a static snapshot in `~/.traceact/imports/`
98
98
  - **Type a path** — collapsible fallback for pasting an absolute path
99
99
 
@@ -17,6 +17,9 @@ traceact/
17
17
  AsyncSink (background-thread wrapper; public as of v0.4)
18
18
  helpers.py — TraceHelpersMixin (trace.db, trace.http, trace.file, trace.model)
19
19
  ids.py — ID generation (trc_, evt_, stp_, corr_ prefixes)
20
+ propagation.py — extract_trace_id, inject_headers, propagate context manager,
21
+ _INCOMING_TRACE_ID ContextVar for cross-service correlation
22
+ middleware.py — TraceActMiddleware (WSGI), TraceActASGIMiddleware (ASGI)
20
23
 
21
24
  viewer/
22
25
  cli.py — `traceact view` / `traceact show` / `traceact doctor` CLI entry point
@@ -59,6 +62,7 @@ tests/ — pytest suite (pip install -e ".[dev]" && pytest)
59
62
  20. [Trace record schema](#trace-record-schema)
60
63
  21. [Viewing traces](#viewing-traces)
61
64
  22. [Integrating the viewer into your app](#integrating-the-viewer-into-your-app)
65
+ 23. [Distributed propagation](#distributed-propagation)
62
66
 
63
67
  ---
64
68
 
@@ -622,7 +626,7 @@ configure(config=TraceConfig(redaction_presets=["filesystem_paths", "env_vars"])
622
626
 
623
627
  An unknown preset name raises `ValueError` immediately at `TraceConfig(...)` construction, not later at trace time.
624
628
 
625
- Also settable per-decorator, same as any other `TraceConfig` field — but note that a decorator-level `redaction_presets` **replaces** the package-level list rather than adding to it, exactly like `capture_inputs` does:
629
+ Also settable per-decorator, same as any other `TraceConfig` field — but note that a decorator-level `redaction_presets` **replaces** the package-level list rather than adding to it, the same way `capture_inputs` does:
626
630
 
627
631
  ```python
628
632
  @traced_action(action="report.export", kind="app",
@@ -631,7 +635,7 @@ def export_report(...):
631
635
  ...
632
636
  ```
633
637
 
634
- **These are field-name patterns, not content scanning.** A value is redacted because of what its *key* is called, not what it contains. `trace.input({"path": "/Users/mo/secret"})` is redacted by the `filesystem_paths` preset; `trace.input({"location": "/Users/mo/secret"})` is not, because `"location"` doesn't match any active pattern. This mirrors the baseline mechanism exactly (same substring, case-insensitive matching) — it's simple and has no false-positive risk from scanning arbitrary string content, at the cost of missing secrets stored under an unexpected field name.
638
+ **These are field-name patterns, not content scanning.** A value is redacted because of what its *key* is called, not what it contains. `trace.input({"path": "/Users/mo/secret"})` is redacted by the `filesystem_paths` preset; `trace.input({"location": "/Users/mo/secret"})` is not, because `"location"` doesn't match any active pattern. This mirrors the baseline mechanism (same substring, case-insensitive matching) — it's simple and has no false-positive risk from scanning arbitrary string content, at the cost of missing secrets stored under an unexpected field name.
635
639
 
636
640
  **Nested redaction example:**
637
641
 
@@ -701,7 +705,7 @@ configure(sinks=[
701
705
  ])
702
706
  ```
703
707
 
704
- **When to use it:** any time the inner sink is slow or remote — an HTTP collector, a database, or a high-latency filesystem. For local files on fast hardware, the overhead of `JsonlSink` alone is rarely worth wrapping.
708
+ **When to use it:** any time the inner sink is slow or remote — an HTTP collector, a database, or a high-latency filesystem. For local files on fast hardware, `JsonlSink` alone rarely needs the extra wrapping.
705
709
 
706
710
  **Backpressure — and why drops are always observable:**
707
711
 
@@ -979,6 +983,32 @@ log.filter(status="failed").render_table(n=25) # cap rows shown
979
983
 
980
984
  Each terminal call re-reads the JSONL file(s) — there is no caching, so you always get the current state of a live source.
981
985
 
986
+ `last()`/`first()` are memory-bounded: they hold at most `n` matching records per file at once rather than collecting every match before truncating. A broad filter (or no filter at all) over a large source costs memory proportional to `n`, not to how much of the source matches.
987
+
988
+ ### query() — bounded result plus two completeness flags
989
+
990
+ ```python
991
+ result = log.filter(status="failed").query(500)
992
+ result["traces"] # up to 500 matches, newest-first
993
+ result["scan_capped"] # True if max_lines_scanned stopped the scan early
994
+ result["limit_reached"] # True if more than 500 traces matched
995
+ ```
996
+
997
+ Two separate reasons a result might not be every match that exists:
998
+
999
+ - `scan_capped` — the scan itself gave up early, per `max_lines_scanned`.
1000
+ - `limit_reached` — the scan finished (or found enough to stop), but `n` (or more) results matched — there may be more beyond what's returned. This is true of `last(n)` too; `last()` just has nowhere to report it, since it returns a plain list. Note that `len(result["traces"]) == n` alone can't tell you this — a bounded scan returns at most `n` regardless of whether `n` or a hundred thousand traces matched, so `limit_reached` is tracked from a count taken during the scan, not inferred from the result's length afterward.
1001
+
1002
+ Use `query()` instead of `last()` when the caller needs to distinguish those two situations — this is what the viewer's `/api/query` endpoint uses under the hood (see [Server-side search](#server-side-search-apiquery) above). `last()`/`first()` keep returning a plain list; `query()` is a separate method rather than a change to their return shape, so nothing about them breaks for existing callers.
1003
+
1004
+ ### max_lines_scanned — bounding scan time
1005
+
1006
+ ```python
1007
+ log = TraceLog("data/traces.jsonl", max_lines_scanned=200_000)
1008
+ ```
1009
+
1010
+ Caps how many lines a scan reads (matched or not) before giving up and returning whatever was found, setting `scan_capped=True` on the next `query()` call. Defaults to `None` — unbounded, the same behaviour as before this existed. Set it when reading a source you don't control the size of, or when a single call needs a predictable worst-case cost (an HTTP handler, for instance — see below).
1011
+
982
1012
  ### Using TraceLog in tests
983
1013
 
984
1014
  ```python
@@ -1301,7 +1331,7 @@ Pass `--new` to bypass this and force a second instance — useful when you want
1301
1331
 
1302
1332
  The "Add source" modal supports three ways to load a source:
1303
1333
 
1304
- - **Choose file / Choose folder buttons** — opens a native macOS file or folder picker. The picker runs on the server side via AppleScript (`osascript`), so it returns the real filesystem path and the viewer can tail it live. (Falls back to a `tkinter` dialog on non-macOS platforms.)
1334
+ - **Choose file / Choose folder buttons** — opens a native macOS file or folder picker. The picker runs on the server side via AppleScript (`osascript`), so it returns the filesystem path and the viewer can tail it live. (Falls back to a `tkinter` dialog on non-macOS platforms.)
1305
1335
  - **Drag and drop** — drop a `.jsonl` file onto the drop zone. The browser reads the file contents and posts them to the server, which saves a copy to `~/.traceact/imports/` and adds it as a source. Because the server holds a copy (not the original), this is a **static snapshot** — new writes to the original file won't appear. The viewer labels imported sources accordingly.
1306
1336
  - **Type a path** — a collapsible text input for pasting or typing an absolute path. This gives live tailing just like the command-line argument.
1307
1337
 
@@ -1315,11 +1345,11 @@ The "Add source" modal supports three ways to load a source:
1315
1345
  4. Installs or upgrades `traceact` inside that venv.
1316
1346
  5. Runs `traceact view`, waits for the server to be ready, then opens the browser.
1317
1347
 
1318
- The Terminal window stays open so Ctrl+C stops the viewer cleanly. To pass a source file at launch from a script: `open launch.command path/to/traces.jsonl`.
1348
+ The Terminal window stays open so Ctrl+C stops the viewer. To pass a source file at launch from a script: `open launch.command path/to/traces.jsonl`.
1319
1349
 
1320
1350
  ### What the viewer shows
1321
1351
 
1322
- - **Trace log** — a live, newest-first table of traces (time, action, status, duration, and touch/error/budget counts). A search box filters by action, kind, status, correlation ID, or touched target. The row count is capped (25 / 50 / 100 / 250, default 100) and paired with live tailing, so the newest traces are always in view.
1352
+ - **Trace log** — a live, newest-first table of traces (time, action, status, duration, and touch/error/budget counts). A search box filters by action, kind, status, correlation ID, or touched target, against the currently tailed rows. The row count is capped (25 / 50 / 100 / 250, default 100) and paired with live tailing, so the newest traces are always in view. A pre-filtered view opened via `TraceLog.view()` instead searches the full source on disk — see [Server-side search](#server-side-search-apiquery) below.
1323
1353
  - **Trace inspector** — selecting a trace shows its own ID, its parent and root trace IDs (when it is a child trace), correlation ID (when present, shown in full), kind, duration, and touch/error counts. "Copy JSON" copies the full record.
1324
1354
  - **Trace map** — a visual of one trace: the action as origin, its events and resources as connected nodes, with per-node status and a red marker on failures. Plays as a sequential step-through replay, with a speed slider (1×–10×, live, persisted) and pause/play.
1325
1355
  - **Settings** — accent colour, display density, default trace view, row count, default replay speed, and a **Run diagnostics** button — all persisted to `localStorage` except diagnostics, which runs fresh each time.
@@ -1341,11 +1371,29 @@ These endpoints are available while a viewer is running. Apps and scripts can ca
1341
1371
  | `GET` | `/api/pick?type=file\|folder` | — | `{"path":"...","cancelled":bool}` |
1342
1372
  | `POST` | `/api/import` | `{"name":"file.jsonl","content":"..."}` | `{"name":"...","path":"...","imported":true}` |
1343
1373
  | `GET` | `/api/stream?source=NAME&limit=N` | — | SSE stream: `snapshot` then `append` events |
1374
+ | `GET` | `/api/query?source=NAME&field[__op]=value&limit=N` | — | `{"traces":[...],"scan_capped":bool,"limit_reached":bool,"count":N}` |
1344
1375
 
1345
1376
  `/api/doctor`'s `status` is `"pass"`, `"fail"`, or `"info"`; `hint` is present only on `"fail"` checks. `ok` is `true` only if every `"pass"`/`"fail"` check passed — `"info"` checks (traceact's version, whether a viewer is running) never affect it.
1346
1377
 
1347
1378
  The SSE stream delivers one `snapshot` message (the last N traces as a JSON array) then `append` messages for each newly-written trace. A `": keepalive"` comment is sent every 0.5 s when nothing new arrives, to keep the connection alive through proxies.
1348
1379
 
1380
+ ### Server-side search (`/api/query`)
1381
+
1382
+ The trace log's search box and the row-limit setting both operate on the live-tailed buffer — whatever the last N traces happen to be. That's fine for the search box (a quick, instant, client-side filter over what's currently in view), but a `TraceLog.view()` pre-filter is a precise, deliberately-specified query — it needs to find its match regardless of whether that match is still inside the tail window. `/api/query` answers filters against the whole source on disk, via `TraceLog`, and the viewer routes pre-filters through it automatically. You don't need to call it directly for that to happen.
1383
+
1384
+ ```
1385
+ GET /api/query?source=traces&status=failed&action__contains=order&limit=200
1386
+ ```
1387
+
1388
+ - Every query param except `source` and `limit` is a filter field, in the same `field` / `field__contains` / `field__startswith` / `field__endswith` form as `TraceLog.filter()`. Multiple params are ANDed, same as chaining `.filter()` calls.
1389
+ - `__re` is not accepted here — it's rejected with `400`. `TraceLog.filter(field__re=...)` only makes sense when the pattern comes from trusted code; over HTTP it's arbitrary caller-supplied input, and a catastrophic-backtracking pattern could hang the request. Use `__re` directly against `TraceLog` in Python instead.
1390
+ - **`limit` is hard-capped at 1000 server-side.** Requesting `limit=5000` against a source with 3000 matches returns only the newest 1000 — but not silently: `count` in the response is the count *returned*, not the count that matched, and `limit_reached` (below) tells you whether more may exist.
1391
+ - The response carries two separate completeness flags, both `false` when the result is everything that matched:
1392
+ - **`scan_capped`** — `true` if the scan hit its internal line-read ceiling before finishing reading the source.
1393
+ - **`limit_reached`** — `true` if more traces matched than `limit` allowed back (including when a too-large requested `limit` was clamped to 1000). `count == limit` is not itself a safe signal that nothing more exists — a bounded scan always returns at most `limit` regardless of whether `limit` or a hundred thousand traces matched, so this comes from a count taken during the scan, not from inspecting the returned list's length afterward.
1394
+
1395
+ Either flag `true` means the same thing to a caller: this may not be every match. The viewer shows "results may be incomplete" next to the pre-filter badges when either is set, rather than presenting a partial result as if it were exhaustive.
1396
+
1349
1397
  ### Notes
1350
1398
 
1351
1399
  - The viewer is a **local, single-node development tool**. It reads files on the machine it runs on. Exposing one machine's traces to another over the network (`traceact serve`) is planned for a later version.
@@ -1464,6 +1512,165 @@ Because `launch_or_connect` starts the viewer on `127.0.0.1` by default, the ret
1464
1512
 
1465
1513
  ---
1466
1514
 
1515
+ ## Distributed propagation
1516
+
1517
+ When a traced action in Service A calls Service B, TraceAct links the two
1518
+ services' traces together with two separate HTTP headers — separate because
1519
+ they answer separate questions:
1520
+
1521
+ | Header | Sets on the receiving trace | Answers |
1522
+ |---|---|---|
1523
+ | `traceact-trace-id` | `upstream_trace_id` | "which trace in another service triggered me?" (causal lineage) |
1524
+ | `traceact-correlation-id` | `correlation_id` | "which wider workflow do I belong to?" (business grouping, passed through untouched) |
1525
+
1526
+ Keeping them separate matters: a trace can have an upstream parent in one
1527
+ service *and* belong to a correlation group that was assigned several hops
1528
+ earlier. Folding one into the other silently discards whichever one loses.
1529
+
1530
+ ### Outbound: stamp both headers
1531
+
1532
+ `inject_headers()` reads the active trace and stamps its `trace_id` (always)
1533
+ and its `correlation_id` (if set) onto an outbound headers dict. It never
1534
+ mutates the dict you pass in.
1535
+
1536
+ ```python
1537
+ import requests
1538
+ from traceact import inject_headers
1539
+
1540
+ with ActionTrace.start(action="order.submit", correlation_id="corr_wf_9f2a") as trace:
1541
+ headers = inject_headers({"Content-Type": "application/json"})
1542
+ requests.post("https://payments.internal/charge", json=payload, headers=headers)
1543
+ ```
1544
+
1545
+ Works the same with `httpx`, `urllib`, or any other HTTP client — `inject_headers`
1546
+ just returns a plain dict.
1547
+
1548
+ ### Inbound: extract the headers (manual)
1549
+
1550
+ Use the `propagate` context manager when you want explicit control, or when
1551
+ your framework isn't covered by the automatic middleware. Pass the framework's
1552
+ header object **directly** — `request.headers` works as-is on Flask, Django,
1553
+ FastAPI, and Starlette, and so does a plain dict in any casing:
1554
+
1555
+ ```python
1556
+ from traceact import propagate, ActionTrace
1557
+
1558
+ def handle_charge(request):
1559
+ with propagate(request.headers):
1560
+ with ActionTrace.start(action="charge.process") as trace:
1561
+ # trace.upstream_trace_id == Service A's trace_id
1562
+ # trace.correlation_id == whatever correlation Service A had
1563
+ ...
1564
+ ```
1565
+
1566
+ HTTP header names are case-insensitive and every framework reconstructs them
1567
+ differently (Flask/Werkzeug and Django hand back `Traceact-Trace-Id`,
1568
+ Title-Case; ASGI delivers raw lowercase bytes). `propagate()` and
1569
+ `extract_trace_id()`/`extract_correlation_id()` normalise all of that
1570
+ internally — pass the header object however your framework gives it to you,
1571
+ including `dict(request.headers)` if you already have that.
1572
+
1573
+ ### Inbound: automatic via middleware (Flask / Django)
1574
+
1575
+ ```python
1576
+ from traceact import TraceActMiddleware
1577
+
1578
+ # Flask
1579
+ app.wsgi_app = TraceActMiddleware(app.wsgi_app)
1580
+
1581
+ # Django (in wsgi.py)
1582
+ from django.core.wsgi import get_wsgi_application
1583
+ application = TraceActMiddleware(get_wsgi_application())
1584
+ ```
1585
+
1586
+ Reads both headers from the WSGI environ and applies them to every trace
1587
+ started during that request — including traces started while a streamed
1588
+ response body is generated (the context is held until the response is fully
1589
+ closed, not just until the view function returns). If neither header is
1590
+ present, the middleware is fully transparent.
1591
+
1592
+ ### Inbound: automatic via middleware (FastAPI / Starlette)
1593
+
1594
+ ```python
1595
+ from traceact import TraceActASGIMiddleware
1596
+ from fastapi import FastAPI
1597
+
1598
+ app = FastAPI()
1599
+ app.add_middleware(TraceActASGIMiddleware)
1600
+ ```
1601
+
1602
+ Or wrap manually:
1603
+
1604
+ ```python
1605
+ app = TraceActASGIMiddleware(app)
1606
+ ```
1607
+
1608
+ ### How the chain looks in the viewer and TraceLog
1609
+
1610
+ Both services write their own trace records. Query by whichever link answers
1611
+ your question — `upstream_trace_id` for "what did this specific call trigger",
1612
+ `correlation_id` for "show me the whole workflow":
1613
+
1614
+ ```python
1615
+ from traceact import TraceLog
1616
+
1617
+ log = TraceLog("traces.jsonl")
1618
+ log.filter(upstream_trace_id="trc_abc123").all() # traces this call triggered
1619
+ log.filter(correlation_id="corr_wf_9f2a").all() # the entire workflow
1620
+ log.filter(correlation_id="corr_wf_9f2a").view() # same, in the browser viewer
1621
+ ```
1622
+
1623
+ The inspector's summary card shows both when present, each in full (not
1624
+ shortened) so they can be copied and searched against another service's logs.
1625
+
1626
+ ### `ai_prompts` redaction preset
1627
+
1628
+ For AI pipelines where trace payloads must not store raw prompt text, model
1629
+ responses, or conversation history, enable the `ai_prompts` preset:
1630
+
1631
+ ```python
1632
+ configure(
1633
+ config=TraceConfig(redaction_presets=["ai_prompts"]),
1634
+ )
1635
+ ```
1636
+
1637
+ Redacted fields include: `raw_prompt`, `prompt_content`, `system_prompt`,
1638
+ `raw_response`, `response_content`, `conversation`, `message_content`,
1639
+ `file_content`, `source_excerpt`, `context_window`, `completion`,
1640
+ `generation`, `output_text`. Safe fields like `model`, `latency_ms`,
1641
+ `prompt_id`, and token counts are unaffected.
1642
+
1643
+ | What gets redacted | What stays |
1644
+ |---|---|
1645
+ | Raw prompts and responses | Model name, version |
1646
+ | System prompts | Token counts (as long as field name has no "token" substring) |
1647
+ | Conversation history | Latency, cost |
1648
+ | File and source excerpts | Trace IDs, correlation IDs |
1649
+
1650
+ Combine with other presets:
1651
+
1652
+ ```python
1653
+ TraceConfig(redaction_presets=["ai_prompts", "api_keys"])
1654
+ ```
1655
+
1656
+ ### Reference: headers
1657
+
1658
+ | Header | Outbound (inject) | Inbound (extract) |
1659
+ |---|---|---|
1660
+ | `traceact-trace-id` | Active `ActionTrace.trace_id` | Sets `upstream_trace_id` |
1661
+ | `traceact-correlation-id` | Active `ActionTrace.correlation_id`, if set | Sets `correlation_id` |
1662
+
1663
+ Accepted header collection types on the inbound side: any object with
1664
+ `.items()` (dict in any casing, Werkzeug/Django/Starlette header objects,
1665
+ `requests.CaseInsensitiveDict`), a list of `(name, value)` pairs, or raw ASGI
1666
+ `[(b"name", b"value")]` byte pairs. Matching is case-insensitive regardless of
1667
+ which form you pass.
1668
+
1669
+ WSGI: the header arrives as `HTTP_TRACEACT_TRACE_ID` in the environ.
1670
+ ASGI: the header arrives as bytes in `scope["headers"]`.
1671
+
1672
+ ---
1673
+
1467
1674
  ## Quick reference
1468
1675
 
1469
1676
  ```python
@@ -1476,5 +1683,9 @@ from traceact import (
1476
1683
  traced_action, # decorator
1477
1684
  JsonlSink, # write traces to a .jsonl file
1478
1685
  ConsoleSink, # print traces to stdout
1686
+ propagate, # context manager for inbound propagation
1687
+ inject_headers, # stamp outbound request headers
1688
+ TraceActMiddleware, # WSGI auto-propagation (Flask, Django)
1689
+ TraceActASGIMiddleware, # ASGI auto-propagation (FastAPI, Starlette)
1479
1690
  )
1480
1691
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "traceact"
7
- version = "0.4.0"
7
+ version = "0.6.0"
8
8
  description = "X-ray vision for your code. Lightweight action-level tracing for Python."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -35,6 +35,22 @@ dependencies = []
35
35
  dev = [
36
36
  "pytest>=7.0",
37
37
  "pytest-asyncio>=0.21",
38
+ # Test-order randomisation. Activates automatically once installed — no
39
+ # flag needed. Kept on deliberately: an order-dependent failure means
40
+ # shared/global state is leaking between tests, which is a real bug.
41
+ # Never pin --randomly-seed in CI to "stabilise" a run; that hides exactly
42
+ # the failure mode this plugin exists to catch.
43
+ "pytest-randomly>=3.15",
44
+ # Real framework objects for propagation tests (extract_trace_id,
45
+ # TraceActMiddleware, TraceActASGIMiddleware). These are test-only —
46
+ # traceact itself has zero runtime dependencies. Testing against a
47
+ # hand-built dict instead of an actual Flask/Django/Starlette request
48
+ # object was exactly the gap that let a header-parsing bug through
49
+ # 43 green tests; these are here so that never happens again.
50
+ "flask>=3.0",
51
+ "starlette>=0.36",
52
+ "django>=4.2",
53
+ "requests>=2.31",
38
54
  ]
39
55
 
40
56
  [project.urls]