Skip to content

Commit 1408ca1

Browse files
committed
Configure http methods to capture in wsgi middleware
1 parent e6ca5a2 commit 1408ca1

File tree

4 files changed

+146
-18
lines changed

4 files changed

+146
-18
lines changed

sentry_sdk/integrations/_wsgi_common.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@
3737
x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_")
3838
)
3939

40+
DEFAULT_HTTP_METHODS_TO_CAPTURE = (
41+
"CONNECT",
42+
"DELETE",
43+
"GET",
44+
# "HEAD", # do not capture HEAD requests
45+
# "OPTIONS", # do not capture OPTIONS requests
46+
"PATCH",
47+
"POST",
48+
"PUT",
49+
"TRACE",
50+
)
51+
4052

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

sentry_sdk/integrations/flask.py

Lines changed: 18 additions & 3 deletions
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

sentry_sdk/integrations/wsgi.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import sys
2+
from contextlib import contextmanager
23
from functools import partial
34

45
import sentry_sdk
56
from sentry_sdk._werkzeug import get_host, _get_headers
67
from sentry_sdk.api import continue_trace
78
from sentry_sdk.consts import OP
89
from sentry_sdk.scope import should_send_default_pii
9-
from sentry_sdk.integrations._wsgi_common import _filter_headers
10+
from sentry_sdk.integrations._wsgi_common import (
11+
DEFAULT_HTTP_METHODS_TO_CAPTURE,
12+
_filter_headers,
13+
)
1014
from sentry_sdk.sessions import track_session
1115
from sentry_sdk.scope import use_isolation_scope
1216
from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_ROUTE
@@ -45,6 +49,13 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore
4549
_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
4650

4751

52+
# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support
53+
@contextmanager
54+
def nullcontext():
55+
# type: () -> Iterator[None]
56+
yield
57+
58+
4859
def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
4960
# type: (str, str, str) -> str
5061
return s.encode("latin1").decode(charset, errors)
@@ -66,13 +77,25 @@ def get_request_url(environ, use_x_forwarded_for=False):
6677

6778

6879
class SentryWsgiMiddleware:
69-
__slots__ = ("app", "use_x_forwarded_for", "span_origin")
80+
__slots__ = (
81+
"app",
82+
"use_x_forwarded_for",
83+
"span_origin",
84+
"http_methods_to_capture",
85+
)
7086

71-
def __init__(self, app, use_x_forwarded_for=False, span_origin="manual"):
72-
# type: (Callable[[Dict[str, str], Callable[..., Any]], Any], bool, str) -> None
87+
def __init__(
88+
self,
89+
app, # type: Callable[[Dict[str, str], Callable[..., Any]], Any]
90+
use_x_forwarded_for=False, # type: bool
91+
span_origin="manual", # type: str
92+
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
93+
):
94+
# type: (...) -> None
7395
self.app = app
7496
self.use_x_forwarded_for = use_x_forwarded_for
7597
self.span_origin = span_origin
98+
self.http_methods_to_capture = http_methods_to_capture
7699

77100
def __call__(self, environ, start_response):
78101
# type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
@@ -92,16 +115,24 @@ def __call__(self, environ, start_response):
92115
)
93116
)
94117

95-
transaction = continue_trace(
96-
environ,
97-
op=OP.HTTP_SERVER,
98-
name="generic WSGI request",
99-
source=TRANSACTION_SOURCE_ROUTE,
100-
origin=self.span_origin,
101-
)
118+
method = environ.get("REQUEST_METHOD", "").upper()
119+
transaction = None
120+
if method in self.http_methods_to_capture:
121+
transaction = continue_trace(
122+
environ,
123+
op=OP.HTTP_SERVER,
124+
name="generic WSGI request",
125+
source=TRANSACTION_SOURCE_ROUTE,
126+
origin=self.span_origin,
127+
)
102128

103-
with sentry_sdk.start_transaction(
104-
transaction, custom_sampling_context={"wsgi_environ": environ}
129+
with (
130+
sentry_sdk.start_transaction(
131+
transaction,
132+
custom_sampling_context={"wsgi_environ": environ},
133+
)
134+
if transaction is not None
135+
else nullcontext()
105136
):
106137
try:
107138
response = self.app(
@@ -120,15 +151,16 @@ def __call__(self, environ, start_response):
120151

121152
def _sentry_start_response( # type: ignore
122153
old_start_response, # type: StartResponse
123-
transaction, # type: Transaction
154+
transaction, # type: Optional[Transaction]
124155
status, # type: str
125156
response_headers, # type: WsgiResponseHeaders
126157
exc_info=None, # type: Optional[WsgiExcInfo]
127158
):
128159
# type: (...) -> WsgiResponseIter
129160
with capture_internal_exceptions():
130161
status_int = int(status.split(" ", 1)[0])
131-
transaction.set_http_status(status_int)
162+
if transaction is not None:
163+
transaction.set_http_status(status_int)
132164

133165
if exc_info is None:
134166
# The Django Rest Framework WSGI test client, and likely other

tests/integrations/flask/test_flask.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def hi():
4747
capture_message("hi")
4848
return "ok"
4949

50+
@app.route("/nomessage")
51+
def nohi():
52+
return "ok"
53+
5054
@app.route("/message/<int:message_id>")
5155
def hi_with_id(message_id):
5256
capture_message("hi again")
@@ -962,3 +966,68 @@ def test_span_origin(sentry_init, app, capture_events):
962966
(_, event) = events
963967

964968
assert event["contexts"]["trace"]["origin"] == "auto.http.flask"
969+
970+
971+
def test_transaction_http_method_default(
972+
sentry_init,
973+
app,
974+
capture_events,
975+
):
976+
"""
977+
By default OPTIONS and HEAD requests do not create a transaction.
978+
"""
979+
sentry_init(
980+
traces_sample_rate=1.0,
981+
integrations=[flask_sentry.FlaskIntegration()],
982+
)
983+
events = capture_events()
984+
985+
client = app.test_client()
986+
response = client.get("/nomessage")
987+
assert response.status_code == 200
988+
989+
response = client.options("/nomessage")
990+
assert response.status_code == 200
991+
992+
response = client.head("/nomessage")
993+
assert response.status_code == 200
994+
995+
(event,) = events
996+
997+
assert len(events) == 1
998+
assert event["request"]["method"] == "GET"
999+
1000+
1001+
def test_transaction_http_method_custom(
1002+
sentry_init,
1003+
app,
1004+
capture_events,
1005+
):
1006+
"""
1007+
Configure FlaskIntegration to ONLY capture OPTIONS and HEAD requests.
1008+
"""
1009+
sentry_init(
1010+
traces_sample_rate=1.0,
1011+
integrations=[
1012+
flask_sentry.FlaskIntegration(
1013+
http_methods_to_capture=("OPTIONS", "HEAD")
1014+
) # case does not matter
1015+
],
1016+
)
1017+
events = capture_events()
1018+
1019+
client = app.test_client()
1020+
response = client.get("/nomessage")
1021+
assert response.status_code == 200
1022+
1023+
response = client.options("/nomessage")
1024+
assert response.status_code == 200
1025+
1026+
response = client.head("/nomessage")
1027+
assert response.status_code == 200
1028+
1029+
assert len(events) == 2
1030+
1031+
(event1, event2) = events
1032+
assert event1["request"]["method"] == "OPTIONS"
1033+
assert event2["request"]["method"] == "HEAD"

0 commit comments

Comments
 (0)