hawkapi-sentry 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ name: Lint
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v4
15
+ with:
16
+ enable-cache: true
17
+ - name: Install dependencies
18
+ run: uv sync --extra dev
19
+ - name: ruff check
20
+ run: uv run ruff check .
21
+ - name: ruff format check
22
+ run: uv run ruff format --check .
23
+
24
+ typecheck:
25
+ name: Typecheck
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v4
30
+ with:
31
+ enable-cache: true
32
+ - name: Install dependencies
33
+ run: uv sync --extra dev
34
+ - name: pyright
35
+ run: uv run pyright src/
36
+
37
+ test:
38
+ name: Test (Python ${{ matrix.python-version }})
39
+ runs-on: ubuntu-latest
40
+ strategy:
41
+ matrix:
42
+ python-version: ["3.12", "3.13"]
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: astral-sh/setup-uv@v4
46
+ with:
47
+ enable-cache: true
48
+ python-version: ${{ matrix.python-version }}
49
+ - name: Install dependencies
50
+ run: uv sync --extra dev
51
+ - name: Run tests
52
+ run: uv run pytest tests/ -q
@@ -0,0 +1,25 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ name: Build and publish to PyPI
10
+ runs-on: ubuntu-latest
11
+ environment: release
12
+ permissions:
13
+ id-token: write # required for trusted publishing
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v4
18
+ with:
19
+ enable-cache: true
20
+ - name: Build package
21
+ run: uv build
22
+ - name: Publish to PyPI
23
+ uses: pypa/gh-action-pypi-publish@release/v1
24
+ with:
25
+ packages-dir: dist/
@@ -0,0 +1,36 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .env
14
+ *.log
15
+ .mypy_cache/
16
+ .pyright/
17
+ .ruff_cache/
18
+ .pytest_cache/
19
+ htmlcov/
20
+ .coverage
21
+ .coverage.*
22
+ coverage.xml
23
+ *.cover
24
+ .hypothesis/
25
+ .tox/
26
+ .nox/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+ .DS_Store
31
+ .idea/
32
+ .vscode/
33
+ .history/
34
+ .claude/
35
+ site/
36
+ .remember/
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-04-19
11
+
12
+ ### Added
13
+
14
+ - `SentryPlugin` — HawkAPI `Plugin` subclass that initialises `sentry_sdk` on startup,
15
+ flushes on shutdown, and captures unhandled exceptions via `on_exception`.
16
+ - `SentryMiddleware` — HawkAPI `Middleware` subclass that starts a Sentry performance
17
+ transaction per request and attaches `http.*` tags on response.
18
+ - No-op mode: when `dsn` is `None` or empty string, `SentryPlugin.on_startup` does
19
+ nothing — safe to use in local/test environments without a real DSN.
20
+ - `ignore_status_codes` parameter (default `(404, 401, 403)`) to suppress Sentry events
21
+ for expected HTTP errors.
22
+ - `user_getter` callable and `include_user_data` flag for attaching user context to events.
23
+ - `before_send` passthrough to `sentry_sdk.init` for full event filtering control.
24
+ - Global `tags` dict applied to every captured event.
25
+ - `traces_sample_rate` and `profiles_sample_rate` wiring.
26
+ - Header redaction for `Authorization`, `Cookie`, `X-Api-Key`, `X-Auth-Token`,
27
+ `Proxy-Authorization` in request context attached to Sentry events.
28
+ - `traceparent` header propagation into Sentry transaction `trace_id`.
29
+ - Full test suite (15 tests) covering plugin lifecycle, middleware behaviour,
30
+ header redaction, user context, tag propagation, and integration with a real
31
+ HawkAPI app via `httpx.AsyncClient`.
32
+ - CI workflow: lint (`ruff`), typecheck (`pyright`), test matrix (Python 3.12 + 3.13).
33
+ - Release workflow: build with `uv build`, publish via PyPI trusted publishing.
34
+
35
+ [Unreleased]: https://github.com/ashimov/hawkapi-sentry/compare/v0.1.0...HEAD
36
+ [0.1.0]: https://github.com/ashimov/hawkapi-sentry/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HawkAPI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: hawkapi-sentry
3
+ Version: 0.1.0
4
+ Summary: Sentry integration for HawkAPI — plugin + middleware
5
+ Project-URL: Homepage, https://github.com/ashimov/hawkapi-sentry
6
+ Project-URL: Repository, https://github.com/ashimov/hawkapi-sentry
7
+ Project-URL: Issues, https://github.com/ashimov/hawkapi-sentry/issues
8
+ Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: error-tracking,hawkapi,monitoring,observability,sentry
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: hawkapi>=0.1.3
23
+ Requires-Dist: sentry-sdk>=2.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: httpx>=0.27; extra == 'dev'
26
+ Requires-Dist: pyright>=1.1; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # hawkapi-sentry
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/hawkapi-sentry)](https://pypi.org/project/hawkapi-sentry/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/hawkapi-sentry)](https://pypi.org/project/hawkapi-sentry/)
36
+ [![License](https://img.shields.io/pypi/l/hawkapi-sentry)](LICENSE)
37
+ [![CI](https://github.com/ashimov/hawkapi-sentry/actions/workflows/ci.yml/badge.svg)](https://github.com/ashimov/hawkapi-sentry/actions/workflows/ci.yml)
38
+ [![Downloads](https://img.shields.io/pypi/dm/hawkapi-sentry)](https://pypi.org/project/hawkapi-sentry/)
39
+
40
+ Sentry integration for [HawkAPI](https://github.com/ashimov/HawkAPI) — a plugin that initialises the Sentry SDK on startup and captures unhandled exceptions, plus a middleware that creates a performance transaction per request.
41
+
42
+ ---
43
+
44
+ ## Quickstart
45
+
46
+ ```bash
47
+ pip install hawkapi-sentry
48
+ ```
49
+
50
+ ```python
51
+ from hawkapi import HawkAPI
52
+ from hawkapi_sentry import SentryPlugin, SentryMiddleware
53
+
54
+ app = HawkAPI()
55
+
56
+ app.add_plugin(
57
+ SentryPlugin(
58
+ dsn="https://key@sentry.io/123",
59
+ environment="production",
60
+ traces_sample_rate=0.1,
61
+ )
62
+ )
63
+ app.add_middleware(SentryMiddleware)
64
+ ```
65
+
66
+ That's it. Every unhandled exception is captured with full request context; every request gets a Sentry performance transaction.
67
+
68
+ ---
69
+
70
+ ## `SentryPlugin` parameter reference
71
+
72
+ | Parameter | Type | Default | Description |
73
+ |---|---|---|---|
74
+ | `dsn` | `str \| None` | `None` | Sentry DSN. Empty/`None` = no-op mode (safe for local dev). |
75
+ | `environment` | `str` | `"production"` | Sentry environment tag. |
76
+ | `release` | `str \| None` | `None` | Release string. `None` = not set. |
77
+ | `traces_sample_rate` | `float` | `0.0` | Fraction of transactions to sample for performance monitoring. |
78
+ | `profiles_sample_rate` | `float` | `0.0` | Fraction of sampled transactions to profile. |
79
+ | `include_user_data` | `bool` | `False` | When `True`, attach `request.state.user` dict to Sentry events. |
80
+ | `user_getter` | `Callable[[Request], dict] \| None` | `None` | Custom callable to extract user data from the request. Takes priority over `include_user_data`. |
81
+ | `before_send` | `Callable \| None` | `None` | Passed directly to `sentry_sdk.init`. Return `None` to drop an event. |
82
+ | `tags` | `dict[str, str] \| None` | `None` | Global tags applied to every captured event. |
83
+ | `ignore_status_codes` | `tuple[int, ...]` | `(404, 401, 403)` | HTTP status codes for which exceptions are **not** sent to Sentry. |
84
+
85
+ ---
86
+
87
+ ## Migration from `sentry-sdk[fastapi]`
88
+
89
+ | FastAPI / starlette-sentry | hawkapi-sentry |
90
+ |---|---|
91
+ | `sentry_sdk.init(integrations=[StarletteIntegration(), FastApiIntegration()])` | `app.add_plugin(SentryPlugin(dsn=...))` |
92
+ | `SentryAsgiMiddleware(app)` | `app.add_middleware(SentryMiddleware)` |
93
+ | `before_send` kwarg to `sentry_sdk.init` | `SentryPlugin(before_send=...)` |
94
+ | Manual `with sentry_sdk.push_scope() as scope: scope.set_user(...)` | `SentryPlugin(user_getter=lambda req: {...})` |
95
+
96
+ The main difference: HawkAPI plugins own the SDK lifecycle, so you never call `sentry_sdk.init()` yourself — `SentryPlugin.on_startup()` does it.
97
+
98
+ ---
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ # Clone and install in editable mode with dev extras
104
+ git clone https://github.com/ashimov/hawkapi-sentry.git
105
+ cd hawkapi-sentry
106
+ uv sync --extra dev
107
+
108
+ # Run tests
109
+ uv run pytest tests/ -q
110
+
111
+ # Lint
112
+ uv run ruff check .
113
+ uv run ruff format .
114
+
115
+ # Type-check
116
+ uv run pyright src/
117
+ ```
118
+
119
+ ---
120
+
121
+ ## License
122
+
123
+ [MIT](LICENSE) — Copyright (c) 2026 HawkAPI Contributors.
@@ -0,0 +1,92 @@
1
+ # hawkapi-sentry
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/hawkapi-sentry)](https://pypi.org/project/hawkapi-sentry/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/hawkapi-sentry)](https://pypi.org/project/hawkapi-sentry/)
5
+ [![License](https://img.shields.io/pypi/l/hawkapi-sentry)](LICENSE)
6
+ [![CI](https://github.com/ashimov/hawkapi-sentry/actions/workflows/ci.yml/badge.svg)](https://github.com/ashimov/hawkapi-sentry/actions/workflows/ci.yml)
7
+ [![Downloads](https://img.shields.io/pypi/dm/hawkapi-sentry)](https://pypi.org/project/hawkapi-sentry/)
8
+
9
+ Sentry integration for [HawkAPI](https://github.com/ashimov/HawkAPI) — a plugin that initialises the Sentry SDK on startup and captures unhandled exceptions, plus a middleware that creates a performance transaction per request.
10
+
11
+ ---
12
+
13
+ ## Quickstart
14
+
15
+ ```bash
16
+ pip install hawkapi-sentry
17
+ ```
18
+
19
+ ```python
20
+ from hawkapi import HawkAPI
21
+ from hawkapi_sentry import SentryPlugin, SentryMiddleware
22
+
23
+ app = HawkAPI()
24
+
25
+ app.add_plugin(
26
+ SentryPlugin(
27
+ dsn="https://key@sentry.io/123",
28
+ environment="production",
29
+ traces_sample_rate=0.1,
30
+ )
31
+ )
32
+ app.add_middleware(SentryMiddleware)
33
+ ```
34
+
35
+ That's it. Every unhandled exception is captured with full request context; every request gets a Sentry performance transaction.
36
+
37
+ ---
38
+
39
+ ## `SentryPlugin` parameter reference
40
+
41
+ | Parameter | Type | Default | Description |
42
+ |---|---|---|---|
43
+ | `dsn` | `str \| None` | `None` | Sentry DSN. Empty/`None` = no-op mode (safe for local dev). |
44
+ | `environment` | `str` | `"production"` | Sentry environment tag. |
45
+ | `release` | `str \| None` | `None` | Release string. `None` = not set. |
46
+ | `traces_sample_rate` | `float` | `0.0` | Fraction of transactions to sample for performance monitoring. |
47
+ | `profiles_sample_rate` | `float` | `0.0` | Fraction of sampled transactions to profile. |
48
+ | `include_user_data` | `bool` | `False` | When `True`, attach `request.state.user` dict to Sentry events. |
49
+ | `user_getter` | `Callable[[Request], dict] \| None` | `None` | Custom callable to extract user data from the request. Takes priority over `include_user_data`. |
50
+ | `before_send` | `Callable \| None` | `None` | Passed directly to `sentry_sdk.init`. Return `None` to drop an event. |
51
+ | `tags` | `dict[str, str] \| None` | `None` | Global tags applied to every captured event. |
52
+ | `ignore_status_codes` | `tuple[int, ...]` | `(404, 401, 403)` | HTTP status codes for which exceptions are **not** sent to Sentry. |
53
+
54
+ ---
55
+
56
+ ## Migration from `sentry-sdk[fastapi]`
57
+
58
+ | FastAPI / starlette-sentry | hawkapi-sentry |
59
+ |---|---|
60
+ | `sentry_sdk.init(integrations=[StarletteIntegration(), FastApiIntegration()])` | `app.add_plugin(SentryPlugin(dsn=...))` |
61
+ | `SentryAsgiMiddleware(app)` | `app.add_middleware(SentryMiddleware)` |
62
+ | `before_send` kwarg to `sentry_sdk.init` | `SentryPlugin(before_send=...)` |
63
+ | Manual `with sentry_sdk.push_scope() as scope: scope.set_user(...)` | `SentryPlugin(user_getter=lambda req: {...})` |
64
+
65
+ The main difference: HawkAPI plugins own the SDK lifecycle, so you never call `sentry_sdk.init()` yourself — `SentryPlugin.on_startup()` does it.
66
+
67
+ ---
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ # Clone and install in editable mode with dev extras
73
+ git clone https://github.com/ashimov/hawkapi-sentry.git
74
+ cd hawkapi-sentry
75
+ uv sync --extra dev
76
+
77
+ # Run tests
78
+ uv run pytest tests/ -q
79
+
80
+ # Lint
81
+ uv run ruff check .
82
+ uv run ruff format .
83
+
84
+ # Type-check
85
+ uv run pyright src/
86
+ ```
87
+
88
+ ---
89
+
90
+ ## License
91
+
92
+ [MIT](LICENSE) — Copyright (c) 2026 HawkAPI Contributors.
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hawkapi-sentry"
7
+ version = "0.1.0"
8
+ description = "Sentry integration for HawkAPI — plugin + middleware"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ authors = [
14
+ { name = "HawkAPI Contributors", email = "hawkapi@users.noreply.github.com" },
15
+ ]
16
+ keywords = ["hawkapi", "sentry", "observability", "error-tracking", "monitoring"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Framework :: AsyncIO",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Internet :: WWW/HTTP",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "hawkapi>=0.1.3",
30
+ "sentry-sdk>=2.0",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/ashimov/hawkapi-sentry"
35
+ Repository = "https://github.com/ashimov/hawkapi-sentry"
36
+ Issues = "https://github.com/ashimov/hawkapi-sentry/issues"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=8.0",
41
+ "pytest-asyncio>=0.24",
42
+ "httpx>=0.27",
43
+ "ruff>=0.8",
44
+ "pyright>=1.1",
45
+ ]
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/hawkapi_sentry"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ asyncio_mode = "auto"
53
+ filterwarnings = [
54
+ "ignore:cannot collect test class:pytest.PytestCollectionWarning",
55
+ ]
56
+
57
+ [tool.ruff]
58
+ target-version = "py312"
59
+ line-length = 100
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E", "F", "I", "UP", "B", "SIM", "S"]
63
+ ignore = ["S101"]
64
+
65
+ [tool.ruff.lint.per-file-ignores]
66
+ "tests/**" = ["S"]
67
+
68
+ [tool.pyright]
69
+ pythonVersion = "3.12"
70
+ typeCheckingMode = "strict"
@@ -0,0 +1,14 @@
1
+ """hawkapi-sentry — Sentry integration for HawkAPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hawkapi_sentry._middleware import SentryMiddleware
6
+ from hawkapi_sentry._plugin import SentryPlugin
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "SentryMiddleware",
12
+ "SentryPlugin",
13
+ "__version__",
14
+ ]
@@ -0,0 +1,61 @@
1
+ """Request context helpers for Sentry event enrichment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ _REDACT_HEADER_NAMES = frozenset(
8
+ [
9
+ "authorization",
10
+ "cookie",
11
+ "x-api-key",
12
+ "x-auth-token",
13
+ "proxy-authorization",
14
+ ]
15
+ )
16
+
17
+ _FILTERED = "[Filtered]"
18
+
19
+
20
+ def redact_headers(headers: Any) -> dict[str, str]:
21
+ """Return a dict of headers with sensitive values masked.
22
+
23
+ Accepts any iterable of (key, value) pairs or an object with an .items()
24
+ method — covers both HawkAPI Headers (__iter__ yields tuples) and plain dicts.
25
+ """
26
+ result: dict[str, str] = {}
27
+ items: Any = headers.items() if hasattr(headers, "items") else headers
28
+ for key, value in items:
29
+ if key.lower() in _REDACT_HEADER_NAMES:
30
+ result[key] = _FILTERED
31
+ else:
32
+ result[key] = value
33
+ return result
34
+
35
+
36
+ def status_class(status_code: int) -> str:
37
+ """Map an HTTP status code to an OTel/Sentry transaction status string."""
38
+ if status_code < 400:
39
+ return "ok"
40
+ if status_code < 500:
41
+ return "invalid_argument"
42
+ return "internal_error"
43
+
44
+
45
+ def request_context(request: Any) -> dict[str, Any]:
46
+ """Build a Sentry request context dict from a HawkAPI Request."""
47
+ url: str = request.url
48
+ qs_raw: Any = request.query_string
49
+ qs: str = qs_raw.decode("latin-1") if hasattr(qs_raw, "decode") else str(qs_raw)
50
+ return {
51
+ "method": request.method,
52
+ "url": url,
53
+ "headers": redact_headers(request.headers),
54
+ "query_string": qs,
55
+ }
56
+
57
+
58
+ # Keep underscore aliases so tests that import _redact_headers etc. still work
59
+ _redact_headers = redact_headers
60
+ _status_class = status_class
61
+ _request_context = request_context
@@ -0,0 +1,61 @@
1
+ """SentryMiddleware — per-request Sentry transaction and breadcrumb."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import sentry_sdk
8
+ from hawkapi.middleware.base import Middleware
9
+ from hawkapi.requests.request import Request
10
+ from hawkapi.responses.json_response import JSONResponse
11
+ from hawkapi.responses.response import Response
12
+
13
+ from hawkapi_sentry._context import status_class
14
+
15
+
16
+ class SentryMiddleware(Middleware):
17
+ """Starts a Sentry performance transaction for every HTTP request."""
18
+
19
+ async def before_request(self, request: Request) -> Request | Response | JSONResponse | None:
20
+ """Start a Sentry transaction and add a breadcrumb."""
21
+ method: str = request.method
22
+ path: str = request.path
23
+
24
+ # Extract traceparent for distributed tracing
25
+ traceparent = request.headers.get("sentry-trace") or request.headers.get("traceparent")
26
+
27
+ transaction = sentry_sdk.start_transaction(
28
+ op="http.server",
29
+ name=f"{method} {path}",
30
+ source="url",
31
+ )
32
+ if traceparent:
33
+ transaction.continuing_trace({"sentry-trace": traceparent}) # type: ignore[attr-defined]
34
+
35
+ request.state._sentry_tx = transaction # type: ignore[attr-defined]
36
+ transaction.__enter__() # type: ignore[attr-defined]
37
+
38
+ sentry_sdk.add_breadcrumb(
39
+ category="request",
40
+ message=f"{method} {path}",
41
+ level="info",
42
+ )
43
+
44
+ return None
45
+
46
+ async def after_response(
47
+ self, request: Request, response: Response | JSONResponse
48
+ ) -> Response | JSONResponse | None:
49
+ """Finish the Sentry transaction with HTTP metadata."""
50
+ tx: Any = getattr(getattr(request, "state", None), "_sentry_tx", None)
51
+ if tx is None:
52
+ return None
53
+
54
+ status_code: int = response.status_code
55
+ tx.set_tag("http.method", request.method)
56
+ tx.set_tag("http.status_code", str(status_code))
57
+ tx.set_tag("http.target", request.path)
58
+ tx.set_status(status_class(status_code))
59
+ tx.__exit__(None, None, None) # type: ignore[attr-defined]
60
+
61
+ return None