stemtrace 0.3.8__tar.gz → 0.3.10__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 (175) hide show
  1. {stemtrace-0.3.8 → stemtrace-0.3.10}/.bumpversion.toml +3 -3
  2. stemtrace-0.3.10/.coderabbit.yaml +92 -0
  3. {stemtrace-0.3.8 → stemtrace-0.3.10}/.dockerignore +1 -0
  4. {stemtrace-0.3.8 → stemtrace-0.3.10}/.github/workflows/ci.yml +10 -0
  5. {stemtrace-0.3.8 → stemtrace-0.3.10}/.github/workflows/release.yml +7 -1
  6. {stemtrace-0.3.8 → stemtrace-0.3.10}/.gitignore +1 -0
  7. {stemtrace-0.3.8 → stemtrace-0.3.10}/.pre-commit-config.yaml +6 -0
  8. {stemtrace-0.3.8 → stemtrace-0.3.10}/CHANGELOG.md +18 -1
  9. {stemtrace-0.3.8 → stemtrace-0.3.10}/CONTRIBUTING.md +36 -3
  10. {stemtrace-0.3.8 → stemtrace-0.3.10}/Dockerfile +3 -1
  11. {stemtrace-0.3.8 → stemtrace-0.3.10}/Makefile +2 -1
  12. {stemtrace-0.3.8 → stemtrace-0.3.10}/PKG-INFO +78 -2
  13. {stemtrace-0.3.8 → stemtrace-0.3.10}/README.md +76 -1
  14. {stemtrace-0.3.8 → stemtrace-0.3.10}/pyproject.toml +78 -1
  15. {stemtrace-0.3.8 → stemtrace-0.3.10}/scripts/wait_for_http.py +0 -1
  16. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/__init__.py +16 -3
  17. stemtrace-0.3.10/src/stemtrace/_version.py +8 -0
  18. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/core/events.py +3 -0
  19. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/core/graph.py +278 -264
  20. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/library/config.py +8 -0
  21. stemtrace-0.3.10/src/stemtrace/library/output_capture.py +536 -0
  22. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/library/scrubbing.py +3 -4
  23. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/library/signals.py +63 -0
  24. stemtrace-0.3.10/src/stemtrace/library/transports/__init__.py +24 -0
  25. stemtrace-0.3.10/src/stemtrace/library/transports/breaker.py +5 -0
  26. stemtrace-0.3.10/src/stemtrace/library/transports/memory.py +5 -0
  27. stemtrace-0.3.10/src/stemtrace/library/transports/rabbitmq.py +29 -0
  28. stemtrace-0.3.10/src/stemtrace/library/transports/redis.py +23 -0
  29. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/__main__.py +2 -1
  30. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/api/routes.py +232 -129
  31. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/api/schemas.py +1 -0
  32. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/consumer.py +30 -12
  33. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/fastapi/form_auth.py +28 -10
  34. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/fastapi/form_auth_middleware.py +54 -21
  35. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/fastapi/router.py +4 -4
  36. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/package-lock.json +2 -2
  37. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/package.json +1 -1
  38. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/playwright.config.ts +9 -6
  39. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/api/client.ts +2 -0
  40. stemtrace-0.3.10/src/stemtrace/server/ui/frontend/src/components/TaskOutput.tsx +118 -0
  41. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +38 -10
  42. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +4 -0
  43. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +2 -0
  44. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/header.spec.ts +50 -1
  45. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +260 -39
  46. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/static.py +1 -1
  47. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/websocket.py +4 -2
  48. {stemtrace-0.3.8/src/stemtrace/library → stemtrace-0.3.10/src/stemtrace}/transports/__init__.py +12 -8
  49. {stemtrace-0.3.8/src/stemtrace/library → stemtrace-0.3.10/src/stemtrace}/transports/rabbitmq.py +139 -96
  50. {stemtrace-0.3.8/src/stemtrace/library → stemtrace-0.3.10/src/stemtrace}/transports/redis.py +119 -63
  51. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/e2e/test_api_e2e.py +1 -2
  52. stemtrace-0.3.10/tests/integration/output_worker_app.py +102 -0
  53. stemtrace-0.3.10/tests/integration/test_output_capture_worker.py +251 -0
  54. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_rabbitmq_publish.py +9 -5
  55. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_rabbitmq_transport.py +18 -19
  56. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_redis_resilience_docker.py +1 -1
  57. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_redis_transport.py +3 -3
  58. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_static_ui.py +29 -0
  59. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_workers_refresh.py +1 -1
  60. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_consumer.py +95 -1
  61. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_events.py +38 -0
  62. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_form_auth.py +68 -2
  63. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_form_login.py +18 -1
  64. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_graph.py +398 -0
  65. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_init.py +36 -4
  66. stemtrace-0.3.10/tests/unit/test_library_transports_alias.py +77 -0
  67. stemtrace-0.3.10/tests/unit/test_output_capture.py +1080 -0
  68. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_publish_breaker.py +1 -1
  69. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_rabbitmq_publish.py +158 -146
  70. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_redis_resilience.py +91 -1
  71. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_routes.py +72 -3
  72. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_signals.py +90 -2
  73. stemtrace-0.3.10/tests/unit/test_signals_output.py +364 -0
  74. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_store.py +1 -1
  75. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_transports.py +112 -61
  76. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_websocket.py +25 -0
  77. {stemtrace-0.3.8 → stemtrace-0.3.10}/uv.lock +139 -2
  78. {stemtrace-0.3.8 → stemtrace-0.3.10}/.github/dependabot.yml +0 -0
  79. {stemtrace-0.3.8 → stemtrace-0.3.10}/.github/workflows/e2e.yml +0 -0
  80. {stemtrace-0.3.8 → stemtrace-0.3.10}/.python-version +0 -0
  81. {stemtrace-0.3.8 → stemtrace-0.3.10}/Dockerfile.e2e +0 -0
  82. {stemtrace-0.3.8 → stemtrace-0.3.10}/LICENSE +0 -0
  83. {stemtrace-0.3.8 → stemtrace-0.3.10}/build_ui.py +0 -0
  84. {stemtrace-0.3.8 → stemtrace-0.3.10}/docker-compose.e2e.rabbitmq.yml +0 -0
  85. {stemtrace-0.3.8 → stemtrace-0.3.10}/docker-compose.e2e.yml +0 -0
  86. {stemtrace-0.3.8 → stemtrace-0.3.10}/docker-compose.rabbitmq.yml +0 -0
  87. {stemtrace-0.3.8 → stemtrace-0.3.10}/docker-compose.yml +0 -0
  88. {stemtrace-0.3.8 → stemtrace-0.3.10}/docs/brand/stemtrace-mark-light.svg +0 -0
  89. {stemtrace-0.3.8 → stemtrace-0.3.10}/docs/brand/stemtrace-mark.svg +0 -0
  90. {stemtrace-0.3.8 → stemtrace-0.3.10}/docs/screenshots/task_details.png +0 -0
  91. {stemtrace-0.3.8 → stemtrace-0.3.10}/docs/screenshots/unregistered.png +0 -0
  92. {stemtrace-0.3.8 → stemtrace-0.3.10}/docs/screenshots/workflow.png +0 -0
  93. {stemtrace-0.3.8 → stemtrace-0.3.10}/examples/celery_app.py +0 -0
  94. {stemtrace-0.3.8 → stemtrace-0.3.10}/examples/fastapi_integration.py +0 -0
  95. {stemtrace-0.3.8 → stemtrace-0.3.10}/examples/with_auth.py +0 -0
  96. {stemtrace-0.3.8 → stemtrace-0.3.10}/examples/with_login.py +0 -0
  97. {stemtrace-0.3.8 → stemtrace-0.3.10}/progress.md +0 -0
  98. {stemtrace-0.3.8 → stemtrace-0.3.10}/scripts/changelog_section.py +0 -0
  99. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/core/__init__.py +0 -0
  100. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/core/exceptions.py +0 -0
  101. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/core/ports.py +0 -0
  102. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/library/__init__.py +0 -0
  103. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/library/bootsteps.py +0 -0
  104. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/py.typed +0 -0
  105. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/__init__.py +0 -0
  106. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/api/__init__.py +0 -0
  107. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/api/websocket.py +0 -0
  108. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/fastapi/__init__.py +0 -0
  109. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/fastapi/auth.py +0 -0
  110. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/fastapi/extension.py +0 -0
  111. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/fastapi/login_routes.py +0 -0
  112. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/store.py +0 -0
  113. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/__init__.py +0 -0
  114. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/biome.json +0 -0
  115. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/index.html +0 -0
  116. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
  117. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
  118. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
  119. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
  120. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/LogoutButton.tsx +0 -0
  121. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/SessionRejectedNotice.tsx +0 -0
  122. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/SproutMark.tsx +0 -0
  123. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
  124. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
  125. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
  126. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
  127. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
  128. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
  129. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
  130. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
  131. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
  132. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
  133. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
  134. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
  135. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
  136. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
  137. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
  138. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
  139. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
  140. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/auth.spec.ts +0 -0
  141. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
  142. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
  143. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
  144. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
  145. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
  146. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
  147. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
  148. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
  149. {stemtrace-0.3.8 → stemtrace-0.3.10}/src/stemtrace/server/ui/templates/login.html +0 -0
  150. {stemtrace-0.3.8/src/stemtrace/library → stemtrace-0.3.10/src/stemtrace}/transports/breaker.py +0 -0
  151. {stemtrace-0.3.8/src/stemtrace/library → stemtrace-0.3.10/src/stemtrace}/transports/memory.py +0 -0
  152. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/__init__.py +0 -0
  153. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/conftest.py +0 -0
  154. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/e2e/__init__.py +0 -0
  155. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/e2e/conftest.py +0 -0
  156. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/e2e/tasks.py +0 -0
  157. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/e2e/test_workers_e2e.py +0 -0
  158. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/__init__.py +0 -0
  159. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_embedded_static.py +0 -0
  160. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_fastapi_integration.py +0 -0
  161. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/integration/test_wait_for_http_redirects.py +0 -0
  162. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/__init__.py +0 -0
  163. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_auth.py +0 -0
  164. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_bootsteps.py +0 -0
  165. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_build_ui_hook.py +0 -0
  166. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_changelog_section_script.py +0 -0
  167. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_cli.py +0 -0
  168. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_cli_form_login.py +0 -0
  169. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_config.py +0 -0
  170. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_examples.py +0 -0
  171. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_exceptions.py +0 -0
  172. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_favicon.py +0 -0
  173. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_scrubbing.py +0 -0
  174. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_ui_static_prefix.py +0 -0
  175. {stemtrace-0.3.8 → stemtrace-0.3.10}/tests/unit/test_wait_for_http_script.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.3.8"
2
+ current_version = "0.3.10"
3
3
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
4
4
  serialize = ["{major}.{minor}.{patch}"]
5
5
  commit = true
@@ -12,9 +12,9 @@ filename = "pyproject.toml"
12
12
  search = 'version = "{current_version}"'
13
13
  replace = 'version = "{new_version}"'
14
14
 
15
- # Python __version__ in package __init__.py
15
+ # Python __version__ (leaf module, re-exported by the package __init__.py)
16
16
  [[tool.bumpversion.files]]
17
- filename = "src/stemtrace/__init__.py"
17
+ filename = "src/stemtrace/_version.py"
18
18
  search = '__version__ = "{current_version}"'
19
19
  replace = '__version__ = "{new_version}"'
20
20
 
@@ -0,0 +1,92 @@
1
+ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2
+ # CodeRabbit review settings. The rules behind these instructions are summarized
3
+ # in CONTRIBUTING.md; ruff, mypy and import-linter already run in CI, so reviews
4
+ # should focus on what tools can't check.
5
+ language: en-US
6
+ tone_instructions: >-
7
+ Be concise and specific. Skip praise and style nits that ruff or Biome already
8
+ enforce. Lead with correctness, boundaries and test gaps.
9
+
10
+ reviews:
11
+ profile: chill
12
+ high_level_summary: true
13
+ poem: false
14
+ in_progress_fortune: false
15
+ sequence_diagrams: false
16
+ collapse_walkthrough: true
17
+ path_filters:
18
+ - "!uv.lock"
19
+ - "!**/package-lock.json"
20
+ - "!src/stemtrace/server/ui/frontend/src/routeTree.gen.ts"
21
+ - "!src/stemtrace/server/ui/frontend/dist/**"
22
+ # Most specific paths first, the general Python entry last. The server and
23
+ # general globs match only *.py, so frontend files get just the frontend entry.
24
+ path_instructions:
25
+ - path: "src/stemtrace/core/**"
26
+ instructions: |
27
+ Domain layer: pure Python plus Pydantic only. Flag any I/O (network, files,
28
+ environment, clocks used for side effects) and any import of
29
+ stemtrace.library, stemtrace.server, Celery, Redis, kombu or FastAPI.
30
+ Events are immutable (frozen=True). Synthetic GROUP/CHORD nodes have
31
+ empty `events`; new graph code must handle that.
32
+ - path: "src/stemtrace/library/**"
33
+ instructions: |
34
+ Runs inside Celery workers. Imports core only, never stemtrace.server.
35
+ Publishing is fire-and-forget: it must never raise into or block a task,
36
+ and a failing broker must stay cheap on repeated events (circuit breaker
37
+ or backoff, never retry per event). Log volume must stay bounded during
38
+ an outage. Check shared state under prefork, threads and gevent/eventlet,
39
+ and fork safety of module-level locks. Per-task state belongs in a
40
+ ContextVar.
41
+ - path: "src/stemtrace/server/ui/frontend/**"
42
+ instructions: |
43
+ React 19 + TypeScript strict + TanStack Router/Query + @xyflow/react.
44
+ No `any`, explicit return types on exported functions, props interfaces
45
+ for components. routeTree.gen.ts is generated and must not be edited by
46
+ hand. Graph or UI behavior changes need Playwright coverage in tests/.
47
+ - path: "src/stemtrace/server/**/*.py"
48
+ instructions: |
49
+ Web server and consumer. Imports core only, never stemtrace.library. The
50
+ server is read-only toward the broker and the user's Celery app. Check
51
+ that API response schema changes are reflected in the frontend types and
52
+ tests, that auth dependencies apply to new routes, and that background
53
+ loops stop within a bounded time.
54
+ - path: "pyproject.toml"
55
+ instructions: |
56
+ Flag any new per-file-ignores entry for the design rules (C90, PLR09xx,
57
+ BLE, ERA, RET, PT) and any added import-linter ignore_imports entry:
58
+ the baseline only shrinks. New dependencies need a stated reason.
59
+ - path: "tests/**"
60
+ instructions: |
61
+ - A new test should fail on the old code with an assertion error that is
62
+ the bug, not an ImportError.
63
+ - Assert exact values, sequences and log counts, not `is not None`.
64
+ - Prefer fakes that model the real library semantics over
65
+ unittest.mock; a fake that always succeeds proves nothing.
66
+ - No real sleeps in unit tests: inject the clock and sleep function, and
67
+ bound loops so a regression fails instead of hanging.
68
+ - New `# noqa` comments need a reason after the codes; bare ones are
69
+ the pre-existing baseline and only shrink.
70
+ - Imports at the top of the file. Names follow
71
+ test_<action>_<condition>_<result>. Tests needing Redis or RabbitMQ
72
+ are marked integration; Docker-based ones e2e.
73
+ - path: "src/stemtrace/**/*.py"
74
+ instructions: |
75
+ - Does each new class or function hide a decision or remove work from its
76
+ callers? Flag pass-through wrappers and options with no current caller.
77
+ - Constants, timeouts and limits are defined once and imported, not copied.
78
+ - Every blocking call (connect, read, close, join, wait, lock) needs an
79
+ explicit timeout smaller than its caller's.
80
+ - Broad `except Exception` only at fire-and-forget boundaries, with a
81
+ `# noqa: BLE001 <reason>` comment and logging with context.
82
+ - Comments explain why or state an invariant. Comments about third-party
83
+ behavior should name the library version and function.
84
+ - Behavior claims in docstrings, README and CHANGELOG must be backed by a
85
+ test. Changes to user-visible behavior should be opt-in.
86
+ - Bare `# noqa: <codes>` comments are the baseline of code that predates
87
+ the lint rules; it only shrinks. Flag a newly added bare noqa: a new
88
+ suppression must give a reason after the codes, and a refactor
89
+ should delete the noqa it makes unnecessary.
90
+
91
+ chat:
92
+ art: false
@@ -39,6 +39,7 @@ ENV/
39
39
  .pytest_cache/
40
40
  .mypy_cache/
41
41
  .ruff_cache/
42
+ .import_linter_cache/
42
43
  .coverage
43
44
  htmlcov/
44
45
  .tox/
@@ -15,6 +15,7 @@ jobs:
15
15
  lint:
16
16
  if: github.event_name == 'pull_request'
17
17
  runs-on: ubuntu-latest
18
+ timeout-minutes: 10
18
19
  env:
19
20
  UV_PYTHON: "3.12"
20
21
  STEMTRACE_SKIP_UI_BUILD: "1"
@@ -40,9 +41,13 @@ jobs:
40
41
  - name: Check formatting
41
42
  run: uv run --locked --no-build ruff format --check src/ tests/
42
43
 
44
+ - name: Check import contracts
45
+ run: uv run --locked --no-build lint-imports
46
+
43
47
  type-check:
44
48
  if: github.event_name == 'pull_request'
45
49
  runs-on: ubuntu-latest
50
+ timeout-minutes: 10
46
51
  env:
47
52
  UV_PYTHON: "3.12"
48
53
  STEMTRACE_SKIP_UI_BUILD: "1"
@@ -68,6 +73,7 @@ jobs:
68
73
  test:
69
74
  if: github.event_name == 'pull_request'
70
75
  runs-on: ubuntu-latest
76
+ timeout-minutes: 15
71
77
  env:
72
78
  UV_PYTHON: ${{ matrix.python-version }}
73
79
  STEMTRACE_SKIP_UI_BUILD: "1"
@@ -104,6 +110,7 @@ jobs:
104
110
  audit:
105
111
  name: Audit runtime dependencies
106
112
  runs-on: ubuntu-latest
113
+ timeout-minutes: 10
107
114
  env:
108
115
  UV_PYTHON: "3.12"
109
116
  steps:
@@ -128,6 +135,7 @@ jobs:
128
135
  frontend:
129
136
  if: github.event_name == 'pull_request'
130
137
  runs-on: ubuntu-latest
138
+ timeout-minutes: 10
131
139
  defaults:
132
140
  run:
133
141
  working-directory: src/stemtrace/server/ui/frontend
@@ -153,6 +161,7 @@ jobs:
153
161
  build:
154
162
  if: github.event_name == 'pull_request'
155
163
  runs-on: ubuntu-latest
164
+ timeout-minutes: 15
156
165
  env:
157
166
  UV_PYTHON: "3.12"
158
167
  needs: [lint, type-check, test, frontend]
@@ -200,6 +209,7 @@ jobs:
200
209
  # actual default-branch commit after merges (or direct pushes / releases).
201
210
  if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
202
211
  runs-on: ubuntu-latest
212
+ timeout-minutes: 15
203
213
  env:
204
214
  UV_PYTHON: "3.12"
205
215
  STEMTRACE_SKIP_UI_BUILD: "1"
@@ -8,6 +8,7 @@ on:
8
8
  jobs:
9
9
  build:
10
10
  runs-on: ubuntu-latest
11
+ timeout-minutes: 15
11
12
  env:
12
13
  UV_PYTHON: "3.12"
13
14
  outputs:
@@ -87,8 +88,11 @@ jobs:
87
88
  path: dist/
88
89
 
89
90
  pypi:
90
- needs: build
91
+ # After the image: a failed or hung docker build then publishes nothing,
92
+ # instead of leaving PyPI ahead of GHCR and the GitHub Release.
93
+ needs: [build, docker]
91
94
  runs-on: ubuntu-latest
95
+ timeout-minutes: 10
92
96
  environment: pypi
93
97
  permissions:
94
98
  id-token: write
@@ -105,6 +109,7 @@ jobs:
105
109
  docker:
106
110
  needs: build
107
111
  runs-on: ubuntu-latest
112
+ timeout-minutes: 25
108
113
  permissions:
109
114
  contents: read
110
115
  packages: write
@@ -147,6 +152,7 @@ jobs:
147
152
  github-release:
148
153
  needs: [build, pypi, docker]
149
154
  runs-on: ubuntu-latest
155
+ timeout-minutes: 10
150
156
  permissions:
151
157
  contents: write
152
158
  steps:
@@ -14,6 +14,7 @@ htmlcov/
14
14
  # Type checking / linting
15
15
  .mypy_cache/
16
16
  .ruff_cache/
17
+ .import_linter_cache/
17
18
 
18
19
  # Virtual environments
19
20
  .venv/
@@ -30,3 +30,9 @@ repos:
30
30
  language: system
31
31
  types: [python]
32
32
  pass_filenames: false
33
+ - id: lint-imports
34
+ name: import contracts
35
+ entry: uv run lint-imports
36
+ language: system
37
+ types: [python]
38
+ pass_filenames: false
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.10] - 2026-10-11
11
+
12
+ ### Changed
13
+ - Docker image: the UI is built once on the build machine's native platform instead of under emulation for each architecture, which also makes release builds faster. Building the image from source now needs BuildKit (the default since Docker 23 and in `docker compose`).
14
+ - Development: ruff now also enforces complexity, function-size, blind-`except` and dead-code rules, and [import-linter](https://import-linter.readthedocs.io/) checks the `core`/`library`/`server` import boundaries in `make lint` and CI (see CONTRIBUTING.md). No runtime change.
15
+ - The broker transports moved from `stemtrace.library.transports` to `stemtrace.transports`, so the Celery integration and the server share them without importing each other. The old module paths (`stemtrace.library.transports` and its `redis`, `rabbitmq`, `memory` and `breaker` submodules) still import the same public classes and functions but emit a `DeprecationWarning`, and will be removed in 1.0; patching module internals through the old paths (e.g. `mock.patch("stemtrace.library.transports.redis.time.sleep")`) no longer reaches the real code, so update such patches to `stemtrace.transports`. The transports' loggers are renamed to match (e.g. `stemtrace.transports.rabbitmq`); logging configured for the `stemtrace` logger is unaffected.
16
+
17
+ ### Fixed
18
+ - Redis: a stream entry whose payload is valid JSON but not an object (e.g. `42`) is now skipped with a warning; it used to end the consumer's read loop and trigger a restart.
19
+
20
+ ## [0.3.9] - 2026-10-10
21
+
22
+ ### Added
23
+ - Task output: with `init_worker(app, capture_output=True)`, what each task attempt prints and logs is shown on its task page, one block per attempt (latest first). Off by default because output isn't scrubbed; each attempt keeps the last `max_output_size` bytes (default 16 KB). Works with prefork, thread and gevent pools (thanks @JonatanMartens for the original implementation — #49; requested by @ohadmata — #47).
24
+
10
25
  ## [0.3.8] - 2026-10-10
11
26
 
12
27
  ### Fixed
@@ -181,7 +196,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
181
196
  - **E2E test suite**: Docker API tests + Playwright browser tests
182
197
  - **Comprehensive test suite**: 350+ Python tests, 90%+ coverage
183
198
 
184
- [unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.8...HEAD
199
+ [unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.10...HEAD
200
+ [0.3.10]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.9...v0.3.10
201
+ [0.3.9]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.8...v0.3.9
185
202
  [0.3.8]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.7...v0.3.8
186
203
  [0.3.7]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.6...v0.3.7
187
204
  [0.3.6]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.5...v0.3.6
@@ -37,7 +37,7 @@ make check
37
37
  make check
38
38
 
39
39
  # Individual checks
40
- make lint # ruff check + format
40
+ make lint # ruff check + format + import contracts
41
41
  make types # mypy --strict
42
42
  make test # pytest with coverage
43
43
  ```
@@ -54,6 +54,38 @@ uv run ruff check --fix src/ tests/
54
54
  uv run ruff format src/ tests/
55
55
  ```
56
56
 
57
+ ### Lint Rules and Import Contracts
58
+
59
+ Besides style, ruff enforces design limits: McCabe complexity 10 (`C90`), at most
60
+ 12 branches, 6 returns, 50 statements and 5 positional arguments per function
61
+ (`PLR09xx`; make extra parameters keyword-only), no blind `except Exception`
62
+ (`BLE`), no commented-out code (`ERA`), plus `RET` and `PT` style rules. A broad
63
+ except is accepted only at a fire-and-forget boundary, with
64
+ `# noqa: BLE001 <reason>` on that line.
65
+
66
+ [import-linter](https://import-linter.readthedocs.io/) checks the architecture
67
+ boundaries (`uv run lint-imports`, part of `make lint` and CI):
68
+
69
+ - `stemtrace.core` imports nothing from `stemtrace.library`, `stemtrace.server` or
70
+ `stemtrace.transports`, and no broker, Celery or web library (`celery`, `kombu`,
71
+ `amqp`, `redis`, `fastapi`, `starlette`, `uvicorn`, `typer`, `httpx`)
72
+ - `stemtrace.transports` imports nothing from `stemtrace.library` or
73
+ `stemtrace.server` (both of them use it)
74
+ - `stemtrace.library` does not import `stemtrace.server`
75
+ - `stemtrace.server` does not import `stemtrace.library`
76
+
77
+ **Baseline policy.** Code that predates these rules carries a per-line
78
+ `# noqa: <codes>` comment with no reason (added with `ruff check --add-noqa`).
79
+ Known import violations, if any, are listed under `ignore_imports` in
80
+ `pyproject.toml` (there are none at the moment). The baseline only shrinks:
81
+
82
+ - New code follows the rules. If a suppression is really needed, it names the
83
+ code and gives a reason: `# noqa: BLE001 fire-and-forget: <why>`. Never add a
84
+ bare `# noqa`.
85
+ - When you fix a baselined line, delete its `# noqa`. Ruff's `RUF100` fails on a
86
+ `# noqa` that no longer suppresses anything, and import-linter fails on an
87
+ `ignore_imports` entry that no longer matches, so stale entries can't linger.
88
+
57
89
  ### Type Checking
58
90
 
59
91
  All code must pass `mypy --strict`:
@@ -125,8 +157,9 @@ uv lock --upgrade
125
157
  ### Key Principles
126
158
 
127
159
  - **core/** - Pure Python, no external dependencies
128
- - **library/** - Celery integration, imports only from core/
129
- - **server/** - Web server, imports only from core/
160
+ - **transports/** - Broker adapters (Redis, RabbitMQ, memory), imports only from core/
161
+ - **library/** - Celery integration, imports only from core/ and transports/
162
+ - **server/** - Web server, imports only from core/ and transports/
130
163
  - Never import sideways (library ↔ server)
131
164
 
132
165
  ### Code Quality
@@ -2,7 +2,9 @@
2
2
  # Multi-stage build for smaller production image
3
3
 
4
4
  # Stage 1: Build frontend
5
- FROM node:22.23.3-alpine3.24@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 AS frontend-builder
5
+ # The UI build output is static files, identical for every platform, so build it
6
+ # once on the native build platform instead of under QEMU (npm ci hung there).
7
+ FROM --platform=$BUILDPLATFORM node:22.23.3-alpine3.24@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402 AS frontend-builder
6
8
 
7
9
  WORKDIR /app/frontend
8
10
  COPY src/stemtrace/server/ui/frontend/package*.json ./
@@ -30,6 +30,7 @@ exit(1) if missing else print('✓ All public APIs have docstrings') \
30
30
  lint:
31
31
  uv run ruff check src/ tests/
32
32
  uv run ruff format --check src/ tests/
33
+ uv run lint-imports
33
34
 
34
35
  format:
35
36
  uv run ruff format src/ tests/
@@ -57,7 +58,7 @@ coverage:
57
58
 
58
59
  # Clean build artifacts
59
60
  clean:
60
- rm -rf .pytest_cache .mypy_cache .ruff_cache htmlcov .coverage dist/
61
+ rm -rf .pytest_cache .mypy_cache .ruff_cache .import_linter_cache htmlcov .coverage dist/
61
62
  find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
62
63
 
63
64
  # =============================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stemtrace
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: A lightweight Celery task flow visualizer
5
5
  Project-URL: Homepage, https://github.com/iansokolskyi/stemtrace
6
6
  Project-URL: Documentation, https://github.com/iansokolskyi/stemtrace#readme
@@ -42,6 +42,7 @@ Provides-Extra: dev
42
42
  Requires-Dist: build>=1.0.0; extra == 'dev'
43
43
  Requires-Dist: bump-my-version>=0.28.0; extra == 'dev'
44
44
  Requires-Dist: hatchling>=1.20.0; extra == 'dev'
45
+ Requires-Dist: import-linter>=2.0; extra == 'dev'
45
46
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
46
47
  Requires-Dist: pre-commit>=3.0.0; extra == 'dev'
47
48
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
@@ -59,7 +60,7 @@ Description-Content-Type: text/markdown
59
60
 
60
61
  **Zero-infrastructure Celery task flow visualizer**
61
62
 
62
- [![PyPI version](https://img.shields.io/badge/pypi-v0.3.8-darklime)](https://pypi.org/project/stemtrace)
63
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.3.10-darklime)](https://pypi.org/project/stemtrace)
63
64
  [![Python](https://img.shields.io/pypi/pyversions/stemtrace.svg)](https://pypi.org/project/stemtrace/)
64
65
  [![CI](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml/badge.svg)](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
65
66
  [![codecov](https://codecov.io/gh/iansokolskyi/stemtrace/graph/badge.svg)](https://codecov.io/gh/iansokolskyi/stemtrace)
@@ -83,6 +84,7 @@ Stemtrace captures your task executions as a graph — visualize parent→child
83
84
  - **Task Flow Graphs** — Visualize chains, groups, and chords as interactive DAGs
84
85
  - **Execution Timeline** — Track queued → started → retried → finished states
85
86
  - **Arguments & Results** — Inspect inputs, outputs, and exceptions
87
+ - **Task Output** — See what each attempt printed and logged, next to the task (opt-in)
86
88
  - **Retry Chains** — Understand exactly when and why retries happened
87
89
 
88
90
  **Canvas Support**
@@ -253,6 +255,10 @@ stemtrace.init_worker(
253
255
  scrub_sensitive_data=True, # Scrub passwords, API keys, etc.
254
256
  additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
255
257
  safe_keys=frozenset({"public_key"}), # Never scrub these keys
258
+
259
+ # Task output (disabled by default, see "Task Output" below)
260
+ capture_output=False, # Capture what each attempt prints/logs
261
+ max_output_size=16384, # Max bytes kept per attempt (the end is kept)
256
262
  )
257
263
 
258
264
  # Introspection (after init)
@@ -271,6 +277,75 @@ By default, stemtrace scrubs common sensitive keys from task arguments:
271
277
 
272
278
  Scrubbed values appear as `[Filtered]` in the UI.
273
279
 
280
+ #### Task Output (print and logging)
281
+
282
+ Turn on `capture_output` on the worker to see what each task attempt printed and
283
+ logged on its task page, instead of searching interleaved worker logs:
284
+
285
+ ```python
286
+ stemtrace.init_worker(app, capture_output=True)
287
+ ```
288
+
289
+ ```python
290
+ from celery.utils.log import get_task_logger
291
+ import logging
292
+
293
+ task_logger = get_task_logger(__name__)
294
+ log = logging.getLogger("myapp.billing")
295
+
296
+ @app.task(bind=True, max_retries=3)
297
+ def charge(self, order_id):
298
+ print(f"charging {order_id}") # -> charging 42
299
+ task_logger.info("calling gateway") # -> INFO myapp.tasks: calling gateway
300
+ log.warning("gateway slow") # -> WARNING myapp.billing: gateway slow
301
+ ...
302
+ ```
303
+
304
+ The task page shows an **Output** section with one block per attempt (latest
305
+ first): the output of a retried attempt is on its `RETRY` event, the final one on
306
+ `SUCCESS`/`FAILURE`.
307
+
308
+ - **What's captured**: `print()` to stdout and stderr, exactly as printed
309
+ (whether or not Celery redirects stdout to its log), and log records
310
+ (`logging.getLogger(...)`, `get_task_logger(...)`) emitted while the task
311
+ runs, in one chronological stream. Records are captured when they reach the
312
+ root logger or `celery.task`, or when a handler writes them to stdout/stderr
313
+ after stemtrace is set up. A logger with `propagate=False` whose handlers were
314
+ set up at worker startup (writing to the real stream or a file) isn't
315
+ captured. Log levels still apply to log records: one your logging config
316
+ filters out (e.g. `DEBUG` under `--loglevel=INFO`) isn't captured.
317
+ stemtrace's own logs and Celery/kombu internals are left out.
318
+ What the worker prints to its console or log file is unchanged (when no
319
+ logging handlers are configured, warnings still print as Python's
320
+ `logging.lastResort` would). One side effect: from the first task on, the root
321
+ logger has stemtrace's handler, so `logging.getLogger().hasHandlers()` is
322
+ `True` and a later `logging.basicConfig()` call without `force=True` does
323
+ nothing. Configure logging at startup (or pass `force=True`). Logging
324
+ handlers added while a task runs, ones that override `handle()`, or ones
325
+ that write to stdout/stderr in unusual ways may show a record twice or show
326
+ Celery/kombu lines in that task's output; handlers set up at worker startup
327
+ don't.
328
+ - **Size**: at most `max_output_size` bytes (UTF-8) per attempt. Longer output
329
+ keeps its end, where failures usually are, after a `[… N bytes truncated]` line.
330
+ Output travels with the task's terminal event, so it is stored wherever events
331
+ are: by the server (up to `max_nodes` tasks) and, with the Redis transport, in
332
+ the stemtrace stream on your broker, which keeps the last `max(ttl, 10000)`
333
+ events (86,400 with the default `ttl`). At 16 KB per attempt that can add up to
334
+ more than a GB of Redis memory on busy workers, so consider a lower
335
+ `max_output_size` or `ttl` there. It isn't sent over the UI's WebSocket; the
336
+ task page fetches it.
337
+ - **Pools**: each task's output is collected in a `ContextVar` (per thread, and
338
+ per greenlet under gevent/eventlet), so concurrent tasks never mix output.
339
+ Output from threads a task starts itself isn't captured.
340
+ - **Not captured**: anything after the task ends, and output of a task whose
341
+ worker process was killed (hard time limit, OOM), since no terminal event is
342
+ sent from that process. Capture of a failed or retried attempt stops when
343
+ Celery reports the failure/retry, so `on_failure`/`on_retry` output is included
344
+ but `after_return` output is included only on success.
345
+ - **Not scrubbed**: output is free text, so the key-based scrubbing above can't
346
+ apply. That's why it's off by default: only enable it if your tasks don't print
347
+ or log secrets.
348
+
274
349
  ### Canvas Graph Visualization
275
350
 
276
351
  `stemtrace` automatically detects and visualizes Celery canvas constructs:
@@ -631,6 +706,7 @@ What is detected depends on the transport:
631
706
  - ✅ **Full lifecycle tracking** — PENDING → RECEIVED → STARTED → SUCCESS/FAILURE
632
707
  - ✅ **Canvas awareness** — Automatic GROUP/CHORD node visualization
633
708
  - ✅ **Arguments & results** — View inputs, outputs, and exceptions
709
+ - ✅ **Task output** — What each attempt printed and logged (opt-in)
634
710
  - ✅ **Sensitive data scrubbing** — Passwords and API keys filtered automatically
635
711
  - ✅ **Real-time updates** — WebSocket-powered live dashboard
636
712
  - ✅ **FastAPI integration** — Mount into your existing app
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Zero-infrastructure Celery task flow visualizer**
6
6
 
7
- [![PyPI version](https://img.shields.io/badge/pypi-v0.3.8-darklime)](https://pypi.org/project/stemtrace)
7
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.3.10-darklime)](https://pypi.org/project/stemtrace)
8
8
  [![Python](https://img.shields.io/pypi/pyversions/stemtrace.svg)](https://pypi.org/project/stemtrace/)
9
9
  [![CI](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml/badge.svg)](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
10
10
  [![codecov](https://codecov.io/gh/iansokolskyi/stemtrace/graph/badge.svg)](https://codecov.io/gh/iansokolskyi/stemtrace)
@@ -28,6 +28,7 @@ Stemtrace captures your task executions as a graph — visualize parent→child
28
28
  - **Task Flow Graphs** — Visualize chains, groups, and chords as interactive DAGs
29
29
  - **Execution Timeline** — Track queued → started → retried → finished states
30
30
  - **Arguments & Results** — Inspect inputs, outputs, and exceptions
31
+ - **Task Output** — See what each attempt printed and logged, next to the task (opt-in)
31
32
  - **Retry Chains** — Understand exactly when and why retries happened
32
33
 
33
34
  **Canvas Support**
@@ -198,6 +199,10 @@ stemtrace.init_worker(
198
199
  scrub_sensitive_data=True, # Scrub passwords, API keys, etc.
199
200
  additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
200
201
  safe_keys=frozenset({"public_key"}), # Never scrub these keys
202
+
203
+ # Task output (disabled by default, see "Task Output" below)
204
+ capture_output=False, # Capture what each attempt prints/logs
205
+ max_output_size=16384, # Max bytes kept per attempt (the end is kept)
201
206
  )
202
207
 
203
208
  # Introspection (after init)
@@ -216,6 +221,75 @@ By default, stemtrace scrubs common sensitive keys from task arguments:
216
221
 
217
222
  Scrubbed values appear as `[Filtered]` in the UI.
218
223
 
224
+ #### Task Output (print and logging)
225
+
226
+ Turn on `capture_output` on the worker to see what each task attempt printed and
227
+ logged on its task page, instead of searching interleaved worker logs:
228
+
229
+ ```python
230
+ stemtrace.init_worker(app, capture_output=True)
231
+ ```
232
+
233
+ ```python
234
+ from celery.utils.log import get_task_logger
235
+ import logging
236
+
237
+ task_logger = get_task_logger(__name__)
238
+ log = logging.getLogger("myapp.billing")
239
+
240
+ @app.task(bind=True, max_retries=3)
241
+ def charge(self, order_id):
242
+ print(f"charging {order_id}") # -> charging 42
243
+ task_logger.info("calling gateway") # -> INFO myapp.tasks: calling gateway
244
+ log.warning("gateway slow") # -> WARNING myapp.billing: gateway slow
245
+ ...
246
+ ```
247
+
248
+ The task page shows an **Output** section with one block per attempt (latest
249
+ first): the output of a retried attempt is on its `RETRY` event, the final one on
250
+ `SUCCESS`/`FAILURE`.
251
+
252
+ - **What's captured**: `print()` to stdout and stderr, exactly as printed
253
+ (whether or not Celery redirects stdout to its log), and log records
254
+ (`logging.getLogger(...)`, `get_task_logger(...)`) emitted while the task
255
+ runs, in one chronological stream. Records are captured when they reach the
256
+ root logger or `celery.task`, or when a handler writes them to stdout/stderr
257
+ after stemtrace is set up. A logger with `propagate=False` whose handlers were
258
+ set up at worker startup (writing to the real stream or a file) isn't
259
+ captured. Log levels still apply to log records: one your logging config
260
+ filters out (e.g. `DEBUG` under `--loglevel=INFO`) isn't captured.
261
+ stemtrace's own logs and Celery/kombu internals are left out.
262
+ What the worker prints to its console or log file is unchanged (when no
263
+ logging handlers are configured, warnings still print as Python's
264
+ `logging.lastResort` would). One side effect: from the first task on, the root
265
+ logger has stemtrace's handler, so `logging.getLogger().hasHandlers()` is
266
+ `True` and a later `logging.basicConfig()` call without `force=True` does
267
+ nothing. Configure logging at startup (or pass `force=True`). Logging
268
+ handlers added while a task runs, ones that override `handle()`, or ones
269
+ that write to stdout/stderr in unusual ways may show a record twice or show
270
+ Celery/kombu lines in that task's output; handlers set up at worker startup
271
+ don't.
272
+ - **Size**: at most `max_output_size` bytes (UTF-8) per attempt. Longer output
273
+ keeps its end, where failures usually are, after a `[… N bytes truncated]` line.
274
+ Output travels with the task's terminal event, so it is stored wherever events
275
+ are: by the server (up to `max_nodes` tasks) and, with the Redis transport, in
276
+ the stemtrace stream on your broker, which keeps the last `max(ttl, 10000)`
277
+ events (86,400 with the default `ttl`). At 16 KB per attempt that can add up to
278
+ more than a GB of Redis memory on busy workers, so consider a lower
279
+ `max_output_size` or `ttl` there. It isn't sent over the UI's WebSocket; the
280
+ task page fetches it.
281
+ - **Pools**: each task's output is collected in a `ContextVar` (per thread, and
282
+ per greenlet under gevent/eventlet), so concurrent tasks never mix output.
283
+ Output from threads a task starts itself isn't captured.
284
+ - **Not captured**: anything after the task ends, and output of a task whose
285
+ worker process was killed (hard time limit, OOM), since no terminal event is
286
+ sent from that process. Capture of a failed or retried attempt stops when
287
+ Celery reports the failure/retry, so `on_failure`/`on_retry` output is included
288
+ but `after_return` output is included only on success.
289
+ - **Not scrubbed**: output is free text, so the key-based scrubbing above can't
290
+ apply. That's why it's off by default: only enable it if your tasks don't print
291
+ or log secrets.
292
+
219
293
  ### Canvas Graph Visualization
220
294
 
221
295
  `stemtrace` automatically detects and visualizes Celery canvas constructs:
@@ -576,6 +650,7 @@ What is detected depends on the transport:
576
650
  - ✅ **Full lifecycle tracking** — PENDING → RECEIVED → STARTED → SUCCESS/FAILURE
577
651
  - ✅ **Canvas awareness** — Automatic GROUP/CHORD node visualization
578
652
  - ✅ **Arguments & results** — View inputs, outputs, and exceptions
653
+ - ✅ **Task output** — What each attempt printed and logged (opt-in)
579
654
  - ✅ **Sensitive data scrubbing** — Passwords and API keys filtered automatically
580
655
  - ✅ **Real-time updates** — WebSocket-powered live dashboard
581
656
  - ✅ **FastAPI integration** — Mount into your existing app