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,20 +1,29 @@
1
1
  import sys
2
-
3
- from sentry_sdk._functools import partial
4
- from sentry_sdk.hub import Hub, _should_send_default_pii
2
+ from functools import partial
3
+
4
+ import sentry_sdk
5
+ from sentry_sdk._werkzeug import get_host, _get_headers
6
+ from sentry_sdk.api import continue_trace
7
+ from sentry_sdk.consts import OP
8
+ from sentry_sdk.scope import should_send_default_pii
9
+ from sentry_sdk.integrations._wsgi_common import (
10
+ DEFAULT_HTTP_METHODS_TO_CAPTURE,
11
+ _filter_headers,
12
+ nullcontext,
13
+ )
14
+ from sentry_sdk.sessions import track_session
15
+ from sentry_sdk.scope import use_isolation_scope
16
+ from sentry_sdk.tracing import Transaction, TransactionSource
5
17
  from sentry_sdk.utils import (
6
18
  ContextVar,
7
19
  capture_internal_exceptions,
8
20
  event_from_exception,
21
+ reraise,
9
22
  )
10
- from sentry_sdk._compat import PY2, reraise, iteritems
11
- from sentry_sdk.tracing import Transaction
12
- from sentry_sdk.sessions import auto_session_tracking
13
- from sentry_sdk.integrations._wsgi_common import _filter_headers
14
23
 
15
- from sentry_sdk._types import MYPY
24
+ from typing import TYPE_CHECKING
16
25
 
17
- if MYPY:
26
+ if TYPE_CHECKING:
18
27
  from typing import Callable
19
28
  from typing import Dict
20
29
  from typing import Iterator
@@ -25,14 +34,14 @@ if MYPY:
25
34
  from typing import Protocol
26
35
 
27
36
  from sentry_sdk.utils import ExcInfo
28
- from sentry_sdk._types import EventProcessor
37
+ from sentry_sdk._types import Event, EventProcessor
29
38
 
30
39
  WsgiResponseIter = TypeVar("WsgiResponseIter")
31
40
  WsgiResponseHeaders = TypeVar("WsgiResponseHeaders")
32
41
  WsgiExcInfo = TypeVar("WsgiExcInfo")
33
42
 
34
43
  class StartResponse(Protocol):
35
- def __call__(self, status, response_headers, exc_info=None):
44
+ def __call__(self, status, response_headers, exc_info=None): # type: ignore
36
45
  # type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter
37
46
  pass
38
47
 
@@ -40,60 +49,46 @@ if MYPY:
40
49
  _wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
41
50
 
42
51
 
43
- if PY2:
44
-
45
- def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
46
- # type: (str, str, str) -> str
47
- return s.decode(charset, errors)
48
-
52
+ def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
53
+ # type: (str, str, str) -> str
54
+ return s.encode("latin1").decode(charset, errors)
49
55
 
50
- else:
51
56
 
52
- def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
53
- # type: (str, str, str) -> str
54
- return s.encode("latin1").decode(charset, errors)
55
-
56
-
57
- def get_host(environ):
58
- # type: (Dict[str, str]) -> str
59
- """Return the host for the given WSGI environment. Yanked from Werkzeug."""
60
- if environ.get("HTTP_HOST"):
61
- rv = environ["HTTP_HOST"]
62
- if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
63
- rv = rv[:-3]
64
- elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
65
- rv = rv[:-4]
66
- elif environ.get("SERVER_NAME"):
67
- rv = environ["SERVER_NAME"]
68
- if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
69
- ("https", "443"),
70
- ("http", "80"),
71
- ):
72
- rv += ":" + environ["SERVER_PORT"]
73
- else:
74
- # In spite of the WSGI spec, SERVER_NAME might not be present.
75
- rv = "unknown"
76
-
77
- return rv
78
-
79
-
80
- def get_request_url(environ):
81
- # type: (Dict[str, str]) -> str
57
+ def get_request_url(environ, use_x_forwarded_for=False):
58
+ # type: (Dict[str, str], bool) -> str
82
59
  """Return the absolute URL without query string for the given WSGI
83
60
  environment."""
61
+ script_name = environ.get("SCRIPT_NAME", "").rstrip("/")
62
+ path_info = environ.get("PATH_INFO", "").lstrip("/")
63
+ path = f"{script_name}/{path_info}"
64
+
84
65
  return "%s://%s/%s" % (
85
66
  environ.get("wsgi.url_scheme"),
86
- get_host(environ),
87
- wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"),
67
+ get_host(environ, use_x_forwarded_for),
68
+ wsgi_decoding_dance(path).lstrip("/"),
88
69
  )
89
70
 
90
71
 
91
- class SentryWsgiMiddleware(object):
92
- __slots__ = ("app",)
72
+ class SentryWsgiMiddleware:
73
+ __slots__ = (
74
+ "app",
75
+ "use_x_forwarded_for",
76
+ "span_origin",
77
+ "http_methods_to_capture",
78
+ )
93
79
 
94
- def __init__(self, app):
95
- # type: (Callable[[Dict[str, str], Callable[..., Any]], Any]) -> None
80
+ def __init__(
81
+ self,
82
+ app, # type: Callable[[Dict[str, str], Callable[..., Any]], Any]
83
+ use_x_forwarded_for=False, # type: bool
84
+ span_origin="manual", # type: str
85
+ http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
86
+ ):
87
+ # type: (...) -> None
96
88
  self.app = app
89
+ self.use_x_forwarded_for = use_x_forwarded_for
90
+ self.span_origin = span_origin
91
+ self.http_methods_to_capture = http_methods_to_capture
97
92
 
98
93
  def __call__(self, environ, start_response):
99
94
  # type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
@@ -102,40 +97,55 @@ class SentryWsgiMiddleware(object):
102
97
 
103
98
  _wsgi_middleware_applied.set(True)
104
99
  try:
105
- hub = Hub(Hub.current)
106
- with auto_session_tracking(hub):
107
- with hub:
100
+ with sentry_sdk.isolation_scope() as scope:
101
+ with track_session(scope, session_mode="request"):
108
102
  with capture_internal_exceptions():
109
- with hub.configure_scope() as scope:
110
- scope.clear_breadcrumbs()
111
- scope._name = "wsgi"
112
- scope.add_event_processor(
113
- _make_wsgi_event_processor(environ)
103
+ scope.clear_breadcrumbs()
104
+ scope._name = "wsgi"
105
+ scope.add_event_processor(
106
+ _make_wsgi_event_processor(
107
+ environ, self.use_x_forwarded_for
114
108
  )
115
-
116
- transaction = Transaction.continue_from_environ(
117
- environ, op="http.server", name="generic WSGI request"
109
+ )
110
+
111
+ method = environ.get("REQUEST_METHOD", "").upper()
112
+ transaction = None
113
+ if method in self.http_methods_to_capture:
114
+ transaction = continue_trace(
115
+ environ,
116
+ op=OP.HTTP_SERVER,
117
+ name="generic WSGI request",
118
+ source=TransactionSource.ROUTE,
119
+ origin=self.span_origin,
120
+ )
121
+
122
+ transaction_context = (
123
+ sentry_sdk.start_transaction(
124
+ transaction,
125
+ custom_sampling_context={"wsgi_environ": environ},
126
+ )
127
+ if transaction is not None
128
+ else nullcontext()
118
129
  )
119
-
120
- with hub.start_transaction(transaction):
130
+ with transaction_context:
121
131
  try:
122
- rv = self.app(
132
+ response = self.app(
123
133
  environ,
124
134
  partial(
125
135
  _sentry_start_response, start_response, transaction
126
136
  ),
127
137
  )
128
138
  except BaseException:
129
- reraise(*_capture_exception(hub))
139
+ reraise(*_capture_exception())
130
140
  finally:
131
141
  _wsgi_middleware_applied.set(False)
132
142
 
133
- return _ScopedResponse(hub, rv)
143
+ return _ScopedResponse(scope, response)
134
144
 
135
145
 
136
- def _sentry_start_response(
146
+ def _sentry_start_response( # type: ignore
137
147
  old_start_response, # type: StartResponse
138
- transaction, # type: Transaction
148
+ transaction, # type: Optional[Transaction]
139
149
  status, # type: str
140
150
  response_headers, # type: WsgiResponseHeaders
141
151
  exc_info=None, # type: Optional[WsgiExcInfo]
@@ -143,7 +153,8 @@ def _sentry_start_response(
143
153
  # type: (...) -> WsgiResponseIter
144
154
  with capture_internal_exceptions():
145
155
  status_int = int(status.split(" ", 1)[0])
146
- transaction.set_http_status(status_int)
156
+ if transaction is not None:
157
+ transaction.set_http_status(status_int)
147
158
 
148
159
  if exc_info is None:
149
160
  # The Django Rest Framework WSGI test client, and likely other
@@ -161,7 +172,7 @@ def _get_environ(environ):
161
172
  capture (server name, port and remote addr if pii is enabled).
162
173
  """
163
174
  keys = ["SERVER_NAME", "SERVER_PORT"]
164
- if _should_send_default_pii():
175
+ if should_send_default_pii():
165
176
  # make debugging of proxy setup easier. Proxy headers are
166
177
  # in headers.
167
178
  keys += ["REMOTE_ADDR"]
@@ -171,27 +182,6 @@ def _get_environ(environ):
171
182
  yield key, environ[key]
172
183
 
173
184
 
174
- # `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
175
- #
176
- # We need this function because Django does not give us a "pure" http header
177
- # dict. So we might as well use it for all WSGI integrations.
178
- def _get_headers(environ):
179
- # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
180
- """
181
- Returns only proper HTTP headers.
182
-
183
- """
184
- for key, value in iteritems(environ):
185
- key = str(key)
186
- if key.startswith("HTTP_") and key not in (
187
- "HTTP_CONTENT_TYPE",
188
- "HTTP_CONTENT_LENGTH",
189
- ):
190
- yield key[5:].replace("_", "-").title(), value
191
- elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
192
- yield key.replace("_", "-").title(), value
193
-
194
-
195
185
  def get_client_ip(environ):
196
186
  # type: (Dict[str, str]) -> Optional[Any]
197
187
  """
@@ -212,33 +202,44 @@ def get_client_ip(environ):
212
202
  return environ.get("REMOTE_ADDR")
213
203
 
214
204
 
215
- def _capture_exception(hub):
216
- # type: (Hub) -> ExcInfo
205
+ def _capture_exception():
206
+ # type: () -> ExcInfo
207
+ """
208
+ Captures the current exception and sends it to Sentry.
209
+ Returns the ExcInfo tuple to it can be reraised afterwards.
210
+ """
217
211
  exc_info = sys.exc_info()
212
+ e = exc_info[1]
213
+
214
+ # SystemExit(0) is the only uncaught exception that is expected behavior
215
+ should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
216
+ if not should_skip_capture:
217
+ event, hint = event_from_exception(
218
+ exc_info,
219
+ client_options=sentry_sdk.get_client().options,
220
+ mechanism={"type": "wsgi", "handled": False},
221
+ )
222
+ sentry_sdk.capture_event(event, hint=hint)
218
223
 
219
- # Check client here as it might have been unset while streaming response
220
- if hub.client is not None:
221
- e = exc_info[1]
224
+ return exc_info
222
225
 
223
- # SystemExit(0) is the only uncaught exception that is expected behavior
224
- should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
225
- if not should_skip_capture:
226
- event, hint = event_from_exception(
227
- exc_info,
228
- client_options=hub.client.options,
229
- mechanism={"type": "wsgi", "handled": False},
230
- )
231
- hub.capture_event(event, hint=hint)
232
226
 
233
- return exc_info
227
+ class _ScopedResponse:
228
+ """
229
+ Users a separate scope for each response chunk.
234
230
 
231
+ This will make WSGI apps more tolerant against:
232
+ - WSGI servers streaming responses from a different thread/from
233
+ different threads than the one that called start_response
234
+ - close() not being called
235
+ - WSGI servers streaming responses interleaved from the same thread
236
+ """
235
237
 
236
- class _ScopedResponse(object):
237
- __slots__ = ("_response", "_hub")
238
+ __slots__ = ("_response", "_scope")
238
239
 
239
- def __init__(self, hub, response):
240
- # type: (Hub, Iterator[bytes]) -> None
241
- self._hub = hub
240
+ def __init__(self, scope, response):
241
+ # type: (sentry_sdk.scope.Scope, Iterator[bytes]) -> None
242
+ self._scope = scope
242
243
  self._response = response
243
244
 
244
245
  def __iter__(self):
@@ -246,33 +247,33 @@ class _ScopedResponse(object):
246
247
  iterator = iter(self._response)
247
248
 
248
249
  while True:
249
- with self._hub:
250
+ with use_isolation_scope(self._scope):
250
251
  try:
251
252
  chunk = next(iterator)
252
253
  except StopIteration:
253
254
  break
254
255
  except BaseException:
255
- reraise(*_capture_exception(self._hub))
256
+ reraise(*_capture_exception())
256
257
 
257
258
  yield chunk
258
259
 
259
260
  def close(self):
260
261
  # type: () -> None
261
- with self._hub:
262
+ with use_isolation_scope(self._scope):
262
263
  try:
263
264
  self._response.close() # type: ignore
264
265
  except AttributeError:
265
266
  pass
266
267
  except BaseException:
267
- reraise(*_capture_exception(self._hub))
268
+ reraise(*_capture_exception())
268
269
 
269
270
 
270
- def _make_wsgi_event_processor(environ):
271
- # type: (Dict[str, str]) -> EventProcessor
271
+ def _make_wsgi_event_processor(environ, use_x_forwarded_for):
272
+ # type: (Dict[str, str], bool) -> EventProcessor
272
273
  # It's a bit unfortunate that we have to extract and parse the request data
273
274
  # from the environ so eagerly, but there are a few good reasons for this.
274
275
  #
275
- # We might be in a situation where the scope/hub never gets torn down
276
+ # We might be in a situation where the scope never gets torn down
276
277
  # properly. In that case we will have an unnecessary strong reference to
277
278
  # all objects in the environ (some of which may take a lot of memory) when
278
279
  # we're really just interested in a few of them.
@@ -282,19 +283,19 @@ def _make_wsgi_event_processor(environ):
282
283
  # https://github.com/unbit/uwsgi/issues/1950
283
284
 
284
285
  client_ip = get_client_ip(environ)
285
- request_url = get_request_url(environ)
286
+ request_url = get_request_url(environ, use_x_forwarded_for)
286
287
  query_string = environ.get("QUERY_STRING")
287
288
  method = environ.get("REQUEST_METHOD")
288
289
  env = dict(_get_environ(environ))
289
290
  headers = _filter_headers(dict(_get_headers(environ)))
290
291
 
291
292
  def event_processor(event, hint):
292
- # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
293
+ # type: (Event, Dict[str, Any]) -> Event
293
294
  with capture_internal_exceptions():
294
295
  # if the code below fails halfway through we at least have some data
295
296
  request_info = event.setdefault("request", {})
296
297
 
297
- if _should_send_default_pii():
298
+ if should_send_default_pii():
298
299
  user_info = event.setdefault("user", {})
299
300
  if client_ip:
300
301
  user_info.setdefault("ip_address", client_ip)
sentry_sdk/logger.py ADDED
@@ -0,0 +1,96 @@
1
+ # NOTE: this is the logger sentry exposes to users, not some generic logger.
2
+ import functools
3
+ import time
4
+ from typing import Any
5
+
6
+ from sentry_sdk import get_client
7
+ from sentry_sdk.utils import safe_repr, capture_internal_exceptions
8
+
9
+ OTEL_RANGES = [
10
+ # ((severity level range), severity text)
11
+ # https://opentelemetry.io/docs/specs/otel/logs/data-model
12
+ ((1, 4), "trace"),
13
+ ((5, 8), "debug"),
14
+ ((9, 12), "info"),
15
+ ((13, 16), "warn"),
16
+ ((17, 20), "error"),
17
+ ((21, 24), "fatal"),
18
+ ]
19
+
20
+
21
+ class _dict_default_key(dict): # type: ignore[type-arg]
22
+ """dict that returns the key if missing."""
23
+
24
+ def __missing__(self, key):
25
+ # type: (str) -> str
26
+ return "{" + key + "}"
27
+
28
+
29
+ def _capture_log(severity_text, severity_number, template, **kwargs):
30
+ # type: (str, int, str, **Any) -> None
31
+ client = get_client()
32
+
33
+ body = template
34
+ attrs = {} # type: dict[str, str | bool | float | int]
35
+ if "attributes" in kwargs:
36
+ attrs.update(kwargs.pop("attributes"))
37
+ for k, v in kwargs.items():
38
+ attrs[f"sentry.message.parameter.{k}"] = v
39
+ if kwargs:
40
+ # only attach template if there are parameters
41
+ attrs["sentry.message.template"] = template
42
+
43
+ with capture_internal_exceptions():
44
+ body = template.format_map(_dict_default_key(kwargs))
45
+
46
+ attrs = {
47
+ k: (
48
+ v
49
+ if (
50
+ isinstance(v, str)
51
+ or isinstance(v, int)
52
+ or isinstance(v, bool)
53
+ or isinstance(v, float)
54
+ )
55
+ else safe_repr(v)
56
+ )
57
+ for (k, v) in attrs.items()
58
+ }
59
+
60
+ # noinspection PyProtectedMember
61
+ client._capture_log(
62
+ {
63
+ "severity_text": severity_text,
64
+ "severity_number": severity_number,
65
+ "attributes": attrs,
66
+ "body": body,
67
+ "time_unix_nano": time.time_ns(),
68
+ "trace_id": None,
69
+ },
70
+ )
71
+
72
+
73
+ trace = functools.partial(_capture_log, "trace", 1)
74
+ debug = functools.partial(_capture_log, "debug", 5)
75
+ info = functools.partial(_capture_log, "info", 9)
76
+ warning = functools.partial(_capture_log, "warn", 13)
77
+ error = functools.partial(_capture_log, "error", 17)
78
+ fatal = functools.partial(_capture_log, "fatal", 21)
79
+
80
+
81
+ def _otel_severity_text(otel_severity_number):
82
+ # type: (int) -> str
83
+ for (lower, upper), severity in OTEL_RANGES:
84
+ if lower <= otel_severity_number <= upper:
85
+ return severity
86
+
87
+ return "default"
88
+
89
+
90
+ def _log_level_to_otel(level, mapping):
91
+ # type: (int, dict[Any, int]) -> tuple[int, str]
92
+ for py_level, otel_severity_number in sorted(mapping.items(), reverse=True):
93
+ if level >= py_level:
94
+ return otel_severity_number, _otel_severity_text(otel_severity_number)
95
+
96
+ return 0, "default"
sentry_sdk/metrics.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ NOTE: This file contains experimental code that may be changed or removed at any
3
+ time without prior notice.
4
+ """
5
+
6
+ import time
7
+ from typing import Any, Optional, TYPE_CHECKING, Union
8
+
9
+ import sentry_sdk
10
+ from sentry_sdk.utils import safe_repr
11
+
12
+ if TYPE_CHECKING:
13
+ from sentry_sdk._types import Metric, MetricType
14
+
15
+
16
+ def _capture_metric(
17
+ name, # type: str
18
+ metric_type, # type: MetricType
19
+ value, # type: float
20
+ unit=None, # type: Optional[str]
21
+ attributes=None, # type: Optional[dict[str, Any]]
22
+ ):
23
+ # type: (...) -> None
24
+ client = sentry_sdk.get_client()
25
+
26
+ attrs = {} # type: dict[str, Union[str, bool, float, int]]
27
+ if attributes:
28
+ for k, v in attributes.items():
29
+ attrs[k] = (
30
+ v
31
+ if (
32
+ isinstance(v, str)
33
+ or isinstance(v, int)
34
+ or isinstance(v, bool)
35
+ or isinstance(v, float)
36
+ )
37
+ else safe_repr(v)
38
+ )
39
+
40
+ metric = {
41
+ "timestamp": time.time(),
42
+ "trace_id": None,
43
+ "span_id": None,
44
+ "name": name,
45
+ "type": metric_type,
46
+ "value": float(value),
47
+ "unit": unit,
48
+ "attributes": attrs,
49
+ } # type: Metric
50
+
51
+ client._capture_metric(metric)
52
+
53
+
54
+ def count(
55
+ name, # type: str
56
+ value, # type: float
57
+ unit=None, # type: Optional[str]
58
+ attributes=None, # type: Optional[dict[str, Any]]
59
+ ):
60
+ # type: (...) -> None
61
+ _capture_metric(name, "counter", value, unit, attributes)
62
+
63
+
64
+ def gauge(
65
+ name, # type: str
66
+ value, # type: float
67
+ unit=None, # type: Optional[str]
68
+ attributes=None, # type: Optional[dict[str, Any]]
69
+ ):
70
+ # type: (...) -> None
71
+ _capture_metric(name, "gauge", value, unit, attributes)
72
+
73
+
74
+ def distribution(
75
+ name, # type: str
76
+ value, # type: float
77
+ unit=None, # type: Optional[str]
78
+ attributes=None, # type: Optional[dict[str, Any]]
79
+ ):
80
+ # type: (...) -> None
81
+ _capture_metric(name, "distribution", value, unit, attributes)
sentry_sdk/monitor.py ADDED
@@ -0,0 +1,120 @@
1
+ import os
2
+ import time
3
+ from threading import Thread, Lock
4
+
5
+ import sentry_sdk
6
+ from sentry_sdk.utils import logger
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Optional
12
+
13
+
14
+ MAX_DOWNSAMPLE_FACTOR = 10
15
+
16
+
17
+ class Monitor:
18
+ """
19
+ Performs health checks in a separate thread once every interval seconds
20
+ and updates the internal state. Other parts of the SDK only read this state
21
+ and act accordingly.
22
+ """
23
+
24
+ name = "sentry.monitor"
25
+
26
+ def __init__(self, transport, interval=10):
27
+ # type: (sentry_sdk.transport.Transport, float) -> None
28
+ self.transport = transport # type: sentry_sdk.transport.Transport
29
+ self.interval = interval # type: float
30
+
31
+ self._healthy = True
32
+ self._downsample_factor = 0 # type: int
33
+
34
+ self._thread = None # type: Optional[Thread]
35
+ self._thread_lock = Lock()
36
+ self._thread_for_pid = None # type: Optional[int]
37
+ self._running = True
38
+
39
+ def _ensure_running(self):
40
+ # type: () -> None
41
+ """
42
+ Check that the monitor has an active thread to run in, or create one if not.
43
+
44
+ Note that this might fail (e.g. in Python 3.12 it's not possible to
45
+ spawn new threads at interpreter shutdown). In that case self._running
46
+ will be False after running this function.
47
+ """
48
+ if self._thread_for_pid == os.getpid() and self._thread is not None:
49
+ return None
50
+
51
+ with self._thread_lock:
52
+ if self._thread_for_pid == os.getpid() and self._thread is not None:
53
+ return None
54
+
55
+ def _thread():
56
+ # type: (...) -> None
57
+ while self._running:
58
+ time.sleep(self.interval)
59
+ if self._running:
60
+ self.run()
61
+
62
+ thread = Thread(name=self.name, target=_thread)
63
+ thread.daemon = True
64
+ try:
65
+ thread.start()
66
+ except RuntimeError:
67
+ # Unfortunately at this point the interpreter is in a state that no
68
+ # longer allows us to spawn a thread and we have to bail.
69
+ self._running = False
70
+ return None
71
+
72
+ self._thread = thread
73
+ self._thread_for_pid = os.getpid()
74
+
75
+ return None
76
+
77
+ def run(self):
78
+ # type: () -> None
79
+ self.check_health()
80
+ self.set_downsample_factor()
81
+
82
+ def set_downsample_factor(self):
83
+ # type: () -> None
84
+ if self._healthy:
85
+ if self._downsample_factor > 0:
86
+ logger.debug(
87
+ "[Monitor] health check positive, reverting to normal sampling"
88
+ )
89
+ self._downsample_factor = 0
90
+ else:
91
+ if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR:
92
+ self._downsample_factor += 1
93
+ logger.debug(
94
+ "[Monitor] health check negative, downsampling with a factor of %d",
95
+ self._downsample_factor,
96
+ )
97
+
98
+ def check_health(self):
99
+ # type: () -> None
100
+ """
101
+ Perform the actual health checks,
102
+ currently only checks if the transport is rate-limited.
103
+ TODO: augment in the future with more checks.
104
+ """
105
+ self._healthy = self.transport.is_healthy()
106
+
107
+ def is_healthy(self):
108
+ # type: () -> bool
109
+ self._ensure_running()
110
+ return self._healthy
111
+
112
+ @property
113
+ def downsample_factor(self):
114
+ # type: () -> int
115
+ self._ensure_running()
116
+ return self._downsample_factor
117
+
118
+ def kill(self):
119
+ # type: () -> None
120
+ self._running = False