sentry-sdk 0.18.0__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 -6
  2. sentry_sdk/_compat.py +64 -56
  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 +81 -19
  8. sentry_sdk/_types.py +311 -11
  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 +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  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 +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  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 +35 -28
  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 +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  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 +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  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 +20 -11
  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 +71 -60
  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 +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,737 @@
1
+ import asyncio
2
+ import functools
3
+ import warnings
4
+ from collections.abc import Set
5
+ from copy import deepcopy
6
+ from json import JSONDecodeError
7
+
8
+ import sentry_sdk
9
+ from sentry_sdk.consts import OP
10
+ from sentry_sdk.integrations import (
11
+ DidNotEnable,
12
+ Integration,
13
+ _DEFAULT_FAILED_REQUEST_STATUS_CODES,
14
+ )
15
+ from sentry_sdk.integrations._wsgi_common import (
16
+ DEFAULT_HTTP_METHODS_TO_CAPTURE,
17
+ HttpCodeRangeContainer,
18
+ _is_json_content_type,
19
+ request_body_within_bounds,
20
+ )
21
+ from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
22
+ from sentry_sdk.scope import should_send_default_pii
23
+ from sentry_sdk.tracing import (
24
+ SOURCE_FOR_STYLE,
25
+ TransactionSource,
26
+ )
27
+ from sentry_sdk.utils import (
28
+ AnnotatedValue,
29
+ capture_internal_exceptions,
30
+ ensure_integration_enabled,
31
+ event_from_exception,
32
+ parse_version,
33
+ transaction_from_function,
34
+ )
35
+
36
+ from typing import TYPE_CHECKING
37
+
38
+ if TYPE_CHECKING:
39
+ from typing import Any, Awaitable, Callable, Container, Dict, Optional, Tuple, Union
40
+
41
+ from sentry_sdk._types import Event, HttpStatusCodeRange
42
+
43
+ try:
44
+ import starlette # type: ignore
45
+ from starlette import __version__ as STARLETTE_VERSION
46
+ from starlette.applications import Starlette # type: ignore
47
+ from starlette.datastructures import UploadFile # type: ignore
48
+ from starlette.middleware import Middleware # type: ignore
49
+ from starlette.middleware.authentication import ( # type: ignore
50
+ AuthenticationMiddleware,
51
+ )
52
+ from starlette.requests import Request # type: ignore
53
+ from starlette.routing import Match # type: ignore
54
+ from starlette.types import ASGIApp, Receive, Scope as StarletteScope, Send # type: ignore
55
+ except ImportError:
56
+ raise DidNotEnable("Starlette is not installed")
57
+
58
+ try:
59
+ # Starlette 0.20
60
+ from starlette.middleware.exceptions import ExceptionMiddleware # type: ignore
61
+ except ImportError:
62
+ # Startlette 0.19.1
63
+ from starlette.exceptions import ExceptionMiddleware # type: ignore
64
+
65
+ try:
66
+ # Optional dependency of Starlette to parse form data.
67
+ try:
68
+ # python-multipart 0.0.13 and later
69
+ import python_multipart as multipart # type: ignore
70
+ except ImportError:
71
+ # python-multipart 0.0.12 and earlier
72
+ import multipart # type: ignore
73
+ except ImportError:
74
+ multipart = None
75
+
76
+
77
+ _DEFAULT_TRANSACTION_NAME = "generic Starlette request"
78
+
79
+ TRANSACTION_STYLE_VALUES = ("endpoint", "url")
80
+
81
+
82
+ class StarletteIntegration(Integration):
83
+ identifier = "starlette"
84
+ origin = f"auto.http.{identifier}"
85
+
86
+ transaction_style = ""
87
+
88
+ def __init__(
89
+ self,
90
+ transaction_style="url", # type: str
91
+ failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Union[Set[int], list[HttpStatusCodeRange], None]
92
+ middleware_spans=True, # type: bool
93
+ http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
94
+ ):
95
+ # type: (...) -> None
96
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
97
+ raise ValueError(
98
+ "Invalid value for transaction_style: %s (must be in %s)"
99
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
100
+ )
101
+ self.transaction_style = transaction_style
102
+ self.middleware_spans = middleware_spans
103
+ self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
104
+
105
+ if isinstance(failed_request_status_codes, Set):
106
+ self.failed_request_status_codes = failed_request_status_codes # type: Container[int]
107
+ else:
108
+ warnings.warn(
109
+ "Passing a list or None for failed_request_status_codes is deprecated. "
110
+ "Please pass a set of int instead.",
111
+ DeprecationWarning,
112
+ stacklevel=2,
113
+ )
114
+
115
+ if failed_request_status_codes is None:
116
+ self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES
117
+ else:
118
+ self.failed_request_status_codes = HttpCodeRangeContainer(
119
+ failed_request_status_codes
120
+ )
121
+
122
+ @staticmethod
123
+ def setup_once():
124
+ # type: () -> None
125
+ version = parse_version(STARLETTE_VERSION)
126
+
127
+ if version is None:
128
+ raise DidNotEnable(
129
+ "Unparsable Starlette version: {}".format(STARLETTE_VERSION)
130
+ )
131
+
132
+ patch_middlewares()
133
+ patch_asgi_app()
134
+ patch_request_response()
135
+
136
+ if version >= (0, 24):
137
+ patch_templates()
138
+
139
+
140
+ def _enable_span_for_middleware(middleware_class):
141
+ # type: (Any) -> type
142
+ old_call = middleware_class.__call__
143
+
144
+ async def _create_span_call(app, scope, receive, send, **kwargs):
145
+ # type: (Any, Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]], Any) -> None
146
+ integration = sentry_sdk.get_client().get_integration(StarletteIntegration)
147
+ if integration is None or not integration.middleware_spans:
148
+ return await old_call(app, scope, receive, send, **kwargs)
149
+
150
+ middleware_name = app.__class__.__name__
151
+
152
+ # Update transaction name with middleware name
153
+ name, source = _get_transaction_from_middleware(app, scope, integration)
154
+ if name is not None:
155
+ sentry_sdk.get_current_scope().set_transaction_name(
156
+ name,
157
+ source=source,
158
+ )
159
+
160
+ with sentry_sdk.start_span(
161
+ op=OP.MIDDLEWARE_STARLETTE,
162
+ name=middleware_name,
163
+ origin=StarletteIntegration.origin,
164
+ ) as middleware_span:
165
+ middleware_span.set_tag("starlette.middleware_name", middleware_name)
166
+
167
+ # Creating spans for the "receive" callback
168
+ async def _sentry_receive(*args, **kwargs):
169
+ # type: (*Any, **Any) -> Any
170
+ with sentry_sdk.start_span(
171
+ op=OP.MIDDLEWARE_STARLETTE_RECEIVE,
172
+ name=getattr(receive, "__qualname__", str(receive)),
173
+ origin=StarletteIntegration.origin,
174
+ ) as span:
175
+ span.set_tag("starlette.middleware_name", middleware_name)
176
+ return await receive(*args, **kwargs)
177
+
178
+ receive_name = getattr(receive, "__name__", str(receive))
179
+ receive_patched = receive_name == "_sentry_receive"
180
+ new_receive = _sentry_receive if not receive_patched else receive
181
+
182
+ # Creating spans for the "send" callback
183
+ async def _sentry_send(*args, **kwargs):
184
+ # type: (*Any, **Any) -> Any
185
+ with sentry_sdk.start_span(
186
+ op=OP.MIDDLEWARE_STARLETTE_SEND,
187
+ name=getattr(send, "__qualname__", str(send)),
188
+ origin=StarletteIntegration.origin,
189
+ ) as span:
190
+ span.set_tag("starlette.middleware_name", middleware_name)
191
+ return await send(*args, **kwargs)
192
+
193
+ send_name = getattr(send, "__name__", str(send))
194
+ send_patched = send_name == "_sentry_send"
195
+ new_send = _sentry_send if not send_patched else send
196
+
197
+ return await old_call(app, scope, new_receive, new_send, **kwargs)
198
+
199
+ not_yet_patched = old_call.__name__ not in [
200
+ "_create_span_call",
201
+ "_sentry_authenticationmiddleware_call",
202
+ "_sentry_exceptionmiddleware_call",
203
+ ]
204
+
205
+ if not_yet_patched:
206
+ middleware_class.__call__ = _create_span_call
207
+
208
+ return middleware_class
209
+
210
+
211
+ @ensure_integration_enabled(StarletteIntegration)
212
+ def _capture_exception(exception, handled=False):
213
+ # type: (BaseException, **Any) -> None
214
+ event, hint = event_from_exception(
215
+ exception,
216
+ client_options=sentry_sdk.get_client().options,
217
+ mechanism={"type": StarletteIntegration.identifier, "handled": handled},
218
+ )
219
+
220
+ sentry_sdk.capture_event(event, hint=hint)
221
+
222
+
223
+ def patch_exception_middleware(middleware_class):
224
+ # type: (Any) -> None
225
+ """
226
+ Capture all exceptions in Starlette app and
227
+ also extract user information.
228
+ """
229
+ old_middleware_init = middleware_class.__init__
230
+
231
+ not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init)
232
+
233
+ if not_yet_patched:
234
+
235
+ def _sentry_middleware_init(self, *args, **kwargs):
236
+ # type: (Any, Any, Any) -> None
237
+ old_middleware_init(self, *args, **kwargs)
238
+
239
+ # Patch existing exception handlers
240
+ old_handlers = self._exception_handlers.copy()
241
+
242
+ async def _sentry_patched_exception_handler(self, *args, **kwargs):
243
+ # type: (Any, Any, Any) -> None
244
+ integration = sentry_sdk.get_client().get_integration(
245
+ StarletteIntegration
246
+ )
247
+
248
+ exp = args[0]
249
+
250
+ if integration is not None:
251
+ is_http_server_error = (
252
+ hasattr(exp, "status_code")
253
+ and isinstance(exp.status_code, int)
254
+ and exp.status_code in integration.failed_request_status_codes
255
+ )
256
+ if is_http_server_error:
257
+ _capture_exception(exp, handled=True)
258
+
259
+ # Find a matching handler
260
+ old_handler = None
261
+ for cls in type(exp).__mro__:
262
+ if cls in old_handlers:
263
+ old_handler = old_handlers[cls]
264
+ break
265
+
266
+ if old_handler is None:
267
+ return
268
+
269
+ if _is_async_callable(old_handler):
270
+ return await old_handler(self, *args, **kwargs)
271
+ else:
272
+ return old_handler(self, *args, **kwargs)
273
+
274
+ for key in self._exception_handlers.keys():
275
+ self._exception_handlers[key] = _sentry_patched_exception_handler
276
+
277
+ middleware_class.__init__ = _sentry_middleware_init
278
+
279
+ old_call = middleware_class.__call__
280
+
281
+ async def _sentry_exceptionmiddleware_call(self, scope, receive, send):
282
+ # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None
283
+ # Also add the user (that was eventually set by be Authentication middle
284
+ # that was called before this middleware). This is done because the authentication
285
+ # middleware sets the user in the scope and then (in the same function)
286
+ # calls this exception middelware. In case there is no exception (or no handler
287
+ # for the type of exception occuring) then the exception bubbles up and setting the
288
+ # user information into the sentry scope is done in auth middleware and the
289
+ # ASGI middleware will then send everything to Sentry and this is fine.
290
+ # But if there is an exception happening that the exception middleware here
291
+ # has a handler for, it will send the exception directly to Sentry, so we need
292
+ # the user information right now.
293
+ # This is why we do it here.
294
+ _add_user_to_sentry_scope(scope)
295
+ await old_call(self, scope, receive, send)
296
+
297
+ middleware_class.__call__ = _sentry_exceptionmiddleware_call
298
+
299
+
300
+ @ensure_integration_enabled(StarletteIntegration)
301
+ def _add_user_to_sentry_scope(scope):
302
+ # type: (Dict[str, Any]) -> None
303
+ """
304
+ Extracts user information from the ASGI scope and
305
+ adds it to Sentry's scope.
306
+ """
307
+ if "user" not in scope:
308
+ return
309
+
310
+ if not should_send_default_pii():
311
+ return
312
+
313
+ user_info = {} # type: Dict[str, Any]
314
+ starlette_user = scope["user"]
315
+
316
+ username = getattr(starlette_user, "username", None)
317
+ if username:
318
+ user_info.setdefault("username", starlette_user.username)
319
+
320
+ user_id = getattr(starlette_user, "id", None)
321
+ if user_id:
322
+ user_info.setdefault("id", starlette_user.id)
323
+
324
+ email = getattr(starlette_user, "email", None)
325
+ if email:
326
+ user_info.setdefault("email", starlette_user.email)
327
+
328
+ sentry_scope = sentry_sdk.get_isolation_scope()
329
+ sentry_scope.set_user(user_info)
330
+
331
+
332
+ def patch_authentication_middleware(middleware_class):
333
+ # type: (Any) -> None
334
+ """
335
+ Add user information to Sentry scope.
336
+ """
337
+ old_call = middleware_class.__call__
338
+
339
+ not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call)
340
+
341
+ if not_yet_patched:
342
+
343
+ async def _sentry_authenticationmiddleware_call(self, scope, receive, send):
344
+ # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None
345
+ await old_call(self, scope, receive, send)
346
+ _add_user_to_sentry_scope(scope)
347
+
348
+ middleware_class.__call__ = _sentry_authenticationmiddleware_call
349
+
350
+
351
+ def patch_middlewares():
352
+ # type: () -> None
353
+ """
354
+ Patches Starlettes `Middleware` class to record
355
+ spans for every middleware invoked.
356
+ """
357
+ old_middleware_init = Middleware.__init__
358
+
359
+ not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init)
360
+
361
+ if not_yet_patched:
362
+
363
+ def _sentry_middleware_init(self, cls, *args, **kwargs):
364
+ # type: (Any, Any, Any, Any) -> None
365
+ if cls == SentryAsgiMiddleware:
366
+ return old_middleware_init(self, cls, *args, **kwargs)
367
+
368
+ span_enabled_cls = _enable_span_for_middleware(cls)
369
+ old_middleware_init(self, span_enabled_cls, *args, **kwargs)
370
+
371
+ if cls == AuthenticationMiddleware:
372
+ patch_authentication_middleware(cls)
373
+
374
+ if cls == ExceptionMiddleware:
375
+ patch_exception_middleware(cls)
376
+
377
+ Middleware.__init__ = _sentry_middleware_init
378
+
379
+
380
+ def patch_asgi_app():
381
+ # type: () -> None
382
+ """
383
+ Instrument Starlette ASGI app using the SentryAsgiMiddleware.
384
+ """
385
+ old_app = Starlette.__call__
386
+
387
+ async def _sentry_patched_asgi_app(self, scope, receive, send):
388
+ # type: (Starlette, StarletteScope, Receive, Send) -> None
389
+ integration = sentry_sdk.get_client().get_integration(StarletteIntegration)
390
+ if integration is None:
391
+ return await old_app(self, scope, receive, send)
392
+
393
+ middleware = SentryAsgiMiddleware(
394
+ lambda *a, **kw: old_app(self, *a, **kw),
395
+ mechanism_type=StarletteIntegration.identifier,
396
+ transaction_style=integration.transaction_style,
397
+ span_origin=StarletteIntegration.origin,
398
+ http_methods_to_capture=(
399
+ integration.http_methods_to_capture
400
+ if integration
401
+ else DEFAULT_HTTP_METHODS_TO_CAPTURE
402
+ ),
403
+ asgi_version=3,
404
+ )
405
+
406
+ return await middleware(scope, receive, send)
407
+
408
+ Starlette.__call__ = _sentry_patched_asgi_app
409
+
410
+
411
+ # This was vendored in from Starlette to support Starlette 0.19.1 because
412
+ # this function was only introduced in 0.20.x
413
+ def _is_async_callable(obj):
414
+ # type: (Any) -> bool
415
+ while isinstance(obj, functools.partial):
416
+ obj = obj.func
417
+
418
+ return asyncio.iscoroutinefunction(obj) or (
419
+ callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
420
+ )
421
+
422
+
423
+ def patch_request_response():
424
+ # type: () -> None
425
+ old_request_response = starlette.routing.request_response
426
+
427
+ def _sentry_request_response(func):
428
+ # type: (Callable[[Any], Any]) -> ASGIApp
429
+ old_func = func
430
+
431
+ is_coroutine = _is_async_callable(old_func)
432
+ if is_coroutine:
433
+
434
+ async def _sentry_async_func(*args, **kwargs):
435
+ # type: (*Any, **Any) -> Any
436
+ integration = sentry_sdk.get_client().get_integration(
437
+ StarletteIntegration
438
+ )
439
+ if integration is None:
440
+ return await old_func(*args, **kwargs)
441
+
442
+ request = args[0]
443
+
444
+ _set_transaction_name_and_source(
445
+ sentry_sdk.get_current_scope(),
446
+ integration.transaction_style,
447
+ request,
448
+ )
449
+
450
+ sentry_scope = sentry_sdk.get_isolation_scope()
451
+ extractor = StarletteRequestExtractor(request)
452
+ info = await extractor.extract_request_info()
453
+
454
+ def _make_request_event_processor(req, integration):
455
+ # type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event]
456
+ def event_processor(event, hint):
457
+ # type: (Event, Dict[str, Any]) -> Event
458
+
459
+ # Add info from request to event
460
+ request_info = event.get("request", {})
461
+ if info:
462
+ if "cookies" in info:
463
+ request_info["cookies"] = info["cookies"]
464
+ if "data" in info:
465
+ request_info["data"] = info["data"]
466
+ event["request"] = deepcopy(request_info)
467
+
468
+ return event
469
+
470
+ return event_processor
471
+
472
+ sentry_scope._name = StarletteIntegration.identifier
473
+ sentry_scope.add_event_processor(
474
+ _make_request_event_processor(request, integration)
475
+ )
476
+
477
+ return await old_func(*args, **kwargs)
478
+
479
+ func = _sentry_async_func
480
+
481
+ else:
482
+
483
+ @functools.wraps(old_func)
484
+ def _sentry_sync_func(*args, **kwargs):
485
+ # type: (*Any, **Any) -> Any
486
+ integration = sentry_sdk.get_client().get_integration(
487
+ StarletteIntegration
488
+ )
489
+ if integration is None:
490
+ return old_func(*args, **kwargs)
491
+
492
+ current_scope = sentry_sdk.get_current_scope()
493
+ if current_scope.transaction is not None:
494
+ current_scope.transaction.update_active_thread()
495
+
496
+ sentry_scope = sentry_sdk.get_isolation_scope()
497
+ if sentry_scope.profile is not None:
498
+ sentry_scope.profile.update_active_thread_id()
499
+
500
+ request = args[0]
501
+
502
+ _set_transaction_name_and_source(
503
+ sentry_scope, integration.transaction_style, request
504
+ )
505
+
506
+ extractor = StarletteRequestExtractor(request)
507
+ cookies = extractor.extract_cookies_from_request()
508
+
509
+ def _make_request_event_processor(req, integration):
510
+ # type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event]
511
+ def event_processor(event, hint):
512
+ # type: (Event, dict[str, Any]) -> Event
513
+
514
+ # Extract information from request
515
+ request_info = event.get("request", {})
516
+ if cookies:
517
+ request_info["cookies"] = cookies
518
+
519
+ event["request"] = deepcopy(request_info)
520
+
521
+ return event
522
+
523
+ return event_processor
524
+
525
+ sentry_scope._name = StarletteIntegration.identifier
526
+ sentry_scope.add_event_processor(
527
+ _make_request_event_processor(request, integration)
528
+ )
529
+
530
+ return old_func(*args, **kwargs)
531
+
532
+ func = _sentry_sync_func
533
+
534
+ return old_request_response(func)
535
+
536
+ starlette.routing.request_response = _sentry_request_response
537
+
538
+
539
+ def patch_templates():
540
+ # type: () -> None
541
+
542
+ # If markupsafe is not installed, then Jinja2 is not installed
543
+ # (markupsafe is a dependency of Jinja2)
544
+ # In this case we do not need to patch the Jinja2Templates class
545
+ try:
546
+ from markupsafe import Markup
547
+ except ImportError:
548
+ return # Nothing to do
549
+
550
+ from starlette.templating import Jinja2Templates # type: ignore
551
+
552
+ old_jinja2templates_init = Jinja2Templates.__init__
553
+
554
+ not_yet_patched = "_sentry_jinja2templates_init" not in str(
555
+ old_jinja2templates_init
556
+ )
557
+
558
+ if not_yet_patched:
559
+
560
+ def _sentry_jinja2templates_init(self, *args, **kwargs):
561
+ # type: (Jinja2Templates, *Any, **Any) -> None
562
+ def add_sentry_trace_meta(request):
563
+ # type: (Request) -> Dict[str, Any]
564
+ trace_meta = Markup(
565
+ sentry_sdk.get_current_scope().trace_propagation_meta()
566
+ )
567
+ return {
568
+ "sentry_trace_meta": trace_meta,
569
+ }
570
+
571
+ kwargs.setdefault("context_processors", [])
572
+
573
+ if add_sentry_trace_meta not in kwargs["context_processors"]:
574
+ kwargs["context_processors"].append(add_sentry_trace_meta)
575
+
576
+ return old_jinja2templates_init(self, *args, **kwargs)
577
+
578
+ Jinja2Templates.__init__ = _sentry_jinja2templates_init
579
+
580
+
581
+ class StarletteRequestExtractor:
582
+ """
583
+ Extracts useful information from the Starlette request
584
+ (like form data or cookies) and adds it to the Sentry event.
585
+ """
586
+
587
+ request = None # type: Request
588
+
589
+ def __init__(self, request):
590
+ # type: (StarletteRequestExtractor, Request) -> None
591
+ self.request = request
592
+
593
+ def extract_cookies_from_request(self):
594
+ # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
595
+ cookies = None # type: Optional[Dict[str, Any]]
596
+ if should_send_default_pii():
597
+ cookies = self.cookies()
598
+
599
+ return cookies
600
+
601
+ async def extract_request_info(self):
602
+ # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
603
+ client = sentry_sdk.get_client()
604
+
605
+ request_info = {} # type: Dict[str, Any]
606
+
607
+ with capture_internal_exceptions():
608
+ # Add cookies
609
+ if should_send_default_pii():
610
+ request_info["cookies"] = self.cookies()
611
+
612
+ # If there is no body, just return the cookies
613
+ content_length = await self.content_length()
614
+ if not content_length:
615
+ return request_info
616
+
617
+ # Add annotation if body is too big
618
+ if content_length and not request_body_within_bounds(
619
+ client, content_length
620
+ ):
621
+ request_info["data"] = AnnotatedValue.removed_because_over_size_limit()
622
+ return request_info
623
+
624
+ # Add JSON body, if it is a JSON request
625
+ json = await self.json()
626
+ if json:
627
+ request_info["data"] = json
628
+ return request_info
629
+
630
+ # Add form as key/value pairs, if request has form data
631
+ form = await self.form()
632
+ if form:
633
+ form_data = {}
634
+ for key, val in form.items():
635
+ is_file = isinstance(val, UploadFile)
636
+ form_data[key] = (
637
+ val
638
+ if not is_file
639
+ else AnnotatedValue.removed_because_raw_data()
640
+ )
641
+
642
+ request_info["data"] = form_data
643
+ return request_info
644
+
645
+ # Raw data, do not add body just an annotation
646
+ request_info["data"] = AnnotatedValue.removed_because_raw_data()
647
+ return request_info
648
+
649
+ async def content_length(self):
650
+ # type: (StarletteRequestExtractor) -> Optional[int]
651
+ if "content-length" in self.request.headers:
652
+ return int(self.request.headers["content-length"])
653
+
654
+ return None
655
+
656
+ def cookies(self):
657
+ # type: (StarletteRequestExtractor) -> Dict[str, Any]
658
+ return self.request.cookies
659
+
660
+ async def form(self):
661
+ # type: (StarletteRequestExtractor) -> Any
662
+ if multipart is None:
663
+ return None
664
+
665
+ # Parse the body first to get it cached, as Starlette does not cache form() as it
666
+ # does with body() and json() https://github.com/encode/starlette/discussions/1933
667
+ # Calling `.form()` without calling `.body()` first will
668
+ # potentially break the users project.
669
+ await self.request.body()
670
+
671
+ return await self.request.form()
672
+
673
+ def is_json(self):
674
+ # type: (StarletteRequestExtractor) -> bool
675
+ return _is_json_content_type(self.request.headers.get("content-type"))
676
+
677
+ async def json(self):
678
+ # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
679
+ if not self.is_json():
680
+ return None
681
+ try:
682
+ return await self.request.json()
683
+ except JSONDecodeError:
684
+ return None
685
+
686
+
687
+ def _transaction_name_from_router(scope):
688
+ # type: (StarletteScope) -> Optional[str]
689
+ router = scope.get("router")
690
+ if not router:
691
+ return None
692
+
693
+ for route in router.routes:
694
+ match = route.matches(scope)
695
+ if match[0] == Match.FULL:
696
+ try:
697
+ return route.path
698
+ except AttributeError:
699
+ # routes added via app.host() won't have a path attribute
700
+ return scope.get("path")
701
+
702
+ return None
703
+
704
+
705
+ def _set_transaction_name_and_source(scope, transaction_style, request):
706
+ # type: (sentry_sdk.Scope, str, Any) -> None
707
+ name = None
708
+ source = SOURCE_FOR_STYLE[transaction_style]
709
+
710
+ if transaction_style == "endpoint":
711
+ endpoint = request.scope.get("endpoint")
712
+ if endpoint:
713
+ name = transaction_from_function(endpoint) or None
714
+
715
+ elif transaction_style == "url":
716
+ name = _transaction_name_from_router(request.scope)
717
+
718
+ if name is None:
719
+ name = _DEFAULT_TRANSACTION_NAME
720
+ source = TransactionSource.ROUTE
721
+
722
+ scope.set_transaction_name(name, source=source)
723
+
724
+
725
+ def _get_transaction_from_middleware(app, asgi_scope, integration):
726
+ # type: (Any, Dict[str, Any], StarletteIntegration) -> Tuple[Optional[str], Optional[str]]
727
+ name = None
728
+ source = None
729
+
730
+ if integration.transaction_style == "endpoint":
731
+ name = transaction_from_function(app.__class__)
732
+ source = TransactionSource.COMPONENT
733
+ elif integration.transaction_style == "url":
734
+ name = _transaction_name_from_router(asgi_scope)
735
+ source = TransactionSource.ROUTE
736
+
737
+ return name, source