Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
16 changes: 16 additions & 0 deletions sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ def removed_because_over_size_limit(cls, value=""):
},
)

@classmethod
def removed_because_body_consumed_and_not_cached(cls, value=""):
# type: (Any) -> AnnotatedValue
"""The actual value was removed because the underlying stream has been consumed, without caching the value."""
return AnnotatedValue(
value=value,
metadata={
"rem": [ # Remark
[
"!consumed", # Because the original stream is unavailable
"x", # The fields original value was removed
]
]
},
)

@classmethod
def substituted_because_contains_sensitive_data(cls):
# type: () -> AnnotatedValue
Expand Down
53 changes: 35 additions & 18 deletions sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Callable, Dict
from typing import Any, Callable, Dict, Optional
from sentry_sdk._types import Event

try:
from sentry_sdk.integrations.starlette import (
StarletteIntegration,
StarletteRequestExtractor,
_patch_request,
)
except DidNotEnable:
raise DidNotEnable("Starlette is not installed")
Expand Down Expand Up @@ -103,38 +104,54 @@ async def _sentry_app(*args, **kwargs):
return await old_app(*args, **kwargs)

request = args[0]
_patch_request(request)

_set_transaction_name_and_source(
sentry_sdk.get_current_scope(), integration.transaction_style, request
)
sentry_scope = sentry_sdk.get_isolation_scope()
extractor = StarletteRequestExtractor(request)
info = await extractor.extract_request_info()
sentry_scope._name = FastApiIntegration.identifier

def _make_request_event_processor(req, integration):
# type: (Any, Any) -> Callable[[Event, Dict[str, Any]], Event]
def _make_cookies_event_processor(cookies):
# type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event]
def event_processor(event, hint):
# type: (Event, Dict[str, Any]) -> Event
if cookies and should_send_default_pii():
event.setdefault("request", {})["cookies"] = deepcopy(cookies)

return event

return event_processor

# Extract information from request
request_info = event.get("request", {})
if info:
if "cookies" in info and should_send_default_pii():
request_info["cookies"] = info["cookies"]
if "data" in info:
request_info["data"] = info["data"]
event["request"] = deepcopy(request_info)
def _make_request_body_event_processor(info):
# type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event]
def event_processor(event, hint):
# type: (Event, Dict[str, Any]) -> Event
if info and "data" in info:
event.setdefault("request", {})["data"] = deepcopy(info["data"])

return event

return event_processor

sentry_scope._name = FastApiIntegration.identifier
sentry_scope.add_event_processor(
_make_request_event_processor(request, integration)
)
extractor = StarletteRequestExtractor(request)
cookies = extractor.extract_cookies_from_request()
sentry_scope.add_event_processor(_make_cookies_event_processor(cookies))

try:
response = await old_app(*args, **kwargs)
except Exception as exception:
info = await extractor.extract_request_info()
sentry_scope.add_event_processor(
_make_request_body_event_processor(info)
)

raise exception

info = await extractor.extract_request_info()
sentry_scope.add_event_processor(_make_request_body_event_processor(info))

return await old_app(*args, **kwargs)
return response

return _sentry_app

Expand Down
28 changes: 17 additions & 11 deletions sentry_sdk/integrations/graphene.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

if TYPE_CHECKING:
from collections.abc import Generator
from typing import Any, Dict, Union
from typing import Any, Dict, Union, Callable
from graphene.language.source import Source # type: ignore
from graphql.execution import ExecutionResult
from graphql.type import GraphQLSchema
Expand Down Expand Up @@ -48,7 +48,7 @@ def _patch_graphql():
def _sentry_patched_graphql_sync(schema, source, *args, **kwargs):
# type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult
scope = sentry_sdk.get_isolation_scope()
scope.add_event_processor(_event_processor)
scope.add_event_processor(_make_event_processor(source))

with graphql_span(schema, source, kwargs):
result = old_graphql_sync(schema, source, *args, **kwargs)
Expand All @@ -75,7 +75,7 @@ async def _sentry_patched_graphql_async(schema, source, *args, **kwargs):
return await old_graphql_async(schema, source, *args, **kwargs)

scope = sentry_sdk.get_isolation_scope()
scope.add_event_processor(_event_processor)
scope.add_event_processor(_make_event_processor(source))

with graphql_span(schema, source, kwargs):
result = await old_graphql_async(schema, source, *args, **kwargs)
Expand All @@ -99,16 +99,22 @@ async def _sentry_patched_graphql_async(schema, source, *args, **kwargs):
graphene_schema.graphql = _sentry_patched_graphql_async


def _event_processor(event, hint):
# type: (Event, Dict[str, Any]) -> Event
if should_send_default_pii():
request_info = event.setdefault("request", {})
request_info["api_target"] = "graphql"
def _make_event_processor(source):
# type: (Any) -> Callable[[Event, dict[str, Any]], Event]
def _event_processor(event, hint):
# type: (Event, Dict[str, Any]) -> Event
if should_send_default_pii():
request_info = event.setdefault("request", {})
request_info["api_target"] = "graphql"
if isinstance(source, str):
request_info.setdefault("data", {})["query"] = source # type: ignore

elif event.get("request", {}).get("data"):
del event["request"]["data"]
elif event.get("request", {}).get("data"):
del event["request"]["data"]

return event
return event

return _event_processor


@contextmanager
Expand Down
120 changes: 102 additions & 18 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,26 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Awaitable, Callable, Container, Dict, Optional, Tuple, Union

from typing import (
Any,
Awaitable,
Callable,
Container,
Dict,
Optional,
Tuple,
Union,
Protocol,
TypeVar,
)
from types import CoroutineType
from sentry_sdk._types import Event, HttpStatusCodeRange

try:
import starlette # type: ignore
from starlette import __version__ as STARLETTE_VERSION
from starlette.applications import Starlette # type: ignore
from starlette.datastructures import UploadFile # type: ignore
from starlette.datastructures import UploadFile, FormData # type: ignore
from starlette.middleware import Middleware # type: ignore
from starlette.middleware.authentication import ( # type: ignore
AuthenticationMiddleware,
Expand All @@ -55,6 +66,16 @@
except ImportError:
raise DidNotEnable("Starlette is not installed")

if TYPE_CHECKING:
from contextlib import AbstractAsyncContextManager

T_co = TypeVar("T_co", covariant=True)

class AwaitableOrContextManager(
Awaitable[T_co], AbstractAsyncContextManager[T_co], Protocol[T_co]
): ...


try:
# Starlette 0.20
from starlette.middleware.exceptions import ExceptionMiddleware # type: ignore
Expand Down Expand Up @@ -420,6 +441,35 @@ def _is_async_callable(obj):
)


def _patch_request(request):
# type: (Request) -> None
_original_body = request.body
_original_json = request.json
_original_form = request.form

@functools.wraps(_original_body)
def sentry_body():
# type: () -> CoroutineType[Any, Any, bytes]
request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True
return _original_body()

@functools.wraps(_original_json)
def sentry_json():
# type: () -> CoroutineType[Any, Any, Any]
request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True
return _original_json()

@functools.wraps(_original_form)
def sentry_form(*args, **kwargs):
# type: (*Any, **Any) -> AwaitableOrContextManager[FormData]
request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True
return _original_form(*args, **kwargs)

request.body = sentry_body
request.json = sentry_json
request.form = sentry_form


def patch_request_response():
# type: () -> None
old_request_response = starlette.routing.request_response
Expand All @@ -440,6 +490,7 @@ async def _sentry_async_func(*args, **kwargs):
return await old_func(*args, **kwargs)

request = args[0]
_patch_request(request)

_set_transaction_name_and_source(
sentry_sdk.get_current_scope(),
Expand All @@ -448,33 +499,54 @@ async def _sentry_async_func(*args, **kwargs):
)

sentry_scope = sentry_sdk.get_isolation_scope()
extractor = StarletteRequestExtractor(request)
info = await extractor.extract_request_info()
sentry_scope._name = StarletteIntegration.identifier

def _make_request_event_processor(req, integration):
# type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event]
def _make_cookies_event_processor(cookies):
# type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event]
def event_processor(event, hint):
# type: (Event, Dict[str, Any]) -> Event
if cookies and should_send_default_pii():
event.setdefault("request", {})["cookies"] = deepcopy(
cookies
)

# Add info from request to event
request_info = event.get("request", {})
if info:
if "cookies" in info:
request_info["cookies"] = info["cookies"]
if "data" in info:
request_info["data"] = info["data"]
event["request"] = deepcopy(request_info)
return event

return event_processor

def _make_request_body_event_processor(info):
# type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event]
def event_processor(event, hint):
# type: (Event, Dict[str, Any]) -> Event
if info and "data" in info:
event.setdefault("request", {})["data"] = deepcopy(
info["data"]
)

return event

return event_processor

sentry_scope._name = StarletteIntegration.identifier
extractor = StarletteRequestExtractor(request)
cookies = extractor.extract_cookies_from_request()
sentry_scope.add_event_processor(_make_cookies_event_processor(cookies))

try:
response = await old_func(*args, **kwargs)
except Exception as exception:
info = await extractor.extract_request_info()
sentry_scope.add_event_processor(
_make_request_body_event_processor(info)
)

raise exception

info = await extractor.extract_request_info()
sentry_scope.add_event_processor(
_make_request_event_processor(request, integration)
_make_request_body_event_processor(info)
)

return await old_func(*args, **kwargs)
return response

func = _sentry_async_func

Expand Down Expand Up @@ -621,6 +693,18 @@ async def extract_request_info(self):
request_info["data"] = AnnotatedValue.removed_because_over_size_limit()
return request_info

# Avoid hangs by not parsing body when ASGI stream is consumed
is_body_cached = (
"state" in self.request.scope
and "sentry_sdk.is_body_cached" in self.request.scope["state"]
and self.request.scope["state"]["sentry_sdk.is_body_cached"]
)
if not is_body_cached:
request_info["data"] = (
AnnotatedValue.removed_because_body_consumed_and_not_cached()
)
return request_info

# Add JSON body, if it is a JSON request
json = await self.json()
if json:
Expand Down
Loading