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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -30
  2. sentry_sdk/_compat.py +74 -61
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +289 -0
  8. sentry_sdk/_types.py +338 -0
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +134 -0
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +141 -0
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +112 -68
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {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
- from sentry_sdk.hub import Hub
4
- from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
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
- if False:
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: (Callable) -> Callable
22
- def sentry_sdk_excepthook(exctype, value, traceback):
23
- hub = Hub.current
24
- integration = hub.get_integration(ExcepthookIntegration)
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
- if integration is not None and _should_send():
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
- (exctype, value, traceback),
30
- client_options=hub.client.options,
62
+ (type_, value, traceback),
63
+ client_options=sentry_sdk.get_client().options,
31
64
  mechanism={"type": "excepthook", "handled": False},
32
65
  )
33
- hub.capture_event(event, hint=hint)
66
+ sentry_sdk.capture_event(event, hint=hint)
34
67
 
35
- return old_excepthook(exctype, value, traceback)
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