traceact 0.2.1__tar.gz → 0.3.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 (37) hide show
  1. {traceact-0.2.1/traceact.egg-info → traceact-0.3.0}/PKG-INFO +16 -1
  2. {traceact-0.2.1 → traceact-0.3.0}/README.md +15 -0
  3. {traceact-0.2.1 → traceact-0.3.0}/USAGE.md +311 -22
  4. {traceact-0.2.1 → traceact-0.3.0}/pyproject.toml +5 -1
  5. traceact-0.3.0/tests/test_decorators.py +146 -0
  6. traceact-0.3.0/tests/test_doctor.py +85 -0
  7. traceact-0.3.0/tests/test_reader.py +109 -0
  8. traceact-0.3.0/tests/test_redaction.py +141 -0
  9. traceact-0.3.0/tests/test_sinks.py +69 -0
  10. {traceact-0.2.1 → traceact-0.3.0}/traceact/__init__.py +6 -1
  11. {traceact-0.2.1 → traceact-0.3.0}/traceact/config.py +36 -0
  12. {traceact-0.2.1 → traceact-0.3.0}/traceact/decorators.py +61 -12
  13. traceact-0.3.0/traceact/redaction.py +63 -0
  14. {traceact-0.2.1 → traceact-0.3.0}/traceact/sinks.py +40 -1
  15. {traceact-0.2.1 → traceact-0.3.0}/traceact/trace.py +67 -22
  16. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/cli.py +48 -0
  17. traceact-0.3.0/traceact/viewer/doctor.py +184 -0
  18. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/server.py +13 -0
  19. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/static/app.js +94 -2
  20. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/static/index.html +14 -0
  21. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/static/styles.css +61 -0
  22. {traceact-0.2.1 → traceact-0.3.0/traceact.egg-info}/PKG-INFO +16 -1
  23. {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/SOURCES.txt +7 -0
  24. {traceact-0.2.1 → traceact-0.3.0}/LICENSE +0 -0
  25. {traceact-0.2.1 → traceact-0.3.0}/MANIFEST.in +0 -0
  26. {traceact-0.2.1 → traceact-0.3.0}/setup.cfg +0 -0
  27. {traceact-0.2.1 → traceact-0.3.0}/traceact/budget.py +0 -0
  28. {traceact-0.2.1 → traceact-0.3.0}/traceact/context.py +0 -0
  29. {traceact-0.2.1 → traceact-0.3.0}/traceact/helpers.py +0 -0
  30. {traceact-0.2.1 → traceact-0.3.0}/traceact/ids.py +0 -0
  31. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/__init__.py +0 -0
  32. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/instance.py +0 -0
  33. {traceact-0.2.1 → traceact-0.3.0}/traceact/viewer/reader.py +0 -0
  34. {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/dependency_links.txt +0 -0
  35. {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/entry_points.txt +0 -0
  36. {traceact-0.2.1 → traceact-0.3.0}/traceact.egg-info/requires.txt +0 -0
  37. {traceact-0.2.1 → traceact-0.3.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.2.1
3
+ Version: 0.3.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
@@ -127,6 +127,14 @@ The source modal (click the source name in the header) lets you:
127
127
  - **Drag and drop** a `.jsonl` file — saved as a static snapshot in `~/.traceact/imports/`
128
128
  - **Type a path** — collapsible fallback for pasting an absolute path
129
129
 
130
+ ### Health checks
131
+
132
+ ```bash
133
+ traceact doctor [SOURCE]
134
+ ```
135
+
136
+ Checks Python version, 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](USAGE.md#viewing-traces) for full output and exit-code details.
137
+
130
138
  ## Concepts
131
139
 
132
140
  | Concept | Meaning |
@@ -174,6 +182,13 @@ document.getElementById("btn-viewer").addEventListener("click", async () => {
174
182
 
175
183
  Python 3.9+. No runtime dependencies.
176
184
 
185
+ ## Development
186
+
187
+ ```bash
188
+ pip install -e ".[dev]"
189
+ pytest
190
+ ```
191
+
177
192
  ## Full reference
178
193
 
179
194
  See [USAGE.md](USAGE.md) for complete API documentation: all decorator and context manager parameters, helper methods (`trace.db`, `trace.http`, `trace.file`, `trace.model`), input capture, parent/child traces, sinks, budget configuration, the trace record schema, test isolation, and the full viewer server API.
@@ -97,6 +97,14 @@ The source modal (click the source name in the header) lets you:
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
 
100
+ ### Health checks
101
+
102
+ ```bash
103
+ traceact doctor [SOURCE]
104
+ ```
105
+
106
+ Checks Python version, 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](USAGE.md#viewing-traces) for full output and exit-code details.
107
+
100
108
  ## Concepts
101
109
 
102
110
  | Concept | Meaning |
@@ -144,6 +152,13 @@ document.getElementById("btn-viewer").addEventListener("click", async () => {
144
152
 
145
153
  Python 3.9+. No runtime dependencies.
146
154
 
155
+ ## Development
156
+
157
+ ```bash
158
+ pip install -e ".[dev]"
159
+ pytest
160
+ ```
161
+
147
162
  ## Full reference
148
163
 
149
164
  See [USAGE.md](USAGE.md) for complete API documentation: all decorator and context manager parameters, helper methods (`trace.db`, `trace.http`, `trace.file`, `trace.model`), input capture, parent/child traces, sinks, budget configuration, the trace record schema, test isolation, and the full viewer server API.
@@ -12,20 +12,27 @@ traceact/
12
12
  config.py — TraceConfig, configure(), reset_config()
13
13
  budget.py — TraceBudget, TraceBudget.production() preset
14
14
  context.py — ContextVar for active trace, SKIP sentinel
15
- sinks.py JsonlSink (thread-safe), ConsoleSink, AsyncSink (not yet public)
15
+ redaction.py SENSITIVE_PATTERNS baseline + REDACTION_PRESETS registry
16
+ sinks.py — JsonlSink (thread-safe, rotation via max_bytes), ConsoleSink,
17
+ AsyncSink (not yet public)
16
18
  helpers.py — TraceHelpersMixin (trace.db, trace.http, trace.file, trace.model)
17
19
  ids.py — ID generation (trc_, evt_, stp_, corr_ prefixes)
18
20
 
19
21
  viewer/
20
- cli.py — `traceact view` / `traceact show` CLI entry point; --new flag
22
+ cli.py — `traceact view` / `traceact show` / `traceact doctor` CLI entry point
21
23
  server.py — stdlib ThreadingHTTPServer; SPA + REST + SSE
22
- reader.py — SourceReader: JSONL snapshot + live byte-offset tail
24
+ reader.py — SourceReader: JSONL snapshot + live byte-offset tail,
25
+ with inode-based delete+recreate detection
26
+ doctor.py — run_checks(): shared health-check logic behind both
27
+ `traceact doctor` and GET /api/doctor (Settings > Run diagnostics)
23
28
  instance.py — single-instance coordination (state file + HTTP probe);
24
29
  launch_or_connect() for embedding in app backends
25
30
  static/
26
31
  index.html — single-page app shell
27
32
  styles.css — design system (dark theme, CSS custom properties)
28
33
  app.js — all frontend logic (no framework, no build step)
34
+
35
+ tests/ — pytest suite (pip install -e ".[dev]" && pytest)
29
36
  ```
30
37
 
31
38
  ## Contents
@@ -41,14 +48,16 @@ traceact/
41
48
  9. [Touches](#touches)
42
49
  10. [Errors](#errors)
43
50
  11. [Parent and child traces](#parent-and-child-traces)
44
- 12. [Input capture](#input-capture)
45
- 13. [Sinks](#sinks)
46
- 14. [Budget configuration](#budget-configuration)
47
- 15. [TraceConfig fields](#traceconfig-fields)
48
- 16. [Test isolation](#test-isolation)
49
- 17. [Trace record schema](#trace-record-schema)
50
- 18. [Viewing traces](#viewing-traces)
51
- 19. [Integrating the viewer into your app](#integrating-the-viewer-into-your-app)
51
+ 12. [Framework recipes](#framework-recipes)
52
+ 13. [Background jobs and correlation IDs](#background-jobs-and-correlation-ids)
53
+ 14. [Input capture](#input-capture)
54
+ 15. [Sinks](#sinks)
55
+ 16. [Budget configuration](#budget-configuration)
56
+ 17. [TraceConfig fields](#traceconfig-fields)
57
+ 18. [Test isolation](#test-isolation)
58
+ 19. [Trace record schema](#trace-record-schema)
59
+ 20. [Viewing traces](#viewing-traces)
60
+ 21. [Integrating the viewer into your app](#integrating-the-viewer-into-your-app)
52
61
 
53
62
  ---
54
63
 
@@ -127,7 +136,7 @@ from traceact import traced_action, TraceConfig, TraceBudget
127
136
  operation="insert", # creates an initial event if provided with target
128
137
  target="notes", # the resource (table, endpoint, file, model)
129
138
  database="sqlite", # for kind="db" traces
130
- capture_inputs=False, # False | True | ["field1", "field2"]
139
+ capture_inputs=True, # None (defer to package config) | False | True | ["field1", "field2"]
131
140
  meta={"release": "v1.2"}, # arbitrary key-value data
132
141
  config=TraceConfig(strict=True),# override package config for this trace only
133
142
  budget=TraceBudget(max_events=50), # override budget for this trace only
@@ -393,9 +402,153 @@ When `sample_rate < 1.0` and a trace is sampled out, a skip sentinel is pushed o
393
402
 
394
403
  ---
395
404
 
405
+ ## Framework recipes
406
+
407
+ `@traced_action` and `ActionTrace` work on any callable — there's no framework integration to install. The only decision per framework is *where* to call `configure()` and *which* functions to wrap. These are patterns, not new API surface.
408
+
409
+ ### FastAPI
410
+
411
+ Call `configure()` once, at import time or in a startup event, then wrap your route handlers or (more usefully) the service functions they call:
412
+
413
+ ```python
414
+ # app/tracing.py — imported once, before routes are registered
415
+ from traceact import configure, TraceConfig, JsonlSink
416
+
417
+ configure(
418
+ config=TraceConfig(sink_mode="blocking"),
419
+ sinks=[JsonlSink("data/traces/traces.jsonl")],
420
+ )
421
+ ```
422
+
423
+ ```python
424
+ # app/routers/notes.py
425
+ from fastapi import APIRouter
426
+ from traceact import traced_action
427
+
428
+ router = APIRouter()
429
+
430
+ @traced_action(action="note.create", kind="app", actor="user")
431
+ async def _create_note(title: str, body: str) -> dict:
432
+ ...
433
+ return {"note_id": "note_123"}
434
+
435
+ @router.post("/notes")
436
+ async def create_note(payload: dict):
437
+ return await _create_note(payload["title"], payload["body"])
438
+ ```
439
+
440
+ Wrapping the inner service function (not the route handler directly) keeps the trace's `action` name stable even if you later rename the route or move it behind a different path. `correlation_id` is a good place to pass FastAPI's request ID if you have request-ID middleware:
441
+
442
+ ```python
443
+ @traced_action(action="note.create", kind="app", correlation_id=request.state.request_id)
444
+ ```
445
+
446
+ ### Django
447
+
448
+ Django's request/response cycle is a natural boundary for `correlation_id`, and views or service functions are the natural place for `@traced_action`:
449
+
450
+ ```python
451
+ # myapp/apps.py — configure once when the app is ready
452
+ from django.apps import AppConfig
453
+ from traceact import configure, TraceConfig, JsonlSink
454
+
455
+ class MyAppConfig(AppConfig):
456
+ name = "myapp"
457
+
458
+ def ready(self):
459
+ configure(
460
+ config=TraceConfig(sink_mode="blocking"),
461
+ sinks=[JsonlSink("data/traces/traces.jsonl")],
462
+ )
463
+ ```
464
+
465
+ ```python
466
+ # myapp/services.py
467
+ from traceact import traced_action
468
+
469
+ @traced_action(action="order.process", kind="app", actor="user")
470
+ def process_order(order_id: int) -> None:
471
+ ...
472
+ ```
473
+
474
+ ```python
475
+ # myapp/views.py
476
+ from django.http import JsonResponse
477
+ from myapp.services import process_order
478
+
479
+ def process_order_view(request, order_id):
480
+ process_order(order_id)
481
+ return JsonResponse({"status": "ok"})
482
+ ```
483
+
484
+ If you want every request to carry a correlation ID automatically (so all traces from one request share it), set it from middleware using Python's `contextvars` and read it in the view before calling the traced function — TraceAct doesn't read Django's request object itself, so the ID has to be passed explicitly into `correlation_id=`.
485
+
486
+ ---
487
+
488
+ ## Background jobs and correlation IDs
489
+
490
+ `correlation_id` links traces that belong to the same logical unit of work — a request, a job, a batch run — even when they happen across different function calls or processes. TraceAct doesn't generate or propagate it automatically across a queue boundary; the queue's message *is* the propagation mechanism, so you pass the ID through it like any other job argument.
491
+
492
+ **Enqueuing** — generate (or reuse) a correlation ID before the job goes on the queue:
493
+
494
+ ```python
495
+ import uuid
496
+ from myqueue import enqueue
497
+
498
+ def start_export(user_id: int):
499
+ correlation_id = f"corr_{uuid.uuid4().hex[:12]}"
500
+ enqueue("export_report", user_id=user_id, correlation_id=correlation_id)
501
+ return correlation_id # e.g. return to the client so it can poll status
502
+ ```
503
+
504
+ **Celery:**
505
+
506
+ ```python
507
+ from celery import shared_task
508
+ from traceact import traced_action
509
+
510
+ @shared_task(name="export_report")
511
+ @traced_action(action="report.export", kind="job", actor="worker")
512
+ def export_report(user_id: int, correlation_id: str = None):
513
+ # correlation_id is picked up automatically by @traced_action's
514
+ # correlation_id kwarg only if passed through explicitly:
515
+ ...
516
+ ```
517
+
518
+ `@traced_action`'s `correlation_id` parameter isn't populated from task kwargs automatically — pass it explicitly so the decorator sees it:
519
+
520
+ ```python
521
+ @shared_task(name="export_report")
522
+ def export_report(user_id: int, correlation_id: str = None):
523
+ _export_report(user_id, correlation_id=correlation_id)
524
+
525
+ @traced_action(action="report.export", kind="job", actor="worker")
526
+ def _export_report(user_id: int, correlation_id: str = None):
527
+ ...
528
+ ```
529
+
530
+ **RQ:**
531
+
532
+ ```python
533
+ from rq import Queue
534
+ from traceact import traced_action
535
+
536
+ @traced_action(action="report.export", kind="job", actor="worker")
537
+ def export_report(user_id: int, correlation_id: str = None):
538
+ ...
539
+
540
+ queue.enqueue(export_report, user_id=42, correlation_id="corr_abc123")
541
+ ```
542
+
543
+ **Why this can't be automatic:** `contextvars.ContextVar` (which powers automatic parent/child linking within one process) does not cross a queue boundary — the worker that picks up the job runs in a different process with a fresh, empty context. The correlation ID has to travel as ordinary job data, the same way you'd pass any other argument the job needs.
544
+
545
+ Traces from the enqueue side and the worker side won't share a `parent_trace_id` (they're different processes, so there's no shared ContextVar to link them), but they will share `correlation_id`. The viewer's search box doesn't currently filter by `correlation_id` (it matches on action, kind, status, and touched targets); to pull together one job's traces today, use "Copy JSON" on a trace to get its `correlation_id`, then `grep` or `jq` the JSONL file for it: `jq 'select(.correlation_id == "corr_abc123")' data/traces/traces.jsonl`.
546
+
547
+ ---
548
+
396
549
  ## Input capture
397
550
 
398
- By default, `@traced_action` doesn't capture function arguments.
551
+ By default, `@traced_action` doesn't capture function arguments — `capture_inputs` defaults to `None`, which means "defer to the package-level setting from `configure()`, or no capture if that isn't set either."
399
552
 
400
553
  **Capture selected fields (recommended):**
401
554
 
@@ -409,7 +562,7 @@ def create_note(title, body, user_id):
409
562
  ...
410
563
  ```
411
564
 
412
- **Capture all arguments:**
565
+ **Capture all arguments (per decorator):**
413
566
 
414
567
  ```python
415
568
  @traced_action(action="note.create", kind="app", capture_inputs=True)
@@ -417,7 +570,19 @@ def create_note(title, body, user_id):
417
570
  ...
418
571
  ```
419
572
 
420
- **Global kill switch** (overrides all decorator-level settings):
573
+ **Capture all arguments (package-wide default):**
574
+
575
+ ```python
576
+ configure(config=TraceConfig(capture_inputs=True))
577
+
578
+ @traced_action(action="note.create", kind="app") # no capture_inputs needed here
579
+ def create_note(title, body, user_id):
580
+ ...
581
+ ```
582
+
583
+ `capture_inputs=` on the decorator is shorthand for `config=TraceConfig(capture_inputs=...)` — both resolve through the same package-default → `configure()` → decorator-override chain, so a package-level default set via `configure()` is honoured by any decorator that doesn't explicitly override it.
584
+
585
+ **Global kill switch** (cannot be re-enabled by any decorator, not even one that explicitly passes `capture_inputs=True`):
421
586
 
422
587
  ```python
423
588
  configure(config=TraceConfig(capture_inputs=False))
@@ -427,12 +592,59 @@ configure(config=TraceConfig(capture_inputs=False))
427
592
  **What TraceAct does when capture is enabled:**
428
593
  1. Maps positional args to parameter names using `inspect.signature()`.
429
594
  2. Skips `self` and `cls`.
430
- 3. Redacts arguments whose names match sensitive patterns: `password`, `passwd`, `secret`, `token`, `api_key`, `apikey`, `private_key`, `privatekey`, `access_key`, `accesskey`, `auth`, `credential`, `credentials`, `credit_card`, `card_number`, `cvv`, `ssn`.
431
- 4. Truncates values larger than `max_payload_bytes`.
432
- 5. Converts non-serialisable types to `[TypeName]`.
595
+ 3. Redacts arguments whose names match sensitive patterns: `password`, `passwd`, `pwd`, `secret`, `token`, `api_key`, `apikey`, `private_key`, `privatekey`, `access_key`, `accesskey`, `auth`, `credential`, `credentials`, `credit_card`, `card_number`, `cvv`, `ssn`.
596
+ 4. Recurses into nested dicts and lists-of-dicts, so a sensitive field buried inside a request body or config object is also redacted — not just top-level keys (see below).
597
+ 5. Truncates values larger than `max_payload_bytes`.
598
+ 6. Converts non-serialisable types to `[TypeName]`.
433
599
 
434
600
  **`trace.input()` always works** regardless of the `capture_inputs` setting.
435
601
 
602
+ ### Redaction presets
603
+
604
+ The pattern list above is always on. Layer additional field-name patterns on top of it with `redaction_presets`:
605
+
606
+ ```python
607
+ from traceact import configure, TraceConfig, REDACTION_PRESETS
608
+
609
+ print(sorted(REDACTION_PRESETS))
610
+ # ['api_keys', 'env_vars', 'filesystem_paths', 'http']
611
+
612
+ configure(config=TraceConfig(redaction_presets=["filesystem_paths", "env_vars"]))
613
+ ```
614
+
615
+ | Preset | Extra patterns |
616
+ |---|---|
617
+ | `"api_keys"` | `jwt`, `bearer`, `signing_key`, `encryption_key`, `hmac_key`, `master_key` |
618
+ | `"http"` | `cookie`, `set_cookie`, `session_id`, `csrf_token`, `x_forwarded_for`, `remote_addr`, `client_ip` |
619
+ | `"filesystem_paths"` | `path`, `filepath`, `file_path`, `dir`, `directory`, `workdir`, `cwd`, `home_dir`, `homedir` |
620
+ | `"env_vars"` | `env`, `environ`, `environment`, `envvar`, `env_var`, `dotenv` |
621
+
622
+ An unknown preset name raises `ValueError` immediately at `TraceConfig(...)` construction, not later at trace time.
623
+
624
+ 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:
625
+
626
+ ```python
627
+ @traced_action(action="report.export", kind="app",
628
+ config=TraceConfig(redaction_presets=["api_keys"]))
629
+ def export_report(...):
630
+ ...
631
+ ```
632
+
633
+ **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.
634
+
635
+ **Nested redaction example:**
636
+
637
+ ```python
638
+ trace.input({
639
+ "request": {
640
+ "headers": {"authorization": "Bearer abc123"},
641
+ "body": {"user_id": 42},
642
+ },
643
+ })
644
+ # stored as:
645
+ # {"request": {"headers": {"authorization": "[redacted]"}, "body": {"user_id": 42}}}
646
+ ```
647
+
436
648
  ---
437
649
 
438
650
  ## Sinks
@@ -448,6 +660,14 @@ JsonlSink("data/traces/traces.jsonl")
448
660
  JsonlSink("/absolute/path/to/traces.jsonl")
449
661
  ```
450
662
 
663
+ **Rotation (`max_bytes`):** by default the file grows without limit. Pass `max_bytes` to cap the active file's size — once the next write would exceed it, the current file is renamed to `<path>.<UTC timestamp>` and a fresh file starts at `path`:
664
+
665
+ ```python
666
+ JsonlSink("data/traces/traces.jsonl", max_bytes=50_000_000) # cap ~50 MB per file
667
+ ```
668
+
669
+ Rotation renames rather than deletes, so history isn't lost — it's just no longer at `path`. Point the viewer at the containing **folder** rather than the single file to see the active file plus every rotated segment merged together: `traceact view data/traces/`. TraceAct doesn't currently delete old rotated segments on a schedule; clean them up yourself (e.g. a cron job or a retention script) if disk usage matters.
670
+
451
671
  ### ConsoleSink
452
672
 
453
673
  Prints traces to stdout.
@@ -592,6 +812,13 @@ def test_create_note():
592
812
  reset_config()
593
813
  ```
594
814
 
815
+ **TraceAct's own test suite** lives in `tests/` at the repo root and follows this exact pattern — a `_clean_config` autouse fixture in `tests/conftest.py` calls `reset_config()` before and after every test. Run it with:
816
+
817
+ ```bash
818
+ pip install -e ".[dev]"
819
+ pytest
820
+ ```
821
+
595
822
  ---
596
823
 
597
824
  ## Trace record schema
@@ -704,6 +931,34 @@ The viewer reads any line that parses as JSON and looks like a trace; malformed
704
931
 
705
932
  You can also run it as a module: `python -m traceact.viewer.cli view SOURCE`.
706
933
 
934
+ ### Health checks (`traceact doctor`)
935
+
936
+ ```bash
937
+ traceact doctor [SOURCE]
938
+ ```
939
+
940
+ Runs a handful of local checks and prints a pass/fail report — useful when tracing "isn't working" and you want to rule out setup problems before debugging your own code:
941
+
942
+ - Python version meets the 3.9 minimum
943
+ - the `~/.traceact` state directory exists and is writable (single-instance coordination and drag-drop imports depend on this)
944
+ - whether a viewer is currently running (informational only — `doctor` doesn't require one)
945
+ - if `SOURCE` is given: that the path exists and its lines parse as valid trace records
946
+
947
+ ```
948
+ $ traceact doctor data/traces/traces.jsonl
949
+ traceact doctor
950
+
951
+ ✓ Python 3.11 (OK, 3.9+ required)
952
+ · traceact 0.3.0
953
+ ✓ State directory (/Users/you/.traceact) is writable
954
+ · No viewer currently running (not required).
955
+ ✓ data/traces/traces.jsonl: 42/42 line(s) look like valid traces across 1 file(s)
956
+
957
+ All checks passed.
958
+ ```
959
+
960
+ Exits `0` if every check that can fail passed, `1` otherwise. A missing running viewer is never treated as a failure.
961
+
707
962
  ### Single-instance behaviour
708
963
 
709
964
  Running `traceact view` a second time — even for a different source — reuses a viewer that is already running rather than spawning a second server. The new source is added to the running viewer and a browser tab is opened on it. This avoids accumulating background processes across repeated launches during a dev session.
@@ -734,10 +989,14 @@ The Terminal window stays open so Ctrl+C stops the viewer cleanly. To pass a sou
734
989
 
735
990
  ### What the viewer shows
736
991
 
737
- - **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, 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.
738
- - **Trace inspector** — selecting a trace shows its own ID, its parent and root trace IDs (when it is a child trace), kind, duration, and touch/error counts. "Copy JSON" copies the full record.
992
+ - **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.
993
+ - **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.
739
994
  - **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.
740
- - **Settings** — accent colour, display density, default trace view, row count, and default replay speed — all persisted to `localStorage`.
995
+ - **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.
996
+
997
+ ### Run diagnostics (Settings)
998
+
999
+ The Settings page has a "Run diagnostics" button that runs the exact same checks as `traceact doctor` on the command line, via `GET /api/doctor` — Python version, state directory writability, whether a viewer is running, and (if a source is loaded) whether its trace data looks valid. Results appear as a checklist with a short progress indicator, and each failing check shows a one-line explanation of what it means and what to do about it. Useful when something isn't showing up in the log and you want to rule out a setup problem without opening a terminal.
741
1000
 
742
1001
  ### Viewer server API
743
1002
 
@@ -745,13 +1004,16 @@ These endpoints are available while a viewer is running. Apps and scripts can ca
745
1004
 
746
1005
  | Method | Path | Request | Response |
747
1006
  |---|---|---|---|
748
- | `GET` | `/api/health` | — | `{"status":"ok","version":"0.2.1","sources":N}` |
1007
+ | `GET` | `/api/health` | — | `{"status":"ok","version":"0.3.0","sources":N}` |
1008
+ | `GET` | `/api/doctor?source=` | — | `{"ok":bool,"version":"...","checks":[{"label","status","message","hint"?}]}` |
749
1009
  | `GET` | `/api/sources` | — | `[{"name":"...","path":"..."}]` |
750
1010
  | `POST` | `/api/sources` | `{"path":"..."}` | `{"name":"...","path":"..."}` |
751
1011
  | `GET` | `/api/pick?type=file\|folder` | — | `{"path":"...","cancelled":bool}` |
752
1012
  | `POST` | `/api/import` | `{"name":"file.jsonl","content":"..."}` | `{"name":"...","path":"...","imported":true}` |
753
1013
  | `GET` | `/api/stream?source=NAME&limit=N` | — | SSE stream: `snapshot` then `append` events |
754
1014
 
1015
+ `/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.
1016
+
755
1017
  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.
756
1018
 
757
1019
  ### Notes
@@ -843,6 +1105,33 @@ async function openViewer() {
843
1105
 
844
1106
  Note: cross-origin `fetch` to the viewer will always be blocked by CORS unless the viewer adds an `Access-Control-Allow-Origin` header (it doesn't, by design). Use `mode: "no-cors"` only to warm up the connection; don't try to read the response. The backend-route approach above is cleaner because it doesn't depend on the client being on the same machine as the viewer.
845
1107
 
1108
+ ### Admin-safe viewer pattern
1109
+
1110
+ **The viewer has no authentication of its own** — no login, no token, no session check. This is intentional: it's a local dev tool, and adding auth would mean TraceAct owning credential storage, which it explicitly doesn't want to do. That means access control is entirely your responsibility if more than one trusted person can reach the machine it runs on.
1111
+
1112
+ **Rule of thumb:** never bind the viewer to a non-localhost host (`--host 0.0.0.0` or similar) unless you put your own authentication in front of it. Binding `127.0.0.1` (the default) means only processes on the same machine can reach it, which is the safe default for local development.
1113
+
1114
+ If you want a "traceact viewer" button inside an internal team tool (like the FastAPI/Flask examples above), gate the *button and the route*, not the viewer itself:
1115
+
1116
+ ```python
1117
+ # The route that launches/connects to the viewer sits behind your app's
1118
+ # own admin-only auth dependency — the viewer itself stays on localhost
1119
+ # and is never exposed directly.
1120
+ from fastapi import Depends
1121
+ from traceact.viewer.instance import launch_or_connect
1122
+
1123
+ @router.get("/api/launch-viewer")
1124
+ async def launch_viewer(user=Depends(require_admin)):
1125
+ loop = asyncio.get_event_loop()
1126
+ url = await loop.run_in_executor(None, launch_or_connect,
1127
+ "data/traces/traces.jsonl")
1128
+ return {"url": url}
1129
+ ```
1130
+
1131
+ Because `launch_or_connect` starts the viewer on `127.0.0.1` by default, the returned URL only works for someone on the same machine as the server process — appropriate for a local dev box, not for handing a link to a remote teammate. If your team needs to view traces from a shared/remote environment, don't punch the viewer through to the network; instead have each person run `traceact view` against a synced or shared copy of the trace file on their own machine, or wait for a future TraceAct release aimed at that use case (see Notes above — network-exposed viewing is explicitly out of scope for the current local-only design).
1132
+
1133
+ **Trace data itself may be sensitive.** Traces can contain request bodies, resource identifiers, and (if `capture_inputs` is misconfigured) unredacted arguments. Treat a running viewer, and the JSONL files it reads, with the same access discipline as application logs.
1134
+
846
1135
  ---
847
1136
 
848
1137
  ## Quick reference
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "traceact"
7
- version = "0.2.1"
7
+ version = "0.3.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"
@@ -57,3 +57,7 @@ include = ["traceact*"]
57
57
  # Ship the viewer's static frontend (HTML/CSS/JS) inside the wheel.
58
58
  [tool.setuptools.package-data]
59
59
  "traceact.viewer" = ["static/*"]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
63
+ asyncio_mode = "auto"