Skip to content

Commit 1c64ff7

Browse files
authored
Configure HTTP methods to capture in WSGI middleware and frameworks (#3531)
- Do not capture transactions for OPTIONS and HEAD HTTP methods by default. - Make it possible with an `http_methods_to_capture` config option for Django, Flask, Starlette, and FastAPI to specify what HTTP methods to capture.
1 parent a3ab1ea commit 1c64ff7

File tree

12 files changed

+477
-72
lines changed

12 files changed

+477
-72
lines changed

Diff for: sentry_sdk/integrations/_wsgi_common.py

+21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from contextlib import contextmanager
12
import json
23
from copy import deepcopy
34

@@ -15,6 +16,7 @@
1516
if TYPE_CHECKING:
1617
from typing import Any
1718
from typing import Dict
19+
from typing import Iterator
1820
from typing import Mapping
1921
from typing import MutableMapping
2022
from typing import Optional
@@ -37,6 +39,25 @@
3739
x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_")
3840
)
3941

42+
DEFAULT_HTTP_METHODS_TO_CAPTURE = (
43+
"CONNECT",
44+
"DELETE",
45+
"GET",
46+
# "HEAD", # do not capture HEAD requests by default
47+
# "OPTIONS", # do not capture OPTIONS requests by default
48+
"PATCH",
49+
"POST",
50+
"PUT",
51+
"TRACE",
52+
)
53+
54+
55+
# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support
56+
@contextmanager
57+
def nullcontext():
58+
# type: () -> Iterator[None]
59+
yield
60+
4061

4162
def request_body_within_bounds(client, content_length):
4263
# type: (Optional[sentry_sdk.client.BaseClient], int) -> bool

Diff for: sentry_sdk/integrations/asgi.py

+57-43
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
_get_request_data,
1919
_get_url,
2020
)
21+
from sentry_sdk.integrations._wsgi_common import (
22+
DEFAULT_HTTP_METHODS_TO_CAPTURE,
23+
nullcontext,
24+
)
2125
from sentry_sdk.sessions import track_session
2226
from sentry_sdk.tracing import (
2327
SOURCE_FOR_STYLE,
@@ -89,17 +93,19 @@ class SentryAsgiMiddleware:
8993
"transaction_style",
9094
"mechanism_type",
9195
"span_origin",
96+
"http_methods_to_capture",
9297
)
9398

9499
def __init__(
95100
self,
96-
app,
97-
unsafe_context_data=False,
98-
transaction_style="endpoint",
99-
mechanism_type="asgi",
100-
span_origin="manual",
101+
app, # type: Any
102+
unsafe_context_data=False, # type: bool
103+
transaction_style="endpoint", # type: str
104+
mechanism_type="asgi", # type: str
105+
span_origin="manual", # type: str
106+
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
101107
):
102-
# type: (Any, bool, str, str, str) -> None
108+
# type: (...) -> None
103109
"""
104110
Instrument an ASGI application with Sentry. Provides HTTP/websocket
105111
data to sent events and basic handling for exceptions bubbling up
@@ -134,6 +140,7 @@ def __init__(
134140
self.mechanism_type = mechanism_type
135141
self.span_origin = span_origin
136142
self.app = app
143+
self.http_methods_to_capture = http_methods_to_capture
137144

138145
if _looks_like_asgi3(app):
139146
self.__call__ = self._run_asgi3 # type: Callable[..., Any]
@@ -185,52 +192,59 @@ async def _run_app(self, scope, receive, send, asgi_version):
185192
scope,
186193
)
187194

188-
if ty in ("http", "websocket"):
189-
transaction = continue_trace(
190-
_get_headers(scope),
191-
op="{}.server".format(ty),
192-
name=transaction_name,
193-
source=transaction_source,
194-
origin=self.span_origin,
195-
)
196-
logger.debug(
197-
"[ASGI] Created transaction (continuing trace): %s",
198-
transaction,
199-
)
200-
else:
201-
transaction = Transaction(
202-
op=OP.HTTP_SERVER,
203-
name=transaction_name,
204-
source=transaction_source,
205-
origin=self.span_origin,
206-
)
195+
method = scope.get("method", "").upper()
196+
transaction = None
197+
if method in self.http_methods_to_capture:
198+
if ty in ("http", "websocket"):
199+
transaction = continue_trace(
200+
_get_headers(scope),
201+
op="{}.server".format(ty),
202+
name=transaction_name,
203+
source=transaction_source,
204+
origin=self.span_origin,
205+
)
206+
logger.debug(
207+
"[ASGI] Created transaction (continuing trace): %s",
208+
transaction,
209+
)
210+
else:
211+
transaction = Transaction(
212+
op=OP.HTTP_SERVER,
213+
name=transaction_name,
214+
source=transaction_source,
215+
origin=self.span_origin,
216+
)
217+
logger.debug(
218+
"[ASGI] Created transaction (new): %s", transaction
219+
)
220+
221+
transaction.set_tag("asgi.type", ty)
207222
logger.debug(
208-
"[ASGI] Created transaction (new): %s", transaction
223+
"[ASGI] Set transaction name and source on transaction: '%s' / '%s'",
224+
transaction.name,
225+
transaction.source,
209226
)
210227

211-
transaction.set_tag("asgi.type", ty)
212-
logger.debug(
213-
"[ASGI] Set transaction name and source on transaction: '%s' / '%s'",
214-
transaction.name,
215-
transaction.source,
216-
)
217-
218-
with sentry_sdk.start_transaction(
219-
transaction,
220-
custom_sampling_context={"asgi_scope": scope},
228+
with (
229+
sentry_sdk.start_transaction(
230+
transaction,
231+
custom_sampling_context={"asgi_scope": scope},
232+
)
233+
if transaction is not None
234+
else nullcontext()
221235
):
222236
logger.debug("[ASGI] Started transaction: %s", transaction)
223237
try:
224238

225239
async def _sentry_wrapped_send(event):
226240
# type: (Dict[str, Any]) -> Any
227-
is_http_response = (
228-
event.get("type") == "http.response.start"
229-
and transaction is not None
230-
and "status" in event
231-
)
232-
if is_http_response:
233-
transaction.set_http_status(event["status"])
241+
if transaction is not None:
242+
is_http_response = (
243+
event.get("type") == "http.response.start"
244+
and "status" in event
245+
)
246+
if is_http_response:
247+
transaction.set_http_status(event["status"])
234248

235249
return await send(event)
236250

Diff for: sentry_sdk/integrations/django/__init__.py

+20-7
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
from sentry_sdk.integrations import Integration, DidNotEnable
2626
from sentry_sdk.integrations.logging import ignore_logger
2727
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
28-
from sentry_sdk.integrations._wsgi_common import RequestExtractor
28+
from sentry_sdk.integrations._wsgi_common import (
29+
DEFAULT_HTTP_METHODS_TO_CAPTURE,
30+
RequestExtractor,
31+
)
2932

3033
try:
3134
from django import VERSION as DJANGO_VERSION
@@ -125,13 +128,14 @@ class DjangoIntegration(Integration):
125128

126129
def __init__(
127130
self,
128-
transaction_style="url",
129-
middleware_spans=True,
130-
signals_spans=True,
131-
cache_spans=False,
132-
signals_denylist=None,
131+
transaction_style="url", # type: str
132+
middleware_spans=True, # type: bool
133+
signals_spans=True, # type: bool
134+
cache_spans=False, # type: bool
135+
signals_denylist=None, # type: Optional[list[signals.Signal]]
136+
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
133137
):
134-
# type: (str, bool, bool, bool, Optional[list[signals.Signal]]) -> None
138+
# type: (...) -> None
135139
if transaction_style not in TRANSACTION_STYLE_VALUES:
136140
raise ValueError(
137141
"Invalid value for transaction_style: %s (must be in %s)"
@@ -145,6 +149,8 @@ def __init__(
145149

146150
self.cache_spans = cache_spans
147151

152+
self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
153+
148154
@staticmethod
149155
def setup_once():
150156
# type: () -> None
@@ -172,10 +178,17 @@ def sentry_patched_wsgi_handler(self, environ, start_response):
172178

173179
use_x_forwarded_for = settings.USE_X_FORWARDED_HOST
174180

181+
integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
182+
175183
middleware = SentryWsgiMiddleware(
176184
bound_old_app,
177185
use_x_forwarded_for,
178186
span_origin=DjangoIntegration.origin,
187+
http_methods_to_capture=(
188+
integration.http_methods_to_capture
189+
if integration
190+
else DEFAULT_HTTP_METHODS_TO_CAPTURE
191+
),
179192
)
180193
return middleware(environ, start_response)
181194

Diff for: sentry_sdk/integrations/flask.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import sentry_sdk
22
from sentry_sdk.integrations import DidNotEnable, Integration
3-
from sentry_sdk.integrations._wsgi_common import RequestExtractor
3+
from sentry_sdk.integrations._wsgi_common import (
4+
DEFAULT_HTTP_METHODS_TO_CAPTURE,
5+
RequestExtractor,
6+
)
47
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
58
from sentry_sdk.scope import should_send_default_pii
69
from sentry_sdk.tracing import SOURCE_FOR_STYLE
@@ -52,14 +55,19 @@ class FlaskIntegration(Integration):
5255

5356
transaction_style = ""
5457

55-
def __init__(self, transaction_style="endpoint"):
56-
# type: (str) -> None
58+
def __init__(
59+
self,
60+
transaction_style="endpoint", # type: str
61+
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
62+
):
63+
# type: (...) -> None
5764
if transaction_style not in TRANSACTION_STYLE_VALUES:
5865
raise ValueError(
5966
"Invalid value for transaction_style: %s (must be in %s)"
6067
% (transaction_style, TRANSACTION_STYLE_VALUES)
6168
)
6269
self.transaction_style = transaction_style
70+
self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
6371

6472
@staticmethod
6573
def setup_once():
@@ -83,9 +91,16 @@ def sentry_patched_wsgi_app(self, environ, start_response):
8391
if sentry_sdk.get_client().get_integration(FlaskIntegration) is None:
8492
return old_app(self, environ, start_response)
8593

94+
integration = sentry_sdk.get_client().get_integration(FlaskIntegration)
95+
8696
middleware = SentryWsgiMiddleware(
8797
lambda *a, **kw: old_app(self, *a, **kw),
8898
span_origin=FlaskIntegration.origin,
99+
http_methods_to_capture=(
100+
integration.http_methods_to_capture
101+
if integration
102+
else DEFAULT_HTTP_METHODS_TO_CAPTURE
103+
),
89104
)
90105
return middleware(environ, start_response)
91106

Diff for: sentry_sdk/integrations/starlette.py

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
1313
)
1414
from sentry_sdk.integrations._wsgi_common import (
15+
DEFAULT_HTTP_METHODS_TO_CAPTURE,
1516
HttpCodeRangeContainer,
1617
_is_json_content_type,
1718
request_body_within_bounds,
@@ -85,6 +86,7 @@ def __init__(
8586
transaction_style="url", # type: str
8687
failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Union[Set[int], list[HttpStatusCodeRange], None]
8788
middleware_spans=True, # type: bool
89+
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
8890
):
8991
# type: (...) -> None
9092
if transaction_style not in TRANSACTION_STYLE_VALUES:
@@ -94,6 +96,7 @@ def __init__(
9496
)
9597
self.transaction_style = transaction_style
9698
self.middleware_spans = middleware_spans
99+
self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
97100

98101
if isinstance(failed_request_status_codes, Set):
99102
self.failed_request_status_codes = (
@@ -390,6 +393,11 @@ async def _sentry_patched_asgi_app(self, scope, receive, send):
390393
mechanism_type=StarletteIntegration.identifier,
391394
transaction_style=integration.transaction_style,
392395
span_origin=StarletteIntegration.origin,
396+
http_methods_to_capture=(
397+
integration.http_methods_to_capture
398+
if integration
399+
else DEFAULT_HTTP_METHODS_TO_CAPTURE
400+
),
393401
)
394402

395403
middleware.__call__ = middleware._run_asgi3

0 commit comments

Comments
 (0)