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
@@ -1,33 +1,49 @@
1
1
  """
2
2
  An ASGI middleware.
3
3
 
4
- Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`_.
4
+ Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`.
5
5
  """
6
6
 
7
7
  import asyncio
8
8
  import inspect
9
- import urllib
10
-
11
- from sentry_sdk._functools import partial
12
- from sentry_sdk._types import MYPY
13
- from sentry_sdk.hub import Hub, _should_send_default_pii
14
- from sentry_sdk.integrations._wsgi_common import _filter_headers
9
+ from copy import deepcopy
10
+ from functools import partial
11
+
12
+ import sentry_sdk
13
+ from sentry_sdk.api import continue_trace
14
+ from sentry_sdk.consts import OP
15
+ from sentry_sdk.integrations._asgi_common import (
16
+ _get_headers,
17
+ _get_request_data,
18
+ _get_url,
19
+ )
20
+ from sentry_sdk.integrations._wsgi_common import (
21
+ DEFAULT_HTTP_METHODS_TO_CAPTURE,
22
+ nullcontext,
23
+ )
24
+ from sentry_sdk.sessions import track_session
25
+ from sentry_sdk.tracing import (
26
+ SOURCE_FOR_STYLE,
27
+ TransactionSource,
28
+ )
15
29
  from sentry_sdk.utils import (
16
30
  ContextVar,
17
31
  event_from_exception,
18
- transaction_from_function,
19
32
  HAS_REAL_CONTEXTVARS,
20
33
  CONTEXTVARS_ERROR_MESSAGE,
34
+ logger,
35
+ transaction_from_function,
36
+ _get_installed_modules,
21
37
  )
22
38
  from sentry_sdk.tracing import Transaction
23
39
 
24
- if MYPY:
25
- from typing import Dict
40
+ from typing import TYPE_CHECKING
41
+
42
+ if TYPE_CHECKING:
26
43
  from typing import Any
44
+ from typing import Dict
27
45
  from typing import Optional
28
- from typing import Callable
29
-
30
- from typing_extensions import Literal
46
+ from typing import Tuple
31
47
 
32
48
  from sentry_sdk._types import Event, Hint
33
49
 
@@ -36,18 +52,18 @@ _asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied")
36
52
 
37
53
  _DEFAULT_TRANSACTION_NAME = "generic ASGI request"
38
54
 
55
+ TRANSACTION_STYLE_VALUES = ("endpoint", "url")
39
56
 
40
- def _capture_exception(hub, exc):
41
- # type: (Hub, Any) -> None
42
57
 
43
- # Check client here as it might have been unset while streaming response
44
- if hub.client is not None:
45
- event, hint = event_from_exception(
46
- exc,
47
- client_options=hub.client.options,
48
- mechanism={"type": "asgi", "handled": False},
49
- )
50
- hub.capture_event(event, hint=hint)
58
+ def _capture_exception(exc, mechanism_type="asgi"):
59
+ # type: (Any, str) -> None
60
+
61
+ event, hint = event_from_exception(
62
+ exc,
63
+ client_options=sentry_sdk.get_client().options,
64
+ mechanism={"type": mechanism_type, "handled": False},
65
+ )
66
+ sentry_sdk.capture_event(event, hint=hint)
51
67
 
52
68
 
53
69
  def _looks_like_asgi3(app):
@@ -67,10 +83,26 @@ def _looks_like_asgi3(app):
67
83
 
68
84
 
69
85
  class SentryAsgiMiddleware:
70
- __slots__ = ("app", "__call__")
71
-
72
- def __init__(self, app, unsafe_context_data=False):
73
- # type: (Any, bool) -> None
86
+ __slots__ = (
87
+ "app",
88
+ "__call__",
89
+ "transaction_style",
90
+ "mechanism_type",
91
+ "span_origin",
92
+ "http_methods_to_capture",
93
+ )
94
+
95
+ def __init__(
96
+ self,
97
+ app, # type: Any
98
+ unsafe_context_data=False, # type: bool
99
+ transaction_style="endpoint", # type: str
100
+ mechanism_type="asgi", # type: str
101
+ span_origin="manual", # type: str
102
+ http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
103
+ asgi_version=None, # type: Optional[int]
104
+ ):
105
+ # type: (...) -> None
74
106
  """
75
107
  Instrument an ASGI application with Sentry. Provides HTTP/websocket
76
108
  data to sent events and basic handling for exceptions bubbling up
@@ -78,7 +110,6 @@ class SentryAsgiMiddleware:
78
110
 
79
111
  :param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default.
80
112
  """
81
-
82
113
  if not unsafe_context_data and not HAS_REAL_CONTEXTVARS:
83
114
  # We better have contextvars or we're going to leak state between
84
115
  # requests.
@@ -86,154 +117,225 @@ class SentryAsgiMiddleware:
86
117
  "The ASGI middleware for Sentry requires Python 3.7+ "
87
118
  "or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
88
119
  )
120
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
121
+ raise ValueError(
122
+ "Invalid value for transaction_style: %s (must be in %s)"
123
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
124
+ )
125
+
126
+ asgi_middleware_while_using_starlette_or_fastapi = (
127
+ mechanism_type == "asgi" and "starlette" in _get_installed_modules()
128
+ )
129
+ if asgi_middleware_while_using_starlette_or_fastapi:
130
+ logger.warning(
131
+ "The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. "
132
+ "Please remove 'SentryAsgiMiddleware' from your project. "
133
+ "See https://docs.sentry.io/platforms/python/guides/asgi/ for more information."
134
+ )
135
+
136
+ self.transaction_style = transaction_style
137
+ self.mechanism_type = mechanism_type
138
+ self.span_origin = span_origin
89
139
  self.app = app
140
+ self.http_methods_to_capture = http_methods_to_capture
141
+
142
+ if asgi_version is None:
143
+ if _looks_like_asgi3(app):
144
+ asgi_version = 3
145
+ else:
146
+ asgi_version = 2
147
+
148
+ if asgi_version == 3:
149
+ self.__call__ = self._run_asgi3
150
+ elif asgi_version == 2:
151
+ self.__call__ = self._run_asgi2 # type: ignore
90
152
 
91
- if _looks_like_asgi3(app):
92
- self.__call__ = self._run_asgi3 # type: Callable[..., Any]
93
- else:
94
- self.__call__ = self._run_asgi2
153
+ def _capture_lifespan_exception(self, exc):
154
+ # type: (Exception) -> None
155
+ """Capture exceptions raise in application lifespan handlers.
156
+
157
+ The separate function is needed to support overriding in derived integrations that use different catching mechanisms.
158
+ """
159
+ return _capture_exception(exc=exc, mechanism_type=self.mechanism_type)
160
+
161
+ def _capture_request_exception(self, exc):
162
+ # type: (Exception) -> None
163
+ """Capture exceptions raised in incoming request handlers.
164
+
165
+ The separate function is needed to support overriding in derived integrations that use different catching mechanisms.
166
+ """
167
+ return _capture_exception(exc=exc, mechanism_type=self.mechanism_type)
95
168
 
96
169
  def _run_asgi2(self, scope):
97
170
  # type: (Any) -> Any
98
171
  async def inner(receive, send):
99
172
  # type: (Any, Any) -> Any
100
- return await self._run_app(scope, lambda: self.app(scope)(receive, send))
173
+ return await self._run_app(scope, receive, send, asgi_version=2)
101
174
 
102
175
  return inner
103
176
 
104
177
  async def _run_asgi3(self, scope, receive, send):
105
178
  # type: (Any, Any, Any) -> Any
106
- return await self._run_app(scope, lambda: self.app(scope, receive, send))
179
+ return await self._run_app(scope, receive, send, asgi_version=3)
107
180
 
108
- async def _run_app(self, scope, callback):
109
- # type: (Any, Any) -> Any
181
+ async def _run_app(self, scope, receive, send, asgi_version):
182
+ # type: (Any, Any, Any, int) -> Any
110
183
  is_recursive_asgi_middleware = _asgi_middleware_applied.get(False)
111
-
112
- if is_recursive_asgi_middleware:
184
+ is_lifespan = scope["type"] == "lifespan"
185
+ if is_recursive_asgi_middleware or is_lifespan:
113
186
  try:
114
- return await callback()
187
+ if asgi_version == 2:
188
+ return await self.app(scope)(receive, send)
189
+ else:
190
+ return await self.app(scope, receive, send)
191
+
115
192
  except Exception as exc:
116
- _capture_exception(Hub.current, exc)
193
+ self._capture_lifespan_exception(exc)
117
194
  raise exc from None
118
195
 
119
196
  _asgi_middleware_applied.set(True)
120
197
  try:
121
- hub = Hub(Hub.current)
122
- with hub:
123
- with hub.configure_scope() as sentry_scope:
198
+ with sentry_sdk.isolation_scope() as sentry_scope:
199
+ with track_session(sentry_scope, session_mode="request"):
124
200
  sentry_scope.clear_breadcrumbs()
125
201
  sentry_scope._name = "asgi"
126
202
  processor = partial(self.event_processor, asgi_scope=scope)
127
203
  sentry_scope.add_event_processor(processor)
128
204
 
129
- ty = scope["type"]
205
+ ty = scope["type"]
206
+ (
207
+ transaction_name,
208
+ transaction_source,
209
+ ) = self._get_transaction_name_and_source(
210
+ self.transaction_style,
211
+ scope,
212
+ )
130
213
 
131
- if ty in ("http", "websocket"):
132
- transaction = Transaction.continue_from_headers(
133
- dict(scope["headers"]),
134
- op="{}.server".format(ty),
214
+ method = scope.get("method", "").upper()
215
+ transaction = None
216
+ if ty in ("http", "websocket"):
217
+ if ty == "websocket" or method in self.http_methods_to_capture:
218
+ transaction = continue_trace(
219
+ _get_headers(scope),
220
+ op="{}.server".format(ty),
221
+ name=transaction_name,
222
+ source=transaction_source,
223
+ origin=self.span_origin,
224
+ )
225
+ else:
226
+ transaction = Transaction(
227
+ op=OP.HTTP_SERVER,
228
+ name=transaction_name,
229
+ source=transaction_source,
230
+ origin=self.span_origin,
231
+ )
232
+
233
+ if transaction:
234
+ transaction.set_tag("asgi.type", ty)
235
+
236
+ transaction_context = (
237
+ sentry_sdk.start_transaction(
238
+ transaction,
239
+ custom_sampling_context={"asgi_scope": scope},
240
+ )
241
+ if transaction is not None
242
+ else nullcontext()
135
243
  )
136
- else:
137
- transaction = Transaction(op="asgi.server")
138
-
139
- transaction.name = _DEFAULT_TRANSACTION_NAME
140
- transaction.set_tag("asgi.type", ty)
141
-
142
- with hub.start_transaction(transaction):
143
- # XXX: Would be cool to have correct span status, but we
144
- # would have to wrap send(). That is a bit hard to do with
145
- # the current abstraction over ASGI 2/3.
146
- try:
147
- return await callback()
148
- except Exception as exc:
149
- _capture_exception(hub, exc)
150
- raise exc from None
244
+ with transaction_context:
245
+ try:
246
+
247
+ async def _sentry_wrapped_send(event):
248
+ # type: (Dict[str, Any]) -> Any
249
+ if transaction is not None:
250
+ is_http_response = (
251
+ event.get("type") == "http.response.start"
252
+ and "status" in event
253
+ )
254
+ if is_http_response:
255
+ transaction.set_http_status(event["status"])
256
+
257
+ return await send(event)
258
+
259
+ if asgi_version == 2:
260
+ return await self.app(scope)(
261
+ receive, _sentry_wrapped_send
262
+ )
263
+ else:
264
+ return await self.app(
265
+ scope, receive, _sentry_wrapped_send
266
+ )
267
+ except Exception as exc:
268
+ self._capture_request_exception(exc)
269
+ raise exc from None
151
270
  finally:
152
271
  _asgi_middleware_applied.set(False)
153
272
 
154
273
  def event_processor(self, event, hint, asgi_scope):
155
274
  # type: (Event, Hint, Any) -> Optional[Event]
156
- request_info = event.get("request", {})
157
-
158
- ty = asgi_scope["type"]
159
- if ty in ("http", "websocket"):
160
- request_info["method"] = asgi_scope.get("method")
161
- request_info["headers"] = headers = _filter_headers(
162
- self._get_headers(asgi_scope)
163
- )
164
- request_info["query_string"] = self._get_query(asgi_scope)
165
-
166
- request_info["url"] = self._get_url(
167
- asgi_scope, "http" if ty == "http" else "ws", headers.get("host")
275
+ request_data = event.get("request", {})
276
+ request_data.update(_get_request_data(asgi_scope))
277
+ event["request"] = deepcopy(request_data)
278
+
279
+ # Only set transaction name if not already set by Starlette or FastAPI (or other frameworks)
280
+ transaction = event.get("transaction")
281
+ transaction_source = (event.get("transaction_info") or {}).get("source")
282
+ already_set = (
283
+ transaction is not None
284
+ and transaction != _DEFAULT_TRANSACTION_NAME
285
+ and transaction_source
286
+ in [
287
+ TransactionSource.COMPONENT,
288
+ TransactionSource.ROUTE,
289
+ TransactionSource.CUSTOM,
290
+ ]
291
+ )
292
+ if not already_set:
293
+ name, source = self._get_transaction_name_and_source(
294
+ self.transaction_style, asgi_scope
168
295
  )
169
-
170
- client = asgi_scope.get("client")
171
- if client and _should_send_default_pii():
172
- request_info["env"] = {"REMOTE_ADDR": client[0]}
173
-
174
- if (
175
- event.get("transaction", _DEFAULT_TRANSACTION_NAME)
176
- == _DEFAULT_TRANSACTION_NAME
177
- ):
178
- endpoint = asgi_scope.get("endpoint")
179
- # Webframeworks like Starlette mutate the ASGI env once routing is
180
- # done, which is sometime after the request has started. If we have
181
- # an endpoint, overwrite our generic transaction name.
182
- if endpoint:
183
- event["transaction"] = transaction_from_function(endpoint)
184
-
185
- event["request"] = request_info
296
+ event["transaction"] = name
297
+ event["transaction_info"] = {"source": source}
186
298
 
187
299
  return event
188
300
 
189
- # Helper functions for extracting request data.
301
+ # Helper functions.
190
302
  #
191
303
  # Note: Those functions are not public API. If you want to mutate request
192
304
  # data to your liking it's recommended to use the `before_send` callback
193
305
  # for that.
194
306
 
195
- def _get_url(self, scope, default_scheme, host):
196
- # type: (Dict[str, Any], Literal["ws", "http"], Optional[str]) -> str
197
- """
198
- Extract URL from the ASGI scope, without also including the querystring.
199
- """
200
- scheme = scope.get("scheme", default_scheme)
201
-
202
- server = scope.get("server", None)
203
- path = scope.get("root_path", "") + scope.get("path", "")
204
-
205
- if host:
206
- return "%s://%s%s" % (scheme, host, path)
307
+ def _get_transaction_name_and_source(self, transaction_style, asgi_scope):
308
+ # type: (SentryAsgiMiddleware, str, Any) -> Tuple[str, str]
309
+ name = None
310
+ source = SOURCE_FOR_STYLE[transaction_style]
311
+ ty = asgi_scope.get("type")
207
312
 
208
- if server is not None:
209
- host, port = server
210
- default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
211
- if port != default_port:
212
- return "%s://%s:%s%s" % (scheme, host, port, path)
213
- return "%s://%s%s" % (scheme, host, path)
214
- return path
313
+ if transaction_style == "endpoint":
314
+ endpoint = asgi_scope.get("endpoint")
315
+ # Webframeworks like Starlette mutate the ASGI env once routing is
316
+ # done, which is sometime after the request has started. If we have
317
+ # an endpoint, overwrite our generic transaction name.
318
+ if endpoint:
319
+ name = transaction_from_function(endpoint) or ""
320
+ else:
321
+ name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None)
322
+ source = TransactionSource.URL
323
+
324
+ elif transaction_style == "url":
325
+ # FastAPI includes the route object in the scope to let Sentry extract the
326
+ # path from it for the transaction name
327
+ route = asgi_scope.get("route")
328
+ if route:
329
+ path = getattr(route, "path", None)
330
+ if path is not None:
331
+ name = path
332
+ else:
333
+ name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None)
334
+ source = TransactionSource.URL
215
335
 
216
- def _get_query(self, scope):
217
- # type: (Any) -> Any
218
- """
219
- Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
220
- """
221
- qs = scope.get("query_string")
222
- if not qs:
223
- return None
224
- return urllib.parse.unquote(qs.decode("latin-1"))
336
+ if name is None:
337
+ name = _DEFAULT_TRANSACTION_NAME
338
+ source = TransactionSource.ROUTE
339
+ return name, source
225
340
 
226
- def _get_headers(self, scope):
227
- # type: (Any) -> Dict[str, str]
228
- """
229
- Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
230
- """
231
- headers = {} # type: Dict[str, str]
232
- for raw_key, raw_value in scope["headers"]:
233
- key = raw_key.decode("latin-1")
234
- value = raw_value.decode("latin-1")
235
- if key in headers:
236
- headers[key] = headers[key] + ", " + value
237
- else:
238
- headers[key] = value
239
- return headers
341
+ return name, source
@@ -0,0 +1,144 @@
1
+ import sys
2
+ import functools
3
+
4
+ import sentry_sdk
5
+ from sentry_sdk.consts import OP
6
+ from sentry_sdk.integrations import Integration, DidNotEnable
7
+ from sentry_sdk.utils import event_from_exception, logger, reraise
8
+
9
+ try:
10
+ import asyncio
11
+ from asyncio.tasks import Task
12
+ except ImportError:
13
+ raise DidNotEnable("asyncio not available")
14
+
15
+ from typing import cast, TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from typing import Any, Callable, TypeVar
19
+ from collections.abc import Coroutine
20
+
21
+ from sentry_sdk._types import ExcInfo
22
+
23
+ T = TypeVar("T", bound=Callable[..., Any])
24
+
25
+
26
+ def get_name(coro):
27
+ # type: (Any) -> str
28
+ return (
29
+ getattr(coro, "__qualname__", None)
30
+ or getattr(coro, "__name__", None)
31
+ or "coroutine without __name__"
32
+ )
33
+
34
+
35
+ def _wrap_coroutine(wrapped):
36
+ # type: (Coroutine[Any, Any, Any]) -> Callable[[T], T]
37
+ # Only __name__ and __qualname__ are copied from function to coroutine in CPython
38
+ return functools.partial(
39
+ functools.update_wrapper,
40
+ wrapped=wrapped, # type: ignore
41
+ assigned=("__name__", "__qualname__"),
42
+ updated=(),
43
+ )
44
+
45
+
46
+ def patch_asyncio():
47
+ # type: () -> None
48
+ orig_task_factory = None
49
+ try:
50
+ loop = asyncio.get_running_loop()
51
+ orig_task_factory = loop.get_task_factory()
52
+
53
+ def _sentry_task_factory(loop, coro, **kwargs):
54
+ # type: (asyncio.AbstractEventLoop, Coroutine[Any, Any, Any], Any) -> asyncio.Future[Any]
55
+
56
+ @_wrap_coroutine(coro)
57
+ async def _task_with_sentry_span_creation():
58
+ # type: () -> Any
59
+ result = None
60
+
61
+ with sentry_sdk.isolation_scope():
62
+ with sentry_sdk.start_span(
63
+ op=OP.FUNCTION,
64
+ name=get_name(coro),
65
+ origin=AsyncioIntegration.origin,
66
+ ):
67
+ try:
68
+ result = await coro
69
+ except StopAsyncIteration as e:
70
+ raise e from None
71
+ except Exception:
72
+ reraise(*_capture_exception())
73
+
74
+ return result
75
+
76
+ task = None
77
+
78
+ # Trying to use user set task factory (if there is one)
79
+ if orig_task_factory:
80
+ task = orig_task_factory(
81
+ loop, _task_with_sentry_span_creation(), **kwargs
82
+ )
83
+
84
+ if task is None:
85
+ # The default task factory in `asyncio` does not have its own function
86
+ # but is just a couple of lines in `asyncio.base_events.create_task()`
87
+ # Those lines are copied here.
88
+
89
+ # WARNING:
90
+ # If the default behavior of the task creation in asyncio changes,
91
+ # this will break!
92
+ task = Task(_task_with_sentry_span_creation(), loop=loop, **kwargs)
93
+ if task._source_traceback: # type: ignore
94
+ del task._source_traceback[-1] # type: ignore
95
+
96
+ # Set the task name to include the original coroutine's name
97
+ try:
98
+ cast("asyncio.Task[Any]", task).set_name(
99
+ f"{get_name(coro)} (Sentry-wrapped)"
100
+ )
101
+ except AttributeError:
102
+ # set_name might not be available in all Python versions
103
+ pass
104
+
105
+ return task
106
+
107
+ loop.set_task_factory(_sentry_task_factory) # type: ignore
108
+
109
+ except RuntimeError:
110
+ # When there is no running loop, we have nothing to patch.
111
+ logger.warning(
112
+ "There is no running asyncio loop so there is nothing Sentry can patch. "
113
+ "Please make sure you call sentry_sdk.init() within a running "
114
+ "asyncio loop for the AsyncioIntegration to work. "
115
+ "See https://docs.sentry.io/platforms/python/integrations/asyncio/"
116
+ )
117
+
118
+
119
+ def _capture_exception():
120
+ # type: () -> ExcInfo
121
+ exc_info = sys.exc_info()
122
+
123
+ client = sentry_sdk.get_client()
124
+
125
+ integration = client.get_integration(AsyncioIntegration)
126
+ if integration is not None:
127
+ event, hint = event_from_exception(
128
+ exc_info,
129
+ client_options=client.options,
130
+ mechanism={"type": "asyncio", "handled": False},
131
+ )
132
+ sentry_sdk.capture_event(event, hint=hint)
133
+
134
+ return exc_info
135
+
136
+
137
+ class AsyncioIntegration(Integration):
138
+ identifier = "asyncio"
139
+ origin = f"auto.function.{identifier}"
140
+
141
+ @staticmethod
142
+ def setup_once():
143
+ # type: () -> None
144
+ patch_asyncio()