Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 by default
# "OPTIONS", # do not capture OPTIONS requests by default
"PATCH",
"POST",
"PUT",
"TRACE",
)


def request_body_within_bounds(client, content_length):
# type: (Optional[sentry_sdk.client.BaseClient], int) -> bool
Expand Down
27 changes: 20 additions & 7 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations._wsgi_common import (
DEFAULT_HTTP_METHODS_TO_CAPTURE,
RequestExtractor,
)

try:
from django import VERSION as DJANGO_VERSION
Expand Down Expand Up @@ -125,13 +128,14 @@ class DjangoIntegration(Integration):

def __init__(
self,
transaction_style="url",
middleware_spans=True,
signals_spans=True,
cache_spans=False,
signals_denylist=None,
transaction_style="url", # type: str
middleware_spans=True, # type: bool
signals_spans=True, # type: bool
cache_spans=False, # type: bool
signals_denylist=None, # type: Optional[list[signals.Signal]]
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
):
# type: (str, bool, bool, bool, Optional[list[signals.Signal]]) -> None
# type: (...) -> None
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
Expand All @@ -145,6 +149,8 @@ def __init__(

self.cache_spans = cache_spans

self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))

@staticmethod
def setup_once():
# type: () -> None
Expand Down Expand Up @@ -172,10 +178,17 @@ def sentry_patched_wsgi_handler(self, environ, start_response):

use_x_forwarded_for = settings.USE_X_FORWARDED_HOST

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

middleware = SentryWsgiMiddleware(
bound_old_app,
use_x_forwarded_for,
span_origin=DjangoIntegration.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
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
1 change: 1 addition & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def path(path, *args, **kwargs):
),
path("middleware-exc", views.message, name="middleware_exc"),
path("message", views.message, name="message"),
path("nomessage", views.nomessage, name="nomessage"),
path("view-with-signal", views.view_with_signal, name="view_with_signal"),
path("mylogin", views.mylogin, name="mylogin"),
path("classbased", views.ClassBasedView.as_view(), name="classbased"),
Expand Down
5 changes: 5 additions & 0 deletions tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def message(request):
return HttpResponse("ok")


@csrf_exempt
def nomessage(request):
return HttpResponse("ok")


@csrf_exempt
def view_with_signal(request):
custom_signal = Signal()
Expand Down
63 changes: 60 additions & 3 deletions tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ def test_transaction_with_class_view(sentry_init, client, capture_events):

def test_has_trace_if_performance_enabled(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
traces_sample_rate=1.0,
)
events = capture_events()
Expand Down Expand Up @@ -192,7 +196,11 @@ def test_has_trace_if_performance_disabled(sentry_init, client, capture_events):

def test_trace_from_headers_if_performance_enabled(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
traces_sample_rate=1.0,
)

Expand Down Expand Up @@ -225,7 +233,11 @@ def test_trace_from_headers_if_performance_disabled(
sentry_init, client, capture_events
):
sentry_init(
integrations=[DjangoIntegration()],
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
)

events = capture_events()
Expand Down Expand Up @@ -1183,3 +1195,48 @@ def test_span_origin(sentry_init, client, capture_events):
signal_span_found = True

assert signal_span_found


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

client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")

(event,) = events

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


def test_transaction_http_method_custom(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
), # capitalization does not matter
)
],
traces_sample_rate=1.0,
)
events = capture_events()

client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")

assert len(events) == 2

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