sentry-sdk 0.7.5__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentry_sdk/__init__.py +48 -30
- sentry_sdk/_compat.py +74 -61
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +289 -0
- sentry_sdk/_types.py +338 -0
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +134 -0
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +141 -0
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +112 -68
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +201 -119
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from sentry_sdk.utils import
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk.utils import (
|
|
5
|
+
capture_internal_exceptions,
|
|
6
|
+
event_from_exception,
|
|
7
|
+
)
|
|
5
8
|
from sentry_sdk.integrations import Integration
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
8
13
|
from typing import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
from typing import Type
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from types import TracebackType
|
|
19
|
+
|
|
20
|
+
Excepthook = Callable[
|
|
21
|
+
[Type[BaseException], BaseException, Optional[TracebackType]],
|
|
22
|
+
Any,
|
|
23
|
+
]
|
|
9
24
|
|
|
10
25
|
|
|
11
26
|
class ExcepthookIntegration(Integration):
|
|
12
27
|
identifier = "excepthook"
|
|
13
28
|
|
|
29
|
+
always_run = False
|
|
30
|
+
|
|
31
|
+
def __init__(self, always_run=False):
|
|
32
|
+
# type: (bool) -> None
|
|
33
|
+
|
|
34
|
+
if not isinstance(always_run, bool):
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"Invalid value for always_run: %s (must be type boolean)"
|
|
37
|
+
% (always_run,)
|
|
38
|
+
)
|
|
39
|
+
self.always_run = always_run
|
|
40
|
+
|
|
14
41
|
@staticmethod
|
|
15
42
|
def setup_once():
|
|
16
43
|
# type: () -> None
|
|
@@ -18,26 +45,36 @@ class ExcepthookIntegration(Integration):
|
|
|
18
45
|
|
|
19
46
|
|
|
20
47
|
def _make_excepthook(old_excepthook):
|
|
21
|
-
# type: (
|
|
22
|
-
def sentry_sdk_excepthook(
|
|
23
|
-
|
|
24
|
-
integration =
|
|
48
|
+
# type: (Excepthook) -> Excepthook
|
|
49
|
+
def sentry_sdk_excepthook(type_, value, traceback):
|
|
50
|
+
# type: (Type[BaseException], BaseException, Optional[TracebackType]) -> None
|
|
51
|
+
integration = sentry_sdk.get_client().get_integration(ExcepthookIntegration)
|
|
25
52
|
|
|
26
|
-
|
|
53
|
+
# Note: If we replace this with ensure_integration_enabled then
|
|
54
|
+
# we break the exceptiongroup backport;
|
|
55
|
+
# See: https://github.com/getsentry/sentry-python/issues/3097
|
|
56
|
+
if integration is None:
|
|
57
|
+
return old_excepthook(type_, value, traceback)
|
|
58
|
+
|
|
59
|
+
if _should_send(integration.always_run):
|
|
27
60
|
with capture_internal_exceptions():
|
|
28
61
|
event, hint = event_from_exception(
|
|
29
|
-
(
|
|
30
|
-
client_options=
|
|
62
|
+
(type_, value, traceback),
|
|
63
|
+
client_options=sentry_sdk.get_client().options,
|
|
31
64
|
mechanism={"type": "excepthook", "handled": False},
|
|
32
65
|
)
|
|
33
|
-
|
|
66
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
34
67
|
|
|
35
|
-
return old_excepthook(
|
|
68
|
+
return old_excepthook(type_, value, traceback)
|
|
36
69
|
|
|
37
70
|
return sentry_sdk_excepthook
|
|
38
71
|
|
|
39
72
|
|
|
40
|
-
def _should_send():
|
|
73
|
+
def _should_send(always_run=False):
|
|
74
|
+
# type: (bool) -> bool
|
|
75
|
+
if always_run:
|
|
76
|
+
return True
|
|
77
|
+
|
|
41
78
|
if hasattr(sys, "ps1"):
|
|
42
79
|
# Disable the excepthook for interactive Python shells, otherwise
|
|
43
80
|
# every typo gets sent to Sentry.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
3
|
+
from sentry_sdk.scope import add_global_event_processor
|
|
4
|
+
from sentry_sdk.utils import walk_exception_chain, iter_stacks
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from sentry_sdk._types import Event, Hint
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import executing
|
|
15
|
+
except ImportError:
|
|
16
|
+
raise DidNotEnable("executing is not installed")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExecutingIntegration(Integration):
|
|
20
|
+
identifier = "executing"
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def setup_once():
|
|
24
|
+
# type: () -> None
|
|
25
|
+
|
|
26
|
+
@add_global_event_processor
|
|
27
|
+
def add_executing_info(event, hint):
|
|
28
|
+
# type: (Event, Optional[Hint]) -> Optional[Event]
|
|
29
|
+
if sentry_sdk.get_client().get_integration(ExecutingIntegration) is None:
|
|
30
|
+
return event
|
|
31
|
+
|
|
32
|
+
if hint is None:
|
|
33
|
+
return event
|
|
34
|
+
|
|
35
|
+
exc_info = hint.get("exc_info", None)
|
|
36
|
+
|
|
37
|
+
if exc_info is None:
|
|
38
|
+
return event
|
|
39
|
+
|
|
40
|
+
exception = event.get("exception", None)
|
|
41
|
+
|
|
42
|
+
if exception is None:
|
|
43
|
+
return event
|
|
44
|
+
|
|
45
|
+
values = exception.get("values", None)
|
|
46
|
+
|
|
47
|
+
if values is None:
|
|
48
|
+
return event
|
|
49
|
+
|
|
50
|
+
for exception, (_exc_type, _exc_value, exc_tb) in zip(
|
|
51
|
+
reversed(values), walk_exception_chain(exc_info)
|
|
52
|
+
):
|
|
53
|
+
sentry_frames = [
|
|
54
|
+
frame
|
|
55
|
+
for frame in exception.get("stacktrace", {}).get("frames", [])
|
|
56
|
+
if frame.get("function")
|
|
57
|
+
]
|
|
58
|
+
tbs = list(iter_stacks(exc_tb))
|
|
59
|
+
if len(sentry_frames) != len(tbs):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
for sentry_frame, tb in zip(sentry_frames, tbs):
|
|
63
|
+
frame = tb.tb_frame
|
|
64
|
+
source = executing.Source.for_frame(frame)
|
|
65
|
+
sentry_frame["function"] = source.code_qualname(frame.f_code)
|
|
66
|
+
|
|
67
|
+
return event
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
|
|
3
|
+
from sentry_sdk.integrations._wsgi_common import RequestExtractor
|
|
4
|
+
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
|
|
5
|
+
from sentry_sdk.tracing import SOURCE_FOR_STYLE
|
|
6
|
+
from sentry_sdk.utils import (
|
|
7
|
+
capture_internal_exceptions,
|
|
8
|
+
ensure_integration_enabled,
|
|
9
|
+
event_from_exception,
|
|
10
|
+
parse_version,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from typing import Any
|
|
17
|
+
from typing import Dict
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from sentry_sdk._types import Event, EventProcessor
|
|
21
|
+
|
|
22
|
+
# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers`
|
|
23
|
+
# and `falcon.API` to `falcon.App`
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import falcon # type: ignore
|
|
27
|
+
|
|
28
|
+
from falcon import __version__ as FALCON_VERSION
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise DidNotEnable("Falcon not installed")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import falcon.app_helpers # type: ignore
|
|
34
|
+
|
|
35
|
+
falcon_helpers = falcon.app_helpers
|
|
36
|
+
falcon_app_class = falcon.App
|
|
37
|
+
FALCON3 = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
import falcon.api_helpers # type: ignore
|
|
40
|
+
|
|
41
|
+
falcon_helpers = falcon.api_helpers
|
|
42
|
+
falcon_app_class = falcon.API
|
|
43
|
+
FALCON3 = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_FALCON_UNSET = None # type: Optional[object]
|
|
47
|
+
if FALCON3: # falcon.request._UNSET is only available in Falcon 3.0+
|
|
48
|
+
with capture_internal_exceptions():
|
|
49
|
+
from falcon.request import _UNSET as _FALCON_UNSET # type: ignore[import-not-found, no-redef]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FalconRequestExtractor(RequestExtractor):
|
|
53
|
+
def env(self):
|
|
54
|
+
# type: () -> Dict[str, Any]
|
|
55
|
+
return self.request.env
|
|
56
|
+
|
|
57
|
+
def cookies(self):
|
|
58
|
+
# type: () -> Dict[str, Any]
|
|
59
|
+
return self.request.cookies
|
|
60
|
+
|
|
61
|
+
def form(self):
|
|
62
|
+
# type: () -> None
|
|
63
|
+
return None # No such concept in Falcon
|
|
64
|
+
|
|
65
|
+
def files(self):
|
|
66
|
+
# type: () -> None
|
|
67
|
+
return None # No such concept in Falcon
|
|
68
|
+
|
|
69
|
+
def raw_data(self):
|
|
70
|
+
# type: () -> Optional[str]
|
|
71
|
+
|
|
72
|
+
# As request data can only be read once we won't make this available
|
|
73
|
+
# to Sentry. Just send back a dummy string in case there was a
|
|
74
|
+
# content length.
|
|
75
|
+
# TODO(jmagnusson): Figure out if there's a way to support this
|
|
76
|
+
content_length = self.content_length()
|
|
77
|
+
if content_length > 0:
|
|
78
|
+
return "[REQUEST_CONTAINING_RAW_DATA]"
|
|
79
|
+
else:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def json(self):
|
|
83
|
+
# type: () -> Optional[Dict[str, Any]]
|
|
84
|
+
# fallback to cached_media = None if self.request._media is not available
|
|
85
|
+
cached_media = None
|
|
86
|
+
with capture_internal_exceptions():
|
|
87
|
+
# self.request._media is the cached self.request.media
|
|
88
|
+
# value. It is only available if self.request.media
|
|
89
|
+
# has already been accessed. Therefore, reading
|
|
90
|
+
# self.request._media will not exhaust the raw request
|
|
91
|
+
# stream (self.request.bounded_stream) because it has
|
|
92
|
+
# already been read if self.request._media is set.
|
|
93
|
+
cached_media = self.request._media
|
|
94
|
+
|
|
95
|
+
if cached_media is not _FALCON_UNSET:
|
|
96
|
+
return cached_media
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SentryFalconMiddleware:
|
|
102
|
+
"""Captures exceptions in Falcon requests and send to Sentry"""
|
|
103
|
+
|
|
104
|
+
def process_request(self, req, resp, *args, **kwargs):
|
|
105
|
+
# type: (Any, Any, *Any, **Any) -> None
|
|
106
|
+
integration = sentry_sdk.get_client().get_integration(FalconIntegration)
|
|
107
|
+
if integration is None:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
scope = sentry_sdk.get_isolation_scope()
|
|
111
|
+
scope._name = "falcon"
|
|
112
|
+
scope.add_event_processor(_make_request_event_processor(req, integration))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
TRANSACTION_STYLE_VALUES = ("uri_template", "path")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class FalconIntegration(Integration):
|
|
119
|
+
identifier = "falcon"
|
|
120
|
+
origin = f"auto.http.{identifier}"
|
|
121
|
+
|
|
122
|
+
transaction_style = ""
|
|
123
|
+
|
|
124
|
+
def __init__(self, transaction_style="uri_template"):
|
|
125
|
+
# type: (str) -> None
|
|
126
|
+
if transaction_style not in TRANSACTION_STYLE_VALUES:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"Invalid value for transaction_style: %s (must be in %s)"
|
|
129
|
+
% (transaction_style, TRANSACTION_STYLE_VALUES)
|
|
130
|
+
)
|
|
131
|
+
self.transaction_style = transaction_style
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def setup_once():
|
|
135
|
+
# type: () -> None
|
|
136
|
+
|
|
137
|
+
version = parse_version(FALCON_VERSION)
|
|
138
|
+
_check_minimum_version(FalconIntegration, version)
|
|
139
|
+
|
|
140
|
+
_patch_wsgi_app()
|
|
141
|
+
_patch_handle_exception()
|
|
142
|
+
_patch_prepare_middleware()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _patch_wsgi_app():
|
|
146
|
+
# type: () -> None
|
|
147
|
+
original_wsgi_app = falcon_app_class.__call__
|
|
148
|
+
|
|
149
|
+
def sentry_patched_wsgi_app(self, env, start_response):
|
|
150
|
+
# type: (falcon.API, Any, Any) -> Any
|
|
151
|
+
integration = sentry_sdk.get_client().get_integration(FalconIntegration)
|
|
152
|
+
if integration is None:
|
|
153
|
+
return original_wsgi_app(self, env, start_response)
|
|
154
|
+
|
|
155
|
+
sentry_wrapped = SentryWsgiMiddleware(
|
|
156
|
+
lambda envi, start_resp: original_wsgi_app(self, envi, start_resp),
|
|
157
|
+
span_origin=FalconIntegration.origin,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return sentry_wrapped(env, start_response)
|
|
161
|
+
|
|
162
|
+
falcon_app_class.__call__ = sentry_patched_wsgi_app
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _patch_handle_exception():
|
|
166
|
+
# type: () -> None
|
|
167
|
+
original_handle_exception = falcon_app_class._handle_exception
|
|
168
|
+
|
|
169
|
+
@ensure_integration_enabled(FalconIntegration, original_handle_exception)
|
|
170
|
+
def sentry_patched_handle_exception(self, *args):
|
|
171
|
+
# type: (falcon.API, *Any) -> Any
|
|
172
|
+
# NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception
|
|
173
|
+
# method signature from `(ex, req, resp, params)` to
|
|
174
|
+
# `(req, resp, ex, params)`
|
|
175
|
+
ex = response = None
|
|
176
|
+
with capture_internal_exceptions():
|
|
177
|
+
ex = next(argument for argument in args if isinstance(argument, Exception))
|
|
178
|
+
response = next(
|
|
179
|
+
argument for argument in args if isinstance(argument, falcon.Response)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
was_handled = original_handle_exception(self, *args)
|
|
183
|
+
|
|
184
|
+
if ex is None or response is None:
|
|
185
|
+
# Both ex and response should have a non-None value at this point; otherwise,
|
|
186
|
+
# there is an error with the SDK that will have been captured in the
|
|
187
|
+
# capture_internal_exceptions block above.
|
|
188
|
+
return was_handled
|
|
189
|
+
|
|
190
|
+
if _exception_leads_to_http_5xx(ex, response):
|
|
191
|
+
event, hint = event_from_exception(
|
|
192
|
+
ex,
|
|
193
|
+
client_options=sentry_sdk.get_client().options,
|
|
194
|
+
mechanism={"type": "falcon", "handled": False},
|
|
195
|
+
)
|
|
196
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
197
|
+
|
|
198
|
+
return was_handled
|
|
199
|
+
|
|
200
|
+
falcon_app_class._handle_exception = sentry_patched_handle_exception
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _patch_prepare_middleware():
|
|
204
|
+
# type: () -> None
|
|
205
|
+
original_prepare_middleware = falcon_helpers.prepare_middleware
|
|
206
|
+
|
|
207
|
+
def sentry_patched_prepare_middleware(
|
|
208
|
+
middleware=None, independent_middleware=False, asgi=False
|
|
209
|
+
):
|
|
210
|
+
# type: (Any, Any, bool) -> Any
|
|
211
|
+
if asgi:
|
|
212
|
+
# We don't support ASGI Falcon apps, so we don't patch anything here
|
|
213
|
+
return original_prepare_middleware(middleware, independent_middleware, asgi)
|
|
214
|
+
|
|
215
|
+
integration = sentry_sdk.get_client().get_integration(FalconIntegration)
|
|
216
|
+
if integration is not None:
|
|
217
|
+
middleware = [SentryFalconMiddleware()] + (middleware or [])
|
|
218
|
+
|
|
219
|
+
# We intentionally omit the asgi argument here, since the default is False anyways,
|
|
220
|
+
# and this way, we remain backwards-compatible with pre-3.0.0 Falcon versions.
|
|
221
|
+
return original_prepare_middleware(middleware, independent_middleware)
|
|
222
|
+
|
|
223
|
+
falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _exception_leads_to_http_5xx(ex, response):
|
|
227
|
+
# type: (Exception, falcon.Response) -> bool
|
|
228
|
+
is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith(
|
|
229
|
+
"5"
|
|
230
|
+
)
|
|
231
|
+
is_unhandled_error = not isinstance(
|
|
232
|
+
ex, (falcon.HTTPError, falcon.http_status.HTTPStatus)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# We only check the HTTP status on Falcon 3 because in Falcon 2, the status on the response
|
|
236
|
+
# at the stage where we capture it is listed as 200, even though we would expect to see a 500
|
|
237
|
+
# status. Since at the time of this change, Falcon 2 is ca. 4 years old, we have decided to
|
|
238
|
+
# only perform this check on Falcon 3+, despite the risk that some handled errors might be
|
|
239
|
+
# reported to Sentry as unhandled on Falcon 2.
|
|
240
|
+
return (is_server_error or is_unhandled_error) and (
|
|
241
|
+
not FALCON3 or _has_http_5xx_status(response)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _has_http_5xx_status(response):
|
|
246
|
+
# type: (falcon.Response) -> bool
|
|
247
|
+
return response.status.startswith("5")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _set_transaction_name_and_source(event, transaction_style, request):
|
|
251
|
+
# type: (Event, str, falcon.Request) -> None
|
|
252
|
+
name_for_style = {
|
|
253
|
+
"uri_template": request.uri_template,
|
|
254
|
+
"path": request.path,
|
|
255
|
+
}
|
|
256
|
+
event["transaction"] = name_for_style[transaction_style]
|
|
257
|
+
event["transaction_info"] = {"source": SOURCE_FOR_STYLE[transaction_style]}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _make_request_event_processor(req, integration):
|
|
261
|
+
# type: (falcon.Request, FalconIntegration) -> EventProcessor
|
|
262
|
+
|
|
263
|
+
def event_processor(event, hint):
|
|
264
|
+
# type: (Event, dict[str, Any]) -> Event
|
|
265
|
+
_set_transaction_name_and_source(event, integration.transaction_style, req)
|
|
266
|
+
|
|
267
|
+
with capture_internal_exceptions():
|
|
268
|
+
FalconRequestExtractor(req).extract_into_event(event)
|
|
269
|
+
|
|
270
|
+
return event
|
|
271
|
+
|
|
272
|
+
return event_processor
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
import sentry_sdk
|
|
6
|
+
from sentry_sdk.integrations import DidNotEnable
|
|
7
|
+
from sentry_sdk.scope import should_send_default_pii
|
|
8
|
+
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
|
|
9
|
+
from sentry_sdk.utils import transaction_from_function
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Any, Callable, Dict
|
|
15
|
+
from sentry_sdk._types import Event
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from sentry_sdk.integrations.starlette import (
|
|
19
|
+
StarletteIntegration,
|
|
20
|
+
StarletteRequestExtractor,
|
|
21
|
+
)
|
|
22
|
+
except DidNotEnable:
|
|
23
|
+
raise DidNotEnable("Starlette is not installed")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import fastapi # type: ignore
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise DidNotEnable("FastAPI is not installed")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_DEFAULT_TRANSACTION_NAME = "generic FastAPI request"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FastApiIntegration(StarletteIntegration):
|
|
35
|
+
identifier = "fastapi"
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def setup_once():
|
|
39
|
+
# type: () -> None
|
|
40
|
+
patch_get_request_handler()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _set_transaction_name_and_source(scope, transaction_style, request):
|
|
44
|
+
# type: (sentry_sdk.Scope, str, Any) -> None
|
|
45
|
+
name = ""
|
|
46
|
+
|
|
47
|
+
if transaction_style == "endpoint":
|
|
48
|
+
endpoint = request.scope.get("endpoint")
|
|
49
|
+
if endpoint:
|
|
50
|
+
name = transaction_from_function(endpoint) or ""
|
|
51
|
+
|
|
52
|
+
elif transaction_style == "url":
|
|
53
|
+
route = request.scope.get("route")
|
|
54
|
+
if route:
|
|
55
|
+
path = getattr(route, "path", None)
|
|
56
|
+
if path is not None:
|
|
57
|
+
name = path
|
|
58
|
+
|
|
59
|
+
if not name:
|
|
60
|
+
name = _DEFAULT_TRANSACTION_NAME
|
|
61
|
+
source = TransactionSource.ROUTE
|
|
62
|
+
else:
|
|
63
|
+
source = SOURCE_FOR_STYLE[transaction_style]
|
|
64
|
+
|
|
65
|
+
scope.set_transaction_name(name, source=source)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def patch_get_request_handler():
|
|
69
|
+
# type: () -> None
|
|
70
|
+
old_get_request_handler = fastapi.routing.get_request_handler
|
|
71
|
+
|
|
72
|
+
def _sentry_get_request_handler(*args, **kwargs):
|
|
73
|
+
# type: (*Any, **Any) -> Any
|
|
74
|
+
dependant = kwargs.get("dependant")
|
|
75
|
+
if (
|
|
76
|
+
dependant
|
|
77
|
+
and dependant.call is not None
|
|
78
|
+
and not asyncio.iscoroutinefunction(dependant.call)
|
|
79
|
+
):
|
|
80
|
+
old_call = dependant.call
|
|
81
|
+
|
|
82
|
+
@wraps(old_call)
|
|
83
|
+
def _sentry_call(*args, **kwargs):
|
|
84
|
+
# type: (*Any, **Any) -> Any
|
|
85
|
+
current_scope = sentry_sdk.get_current_scope()
|
|
86
|
+
if current_scope.transaction is not None:
|
|
87
|
+
current_scope.transaction.update_active_thread()
|
|
88
|
+
|
|
89
|
+
sentry_scope = sentry_sdk.get_isolation_scope()
|
|
90
|
+
if sentry_scope.profile is not None:
|
|
91
|
+
sentry_scope.profile.update_active_thread_id()
|
|
92
|
+
|
|
93
|
+
return old_call(*args, **kwargs)
|
|
94
|
+
|
|
95
|
+
dependant.call = _sentry_call
|
|
96
|
+
|
|
97
|
+
old_app = old_get_request_handler(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
async def _sentry_app(*args, **kwargs):
|
|
100
|
+
# type: (*Any, **Any) -> Any
|
|
101
|
+
integration = sentry_sdk.get_client().get_integration(FastApiIntegration)
|
|
102
|
+
if integration is None:
|
|
103
|
+
return await old_app(*args, **kwargs)
|
|
104
|
+
|
|
105
|
+
request = args[0]
|
|
106
|
+
|
|
107
|
+
_set_transaction_name_and_source(
|
|
108
|
+
sentry_sdk.get_current_scope(), integration.transaction_style, request
|
|
109
|
+
)
|
|
110
|
+
sentry_scope = sentry_sdk.get_isolation_scope()
|
|
111
|
+
extractor = StarletteRequestExtractor(request)
|
|
112
|
+
info = await extractor.extract_request_info()
|
|
113
|
+
|
|
114
|
+
def _make_request_event_processor(req, integration):
|
|
115
|
+
# type: (Any, Any) -> Callable[[Event, Dict[str, Any]], Event]
|
|
116
|
+
def event_processor(event, hint):
|
|
117
|
+
# type: (Event, Dict[str, Any]) -> Event
|
|
118
|
+
|
|
119
|
+
# Extract information from request
|
|
120
|
+
request_info = event.get("request", {})
|
|
121
|
+
if info:
|
|
122
|
+
if "cookies" in info and should_send_default_pii():
|
|
123
|
+
request_info["cookies"] = info["cookies"]
|
|
124
|
+
if "data" in info:
|
|
125
|
+
request_info["data"] = info["data"]
|
|
126
|
+
event["request"] = deepcopy(request_info)
|
|
127
|
+
|
|
128
|
+
return event
|
|
129
|
+
|
|
130
|
+
return event_processor
|
|
131
|
+
|
|
132
|
+
sentry_scope._name = FastApiIntegration.identifier
|
|
133
|
+
sentry_scope.add_event_processor(
|
|
134
|
+
_make_request_event_processor(request, integration)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return await old_app(*args, **kwargs)
|
|
138
|
+
|
|
139
|
+
return _sentry_app
|
|
140
|
+
|
|
141
|
+
fastapi.routing.get_request_handler = _sentry_get_request_handler
|