django-lambda-tasks 0.11.0__tar.gz → 0.11.2__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 (115) hide show
  1. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.github/workflows/ci.yml +32 -0
  2. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/steering/product.md +17 -8
  3. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/steering/structure.md +3 -3
  4. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/steering/tech.md +1 -1
  5. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/PKG-INFO +46 -12
  6. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/README.md +45 -11
  7. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/README.md +13 -0
  8. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/sentry.py +13 -6
  9. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/handler.py +39 -15
  10. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/models.py +3 -3
  11. django_lambda_tasks-0.11.2/lambda_tasks/sentry.py +201 -0
  12. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/pyproject.toml +2 -2
  13. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_handler.py +140 -0
  14. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_sentry.py +212 -37
  15. django_lambda_tasks-0.11.0/lambda_tasks/sentry.py +0 -143
  16. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.github/dependabot.yml +0 -0
  17. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.github/workflows/release.yml +0 -0
  18. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.gitignore +0 -0
  19. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  20. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/design.md +0 -0
  21. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/requirements.md +0 -0
  22. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/tasks.md +0 -0
  23. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  24. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  25. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  26. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  27. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  28. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  29. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  30. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  31. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  32. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  33. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  34. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  35. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  36. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  37. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  38. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  39. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
  40. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/design.md +0 -0
  41. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/requirements.md +0 -0
  42. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/tasks.md +0 -0
  43. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  44. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
  45. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  46. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  47. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  48. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  49. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  50. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  51. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
  52. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/design.md +0 -0
  53. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/requirements.md +0 -0
  54. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/tasks.md +0 -0
  55. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  56. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  57. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  58. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  59. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/.config.kiro +0 -0
  60. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/design.md +0 -0
  61. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/requirements.md +0 -0
  62. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/tasks.md +0 -0
  63. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.pre-commit-config.yaml +0 -0
  64. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/.vscode/settings.json +0 -0
  65. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/docker-compose.yml +0 -0
  66. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/docker-compose.yml +0 -0
  67. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/__init__.py +0 -0
  68. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/apps.py +0 -0
  69. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/tasks.py +0 -0
  70. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/urls.py +0 -0
  71. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_app/views.py +0 -0
  72. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/__init__.py +0 -0
  73. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/settings.py +0 -0
  74. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/urls.py +0 -0
  75. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/example_project/wsgi.py +0 -0
  76. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/example/manage.py +0 -0
  77. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/__init__.py +0 -0
  78. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/admin.py +0 -0
  79. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/apps.py +0 -0
  80. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/checks.py +0 -0
  81. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/decorators.py +0 -0
  82. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/environment_loader.py +0 -0
  83. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/local_executor.py +0 -0
  84. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/logging.py +0 -0
  85. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0001_initial.py +0 -0
  86. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  87. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
  88. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0004_remove_taskrecord_taskrecord_kwargs_trgm_and_more.py +0 -0
  89. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/__init__.py +0 -0
  90. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/secret_loader.py +0 -0
  91. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/settings.py +0 -0
  92. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/tasks.py +0 -0
  93. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/lambda_tasks/timeouts.py +0 -0
  94. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/conftest.py +0 -0
  95. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/settings.py +0 -0
  96. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_admin.py +0 -0
  97. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_batch.py +0 -0
  98. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_batch_worker.py +0 -0
  99. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_checks.py +0 -0
  100. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_decorator.py +0 -0
  101. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_decorators.py +0 -0
  102. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_deferred_enqueue.py +0 -0
  103. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_environment_loader.py +0 -0
  104. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_kwargs_only.py +0 -0
  105. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_local_executor.py +0 -0
  106. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_logging.py +0 -0
  107. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_memory_limit.py +0 -0
  108. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_models.py +0 -0
  109. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_noop_execution.py +0 -0
  110. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_secret_loader.py +0 -0
  111. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_serializer.py +0 -0
  112. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_settings.py +0 -0
  113. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_tasks.py +0 -0
  114. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_timeout_validation.py +0 -0
  115. {django_lambda_tasks-0.11.0 → django_lambda_tasks-0.11.2}/tests/test_timeouts.py +0 -0
@@ -53,3 +53,35 @@ jobs:
53
53
  - name: Run tests
54
54
  run: |
55
55
  uv run pytest --cov-branch --cov-report term-missing --cov=tests/ --cov=lambda_tasks/
56
+
57
+ sentry-prerelease:
58
+ # Exercise the Sentry integration against the latest 3.x pre-release of
59
+ # sentry-sdk (stream mode). The committed dev dependency is pinned to the
60
+ # stable 2.x line; this job overrides it at runtime via `uv run --with`
61
+ # without touching pyproject.toml, so regressions in the 3.x API are caught
62
+ # before the SDK goes stable.
63
+ runs-on: ubuntu-latest
64
+ services:
65
+ postgres:
66
+ image: postgres:17
67
+ env:
68
+ POSTGRES_DB: lambda_tasks_test
69
+ POSTGRES_USER: postgres
70
+ POSTGRES_PASSWORD: postgres
71
+ ports:
72
+ - 5432:5432
73
+ options: >-
74
+ --health-cmd "pg_isready -U postgres"
75
+ --health-interval 10s
76
+ --health-timeout 5s
77
+ --health-retries 5
78
+ steps:
79
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
80
+ - name: Install uv and set the python version
81
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
82
+ with:
83
+ python-version: '3.14'
84
+ - name: Run tests against the latest sentry-sdk 3.x pre-release
85
+ run: |
86
+ uv run --prerelease=allow --with "sentry-sdk>=3.0.0b1" \
87
+ pytest --cov-branch --cov-report term-missing --cov=tests/ --cov=lambda_tasks/
@@ -315,21 +315,30 @@ def my_task(*, user_id: int) -> None:
315
315
 
316
316
  ## Sentry
317
317
 
318
- The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup (typically `DjangoIntegration` + `AwsLambdaIntegration`). The problem those integrations leave is attribution: on Lambda, `AwsLambdaIntegration` names every event's transaction after the Lambda function, so all tasks share one transaction (and a single invocation-level transaction is shared across a batch of SQS records); on AWS Batch there is no request and no Lambda integration, so the transaction and URL are `"--"` and `traces_sample_rate` produces no trace for the task body. Either way you cannot tell which task ran or raised.
318
+ The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup. The **recommended** host configuration is `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **without** `AwsLambdaIntegration`. `AwsLambdaIntegration` names every event's transaction after the Lambda function, which hides which task ran; omitting it lets each task be traced under its own task-named transaction.
319
319
 
320
- `lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)` fixes this. `execute_immediately` wraps the task body in it, which — when a Sentry client is active — opens an isolated `sentry_sdk.new_scope()` **and a per-task root span/transaction** named after the task. This gives one trace per task on both Lambda and Batch. It records, both on the span and as scope tags:
320
+ `lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)` does the per-task tracing. `execute_immediately` wraps the task body in it, which — when a Sentry client is active — opens an isolated `sentry_sdk.new_scope()` and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, both on the span and as scope tags:
321
321
 
322
- - transaction/span **name** = the fully-qualified task name (e.g. `myapp.tasks.my_task`)
323
- - `lambda_task` = task name
322
+ - `lambda_task` = fully-qualified task name (e.g. `myapp.tasks.my_task`) — also the transaction/span name
324
323
  - `lambda_task.message_id` = the `TaskRecord` primary key
325
324
  - `lambda_task.n_retries` = the retry counter
326
- - `lambda_task.runtime` = the physical execution environment, read from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (the Batch job queue name) on Batch; omitted when neither is present
325
+ - `lambda_task.runtime` = the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (Batch job queue name); omitted when neither is present
327
326
 
328
- The scope covers the success, retry, and failure paths, so task-logic exceptions (which the executor records as `FAILED` and does not re-raise to the handler) still carry the task name when captured by Sentry. The scope tags are kept in addition to span metadata so error events are attributed even when the performance span is not sampled.
327
+ Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
329
328
 
330
- **SDK version compatibility:** `task_scope` is future-proof across the sentry-sdk 2.x→3.0 boundary. On 2.x it uses `start_transaction` + `set_tag` (a root span must be a transaction to be sent); on 3.0 (stream mode, where transactions are removed and tags no longer propagate to spans) it uses `start_span` + `set_attribute`. The correct API is detected at runtime via `hasattr`, so no code change is needed when the host upgrades the SDK.
329
+ **If a host keeps `AwsLambdaIntegration` anyway:** the library still behaves correctly. `_start_task_root` detects the active invocation transaction (via `sentry_sdk.get_current_scope().transaction` on 2.x; on 3.0 `start_span` is automatically root-or-child) and traces the task as a **child span** of it rather than a second, competing transaction. A nested *transaction* per task would inherit (not independently re-roll) the invocation's sampling decision, doubling transaction volume and defeating `traces_sample_rate`; a child span is sent only if its parent is sampled. In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
331
330
 
332
- `task_scope` is a no-op when `sentry_sdk` is not installed or no client is initialised, so Sentry is not a dependency of the library. `sentry-sdk` is a dev dependency only, for the test suite. No custom Sentry integration is needed — per-task tracing at the one execution entry point covers both Lambda and Batch.
331
+ `task_scope` deliberately does **not** call `scope.set_transaction_name(task_name)`: when an invocation transaction is active it would rename the shared transaction (fighting across sibling records in a batch) and, on SDK 3.0 stream mode, corrupt the root-segment naming. The `lambda_task` tag is the reliable, version-independent attribution.
332
+
333
+ **Reporting task failures:** the executor catches task exceptions (recording `FAILED` and not re-raising). On the two **terminal** failure branches in `models.py` (plain failure and max-retries-exceeded) it logs the exception at ERROR via `task_logger.error(error, exc_info=True)`. With the assumed host config `LoggingIntegration(event_level=logging.ERROR)`, that log becomes a Sentry event, attributed to the task by the active `task_scope` tags. No explicit `capture_exception` call is made — the error log is the single reporting path, so there are no duplicate events. The **retry** branch logs at WARNING and is deliberately not reported — transient, to-be-re-enqueued failures would flood Sentry; only terminal failures are reported.
334
+
335
+ **Reporting failures outside a task:** pre-execution failures in the handler (malformed SQS message, un-importable task, misconfiguration, cold-start errors) happen before `task_scope` opens. `handler.py` logs them at ERROR inside `lambda_tasks.sentry.handler_error_scope(message_id=...)`, a context manager that tags an isolated scope (`lambda_task.handler_error` + the message id) so the resulting log-driven Sentry event is identifiable. This relies on the same `LoggingIntegration(event_level=ERROR)` as task failures. Cold-start failures are additionally re-raised so AWS marks the invocation failed and redrives.
336
+
337
+ **Error-level log forwarding (required):** both task-failure and handler-failure reporting flow through `logger.error(..., exc_info=True)` + the host's `LoggingIntegration(event_level=logging.ERROR)`. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so the host **must** configure it explicitly for error reporting to work (it is part of the recommended config and the example app). The library does not call `capture_exception` itself, so this integration is not optional for error visibility.
338
+
339
+ **SDK version compatibility:** the integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. On 2.x it uses `start_transaction` + `set_tag` for a root and `start_span` for a child; on 3.0 (stream mode) `start_span` is both root and child and metadata is attached via `set_attribute`. The correct API is detected at runtime via `hasattr`, so no code change is needed when the host upgrades the SDK.
340
+
341
+ `task_scope` and `handler_error_scope` are no-ops when `sentry_sdk` is not installed or no client is initialised, so Sentry is not a dependency of the library. `sentry-sdk` is a dev dependency only (pinned `>=2,<3`), for the test suite; a dedicated CI job additionally runs the suite against the latest sentry-sdk 3.x pre-release on Python 3.14.
333
342
 
334
343
  ## Built-in Tasks
335
344
 
@@ -16,7 +16,7 @@ django-lambda-tasks/
16
16
  │ ├── decorators.py # LambdaTaskWrapper, @lambda_task
17
17
  │ ├── handler.py # AWS Lambda entry point + AWS Batch container entry point (main)
18
18
  │ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
19
- │ ├── sentry.py # task_scope — per-task Sentry transaction name + tags
19
+ │ ├── sentry.py # task_scope + handler_error_scope — per-task trace, tags, failure attribution
20
20
  │ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
21
21
  │ ├── settings.py # LambdaTasksSettings, constants, queue type helpers
22
22
  │ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
@@ -44,11 +44,11 @@ django-lambda-tasks/
44
44
  - `decorators.py` — defines `LambdaTaskWrapper` and `@lambda_task` decorator factory; enforces kwargs-only at decoration time; validates timeouts (> 0, soft < hard) at decoration time; upper-bound timeout validation deferred to execution time based on queue type
45
45
  - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic; `execute_immediately` owns the logging + Sentry context and delegates the run to a private `_run` method), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` inspects queue config to publish to SQS, submit to Batch via `submit_batch_job`, execute eagerly, or submit to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
46
46
  - `local_executor.py` — `ProcessPoolExecutor`-based async local execution; `get_pool()` lazily creates a module-level pool; `submit_task()` fire-and-forget submission; `_execute_in_worker()` deserializes and runs the task in a child process; `_pool_initializer()` calls `django.setup()` per worker (and sets `SIGINT` to `SIG_IGN` so workers ignore Ctrl+C); `_install_shutdown_handlers()` installs main-thread `SIGINT`/`SIGTERM` handlers that release the pool before chaining to the previous handler
47
- - `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; calls `db.close_old_connections()` before each SQS record to handle stale connections on warm starts; processes SQS records independently; returns `batchItemFailures`. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event, and delegates to `handler()`
47
+ - `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; calls `db.close_old_connections()` before each SQS record to handle stale connections on warm starts; processes SQS records independently; returns `batchItemFailures`; logs pre-execution failures (malformed message, import error, misconfiguration) at ERROR inside `handler_error_scope` so the host's `LoggingIntegration` reports them tagged `lambda_task.handler_error`; cold-start failures are reported the same way and re-raised. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event, and delegates to `handler()`
48
48
  - `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
49
49
  - `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
50
50
  - `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
51
- - `sentry.py` — `task_scope` context manager; opens an isolated `new_scope` and a per-task root span/transaction named after the task, so both errors and performance traces are attributed per task (one trace per task on Lambda and Batch, not one invocation-level transaction shared across an SQS batch). Records `lambda_task` / `lambda_task.message_id` / `lambda_task.n_retries` / `lambda_task.runtime` as span metadata and scope tags; `lambda_task.runtime` is the execution environment read from `AWS_LAMBDA_FUNCTION_NAME` (Lambda) or `AWS_BATCH_JQ_NAME` (Batch). Future-proof across the SDK 2.x→3.0 boundary: it uses `start_transaction`+`set_tag` on 2.x and `start_span`+`set_attribute` on 3.0 (stream mode), detected at runtime via `hasattr`. A no-op when `sentry_sdk` is absent or uninitialised, so the library has no hard dependency on Sentry (`sentry-sdk` is a dev dependency only). Wrapped around the task body in `SQSLambdaTaskMessage.execute_immediately`
51
+ - `sentry.py` — `task_scope` context manager + `handler_error_scope` context manager. `task_scope` opens an isolated `new_scope` and traces the task. With the **recommended** host config (no `AwsLambdaIntegration`) nothing is active, so the task starts its **own transaction named after the task** on both Lambda and Batch. If a host keeps `AwsLambdaIntegration`, the task is instead traced as a **child span** of the active invocation transaction (detected via `sentry_sdk.get_current_scope().transaction` on 2.x; `start_span` is auto root-or-child on 3.0) — never a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`. Records `lambda_task` / `lambda_task.message_id` / `lambda_task.n_retries` / `lambda_task.runtime` as span metadata and scope tags (runtime read from `AWS_LAMBDA_FUNCTION_NAME` or `AWS_BATCH_JQ_NAME`). Deliberately does not call `set_transaction_name` (would rename an active invocation transaction and break 3.0 segment naming); error attribution rides on the `lambda_task` tag. **Failure reporting** is via error logs, not explicit captures: the terminal failure branches in `models.py` (plain failure + max-retries) call `task_logger.error(error, exc_info=True)`, and the host's `LoggingIntegration(event_level=ERROR)` turns that into the Sentry event (the retry branch logs at WARNING and is not reported). `handler_error_scope(message_id=...)` is a context manager used in `handler.py` to tag an isolated scope (`lambda_task.handler_error` + message id) around the handler's `logger.error`, so pre-task failures are reported and identifiable via the same LoggingIntegration. Future-proof across the SDK 2.x→3.0 boundary (verified against 2.74 and 3.0.0b4): `start_transaction`/`set_tag`/child `start_span` on 2.x, `start_span`/`set_attribute` on 3.0, detected at runtime via `hasattr`. No-op when `sentry_sdk` is absent or uninitialised (`sentry-sdk` is a dev dependency only, pinned `>=2,<3`). Wrapped around the task body in `SQSLambdaTaskMessage.execute_immediately`. Error reporting requires `LoggingIntegration(event_level=logging.ERROR)` in the host init (default on 2.x, must be set on 3.0); demonstrated in the example app
52
52
  - `settings.py` — constants (`MAX_DELAY`, `MAX_TIMEOUT`, `MAX_BATCH_TIMEOUT`); `_is_sqs_queue()` and `_is_batch_queue()` helpers; `LambdaTasksSettings` instantiated fresh per use (reads live Django settings); `queue_max_timeout()` method returns 900 or 3600 based on queue type
53
53
  - `admin.py` — Django admin registration for `TaskRecord`
54
54
  - `apps.py` — Django `AppConfig`; `ready()` imports `checks` to register deployment checks and installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
@@ -23,7 +23,7 @@ inclusion: always
23
23
  |---|---|---|
24
24
  | `pytest` | `>= 9.0.2` | Test runner |
25
25
  | `hypothesis` | present | Property-based testing (`.hypothesis/` directory exists) |
26
- | `sentry-sdk` | present | Testing the `task_scope` Sentry integration; not a runtime dependency |
26
+ | `sentry-sdk` | `>=2,<3` | Testing the Sentry integration (`task_scope`, `handler_error_scope`); verified against 2.74 and 3.0.0b4. Not a runtime dependency |
27
27
 
28
28
  ## Package Manager
29
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-lambda-tasks
3
- Version: 0.11.0
3
+ Version: 0.11.2
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: awslambdaric
@@ -446,29 +446,63 @@ fields @timestamp, @message
446
446
 
447
447
  ### Sentry
448
448
 
449
- If your application uses Sentry, tasks are automatically attributed to their task name. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks share one transaction), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task raised an error.
449
+ If your application uses Sentry, task errors are automatically attributed to the task that raised them, and each task is traced under its own task-named transaction.
450
450
 
451
- You do **not** need to add a custom integration or change your `sentry_sdk.init(...)`. Keep your existing setup:
451
+ #### Recommended `sentry_sdk.init(...)`
452
+
453
+ Use `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **do not** add `AwsLambdaIntegration`:
452
454
 
453
455
  ```python
456
+ import logging
457
+ from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
458
+ from sentry_sdk.integrations.django import DjangoIntegration
459
+ from sentry_sdk.integrations.logging import LoggingIntegration
460
+
454
461
  sentry_sdk.init(
455
462
  dsn=SENTRY_DSN,
456
- integrations=[DjangoIntegration(), AwsLambdaIntegration()],
457
- # ...
463
+ integrations=[
464
+ DjangoIntegration(),
465
+ LoggingIntegration(event_level=logging.ERROR),
466
+ ],
467
+ disabled_integrations=[
468
+ AwsLambdaIntegration(),
469
+ ],
470
+ traces_sample_rate=0.1,
458
471
  )
459
472
  ```
460
473
 
461
- When a task runs, `execute_immediately()` opens an isolated Sentry scope **and a per-task trace** (a root span/transaction named after the task). With `traces_sample_rate` set, you get one trace per task on both Lambda and Batch — rather than a single invocation-level transaction shared across a batch of SQS records. It sets, on both the span and the scope:
474
+ Two deliberate choices:
475
+
476
+ - **No `AwsLambdaIntegration`.** It names every event's transaction after the Lambda function, which hides which task ran — all tasks would look the same. Without it, each task's trace is named after the task (see below). You lose `AwsLambdaIntegration`'s automatic reporting of handler-level failures, but the library reports those itself (see "Failures outside a task"), so nothing is lost in practice.
477
+ - **`LoggingIntegration(event_level=logging.ERROR)`.** This makes `logger.error(...)` (including `task_logger`) become Sentry events. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so you must set it explicitly. It is harmless on 2.x.
478
+
479
+ #### What the library does per task
480
+
481
+ When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, as both span metadata and scope tags:
482
+
483
+ - `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`); also the transaction/span name.
484
+ - `lambda_task.message_id`, `lambda_task.n_retries`
485
+ - `lambda_task.runtime` — the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (the Batch job queue name) on Batch. Omitted when neither is set (local/eager).
486
+
487
+ Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
488
+
489
+ > If you keep `AwsLambdaIntegration` for other reasons, the library still does the right thing: it detects the active invocation transaction and traces the task as a **child span** of it (rather than a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`). In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
490
+
491
+ #### Task failures are reported
492
+
493
+ The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. On the **terminal failure paths** — a plain failure and max-retries-exceeded — the library logs the exception at ERROR (`task_logger.error(..., exc_info=True)`), which your `LoggingIntegration(event_level=ERROR)` turns into a Sentry event, attributed to the task via the active scope tags. This is why that integration is part of the recommended config: without it, a merely-caught exception produces no error event on 3.0.
494
+
495
+ Transient, retryable failures (a task that will be re-enqueued) are logged at WARNING and **not** reported to Sentry, to avoid flooding on every retry; only the final, terminal failure is reported.
496
+
497
+ #### Failures outside a task
462
498
 
463
- - the **transaction/span name** to the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`), overriding the Lambda function name on Lambda and replacing `"--"` on Batch
464
- - `lambda_task` (task name), `lambda_task.message_id`, and `lambda_task.n_retries`
465
- - `lambda_task.runtime` — the physical execution environment, derived from the AWS environment: the Lambda function name (`AWS_LAMBDA_FUNCTION_NAME`) on Lambda, or the Batch job queue name (`AWS_BATCH_JQ_NAME`) on Batch. Omitted when neither is set (e.g. local/eager execution).
499
+ Pre-execution failures in the Lambda/Batch handler — a malformed SQS message, a task that cannot be imported, a misconfiguration, or a cold-start error — happen before the task scope opens and are caught by the handler. The handler logs them at ERROR inside a `handler_error_scope` that tags the event `lambda_task.handler_error` (plus the message id). With `LoggingIntegration(event_level=ERROR)` that log becomes a single, tagged Sentry event — so you are still alerted even though `AwsLambdaIntegration` is not installed. Cold-start failures are also re-raised so AWS marks the invocation failed and redrives it.
466
500
 
467
- This covers task failures too: the executor records failures as `FAILED` `TaskRecord`s rather than re-raising, so the trace wraps the task body to ensure Sentry error events still carry the task name.
501
+ #### SDK version compatibility
468
502
 
469
- The integration is future-proof across the sentry-sdk 2.x→3.0 boundary: it uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade the SDK.
503
+ The integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. It uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade.
470
504
 
471
- `sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, the enrichment is a silent no-op.
505
+ `sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, all of the above is a silent no-op.
472
506
 
473
507
  ---
474
508
 
@@ -434,29 +434,63 @@ fields @timestamp, @message
434
434
 
435
435
  ### Sentry
436
436
 
437
- If your application uses Sentry, tasks are automatically attributed to their task name. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks share one transaction), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task raised an error.
437
+ If your application uses Sentry, task errors are automatically attributed to the task that raised them, and each task is traced under its own task-named transaction.
438
438
 
439
- You do **not** need to add a custom integration or change your `sentry_sdk.init(...)`. Keep your existing setup:
439
+ #### Recommended `sentry_sdk.init(...)`
440
+
441
+ Use `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **do not** add `AwsLambdaIntegration`:
440
442
 
441
443
  ```python
444
+ import logging
445
+ from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
446
+ from sentry_sdk.integrations.django import DjangoIntegration
447
+ from sentry_sdk.integrations.logging import LoggingIntegration
448
+
442
449
  sentry_sdk.init(
443
450
  dsn=SENTRY_DSN,
444
- integrations=[DjangoIntegration(), AwsLambdaIntegration()],
445
- # ...
451
+ integrations=[
452
+ DjangoIntegration(),
453
+ LoggingIntegration(event_level=logging.ERROR),
454
+ ],
455
+ disabled_integrations=[
456
+ AwsLambdaIntegration(),
457
+ ],
458
+ traces_sample_rate=0.1,
446
459
  )
447
460
  ```
448
461
 
449
- When a task runs, `execute_immediately()` opens an isolated Sentry scope **and a per-task trace** (a root span/transaction named after the task). With `traces_sample_rate` set, you get one trace per task on both Lambda and Batch — rather than a single invocation-level transaction shared across a batch of SQS records. It sets, on both the span and the scope:
462
+ Two deliberate choices:
463
+
464
+ - **No `AwsLambdaIntegration`.** It names every event's transaction after the Lambda function, which hides which task ran — all tasks would look the same. Without it, each task's trace is named after the task (see below). You lose `AwsLambdaIntegration`'s automatic reporting of handler-level failures, but the library reports those itself (see "Failures outside a task"), so nothing is lost in practice.
465
+ - **`LoggingIntegration(event_level=logging.ERROR)`.** This makes `logger.error(...)` (including `task_logger`) become Sentry events. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so you must set it explicitly. It is harmless on 2.x.
466
+
467
+ #### What the library does per task
468
+
469
+ When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, as both span metadata and scope tags:
470
+
471
+ - `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`); also the transaction/span name.
472
+ - `lambda_task.message_id`, `lambda_task.n_retries`
473
+ - `lambda_task.runtime` — the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (the Batch job queue name) on Batch. Omitted when neither is set (local/eager).
474
+
475
+ Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
476
+
477
+ > If you keep `AwsLambdaIntegration` for other reasons, the library still does the right thing: it detects the active invocation transaction and traces the task as a **child span** of it (rather than a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`). In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
478
+
479
+ #### Task failures are reported
480
+
481
+ The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. On the **terminal failure paths** — a plain failure and max-retries-exceeded — the library logs the exception at ERROR (`task_logger.error(..., exc_info=True)`), which your `LoggingIntegration(event_level=ERROR)` turns into a Sentry event, attributed to the task via the active scope tags. This is why that integration is part of the recommended config: without it, a merely-caught exception produces no error event on 3.0.
482
+
483
+ Transient, retryable failures (a task that will be re-enqueued) are logged at WARNING and **not** reported to Sentry, to avoid flooding on every retry; only the final, terminal failure is reported.
484
+
485
+ #### Failures outside a task
450
486
 
451
- - the **transaction/span name** to the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`), overriding the Lambda function name on Lambda and replacing `"--"` on Batch
452
- - `lambda_task` (task name), `lambda_task.message_id`, and `lambda_task.n_retries`
453
- - `lambda_task.runtime` — the physical execution environment, derived from the AWS environment: the Lambda function name (`AWS_LAMBDA_FUNCTION_NAME`) on Lambda, or the Batch job queue name (`AWS_BATCH_JQ_NAME`) on Batch. Omitted when neither is set (e.g. local/eager execution).
487
+ Pre-execution failures in the Lambda/Batch handler — a malformed SQS message, a task that cannot be imported, a misconfiguration, or a cold-start error — happen before the task scope opens and are caught by the handler. The handler logs them at ERROR inside a `handler_error_scope` that tags the event `lambda_task.handler_error` (plus the message id). With `LoggingIntegration(event_level=ERROR)` that log becomes a single, tagged Sentry event — so you are still alerted even though `AwsLambdaIntegration` is not installed. Cold-start failures are also re-raised so AWS marks the invocation failed and redrives it.
454
488
 
455
- This covers task failures too: the executor records failures as `FAILED` `TaskRecord`s rather than re-raising, so the trace wraps the task body to ensure Sentry error events still carry the task name.
489
+ #### SDK version compatibility
456
490
 
457
- The integration is future-proof across the sentry-sdk 2.x→3.0 boundary: it uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade the SDK.
491
+ The integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. It uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade.
458
492
 
459
- `sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, the enrichment is a silent no-op.
493
+ `sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, all of the above is a silent no-op.
460
494
 
461
495
  ---
462
496
 
@@ -47,3 +47,16 @@ The task runs in a background worker process and the response confirms it was tr
47
47
  Visit: `http://127.0.0.1:8000/admin/` → Lambda Tasks → Task records
48
48
 
49
49
  Log in with the superuser credentials you created above.
50
+
51
+ ## Testing the Sentry integration (optional)
52
+
53
+ Set `SENTRY_DSN` to see per-task traces and error attribution in Sentry. The init lives in `example_project/sentry.py` and uses `DjangoIntegration` + `LoggingIntegration(event_level=ERROR)` **without** `AwsLambdaIntegration`, so each task is traced under its own task-named transaction (see the library README's Sentry section for why).
54
+
55
+ ```bash
56
+ SENTRY_DSN="https://<key>@<org>.ingest.sentry.io/<project>" uv run python manage.py runserver
57
+ ```
58
+
59
+ - `http://127.0.0.1:8000/trigger/?name=Alice` — a successful task; produces a transaction named `example_app.tasks.greet`.
60
+ - `http://127.0.0.1:8000/trigger-error/?name=Bob` — a failing task; produces an error attributed to `example_app.tasks.boom` (via the library's `capture_exception` on the terminal failure path).
61
+
62
+ Without `SENTRY_DSN` set, Sentry initialisation is skipped and the app runs exactly as above.
@@ -36,14 +36,21 @@ def init_sentry() -> None:
36
36
  dsn=dsn,
37
37
  integrations=[
38
38
  DjangoIntegration(),
39
- AwsLambdaIntegration(),
40
- # On sentry-sdk 2.x the logging integration defaulted to
41
- # event_level=ERROR, so logger.error(...) calls (e.g. task_logger)
42
- # became Sentry events automatically. On 3.0 the default is None, so
43
- # error logs no longer surface as events. Set it explicitly so
44
- # task_logger.error output appears in Sentry on both SDK versions.
39
+ # LoggingIntegration(event_level=ERROR) is REQUIRED, not optional:
40
+ # the library reports task and handler failures by logging them at
41
+ # ERROR (not by calling capture_exception), so this integration is
42
+ # what turns those logs into Sentry events. On sentry-sdk 2.x
43
+ # event_level=ERROR was the default; on 3.0 the default is None, so
44
+ # it must be set explicitly.
45
45
  LoggingIntegration(event_level=logging.ERROR),
46
46
  ],
47
+ disabled_integrations=[
48
+ # AwsLambdaIntegration is intentionally omitted. It names every
49
+ # event's transaction after the Lambda function, which hides the
50
+ # task name. Without it, each task's trace is named after the task
51
+ # (see lambda_tasks.sentry).
52
+ AwsLambdaIntegration(),
53
+ ],
47
54
  # Sample every transaction so the per-task trace is always sent while
48
55
  # you are testing. Lower this in a real deployment.
49
56
  traces_sample_rate=float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "1.0")),
@@ -26,6 +26,7 @@ from django.apps import apps
26
26
 
27
27
  from lambda_tasks.environment_loader import resolve_environment
28
28
  from lambda_tasks.secret_loader import resolve_secrets_into_env
29
+ from lambda_tasks.sentry import handler_error_scope
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
@@ -124,7 +125,17 @@ def handler(event: dict, context: object) -> dict:
124
125
  Returns a partial-batch failure report so AWS only re-drives failed records.
125
126
  Signature is fixed by AWS and uses two args only.
126
127
  """
127
- _perform_cold_start()
128
+ try:
129
+ _perform_cold_start()
130
+ except Exception:
131
+ # Cold-start failures (secret resolution, django.setup(), memory limit)
132
+ # escape handler() so the invocation is marked failed and redriven, as
133
+ # AWS expects. The error log is reported to Sentry by the host's
134
+ # LoggingIntegration; handler_error_scope tags it as a handler error.
135
+ # Re-raise to preserve the AWS failure contract.
136
+ with handler_error_scope():
137
+ logger.error("Cold-start initialisation failed", exc_info=True)
138
+ raise
128
139
 
129
140
  # Local import due to AppRegistryNotReady
130
141
  from lambda_tasks.models import SQSLambdaTaskMessage
@@ -138,10 +149,13 @@ def handler(event: dict, context: object) -> dict:
138
149
  record["body"]
139
150
  ).execute_immediately(message_id=record["messageId"])
140
151
  except Exception:
141
- logger.error(
142
- "Failed to process SQS record",
143
- exc_info=True,
144
- )
152
+ # Pre-execution failures (malformed message, import error,
153
+ # misconfiguration) happen before execute_immediately opens its
154
+ # task_scope. The error log is reported to Sentry by the host's
155
+ # LoggingIntegration; handler_error_scope tags it with the message
156
+ # id and marks it a handler error.
157
+ with handler_error_scope(message_id=record.get("messageId")):
158
+ logger.error("Failed to process SQS record", exc_info=True)
145
159
  batch_item_failures.append({"itemIdentifier": record["messageId"]})
146
160
 
147
161
  return {"batchItemFailures": batch_item_failures}
@@ -154,21 +168,31 @@ def main() -> int:
154
168
  constructs a synthetic SQS event, and delegates to handler().
155
169
 
156
170
  Returns 0 on success, 1 on failure.
171
+
172
+ Wrapped in a top-level handler so that failures on the Batch path — a
173
+ missing message, or an exception escaping cold-start/``handler()`` (e.g.
174
+ secret resolution or ``django.setup()``) — are reported to Sentry. The
175
+ Batch container has no AWS-side Sentry integration, so without this such
176
+ failures would only reach stderr.
157
177
  """
158
178
  logging.basicConfig(level=logging.INFO)
159
179
 
160
- message_json = os.environ.get("LAMBDA_TASKS_MESSAGE")
161
- if not message_json:
162
- logger.error("LAMBDA_TASKS_MESSAGE environment variable is not set.")
180
+ try:
181
+ message_json = os.environ.get("LAMBDA_TASKS_MESSAGE")
182
+ if not message_json:
183
+ raise RuntimeError("LAMBDA_TASKS_MESSAGE environment variable is not set.")
184
+
185
+ message_id = os.environ.get("AWS_BATCH_JOB_ID", str(uuid.uuid4()))
186
+
187
+ result = handler(
188
+ event={"Records": [{"messageId": message_id, "body": message_json}]},
189
+ context=None,
190
+ )
191
+ except Exception:
192
+ with handler_error_scope(message_id=os.environ.get("AWS_BATCH_JOB_ID")):
193
+ logger.error("AWS Batch entry point failed", exc_info=True)
163
194
  return 1
164
195
 
165
- message_id = os.environ.get("AWS_BATCH_JOB_ID", str(uuid.uuid4()))
166
-
167
- result = handler(
168
- event={"Records": [{"messageId": message_id, "body": message_json}]},
169
- context=None,
170
- )
171
-
172
196
  if result["batchItemFailures"]:
173
197
  return 1
174
198
  else:
@@ -21,7 +21,7 @@ from redis.exceptions import LockError
21
21
 
22
22
  from lambda_tasks.local_executor import submit_task
23
23
  from lambda_tasks.logging import task_logger
24
- from lambda_tasks.sentry import capture_task_exception, task_scope
24
+ from lambda_tasks.sentry import task_scope
25
25
  from lambda_tasks.settings import (
26
26
  MAX_DELAY,
27
27
  BatchQueueConfig,
@@ -210,7 +210,8 @@ class SQSLambdaTaskMessage(BaseModel):
210
210
  elif effective_retry_on and isinstance(error, effective_retry_on):
211
211
  conf = LambdaTasksSettings()
212
212
  if self.n_retries >= conf.MAX_RETRIES:
213
- capture_task_exception()
213
+ task_logger.error("Maximum retries exceeded", exc_info=True)
214
+
214
215
  record.status = TaskStatus.FAILED
215
216
  record.traceback = traceback.format_exc()
216
217
  record.end_time = now()
@@ -252,7 +253,6 @@ class SQSLambdaTaskMessage(BaseModel):
252
253
  else:
253
254
  task_logger.error(error, exc_info=True)
254
255
 
255
- capture_task_exception()
256
256
  record.status = TaskStatus.FAILED
257
257
  record.traceback = traceback.format_exc()
258
258
  record.end_time = now()