sentry-sdk 3.0.0a5__py2.py3-none-any.whl → 3.0.0a7__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.

Potentially problematic release.


This version of sentry-sdk might be problematic. Click here for more details.

Files changed (33) hide show
  1. sentry_sdk/_init_implementation.py +5 -0
  2. sentry_sdk/ai/utils.py +7 -8
  3. sentry_sdk/api.py +13 -2
  4. sentry_sdk/client.py +93 -17
  5. sentry_sdk/consts.py +17 -7
  6. sentry_sdk/crons/api.py +5 -0
  7. sentry_sdk/integrations/anthropic.py +133 -73
  8. sentry_sdk/integrations/asgi.py +10 -9
  9. sentry_sdk/integrations/asyncio.py +85 -20
  10. sentry_sdk/integrations/clickhouse_driver.py +55 -28
  11. sentry_sdk/integrations/fastapi.py +1 -7
  12. sentry_sdk/integrations/gnu_backtrace.py +6 -3
  13. sentry_sdk/integrations/langchain.py +462 -218
  14. sentry_sdk/integrations/litestar.py +1 -1
  15. sentry_sdk/integrations/openai_agents/patches/agent_run.py +0 -2
  16. sentry_sdk/integrations/openai_agents/patches/runner.py +18 -15
  17. sentry_sdk/integrations/quart.py +1 -1
  18. sentry_sdk/integrations/starlette.py +1 -5
  19. sentry_sdk/integrations/starlite.py +2 -2
  20. sentry_sdk/integrations/threading.py +1 -1
  21. sentry_sdk/scope.py +11 -11
  22. sentry_sdk/spotlight.py +1 -162
  23. sentry_sdk/tracing.py +94 -17
  24. sentry_sdk/tracing_utils.py +330 -33
  25. sentry_sdk/transport.py +363 -63
  26. sentry_sdk/utils.py +23 -5
  27. sentry_sdk/worker.py +197 -3
  28. {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a7.dist-info}/METADATA +3 -1
  29. {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a7.dist-info}/RECORD +33 -33
  30. {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a7.dist-info}/WHEEL +0 -0
  31. {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a7.dist-info}/entry_points.txt +0 -0
  32. {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a7.dist-info}/licenses/LICENSE +0 -0
  33. {sentry_sdk-3.0.0a5.dist-info → sentry_sdk-3.0.0a7.dist-info}/top_level.txt +0 -0
@@ -105,6 +105,7 @@ class SentryAsgiMiddleware:
105
105
  mechanism_type: str = "asgi",
106
106
  span_origin: Optional[str] = None,
107
107
  http_methods_to_capture: Tuple[str, ...] = DEFAULT_HTTP_METHODS_TO_CAPTURE,
108
+ asgi_version: Optional[int] = None,
108
109
  ) -> None:
109
110
  """
110
111
  Instrument an ASGI application with Sentry. Provides HTTP/websocket
@@ -142,10 +143,16 @@ class SentryAsgiMiddleware:
142
143
  self.app = app
143
144
  self.http_methods_to_capture = http_methods_to_capture
144
145
 
145
- if _looks_like_asgi3(app):
146
+ if asgi_version is None:
147
+ if _looks_like_asgi3(app):
148
+ asgi_version = 3
149
+ else:
150
+ asgi_version = 2
151
+
152
+ if asgi_version == 3:
146
153
  self.__call__: Callable[..., Any] = self._run_asgi3
147
- else:
148
- self.__call__ = self._run_asgi2
154
+ elif asgi_version == 2:
155
+ self.__call__: Callable[..., Any] = self._run_asgi2 # type: ignore
149
156
 
150
157
  def _capture_lifespan_exception(self, exc: Exception) -> None:
151
158
  """Capture exceptions raise in application lifespan handlers.
@@ -299,12 +306,6 @@ class SentryAsgiMiddleware:
299
306
  event["transaction"] = name
300
307
  event["transaction_info"] = {"source": source}
301
308
 
302
- logger.debug(
303
- "[ASGI] Set transaction name and source in event_processor: '%s' / '%s'",
304
- event["transaction"],
305
- event["transaction_info"]["source"],
306
- )
307
-
308
309
  return event
309
310
 
310
311
  # Helper functions.
@@ -4,7 +4,13 @@ import sys
4
4
  import sentry_sdk
5
5
  from sentry_sdk.consts import OP
6
6
  from sentry_sdk.integrations import Integration, DidNotEnable
7
- from sentry_sdk.utils import event_from_exception, logger, reraise
7
+ from sentry_sdk.utils import (
8
+ event_from_exception,
9
+ logger,
10
+ reraise,
11
+ is_internal_task,
12
+ )
13
+ from sentry_sdk.transport import AsyncHttpTransport
8
14
 
9
15
  try:
10
16
  import asyncio
@@ -29,6 +35,72 @@ def get_name(coro: Any) -> str:
29
35
  )
30
36
 
31
37
 
38
+ def patch_loop_close() -> None:
39
+ """Patch loop.close to flush pending events before shutdown."""
40
+ # Atexit shutdown hook happens after the event loop is closed.
41
+ # Therefore, it is necessary to patch the loop.close method to ensure
42
+ # that pending events are flushed before the interpreter shuts down.
43
+ try:
44
+ loop = asyncio.get_running_loop()
45
+ except RuntimeError:
46
+ # No running loop → cannot patch now
47
+ return
48
+
49
+ if getattr(loop, "_sentry_flush_patched", False):
50
+ return
51
+
52
+ async def _flush() -> None:
53
+ client = sentry_sdk.get_client()
54
+ if not client:
55
+ return
56
+
57
+ try:
58
+ if not isinstance(client.transport, AsyncHttpTransport):
59
+ return
60
+
61
+ await client.close_async()
62
+ except Exception:
63
+ logger.warning("Sentry flush failed during loop shutdown", exc_info=True)
64
+
65
+ orig_close = loop.close
66
+
67
+ def _patched_close() -> None:
68
+ try:
69
+ loop.run_until_complete(_flush())
70
+ finally:
71
+ orig_close()
72
+
73
+ loop.close = _patched_close # type: ignore
74
+ loop._sentry_flush_patched = True # type: ignore
75
+
76
+
77
+ def _create_task_with_factory(
78
+ orig_task_factory: Any,
79
+ loop: asyncio.AbstractEventLoop,
80
+ coro: Coroutine[Any, Any, Any],
81
+ **kwargs: Any,
82
+ ) -> asyncio.Task[Any]:
83
+ task = None
84
+
85
+ # Trying to use user set task factory (if there is one)
86
+ if orig_task_factory:
87
+ task = orig_task_factory(loop, coro, **kwargs)
88
+
89
+ if task is None:
90
+ # The default task factory in `asyncio` does not have its own function
91
+ # but is just a couple of lines in `asyncio.base_events.create_task()`
92
+ # Those lines are copied here.
93
+
94
+ # WARNING:
95
+ # If the default behavior of the task creation in asyncio changes,
96
+ # this will break!
97
+ task = Task(coro, loop=loop, **kwargs)
98
+ if task._source_traceback: # type: ignore
99
+ del task._source_traceback[-1] # type: ignore
100
+
101
+ return task
102
+
103
+
32
104
  def patch_asyncio() -> None:
33
105
  orig_task_factory = None
34
106
  try:
@@ -41,6 +113,14 @@ def patch_asyncio() -> None:
41
113
  **kwargs: Any,
42
114
  ) -> asyncio.Future[Any]:
43
115
 
116
+ # Check if this is an internal Sentry task
117
+ is_internal = is_internal_task()
118
+
119
+ if is_internal:
120
+ return _create_task_with_factory(
121
+ orig_task_factory, loop, coro, **kwargs
122
+ )
123
+
44
124
  async def _task_with_sentry_span_creation() -> Any:
45
125
  result = None
46
126
 
@@ -58,25 +138,9 @@ def patch_asyncio() -> None:
58
138
 
59
139
  return result
60
140
 
61
- task = None
62
-
63
- # Trying to use user set task factory (if there is one)
64
- if orig_task_factory:
65
- task = orig_task_factory(
66
- loop, _task_with_sentry_span_creation(), **kwargs
67
- )
68
-
69
- if task is None:
70
- # The default task factory in `asyncio` does not have its own function
71
- # but is just a couple of lines in `asyncio.base_events.create_task()`
72
- # Those lines are copied here.
73
-
74
- # WARNING:
75
- # If the default behavior of the task creation in asyncio changes,
76
- # this will break!
77
- task = Task(_task_with_sentry_span_creation(), loop=loop, **kwargs)
78
- if task._source_traceback: # type: ignore
79
- del task._source_traceback[-1] # type: ignore
141
+ task = _create_task_with_factory(
142
+ orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs
143
+ )
80
144
 
81
145
  # Set the task name to include the original coroutine's name
82
146
  try:
@@ -124,3 +188,4 @@ class AsyncioIntegration(Integration):
124
188
  @staticmethod
125
189
  def setup_once() -> None:
126
190
  patch_asyncio()
191
+ patch_loop_close()
@@ -13,7 +13,8 @@ from sentry_sdk.utils import (
13
13
  from typing import TYPE_CHECKING
14
14
 
15
15
  if TYPE_CHECKING:
16
- from typing import ParamSpec, Callable, Any, Dict, TypeVar
16
+ from collections.abc import Iterator
17
+ from typing import Any, ParamSpec, Callable, TypeVar
17
18
 
18
19
  P = ParamSpec("P")
19
20
  T = TypeVar("T")
@@ -40,9 +41,7 @@ class ClickhouseDriverIntegration(Integration):
40
41
  )
41
42
 
42
43
  # If the query contains parameters then the send_data function is used to send those parameters to clickhouse
43
- clickhouse_driver.client.Client.send_data = _wrap_send_data(
44
- clickhouse_driver.client.Client.send_data
45
- )
44
+ _wrap_send_data()
46
45
 
47
46
  # Every query ends either with the Client's `receive_end_of_query` (no result expected)
48
47
  # or its `receive_result` (result expected)
@@ -134,36 +133,64 @@ def _wrap_end(f: Callable[P, T]) -> Callable[P, T]:
134
133
  return _inner_end
135
134
 
136
135
 
137
- def _wrap_send_data(f: Callable[P, T]) -> Callable[P, T]:
138
- def _inner_send_data(*args: P.args, **kwargs: P.kwargs) -> T:
139
- client = args[0]
140
- if not isinstance(client, clickhouse_driver.client.Client):
141
- return f(*args, **kwargs)
136
+ def _wrap_send_data() -> None:
137
+ original_send_data = clickhouse_driver.client.Client.send_data
138
+
139
+ def _inner_send_data(
140
+ self: clickhouse_driver.client.Client,
141
+ sample_block: Any,
142
+ data: Any,
143
+ types_check: bool = False,
144
+ columnar: bool = False,
145
+ *args: Any,
146
+ **kwargs: Any,
147
+ ) -> Any:
148
+ span = getattr(self.connection, "_sentry_span", None)
149
+ if span is None:
150
+ return original_send_data(
151
+ self, sample_block, data, types_check, columnar, *args, **kwargs
152
+ )
142
153
 
143
- connection = client.connection
144
- span = getattr(connection, "_sentry_span", None)
154
+ db_data = _get_db_data(self.connection)
155
+ _set_on_span(span, db_data)
145
156
 
146
- if span is not None:
147
- data = _get_db_data(connection)
148
- _set_on_span(span, data)
157
+ saved_db_data: dict[str, Any] = getattr(self.connection, "_sentry_db_data", {})
158
+ db_params: list[Any] = saved_db_data.get("db.params") or []
159
+
160
+ if should_send_default_pii():
161
+ if isinstance(data, (list, tuple)):
162
+ db_params.extend(data)
163
+
164
+ else: # data is a generic iterator
165
+ orig_data = data
166
+
167
+ # Wrap the generator to add items to db.params as they are yielded.
168
+ # This allows us to send the params to Sentry without needing to allocate
169
+ # memory for the entire generator at once.
170
+ def wrapped_generator() -> "Iterator[Any]":
171
+ for item in orig_data:
172
+ db_params.append(item)
173
+ yield item
174
+
175
+ # Replace the original iterator with the wrapped one.
176
+ data = wrapped_generator()
177
+
178
+ rv = original_send_data(
179
+ self, sample_block, data, types_check, columnar, *args, **kwargs
180
+ )
149
181
 
150
- if should_send_default_pii():
151
- saved_db_data: dict[str, Any] = getattr(
152
- connection, "_sentry_db_data", {}
153
- )
154
- db_params: list[Any] = saved_db_data.get("db.params") or []
155
- db_params_data = args[2]
156
- if isinstance(db_params_data, list):
157
- db_params.extend(db_params_data)
158
- saved_db_data["db.params"] = db_params
159
- span.set_attribute("db.params", _serialize_span_attribute(db_params))
182
+ if should_send_default_pii() and db_params:
183
+ # need to do this after the original function call to make sure
184
+ # db_params is populated correctly
185
+ saved_db_data["db.params"] = db_params
186
+ span.set_attribute("db.params", _serialize_span_attribute(db_params))
160
187
 
161
- return f(*args, **kwargs)
188
+ return rv
162
189
 
163
- return _inner_send_data
190
+ clickhouse_driver.client.Client.send_data = _inner_send_data
164
191
 
165
192
 
166
- def _get_db_data(connection: clickhouse_driver.connection.Connection) -> Dict[str, str]:
193
+ def _get_db_data(connection: clickhouse_driver.connection.Connection) -> dict[str, str]:
167
194
  return {
168
195
  SPANDATA.DB_SYSTEM: "clickhouse",
169
196
  SPANDATA.SERVER_ADDRESS: connection.host,
@@ -173,6 +200,6 @@ def _get_db_data(connection: clickhouse_driver.connection.Connection) -> Dict[st
173
200
  }
174
201
 
175
202
 
176
- def _set_on_span(span: Span, data: Dict[str, Any]) -> None:
203
+ def _set_on_span(span: Span, data: dict[str, Any]) -> None:
177
204
  for key, value in data.items():
178
205
  span.set_attribute(key, _serialize_span_attribute(value))
@@ -7,10 +7,7 @@ import sentry_sdk
7
7
  from sentry_sdk.consts import SOURCE_FOR_STYLE, TransactionSource
8
8
  from sentry_sdk.integrations import DidNotEnable
9
9
  from sentry_sdk.scope import should_send_default_pii
10
- from sentry_sdk.utils import (
11
- transaction_from_function,
12
- logger,
13
- )
10
+ from sentry_sdk.utils import transaction_from_function
14
11
 
15
12
  from typing import TYPE_CHECKING
16
13
 
@@ -67,9 +64,6 @@ def _set_transaction_name_and_source(
67
64
  source = SOURCE_FOR_STYLE[transaction_style]
68
65
 
69
66
  scope.set_transaction_name(name, source=source)
70
- logger.debug(
71
- "[FastAPI] Set transaction name and source on scope: %s / %s", name, source
72
- )
73
67
 
74
68
 
75
69
  def patch_get_request_handler() -> None:
@@ -12,13 +12,16 @@ if TYPE_CHECKING:
12
12
  from typing import Any
13
13
  from sentry_sdk._types import Event
14
14
 
15
-
16
- FUNCTION_RE = r"[^@]+?)\s+@\s+0x[0-9a-fA-F]+"
15
+ # function is everything between index at @
16
+ # and then we match on the @ plus the hex val
17
+ FUNCTION_RE = r"[^@]+?"
18
+ HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+"
17
19
 
18
20
  FRAME_RE = r"""
19
- ^(?P<index>\d+)\.\s+(?P<function>{FUNCTION_RE}\s+in\s+(?P<package>.+)$
21
+ ^(?P<index>\d+)\.\s+(?P<function>{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P<package>.+))?$
20
22
  """.format(
21
23
  FUNCTION_RE=FUNCTION_RE,
24
+ HEX_ADDRESS=HEX_ADDRESS,
22
25
  )
23
26
 
24
27
  FRAME_RE = re.compile(FRAME_RE, re.MULTILINE | re.VERBOSE)