Skip to content

Commit

Permalink
Configure http methods to capture in wsgi middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
antonpirker committed Sep 12, 2024
1 parent e6ca5a2 commit 1408ca1
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 18 deletions.
12 changes: 12 additions & 0 deletions sentry_sdk/integrations/_wsgi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@
x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_")
)

DEFAULT_HTTP_METHODS_TO_CAPTURE = (
"CONNECT",
"DELETE",
"GET",
# "HEAD", # do not capture HEAD requests
# "OPTIONS", # do not capture OPTIONS requests
"PATCH",
"POST",
"PUT",
"TRACE",
)


def request_body_within_bounds(client, content_length):
# type: (Optional[sentry_sdk.client.BaseClient], int) -> bool
Expand Down
21 changes: 18 additions & 3 deletions sentry_sdk/integrations/flask.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import sentry_sdk
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations._wsgi_common import (
DEFAULT_HTTP_METHODS_TO_CAPTURE,
RequestExtractor,
)
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.tracing import SOURCE_FOR_STYLE
Expand Down Expand Up @@ -52,14 +55,19 @@ class FlaskIntegration(Integration):

transaction_style = ""

def __init__(self, transaction_style="endpoint"):
# type: (str) -> None
def __init__(
self,
transaction_style="endpoint", # type: str
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
):
# type: (...) -> None
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))

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

integration = sentry_sdk.get_client().get_integration(FlaskIntegration)

middleware = SentryWsgiMiddleware(
lambda *a, **kw: old_app(self, *a, **kw),
span_origin=FlaskIntegration.origin,
http_methods_to_capture=(
integration.http_methods_to_capture
if integration
else DEFAULT_HTTP_METHODS_TO_CAPTURE
),
)
return middleware(environ, start_response)

Expand Down
62 changes: 47 additions & 15 deletions sentry_sdk/integrations/wsgi.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import sys
from contextlib import contextmanager
from functools import partial

import sentry_sdk
from sentry_sdk._werkzeug import get_host, _get_headers
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.integrations._wsgi_common import (
DEFAULT_HTTP_METHODS_TO_CAPTURE,
_filter_headers,
)
from sentry_sdk.sessions import track_session
from sentry_sdk.scope import use_isolation_scope
from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_ROUTE
Expand Down Expand Up @@ -45,6 +49,13 @@ def __call__(self, status, response_headers, exc_info=None): # type: ignore
_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")


# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support
@contextmanager
def nullcontext():
# type: () -> Iterator[None]
yield


def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
# type: (str, str, str) -> str
return s.encode("latin1").decode(charset, errors)
Expand All @@ -66,13 +77,25 @@ def get_request_url(environ, use_x_forwarded_for=False):


class SentryWsgiMiddleware:
__slots__ = ("app", "use_x_forwarded_for", "span_origin")
__slots__ = (
"app",
"use_x_forwarded_for",
"span_origin",
"http_methods_to_capture",
)

def __init__(self, app, use_x_forwarded_for=False, span_origin="manual"):
# type: (Callable[[Dict[str, str], Callable[..., Any]], Any], bool, str) -> None
def __init__(
self,
app, # type: Callable[[Dict[str, str], Callable[..., Any]], Any]
use_x_forwarded_for=False, # type: bool
span_origin="manual", # type: str
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
):
# type: (...) -> None
self.app = app
self.use_x_forwarded_for = use_x_forwarded_for
self.span_origin = span_origin
self.http_methods_to_capture = http_methods_to_capture

def __call__(self, environ, start_response):
# type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
Expand All @@ -92,16 +115,24 @@ def __call__(self, environ, start_response):
)
)

transaction = continue_trace(
environ,
op=OP.HTTP_SERVER,
name="generic WSGI request",
source=TRANSACTION_SOURCE_ROUTE,
origin=self.span_origin,
)
method = environ.get("REQUEST_METHOD", "").upper()
transaction = None
if method in self.http_methods_to_capture:
transaction = continue_trace(
environ,
op=OP.HTTP_SERVER,
name="generic WSGI request",
source=TRANSACTION_SOURCE_ROUTE,
origin=self.span_origin,
)

with sentry_sdk.start_transaction(
transaction, custom_sampling_context={"wsgi_environ": environ}
with (
sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"wsgi_environ": environ},
)
if transaction is not None
else nullcontext()
):
try:
response = self.app(
Expand All @@ -120,15 +151,16 @@ def __call__(self, environ, start_response):

def _sentry_start_response( # type: ignore
old_start_response, # type: StartResponse
transaction, # type: Transaction
transaction, # type: Optional[Transaction]
status, # type: str
response_headers, # type: WsgiResponseHeaders
exc_info=None, # type: Optional[WsgiExcInfo]
):
# type: (...) -> WsgiResponseIter
with capture_internal_exceptions():
status_int = int(status.split(" ", 1)[0])
transaction.set_http_status(status_int)
if transaction is not None:
transaction.set_http_status(status_int)

if exc_info is None:
# The Django Rest Framework WSGI test client, and likely other
Expand Down
69 changes: 69 additions & 0 deletions tests/integrations/flask/test_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def hi():
capture_message("hi")
return "ok"

@app.route("/nomessage")
def nohi():
return "ok"

@app.route("/message/<int:message_id>")
def hi_with_id(message_id):
capture_message("hi again")
Expand Down Expand Up @@ -962,3 +966,68 @@ def test_span_origin(sentry_init, app, capture_events):
(_, event) = events

assert event["contexts"]["trace"]["origin"] == "auto.http.flask"


def test_transaction_http_method_default(
sentry_init,
app,
capture_events,
):
"""
By default OPTIONS and HEAD requests do not create a transaction.
"""
sentry_init(
traces_sample_rate=1.0,
integrations=[flask_sentry.FlaskIntegration()],
)
events = capture_events()

client = app.test_client()
response = client.get("/nomessage")
assert response.status_code == 200

response = client.options("/nomessage")
assert response.status_code == 200

response = client.head("/nomessage")
assert response.status_code == 200

(event,) = events

assert len(events) == 1
assert event["request"]["method"] == "GET"


def test_transaction_http_method_custom(
sentry_init,
app,
capture_events,
):
"""
Configure FlaskIntegration to ONLY capture OPTIONS and HEAD requests.
"""
sentry_init(
traces_sample_rate=1.0,
integrations=[
flask_sentry.FlaskIntegration(
http_methods_to_capture=("OPTIONS", "HEAD")
) # case does not matter
],
)
events = capture_events()

client = app.test_client()
response = client.get("/nomessage")
assert response.status_code == 200

response = client.options("/nomessage")
assert response.status_code == 200

response = client.head("/nomessage")
assert response.status_code == 200

assert len(events) == 2

(event1, event2) = events
assert event1["request"]["method"] == "OPTIONS"
assert event2["request"]["method"] == "HEAD"

0 comments on commit 1408ca1

Please sign in to comment.