stemtrace 0.3.7__tar.gz → 0.3.9__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 (169) hide show
  1. {stemtrace-0.3.7 → stemtrace-0.3.9}/.bumpversion.toml +10 -3
  2. {stemtrace-0.3.7 → stemtrace-0.3.9}/.github/workflows/release.yml +25 -25
  3. {stemtrace-0.3.7 → stemtrace-0.3.9}/.gitignore +1 -0
  4. {stemtrace-0.3.7 → stemtrace-0.3.9}/CHANGELOG.md +26 -1
  5. {stemtrace-0.3.7 → stemtrace-0.3.9}/CONTRIBUTING.md +12 -0
  6. {stemtrace-0.3.7 → stemtrace-0.3.9}/Makefile +47 -12
  7. {stemtrace-0.3.7 → stemtrace-0.3.9}/PKG-INFO +135 -6
  8. {stemtrace-0.3.7 → stemtrace-0.3.9}/README.md +134 -5
  9. {stemtrace-0.3.7 → stemtrace-0.3.9}/pyproject.toml +1 -1
  10. stemtrace-0.3.9/scripts/changelog_section.py +59 -0
  11. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/__init__.py +59 -9
  12. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/core/events.py +3 -0
  13. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/config.py +13 -4
  14. stemtrace-0.3.9/src/stemtrace/library/output_capture.py +536 -0
  15. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/signals.py +56 -2
  16. stemtrace-0.3.9/src/stemtrace/library/transports/breaker.py +426 -0
  17. stemtrace-0.3.9/src/stemtrace/library/transports/rabbitmq.py +649 -0
  18. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/__main__.py +24 -0
  19. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/api/routes.py +17 -4
  20. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/api/schemas.py +1 -0
  21. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/package-lock.json +2 -2
  22. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/package.json +1 -1
  23. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/api/client.ts +2 -0
  24. stemtrace-0.3.9/src/stemtrace/server/ui/frontend/src/components/TaskOutput.tsx +118 -0
  25. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/hooks/WebSocketContext.tsx +26 -8
  26. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/routes/tasks.$taskId.tsx +4 -0
  27. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-data.ts +2 -0
  28. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/task-detail.spec.ts +219 -2
  29. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/websocket.py +3 -1
  30. stemtrace-0.3.9/tests/integration/output_worker_app.py +102 -0
  31. stemtrace-0.3.9/tests/integration/test_output_capture_worker.py +251 -0
  32. stemtrace-0.3.9/tests/integration/test_rabbitmq_publish.py +349 -0
  33. stemtrace-0.3.9/tests/integration/test_rabbitmq_transport.py +342 -0
  34. stemtrace-0.3.9/tests/unit/test_changelog_section_script.py +102 -0
  35. stemtrace-0.3.9/tests/unit/test_cli.py +171 -0
  36. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_events.py +38 -0
  37. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_init.py +203 -0
  38. stemtrace-0.3.9/tests/unit/test_output_capture.py +1080 -0
  39. stemtrace-0.3.9/tests/unit/test_publish_breaker.py +771 -0
  40. stemtrace-0.3.9/tests/unit/test_rabbitmq_publish.py +705 -0
  41. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_routes.py +40 -0
  42. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_signals.py +22 -0
  43. stemtrace-0.3.9/tests/unit/test_signals_output.py +364 -0
  44. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_transports.py +755 -35
  45. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_websocket.py +25 -0
  46. {stemtrace-0.3.7 → stemtrace-0.3.9}/uv.lock +1 -1
  47. stemtrace-0.3.7/src/stemtrace/library/transports/rabbitmq.py +0 -250
  48. stemtrace-0.3.7/tests/integration/test_rabbitmq_transport.py +0 -139
  49. {stemtrace-0.3.7 → stemtrace-0.3.9}/.dockerignore +0 -0
  50. {stemtrace-0.3.7 → stemtrace-0.3.9}/.github/dependabot.yml +0 -0
  51. {stemtrace-0.3.7 → stemtrace-0.3.9}/.github/workflows/ci.yml +0 -0
  52. {stemtrace-0.3.7 → stemtrace-0.3.9}/.github/workflows/e2e.yml +0 -0
  53. {stemtrace-0.3.7 → stemtrace-0.3.9}/.pre-commit-config.yaml +0 -0
  54. {stemtrace-0.3.7 → stemtrace-0.3.9}/.python-version +0 -0
  55. {stemtrace-0.3.7 → stemtrace-0.3.9}/Dockerfile +0 -0
  56. {stemtrace-0.3.7 → stemtrace-0.3.9}/Dockerfile.e2e +0 -0
  57. {stemtrace-0.3.7 → stemtrace-0.3.9}/LICENSE +0 -0
  58. {stemtrace-0.3.7 → stemtrace-0.3.9}/build_ui.py +0 -0
  59. {stemtrace-0.3.7 → stemtrace-0.3.9}/docker-compose.e2e.rabbitmq.yml +0 -0
  60. {stemtrace-0.3.7 → stemtrace-0.3.9}/docker-compose.e2e.yml +0 -0
  61. {stemtrace-0.3.7 → stemtrace-0.3.9}/docker-compose.rabbitmq.yml +0 -0
  62. {stemtrace-0.3.7 → stemtrace-0.3.9}/docker-compose.yml +0 -0
  63. {stemtrace-0.3.7 → stemtrace-0.3.9}/docs/brand/stemtrace-mark-light.svg +0 -0
  64. {stemtrace-0.3.7 → stemtrace-0.3.9}/docs/brand/stemtrace-mark.svg +0 -0
  65. {stemtrace-0.3.7 → stemtrace-0.3.9}/docs/screenshots/task_details.png +0 -0
  66. {stemtrace-0.3.7 → stemtrace-0.3.9}/docs/screenshots/unregistered.png +0 -0
  67. {stemtrace-0.3.7 → stemtrace-0.3.9}/docs/screenshots/workflow.png +0 -0
  68. {stemtrace-0.3.7 → stemtrace-0.3.9}/examples/celery_app.py +0 -0
  69. {stemtrace-0.3.7 → stemtrace-0.3.9}/examples/fastapi_integration.py +0 -0
  70. {stemtrace-0.3.7 → stemtrace-0.3.9}/examples/with_auth.py +0 -0
  71. {stemtrace-0.3.7 → stemtrace-0.3.9}/examples/with_login.py +0 -0
  72. {stemtrace-0.3.7 → stemtrace-0.3.9}/progress.md +0 -0
  73. {stemtrace-0.3.7 → stemtrace-0.3.9}/scripts/wait_for_http.py +0 -0
  74. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/core/__init__.py +0 -0
  75. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/core/exceptions.py +0 -0
  76. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/core/graph.py +0 -0
  77. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/core/ports.py +0 -0
  78. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/__init__.py +0 -0
  79. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/bootsteps.py +0 -0
  80. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/scrubbing.py +0 -0
  81. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/transports/__init__.py +0 -0
  82. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/transports/memory.py +0 -0
  83. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/library/transports/redis.py +0 -0
  84. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/py.typed +0 -0
  85. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/__init__.py +0 -0
  86. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/api/__init__.py +0 -0
  87. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/api/websocket.py +0 -0
  88. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/consumer.py +0 -0
  89. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/fastapi/__init__.py +0 -0
  90. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/fastapi/auth.py +0 -0
  91. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/fastapi/extension.py +0 -0
  92. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/fastapi/form_auth.py +0 -0
  93. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/fastapi/form_auth_middleware.py +0 -0
  94. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/fastapi/login_routes.py +0 -0
  95. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/fastapi/router.py +0 -0
  96. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/store.py +0 -0
  97. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/__init__.py +0 -0
  98. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/biome.json +0 -0
  99. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/index.html +0 -0
  100. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/playwright.config.ts +0 -0
  101. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/postcss.config.js +0 -0
  102. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/api/queries.ts +0 -0
  103. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/DateRangePicker.tsx +0 -0
  104. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/Filters.tsx +0 -0
  105. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/LogoutButton.tsx +0 -0
  106. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/SessionRejectedNotice.tsx +0 -0
  107. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/SproutMark.tsx +0 -0
  108. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/TaskGraph.tsx +0 -0
  109. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/TaskList.tsx +0 -0
  110. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/TaskStateBadge.tsx +0 -0
  111. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/TaskTimeline.tsx +0 -0
  112. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/TimelineBar.tsx +0 -0
  113. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/components/Tooltip.tsx +0 -0
  114. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/hooks/useInfiniteScroll.ts +0 -0
  115. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/index.css +0 -0
  116. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/main.tsx +0 -0
  117. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/routes/__root.tsx +0 -0
  118. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/routes/graph.$rootId.tsx +0 -0
  119. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/routes/graphs.tsx +0 -0
  120. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/routes/index.tsx +0 -0
  121. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/routes/registry.tsx +0 -0
  122. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/routes/workers.tsx +0 -0
  123. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/utils/format.ts +0 -0
  124. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/src/vite-env.d.ts +0 -0
  125. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/auth.spec.ts +0 -0
  126. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/fixtures/mock-api.ts +0 -0
  127. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/graphs.spec.ts +0 -0
  128. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/header.spec.ts +0 -0
  129. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/registry.spec.ts +0 -0
  130. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/tasks.spec.ts +0 -0
  131. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tests/workers.spec.ts +0 -0
  132. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tsconfig.json +0 -0
  133. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/tsconfig.tsbuildinfo +0 -0
  134. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/frontend/vite.config.ts +0 -0
  135. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/static.py +0 -0
  136. {stemtrace-0.3.7 → stemtrace-0.3.9}/src/stemtrace/server/ui/templates/login.html +0 -0
  137. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/__init__.py +0 -0
  138. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/conftest.py +0 -0
  139. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/e2e/__init__.py +0 -0
  140. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/e2e/conftest.py +0 -0
  141. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/e2e/tasks.py +0 -0
  142. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/e2e/test_api_e2e.py +0 -0
  143. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/e2e/test_workers_e2e.py +0 -0
  144. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/__init__.py +0 -0
  145. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/test_embedded_static.py +0 -0
  146. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/test_fastapi_integration.py +0 -0
  147. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/test_redis_resilience_docker.py +0 -0
  148. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/test_redis_transport.py +0 -0
  149. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/test_static_ui.py +0 -0
  150. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/test_wait_for_http_redirects.py +0 -0
  151. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/integration/test_workers_refresh.py +0 -0
  152. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/__init__.py +0 -0
  153. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_auth.py +0 -0
  154. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_bootsteps.py +0 -0
  155. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_build_ui_hook.py +0 -0
  156. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_cli_form_login.py +0 -0
  157. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_config.py +0 -0
  158. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_consumer.py +0 -0
  159. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_examples.py +0 -0
  160. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_exceptions.py +0 -0
  161. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_favicon.py +0 -0
  162. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_form_auth.py +0 -0
  163. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_form_login.py +0 -0
  164. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_graph.py +0 -0
  165. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_redis_resilience.py +0 -0
  166. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_scrubbing.py +0 -0
  167. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_store.py +0 -0
  168. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_ui_static_prefix.py +0 -0
  169. {stemtrace-0.3.7 → stemtrace-0.3.9}/tests/unit/test_wait_for_http_script.py +0 -0
@@ -1,10 +1,10 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.3.7"
2
+ current_version = "0.3.9"
3
3
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
4
4
  serialize = ["{major}.{minor}.{patch}"]
5
5
  commit = true
6
- tag = false # Tag created via GitHub Release UI
7
- commit_message = "chore: release v{new_version}"
6
+ tag = false # Tagged after the release PR merges (make release-tag)
7
+ message = "chore: release v{new_version}"
8
8
 
9
9
  # Python package version in pyproject.toml
10
10
  [[tool.bumpversion.files]]
@@ -60,3 +60,10 @@ replace = """## [Unreleased]
60
60
  filename = "README.md"
61
61
  search = "[![PyPI version](https://img.shields.io/badge/pypi-v{current_version}-darklime)](https://pypi.org/project/stemtrace)"
62
62
  replace = "[![PyPI version](https://img.shields.io/badge/pypi-v{new_version}-darklime)](https://pypi.org/project/stemtrace)"
63
+
64
+ # CHANGELOG compare links: advance [unreleased], add the new version's link
65
+ [[tool.bumpversion.files]]
66
+ filename = "CHANGELOG.md"
67
+ search = "[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v{current_version}...HEAD"
68
+ replace = """[unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v{new_version}...HEAD
69
+ [{new_version}]: https://github.com/iansokolskyi/stemtrace/compare/v{current_version}...v{new_version}"""
@@ -1,19 +1,9 @@
1
1
  name: Release
2
2
 
3
3
  on:
4
+ # Releases are tagged on main after the release PR merges (make release-tag).
4
5
  push:
5
6
  tags: ["v*"]
6
- # Manual trigger from Actions tab
7
- workflow_dispatch:
8
- inputs:
9
- version:
10
- description: "Version to release (e.g., 0.1.0) - must match version in pyproject.toml"
11
- required: true
12
- type: string
13
-
14
- env:
15
- # Determine version from tag or manual input
16
- VERSION: ${{ github.event.inputs.version || github.ref_name }}
17
7
 
18
8
  jobs:
19
9
  build:
@@ -25,17 +15,14 @@ jobs:
25
15
  tag: ${{ steps.version.outputs.tag }}
26
16
  steps:
27
17
  - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
18
+ with:
19
+ fetch-depth: 0 # full history for the "tag is on main" check
28
20
 
29
21
  - name: Determine version
30
22
  id: version
31
23
  run: |
32
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
33
- echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
34
- echo "tag=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
35
- else
36
- echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
37
- echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
38
- fi
24
+ echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
25
+ echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
39
26
 
40
27
  - name: Verify version matches pyproject.toml
41
28
  run: |
@@ -50,6 +37,19 @@ jobs:
50
37
  fi
51
38
  echo "✅ Version $PYPROJECT_VERSION matches"
52
39
 
40
+ - name: Verify the tagged commit is on main
41
+ run: |
42
+ # Releases are tagged after the release PR merges; never from a branch.
43
+ git merge-base --is-ancestor "$GITHUB_SHA" origin/main || {
44
+ echo "❌ ${GITHUB_REF_NAME} points at a commit that isn't on main"; exit 1; }
45
+
46
+ - name: Check release notes exist in CHANGELOG.md
47
+ run: |
48
+ # Fail before anything is published: every release needs notes.
49
+ python3 scripts/changelog_section.py "$VERSION"
50
+ env:
51
+ VERSION: ${{ steps.version.outputs.version }}
52
+
53
53
  - name: Set up Python
54
54
  uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
55
55
  with:
@@ -158,17 +158,17 @@ jobs:
158
158
  name: dist
159
159
  path: dist/
160
160
 
161
- - name: Create tag (for manual dispatch)
162
- if: github.event_name == 'workflow_dispatch'
163
- run: |
164
- git config user.name "github-actions[bot]"
165
- git config user.email "github-actions[bot]@users.noreply.github.com"
166
- git tag -a "${{ needs.build.outputs.tag }}" -m "Release ${{ needs.build.outputs.tag }}"
167
- git push origin "${{ needs.build.outputs.tag }}"
161
+ - name: Extract release notes from CHANGELOG.md
162
+ run: python3 scripts/changelog_section.py "$VERSION" > "$RUNNER_TEMP/release_notes.md"
163
+ env:
164
+ VERSION: ${{ needs.build.outputs.version }}
168
165
 
169
166
  - name: Create GitHub Release
170
167
  uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
171
168
  with:
172
169
  tag_name: ${{ needs.build.outputs.tag }}
170
+ # CHANGELOG.md section first; GitHub appends contributors, PR list
171
+ # and the compare link.
172
+ body_path: ${{ runner.temp }}/release_notes.md
173
173
  generate_release_notes: true
174
174
  files: dist/*
@@ -33,3 +33,4 @@ src/stemtrace/server/ui/frontend/src/routeTree.gen.ts
33
33
  src/stemtrace/server/ui/frontend/playwright-report/
34
34
  src/stemtrace/server/ui/frontend/test-results/
35
35
  src/stemtrace/server/ui/frontend/playwright/.cache/
36
+ .release-notes.md
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.9] - 2026-10-10
11
+
12
+ ### Added
13
+ - 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).
14
+
15
+ ## [0.3.8] - 2026-10-10
16
+
17
+ ### Fixed
18
+ - `node_alias_from_arguments` can now be set on the standalone server and Docker image: `stemtrace server --node-alias-from-arguments <key>` or `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS=<key>` (a kwarg name or a positional index). `init_app()` reads the same env var when the argument isn't passed (#69).
19
+ - RabbitMQ: publishing from workers no longer blocks tasks while the broker is down, stalled or in a memory/disk alarm. Each publish tries every host of the URL once with a 2s timeout; repeated or slow failures open a circuit breaker that drops events (instead of queueing them) until the broker answers again, and the worker logs at most four warnings a minute with counts of dropped events. The README's broker section has the details and limits.
20
+ - RabbitMQ: the server's consumer now reconnects after the broker restarts or drops the connection; it used to retry forever on a dead channel.
21
+ - RabbitMQ: a broker outage no longer floods the server log with one ERROR traceback per second; reconnects back off from 1s to 30s and log one line per attempt, plus one when the connection is back (thanks @ohadmata — #70, #71).
22
+ - RabbitMQ: a message the consumer can't decode is dropped with a warning instead of blocking the queue forever.
23
+ - RabbitMQ: stopping the server no longer waits out the reconnect delay or hangs on a frozen broker.
24
+
25
+ ### Added
26
+ - Health endpoint: `consumer_connected` is now reported for RabbitMQ, so `/api/health` shows `degraded` while the server can't read from the broker.
27
+
28
+ ### Deprecated
29
+ - `init_worker(node_alias_from_arguments=...)` has no effect (node names are resolved by the server) and now logs a deprecation warning; it will be removed in a future major release.
30
+
10
31
  ## [0.3.7] - 2026-10-10
11
32
 
12
33
  ### Changed
@@ -165,7 +186,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
165
186
  - **E2E test suite**: Docker API tests + Playwright browser tests
166
187
  - **Comprehensive test suite**: 350+ Python tests, 90%+ coverage
167
188
 
168
- [unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.5...HEAD
189
+ [unreleased]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.9...HEAD
190
+ [0.3.9]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.8...v0.3.9
191
+ [0.3.8]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.7...v0.3.8
192
+ [0.3.7]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.6...v0.3.7
193
+ [0.3.6]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.5...v0.3.6
169
194
  [0.3.5]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.4...v0.3.5
170
195
  [0.3.4]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.3...v0.3.4
171
196
  [0.3.3]: https://github.com/iansokolskyi/stemtrace/compare/v0.3.2...v0.3.3
@@ -136,6 +136,18 @@ uv lock --upgrade
136
136
  - Fire-and-forget pattern for publishers (never block)
137
137
  - Fakes over mocks in tests
138
138
 
139
+ ## Releasing (maintainers)
140
+
141
+ Every release goes through a pull request into `main` and is published by CI from a tag on `main`.
142
+
143
+ 1. Add user-facing entries under `## [Unreleased]` in `CHANGELOG.md`.
144
+ 2. `make release-pr BUMP=patch|minor|major` runs the checks, creates `release/vX.Y.Z`, bumps the
145
+ version and opens a PR whose description is the changelog section.
146
+ 3. When CI is green, merge the PR with a merge commit (not squash or rebase).
147
+ 4. `make release-tag` tags the merge on `main`. The tag triggers the Release workflow, which
148
+ publishes to PyPI and GHCR and creates the GitHub Release from the changelog section. It
149
+ refuses to publish if the section is empty or the tag isn't on `main`.
150
+
139
151
  ## Questions?
140
152
 
141
153
  - Open an issue for bugs or feature requests
@@ -1,4 +1,4 @@
1
- .PHONY: install check types lint format test coverage clean ui-install ui-dev ui-build e2e e2e-api e2e-playwright build release-check version bump-dry bump-patch bump-minor bump-major release
1
+ .PHONY: install check types lint format test coverage clean ui-install ui-dev ui-build e2e e2e-api e2e-playwright build release-check version bump-dry bump-patch bump-minor bump-major release release-pr release-tag
2
2
 
3
3
  # Install all dependencies
4
4
  install:
@@ -99,6 +99,8 @@ build:
99
99
  # Full pre-release checklist
100
100
  release-check:
101
101
  @echo "=== Pre-release Checklist ==="
102
+ @echo "0. Checking CHANGELOG.md has [Unreleased] entries..."
103
+ @uv run python scripts/changelog_section.py Unreleased > /dev/null
102
104
  @echo "1. Running all checks..."
103
105
  $(MAKE) check
104
106
  @echo ""
@@ -107,7 +109,6 @@ release-check:
107
109
  @echo ""
108
110
  @echo "✅ Ready to release!"
109
111
  @echo ""
110
- @echo "Next: make release"
111
112
 
112
113
  # =============================================================================
113
114
  # Versioning (bump-my-version)
@@ -126,7 +127,7 @@ bump-patch:
126
127
  @NEW_VER=$$(uv run bump-my-version show current_version); \
127
128
  echo "✅ Version bumped to $$NEW_VER"; \
128
129
  echo ""; \
129
- echo "Next: make release"
130
+ echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
130
131
 
131
132
  # Bump minor version (0.1.0 -> 0.2.0)
132
133
  bump-minor:
@@ -134,7 +135,7 @@ bump-minor:
134
135
  @NEW_VER=$$(uv run bump-my-version show current_version); \
135
136
  echo "✅ Version bumped to $$NEW_VER"; \
136
137
  echo ""; \
137
- echo "Next: make release"
138
+ echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
138
139
 
139
140
  # Bump major version (0.1.0 -> 1.0.0)
140
141
  bump-major:
@@ -142,20 +143,54 @@ bump-major:
142
143
  @NEW_VER=$$(uv run bump-my-version show current_version); \
143
144
  echo "✅ Version bumped to $$NEW_VER"; \
144
145
  echo ""; \
145
- echo "Next: make release"
146
+ echo "Note: releases use make release-pr, which runs the bump itself; this target is for manual use only"
146
147
 
147
148
  # Tag and push to trigger release workflow
148
149
  release:
149
- @VERSION=$$(uv run bump-my-version show current_version); \
150
+ @echo "Releases go through a PR: make release-pr BUMP=patch|minor|major, then make release-tag."
151
+ @exit 1
152
+
153
+ # Open a release PR: preflight, checks, release/vX.Y.Z branch, version bump, PR with notes.
154
+ # Usage: make release-pr BUMP=patch|minor|major
155
+ release-pr:
156
+ @set -e; \
157
+ case "$(BUMP)" in patch|minor|major) ;; *) echo "Usage: make release-pr BUMP=patch|minor|major"; exit 1;; esac; \
158
+ test "$$(git rev-parse --abbrev-ref HEAD)" = main || { echo "❌ Run from main"; exit 1; }; \
159
+ test -z "$$(git status --porcelain)" || { echo "❌ Working tree not clean"; exit 1; }; \
160
+ gh auth status >/dev/null 2>&1 || { echo "❌ gh is not authenticated (gh auth login)"; exit 1; }; \
161
+ git fetch -q --tags origin main; \
162
+ test -z "$$(git log HEAD..origin/main --oneline)" || { echo "❌ main is behind origin/main"; exit 1; }; \
163
+ NEW=$$(uv run bump-my-version show new_version --increment $(BUMP)); \
164
+ ! git rev-parse -q --verify "refs/heads/release/v$$NEW" >/dev/null || { echo "❌ Local branch release/v$$NEW exists"; exit 1; }; \
165
+ test -z "$$(git ls-remote --heads origin "release/v$$NEW")" || { echo "❌ Remote branch release/v$$NEW exists"; exit 1; }; \
166
+ ! git rev-parse -q --verify "refs/tags/v$$NEW" >/dev/null || { echo "❌ Tag v$$NEW exists"; exit 1; }; \
167
+ $(MAKE) release-check; \
168
+ git switch -c "release/v$$NEW"; \
169
+ uv run bump-my-version bump $(BUMP); \
170
+ NOTES=$$(mktemp); trap 'rm -f "$$NOTES"' EXIT; \
171
+ uv run python scripts/changelog_section.py "$$NEW" > "$$NOTES"; \
172
+ git push -u origin "release/v$$NEW" || { echo "❌ Push failed; local branch release/v$$NEW holds the bump commit"; exit 1; }; \
173
+ gh pr create --base main --head "release/v$$NEW" --title "chore: release v$$NEW" --body-file "$$NOTES" \
174
+ || { echo "❌ PR creation failed; release/v$$NEW is pushed, open the PR manually"; exit 1; }; \
175
+ echo ""; \
176
+ echo "✅ Release PR opened for v$$NEW"; \
177
+ echo "Next: gh pr checks --watch, merge with a merge commit, then make release-tag"
178
+
179
+ # Tag the merged release on main; the tag push triggers the Release workflow.
180
+ release-tag:
181
+ @set -e; \
182
+ git switch -q main; \
183
+ git pull -q --ff-only origin main; \
184
+ VERSION=$$(uv run bump-my-version show current_version); \
150
185
  TAG="v$$VERSION"; \
151
- echo "Tagging $$TAG..."; \
186
+ SUBJECT=$$(git log -1 --format=%s); \
187
+ test "$$(git rev-list --parents -n 1 HEAD | wc -w)" -eq 3 || { echo "❌ HEAD of main is not a merge commit (merge the release PR with a merge commit)"; exit 1; }; \
188
+ case "$$SUBJECT" in *"/release/$$TAG"|*"release $$TAG"|*"release $$TAG "*) ;; *) echo "❌ HEAD of main is not the $$TAG release PR merge: $$SUBJECT"; exit 1;; esac; \
189
+ git fetch -q --tags origin; \
190
+ ! git rev-parse -q --verify "refs/tags/$$TAG" >/dev/null || { echo "❌ $$TAG already exists"; exit 1; }; \
152
191
  git tag -a "$$TAG" -m "Release $$TAG"; \
153
- echo "Pushing to origin..."; \
154
- git push origin main; \
155
192
  git push origin "$$TAG"; \
156
- echo ""; \
157
- echo "✅ Released $$TAG"; \
158
- echo " → GitHub Actions will publish to PyPI and Docker"
193
+ echo "✅ Tagged $$TAG → Release workflow publishes PyPI, GHCR and the GitHub Release"
159
194
 
160
195
  # =============================================================================
161
196
  # E2E Testing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stemtrace
3
- Version: 0.3.7
3
+ Version: 0.3.9
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
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
59
59
 
60
60
  **Zero-infrastructure Celery task flow visualizer**
61
61
 
62
- [![PyPI version](https://img.shields.io/badge/pypi-v0.3.7-darklime)](https://pypi.org/project/stemtrace)
62
+ [![PyPI version](https://img.shields.io/badge/pypi-v0.3.9-darklime)](https://pypi.org/project/stemtrace)
63
63
  [![Python](https://img.shields.io/pypi/pyversions/stemtrace.svg)](https://pypi.org/project/stemtrace/)
64
64
  [![CI](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml/badge.svg)](https://github.com/iansokolskyi/stemtrace/actions/workflows/ci.yml)
65
65
  [![codecov](https://codecov.io/gh/iansokolskyi/stemtrace/graph/badge.svg)](https://codecov.io/gh/iansokolskyi/stemtrace)
@@ -83,6 +83,7 @@ Stemtrace captures your task executions as a graph — visualize parent→child
83
83
  - **Task Flow Graphs** — Visualize chains, groups, and chords as interactive DAGs
84
84
  - **Execution Timeline** — Track queued → started → retried → finished states
85
85
  - **Arguments & Results** — Inspect inputs, outputs, and exceptions
86
+ - **Task Output** — See what each attempt printed and logged, next to the task (opt-in)
86
87
  - **Retry Chains** — Understand exactly when and why retries happened
87
88
 
88
89
  **Canvas Support**
@@ -254,9 +255,9 @@ stemtrace.init_worker(
254
255
  additional_sensitive_keys=frozenset({"my_secret"}), # Add custom keys
255
256
  safe_keys=frozenset({"public_key"}), # Never scrub these keys
256
257
 
257
- # UI display
258
- node_alias_from_arguments="operator_type", # Use kwargs["operator_type"] as node name
259
- # node_alias_from_arguments="0", # Or use args[0] (digit string = positional index)
258
+ # Task output (disabled by default, see "Task Output" below)
259
+ capture_output=False, # Capture what each attempt prints/logs
260
+ max_output_size=16384, # Max bytes kept per attempt (the end is kept)
260
261
  )
261
262
 
262
263
  # Introspection (after init)
@@ -275,6 +276,75 @@ By default, stemtrace scrubs common sensitive keys from task arguments:
275
276
 
276
277
  Scrubbed values appear as `[Filtered]` in the UI.
277
278
 
279
+ #### Task Output (print and logging)
280
+
281
+ Turn on `capture_output` on the worker to see what each task attempt printed and
282
+ logged on its task page, instead of searching interleaved worker logs:
283
+
284
+ ```python
285
+ stemtrace.init_worker(app, capture_output=True)
286
+ ```
287
+
288
+ ```python
289
+ from celery.utils.log import get_task_logger
290
+ import logging
291
+
292
+ task_logger = get_task_logger(__name__)
293
+ log = logging.getLogger("myapp.billing")
294
+
295
+ @app.task(bind=True, max_retries=3)
296
+ def charge(self, order_id):
297
+ print(f"charging {order_id}") # -> charging 42
298
+ task_logger.info("calling gateway") # -> INFO myapp.tasks: calling gateway
299
+ log.warning("gateway slow") # -> WARNING myapp.billing: gateway slow
300
+ ...
301
+ ```
302
+
303
+ The task page shows an **Output** section with one block per attempt (latest
304
+ first): the output of a retried attempt is on its `RETRY` event, the final one on
305
+ `SUCCESS`/`FAILURE`.
306
+
307
+ - **What's captured**: `print()` to stdout and stderr, exactly as printed
308
+ (whether or not Celery redirects stdout to its log), and log records
309
+ (`logging.getLogger(...)`, `get_task_logger(...)`) emitted while the task
310
+ runs, in one chronological stream. Records are captured when they reach the
311
+ root logger or `celery.task`, or when a handler writes them to stdout/stderr
312
+ after stemtrace is set up. A logger with `propagate=False` whose handlers were
313
+ set up at worker startup (writing to the real stream or a file) isn't
314
+ captured. Log levels still apply to log records: one your logging config
315
+ filters out (e.g. `DEBUG` under `--loglevel=INFO`) isn't captured.
316
+ stemtrace's own logs and Celery/kombu internals are left out.
317
+ What the worker prints to its console or log file is unchanged (when no
318
+ logging handlers are configured, warnings still print as Python's
319
+ `logging.lastResort` would). One side effect: from the first task on, the root
320
+ logger has stemtrace's handler, so `logging.getLogger().hasHandlers()` is
321
+ `True` and a later `logging.basicConfig()` call without `force=True` does
322
+ nothing. Configure logging at startup (or pass `force=True`). Logging
323
+ handlers added while a task runs, ones that override `handle()`, or ones
324
+ that write to stdout/stderr in unusual ways may show a record twice or show
325
+ Celery/kombu lines in that task's output; handlers set up at worker startup
326
+ don't.
327
+ - **Size**: at most `max_output_size` bytes (UTF-8) per attempt. Longer output
328
+ keeps its end, where failures usually are, after a `[… N bytes truncated]` line.
329
+ Output travels with the task's terminal event, so it is stored wherever events
330
+ are: by the server (up to `max_nodes` tasks) and, with the Redis transport, in
331
+ the stemtrace stream on your broker, which keeps the last `max(ttl, 10000)`
332
+ events (86,400 with the default `ttl`). At 16 KB per attempt that can add up to
333
+ more than a GB of Redis memory on busy workers, so consider a lower
334
+ `max_output_size` or `ttl` there. It isn't sent over the UI's WebSocket; the
335
+ task page fetches it.
336
+ - **Pools**: each task's output is collected in a `ContextVar` (per thread, and
337
+ per greenlet under gevent/eventlet), so concurrent tasks never mix output.
338
+ Output from threads a task starts itself isn't captured.
339
+ - **Not captured**: anything after the task ends, and output of a task whose
340
+ worker process was killed (hard time limit, OOM), since no terminal event is
341
+ sent from that process. Capture of a failed or retried attempt stops when
342
+ Celery reports the failure/retry, so `on_failure`/`on_retry` output is included
343
+ but `after_return` output is included only on success.
344
+ - **Not scrubbed**: output is free text, so the key-based scrubbing above can't
345
+ apply. That's why it's off by default: only enable it if your tasks don't print
346
+ or log secrets.
347
+
278
348
  ### Canvas Graph Visualization
279
349
 
280
350
  `stemtrace` automatically detects and visualizes Celery canvas constructs:
@@ -336,6 +406,7 @@ stemtrace.init_app(
336
406
  |----------|-------------|---------|
337
407
  | `STEMTRACE_BROKER_URL` | Celery broker URL (used for on-demand worker/registry inspection). Also used as the default for `STEMTRACE_TRANSPORT_URL`. | `redis://localhost:6379/0` |
338
408
  | `STEMTRACE_TRANSPORT_URL` | Event transport URL (where stemtrace publishes/consumes events). | Defaults to `STEMTRACE_BROKER_URL`. |
409
+ | `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS` | Server only: show a task argument as the graph node name (see [Node Names from Task Arguments](#node-names-from-task-arguments)). Same as `--node-alias-from-arguments` / `init_app(node_alias_from_arguments=...)`. | Unset (task names are shown). |
339
410
 
340
411
  ### Supported Brokers
341
412
 
@@ -356,6 +427,36 @@ again once events are re-consumed.
356
427
  - Events already consumed/acked by the server are **gone**.
357
428
  - Events published while the server is down are only visible after restart if the server’s
358
429
  durable per-consumer queue still exists and the messages are still within TTL.
430
+ - Events that can't be published to RabbitMQ are dropped, not buffered on the worker, and a
431
+ broker outage doesn't slow tasks down. Each publish tries every host of the broker URL once
432
+ (2s connect timeout, 2s per read or write after that). A single quick failure, such as a
433
+ refused connection, drops only that event; a failure that took 0.5s or more (a timeout) or
434
+ three quick failures in a row make the worker skip publishing for a cooldown that grows from
435
+ 1s to 30s and resets on the next successful publish.
436
+ - During a RabbitMQ memory or disk alarm, the broker accepts connections but stops reading
437
+ from publishers, so a publish is sent but never confirmed. The worker counts such events as
438
+ "sent but unconfirmed" (RabbitMQ may still deliver them once the alarm clears); after two
439
+ unconfirmed attempts in a row it retries only every 30s. Each attempt leaves one blocked
440
+ connection on the broker until the alarm clears, so expect 2 to 3 connections per worker
441
+ process in the first minute of an alarm and about 2 per minute after that, all released
442
+ when the alarm ends. Each retry also logs `Received method (10, 60) during closing channel 0`
443
+ from py-amqp's `amqp` logger (not stemtrace's): that's RabbitMQ's "connection blocked"
444
+ notice arriving while the connection closes.
445
+ - Logging is bounded: at most four warnings a minute however the broker behaves (failures,
446
+ an outage starting, an outage ending, and a summary). Each reports the number of events
447
+ dropped, and sent but unconfirmed, since the previous one, and losses are reported within
448
+ about 1.5 minutes even when the broker flaps.
449
+ - Limitation: a broker that answers but slowly (say, close to the 2s timeout for each step)
450
+ still costs every event several seconds, because publishes that eventually succeed don't
451
+ open the breaker.
452
+ - A failover broker URL (`amqp://h1//;amqp://h2//`) must list nodes of the same RabbitMQ
453
+ cluster: workers keep publishing to whichever host last worked, so the server must be able to
454
+ consume those events from any of them. Entries resolve as in kombu: a bare host (`;h2`)
455
+ takes user, password, port, virtual host and TLS options from the first entry, while a
456
+ URL-form entry (`;amqp://h2`) gets guest/guest unless it names credentials, and takes port
457
+ and virtual host from the previous entry only when it omits them (`//` means `/`). A host
458
+ that refuses the login ends that publish attempt without trying the later hosts, as in
459
+ kombu.
359
460
  - **Workers + Registry tabs**: stemtrace uses **Celery inspect** on demand to populate workers
360
461
  and registered tasks, so those pages work even if the server missed `worker_ready` events.
361
462
  - **If you need durable history across restarts**: point stemtrace events at Redis even if your
@@ -387,6 +488,15 @@ docker run -p 8000:8000 \
387
488
  ghcr.io/iansokolskyi/stemtrace
388
489
  ```
389
490
 
491
+ Show a task argument as the graph node name (here `kwargs["operator_type"]`):
492
+
493
+ ```bash
494
+ docker run -p 8000:8000 \
495
+ -e STEMTRACE_BROKER_URL=redis://host.docker.internal:6379/0 \
496
+ -e STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS=operator_type \
497
+ ghcr.io/iansokolskyi/stemtrace
498
+ ```
499
+
390
500
  Or with Docker Compose:
391
501
 
392
502
  ```yaml
@@ -430,9 +540,26 @@ stemtrace server \
430
540
  --transport-url redis://myredis:6379/0 \
431
541
  --host 0.0.0.0 \
432
542
  --port 8000 \
543
+ --node-alias-from-arguments operator_type \
433
544
  --reload # For development
434
545
  ```
435
546
 
547
+ #### Node Names from Task Arguments
548
+
549
+ By default, graph nodes are labeled with the task name. Generic tasks (e.g. one `run_operator`
550
+ task that does different things depending on its arguments) are easier to tell apart when the
551
+ graph shows an argument instead:
552
+
553
+ - `--node-alias-from-arguments operator_type` uses `kwargs["operator_type"]`
554
+ - `--node-alias-from-arguments 0` uses `args[0]` (a digit string is a positional index)
555
+
556
+ If the argument is missing, the task name is shown. This is a **server** setting: set it on
557
+ `stemtrace server` (flag or `STEMTRACE_NODE_ALIAS_FROM_ARGUMENTS`, e.g. in Docker) or pass
558
+ `node_alias_from_arguments` to `stemtrace.init_app(...)`. Surrounding whitespace is ignored, and
559
+ `stemtrace server` prints `Node alias: <key>` at startup when it is set. Workers need
560
+ `capture_args=True` (the default) so the arguments are available. Passing it to
561
+ `stemtrace.init_worker(...)` has no effect and is deprecated.
562
+
436
563
  #### Protecting the Server (Built-in Login Page)
437
564
 
438
565
  ```bash
@@ -486,6 +613,7 @@ extension = stemtrace.init_app(
486
613
  embedded_consumer=True, # Run consumer in FastAPI process
487
614
  serve_ui=True, # Serve React dashboard
488
615
  auth_dependency=None, # Optional auth (see below)
616
+ node_alias_from_arguments=None, # e.g. "operator_type" or "0"; see Node Names from Task Arguments
489
617
  )
490
618
  ```
491
619
 
@@ -567,7 +695,7 @@ Because the status code stays 200, check `status` in the body if you want monito
567
695
  What is detected depends on the transport:
568
696
 
569
697
  - **Redis:** refused or reset connections, connect timeouts, repeated read timeouts, and consumer loop failures. On redis-py < 8 the default socket timeout is unbounded, so a half-open or hung broker connection may go unnoticed; add `?socket_timeout=…` or `?health_check_interval=…` to the broker URL if you need that.
570
- - **RabbitMQ:** connectivity isn't reported (`consumer_connected` is `null`), so `"degraded"` only reflects consumer loop failures.
698
+ - **RabbitMQ:** refused or dropped connections and channel errors while connecting or draining, and consumer loop failures. The consumer connection is read about once a second and a read that times out counts as connected, so a half-open connection, or a broker that is frozen but keeps the socket open, may go unnoticed. Messages the consumer can't decode (not JSON) are dropped with a warning and don't count as failures.
571
699
 
572
700
  ## 🗺️ Roadmap
573
701
 
@@ -577,6 +705,7 @@ What is detected depends on the transport:
577
705
  - ✅ **Full lifecycle tracking** — PENDING → RECEIVED → STARTED → SUCCESS/FAILURE
578
706
  - ✅ **Canvas awareness** — Automatic GROUP/CHORD node visualization
579
707
  - ✅ **Arguments & results** — View inputs, outputs, and exceptions
708
+ - ✅ **Task output** — What each attempt printed and logged (opt-in)
580
709
  - ✅ **Sensitive data scrubbing** — Passwords and API keys filtered automatically
581
710
  - ✅ **Real-time updates** — WebSocket-powered live dashboard
582
711
  - ✅ **FastAPI integration** — Mount into your existing app