sentry-sdk 2.40.0__py2.py3-none-any.whl → 2.42.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.

Potentially problematic release.


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

Files changed (39) hide show
  1. sentry_sdk/_metrics.py +81 -0
  2. sentry_sdk/_metrics_batcher.py +156 -0
  3. sentry_sdk/_types.py +27 -22
  4. sentry_sdk/ai/__init__.py +7 -0
  5. sentry_sdk/ai/utils.py +48 -0
  6. sentry_sdk/client.py +81 -30
  7. sentry_sdk/consts.py +13 -8
  8. sentry_sdk/envelope.py +3 -3
  9. sentry_sdk/integrations/__init__.py +1 -0
  10. sentry_sdk/integrations/aiohttp.py +4 -1
  11. sentry_sdk/integrations/anthropic.py +10 -2
  12. sentry_sdk/integrations/google_genai/__init__.py +298 -0
  13. sentry_sdk/integrations/google_genai/consts.py +16 -0
  14. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  15. sentry_sdk/integrations/google_genai/utils.py +566 -0
  16. sentry_sdk/integrations/httpx.py +16 -5
  17. sentry_sdk/integrations/langchain.py +29 -4
  18. sentry_sdk/integrations/langgraph.py +5 -3
  19. sentry_sdk/integrations/logging.py +1 -1
  20. sentry_sdk/integrations/loguru.py +1 -1
  21. sentry_sdk/integrations/openai.py +3 -2
  22. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +10 -2
  23. sentry_sdk/integrations/openai_agents/utils.py +35 -18
  24. sentry_sdk/integrations/ray.py +20 -4
  25. sentry_sdk/integrations/stdlib.py +8 -1
  26. sentry_sdk/integrations/threading.py +52 -8
  27. sentry_sdk/logger.py +1 -1
  28. sentry_sdk/tracing.py +0 -26
  29. sentry_sdk/tracing_utils.py +64 -24
  30. sentry_sdk/transport.py +1 -17
  31. sentry_sdk/types.py +3 -0
  32. sentry_sdk/utils.py +17 -1
  33. {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/METADATA +3 -1
  34. {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/RECORD +38 -33
  35. sentry_sdk/metrics.py +0 -971
  36. {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/WHEEL +0 -0
  37. {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/entry_points.txt +0 -0
  38. {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/licenses/LICENSE +0 -0
  39. {sentry_sdk-2.40.0.dist-info → sentry_sdk-2.42.0.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@ from functools import wraps
2
2
  from typing import Any, Callable, List, Optional
3
3
 
4
4
  import sentry_sdk
5
- from sentry_sdk.ai.utils import set_data_normalized
5
+ from sentry_sdk.ai.utils import set_data_normalized, normalize_message_roles
6
6
  from sentry_sdk.consts import OP, SPANDATA
7
7
  from sentry_sdk.integrations import DidNotEnable, Integration
8
8
  from sentry_sdk.scope import should_send_default_pii
@@ -180,10 +180,11 @@ def _wrap_pregel_invoke(f):
180
180
  ):
181
181
  input_messages = _parse_langgraph_messages(args[0])
182
182
  if input_messages:
183
+ normalized_input_messages = normalize_message_roles(input_messages)
183
184
  set_data_normalized(
184
185
  span,
185
186
  SPANDATA.GEN_AI_REQUEST_MESSAGES,
186
- input_messages,
187
+ normalized_input_messages,
187
188
  unpack=False,
188
189
  )
189
190
 
@@ -230,10 +231,11 @@ def _wrap_pregel_ainvoke(f):
230
231
  ):
231
232
  input_messages = _parse_langgraph_messages(args[0])
232
233
  if input_messages:
234
+ normalized_input_messages = normalize_message_roles(input_messages)
233
235
  set_data_normalized(
234
236
  span,
235
237
  SPANDATA.GEN_AI_REQUEST_MESSAGES,
236
- input_messages,
238
+ normalized_input_messages,
237
239
  unpack=False,
238
240
  )
239
241
 
@@ -409,7 +409,7 @@ class SentryLogsHandler(_BaseHandler):
409
409
  attrs["logger.name"] = record.name
410
410
 
411
411
  # noinspection PyProtectedMember
412
- client._capture_experimental_log(
412
+ client._capture_log(
413
413
  {
414
414
  "severity_text": otel_severity_text,
415
415
  "severity_number": otel_severity_number,
@@ -193,7 +193,7 @@ def loguru_sentry_logs_handler(message):
193
193
  if record.get("name"):
194
194
  attrs["logger.name"] = record["name"]
195
195
 
196
- client._capture_experimental_log(
196
+ client._capture_log(
197
197
  {
198
198
  "severity_text": otel_severity_text,
199
199
  "severity_number": otel_severity_number,
@@ -3,7 +3,7 @@ from functools import wraps
3
3
  import sentry_sdk
4
4
  from sentry_sdk import consts
5
5
  from sentry_sdk.ai.monitoring import record_token_usage
6
- from sentry_sdk.ai.utils import set_data_normalized
6
+ from sentry_sdk.ai.utils import set_data_normalized, normalize_message_roles
7
7
  from sentry_sdk.consts import SPANDATA
8
8
  from sentry_sdk.integrations import DidNotEnable, Integration
9
9
  from sentry_sdk.scope import should_send_default_pii
@@ -182,8 +182,9 @@ def _set_input_data(span, kwargs, operation, integration):
182
182
  and should_send_default_pii()
183
183
  and integration.include_prompts
184
184
  ):
185
+ normalized_messages = normalize_message_roles(messages)
185
186
  set_data_normalized(
186
- span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False
187
+ span, SPANDATA.GEN_AI_REQUEST_MESSAGES, normalized_messages, unpack=False
187
188
  )
188
189
 
189
190
  # Input attributes: Common
@@ -1,5 +1,9 @@
1
1
  import sentry_sdk
2
- from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized
2
+ from sentry_sdk.ai.utils import (
3
+ get_start_span_function,
4
+ set_data_normalized,
5
+ normalize_message_roles,
6
+ )
3
7
  from sentry_sdk.consts import OP, SPANDATA
4
8
  from sentry_sdk.scope import should_send_default_pii
5
9
  from sentry_sdk.utils import safe_serialize
@@ -56,8 +60,12 @@ def invoke_agent_span(context, agent, kwargs):
56
60
  )
57
61
 
58
62
  if len(messages) > 0:
63
+ normalized_messages = normalize_message_roles(messages)
59
64
  set_data_normalized(
60
- span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False
65
+ span,
66
+ SPANDATA.GEN_AI_REQUEST_MESSAGES,
67
+ normalized_messages,
68
+ unpack=False,
61
69
  )
62
70
 
63
71
  _set_agent_data(span, agent)
@@ -1,5 +1,10 @@
1
1
  import sentry_sdk
2
- from sentry_sdk.ai.utils import set_data_normalized
2
+ from sentry_sdk.ai.utils import (
3
+ GEN_AI_ALLOWED_MESSAGE_ROLES,
4
+ normalize_message_roles,
5
+ set_data_normalized,
6
+ normalize_message_role,
7
+ )
3
8
  from sentry_sdk.consts import SPANDATA, SPANSTATUS, OP
4
9
  from sentry_sdk.integrations import DidNotEnable
5
10
  from sentry_sdk.scope import should_send_default_pii
@@ -94,35 +99,47 @@ def _set_input_data(span, get_response_kwargs):
94
99
  # type: (sentry_sdk.tracing.Span, dict[str, Any]) -> None
95
100
  if not should_send_default_pii():
96
101
  return
102
+ request_messages = []
97
103
 
98
- messages_by_role = {
99
- "system": [],
100
- "user": [],
101
- "assistant": [],
102
- "tool": [],
103
- } # type: (dict[str, list[Any]])
104
104
  system_instructions = get_response_kwargs.get("system_instructions")
105
105
  if system_instructions:
106
- messages_by_role["system"].append({"type": "text", "text": system_instructions})
106
+ request_messages.append(
107
+ {
108
+ "role": GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM,
109
+ "content": [{"type": "text", "text": system_instructions}],
110
+ }
111
+ )
107
112
 
108
113
  for message in get_response_kwargs.get("input", []):
109
114
  if "role" in message:
110
- messages_by_role[message.get("role")].append(
111
- {"type": "text", "text": message.get("content")}
115
+ normalized_role = normalize_message_role(message.get("role"))
116
+ request_messages.append(
117
+ {
118
+ "role": normalized_role,
119
+ "content": [{"type": "text", "text": message.get("content")}],
120
+ }
112
121
  )
113
122
  else:
114
123
  if message.get("type") == "function_call":
115
- messages_by_role["assistant"].append(message)
124
+ request_messages.append(
125
+ {
126
+ "role": GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT,
127
+ "content": [message],
128
+ }
129
+ )
116
130
  elif message.get("type") == "function_call_output":
117
- messages_by_role["tool"].append(message)
118
-
119
- request_messages = []
120
- for role, messages in messages_by_role.items():
121
- if len(messages) > 0:
122
- request_messages.append({"role": role, "content": messages})
131
+ request_messages.append(
132
+ {
133
+ "role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL,
134
+ "content": [message],
135
+ }
136
+ )
123
137
 
124
138
  set_data_normalized(
125
- span, SPANDATA.GEN_AI_REQUEST_MESSAGES, request_messages, unpack=False
139
+ span,
140
+ SPANDATA.GEN_AI_REQUEST_MESSAGES,
141
+ normalize_message_roles(request_messages),
142
+ unpack=False,
126
143
  )
127
144
 
128
145
 
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import functools
2
3
  import sys
3
4
 
4
5
  import sentry_sdk
@@ -17,7 +18,6 @@ try:
17
18
  import ray # type: ignore[import-not-found]
18
19
  except ImportError:
19
20
  raise DidNotEnable("Ray not installed.")
20
- import functools
21
21
 
22
22
  from typing import TYPE_CHECKING
23
23
 
@@ -54,12 +54,13 @@ def _patch_ray_remote():
54
54
 
55
55
  def wrapper(user_f):
56
56
  # type: (Callable[..., Any]) -> Any
57
- def new_func(*f_args, _tracing=None, **f_kwargs):
57
+ @functools.wraps(user_f)
58
+ def new_func(*f_args, _sentry_tracing=None, **f_kwargs):
58
59
  # type: (Any, Optional[dict[str, Any]], Any) -> Any
59
60
  _check_sentry_initialized()
60
61
 
61
62
  transaction = sentry_sdk.continue_trace(
62
- _tracing or {},
63
+ _sentry_tracing or {},
63
64
  op=OP.QUEUE_TASK_RAY,
64
65
  name=qualname_from_function(user_f),
65
66
  origin=RayIntegration.origin,
@@ -78,6 +79,19 @@ def _patch_ray_remote():
78
79
 
79
80
  return result
80
81
 
82
+ # Patching new_func signature to add the _sentry_tracing parameter to it
83
+ # Ray later inspects the signature and finds the unexpected parameter otherwise
84
+ signature = inspect.signature(new_func)
85
+ params = list(signature.parameters.values())
86
+ params.append(
87
+ inspect.Parameter(
88
+ "_sentry_tracing",
89
+ kind=inspect.Parameter.KEYWORD_ONLY,
90
+ default=None,
91
+ )
92
+ )
93
+ new_func.__signature__ = signature.replace(parameters=params) # type: ignore[attr-defined]
94
+
81
95
  if f:
82
96
  rv = old_remote(new_func)
83
97
  else:
@@ -99,7 +113,9 @@ def _patch_ray_remote():
99
113
  for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers()
100
114
  }
101
115
  try:
102
- result = old_remote_method(*args, **kwargs, _tracing=tracing)
116
+ result = old_remote_method(
117
+ *args, **kwargs, _sentry_tracing=tracing
118
+ )
103
119
  span.set_status(SPANSTATUS.OK)
104
120
  except Exception:
105
121
  span.set_status(SPANSTATUS.INTERNAL_ERROR)
@@ -8,7 +8,11 @@ import sentry_sdk
8
8
  from sentry_sdk.consts import OP, SPANDATA
9
9
  from sentry_sdk.integrations import Integration
10
10
  from sentry_sdk.scope import add_global_event_processor
11
- from sentry_sdk.tracing_utils import EnvironHeaders, should_propagate_trace
11
+ from sentry_sdk.tracing_utils import (
12
+ EnvironHeaders,
13
+ should_propagate_trace,
14
+ add_http_request_source,
15
+ )
12
16
  from sentry_sdk.utils import (
13
17
  SENSITIVE_DATA_SUBSTITUTE,
14
18
  capture_internal_exceptions,
@@ -135,6 +139,9 @@ def _install_httplib():
135
139
  finally:
136
140
  span.finish()
137
141
 
142
+ with capture_internal_exceptions():
143
+ add_http_request_source(span)
144
+
138
145
  return rv
139
146
 
140
147
  HTTPConnection.putrequest = putrequest # type: ignore[method-assign]
@@ -2,6 +2,7 @@ import sys
2
2
  import warnings
3
3
  from functools import wraps
4
4
  from threading import Thread, current_thread
5
+ from concurrent.futures import ThreadPoolExecutor, Future
5
6
 
6
7
  import sentry_sdk
7
8
  from sentry_sdk.integrations import Integration
@@ -24,6 +25,7 @@ if TYPE_CHECKING:
24
25
  from sentry_sdk._types import ExcInfo
25
26
 
26
27
  F = TypeVar("F", bound=Callable[..., Any])
28
+ T = TypeVar("T", bound=Any)
27
29
 
28
30
 
29
31
  class ThreadingIntegration(Integration):
@@ -59,6 +61,15 @@ class ThreadingIntegration(Integration):
59
61
  django_version = None
60
62
  channels_version = None
61
63
 
64
+ is_async_emulated_with_threads = (
65
+ sys.version_info < (3, 9)
66
+ and channels_version is not None
67
+ and channels_version < "4.0.0"
68
+ and django_version is not None
69
+ and django_version >= (3, 0)
70
+ and django_version < (4, 0)
71
+ )
72
+
62
73
  @wraps(old_start)
63
74
  def sentry_start(self, *a, **kw):
64
75
  # type: (Thread, *Any, **Any) -> Any
@@ -67,14 +78,7 @@ class ThreadingIntegration(Integration):
67
78
  return old_start(self, *a, **kw)
68
79
 
69
80
  if integration.propagate_scope:
70
- if (
71
- sys.version_info < (3, 9)
72
- and channels_version is not None
73
- and channels_version < "4.0.0"
74
- and django_version is not None
75
- and django_version >= (3, 0)
76
- and django_version < (4, 0)
77
- ):
81
+ if is_async_emulated_with_threads:
78
82
  warnings.warn(
79
83
  "There is a known issue with Django channels 2.x and 3.x when using Python 3.8 or older. "
80
84
  "(Async support is emulated using threads and some Sentry data may be leaked between those threads.) "
@@ -109,6 +113,9 @@ class ThreadingIntegration(Integration):
109
113
  return old_start(self, *a, **kw)
110
114
 
111
115
  Thread.start = sentry_start # type: ignore
116
+ ThreadPoolExecutor.submit = _wrap_threadpool_executor_submit( # type: ignore
117
+ ThreadPoolExecutor.submit, is_async_emulated_with_threads
118
+ )
112
119
 
113
120
 
114
121
  def _wrap_run(isolation_scope_to_use, current_scope_to_use, old_run_func):
@@ -134,6 +141,43 @@ def _wrap_run(isolation_scope_to_use, current_scope_to_use, old_run_func):
134
141
  return run # type: ignore
135
142
 
136
143
 
144
+ def _wrap_threadpool_executor_submit(func, is_async_emulated_with_threads):
145
+ # type: (Callable[..., Future[T]], bool) -> Callable[..., Future[T]]
146
+ """
147
+ Wrap submit call to propagate scopes on task submission.
148
+ """
149
+
150
+ @wraps(func)
151
+ def sentry_submit(self, fn, *args, **kwargs):
152
+ # type: (ThreadPoolExecutor, Callable[..., T], *Any, **Any) -> Future[T]
153
+ integration = sentry_sdk.get_client().get_integration(ThreadingIntegration)
154
+ if integration is None:
155
+ return func(self, fn, *args, **kwargs)
156
+
157
+ if integration.propagate_scope and is_async_emulated_with_threads:
158
+ isolation_scope = sentry_sdk.get_isolation_scope()
159
+ current_scope = sentry_sdk.get_current_scope()
160
+ elif integration.propagate_scope:
161
+ isolation_scope = sentry_sdk.get_isolation_scope().fork()
162
+ current_scope = sentry_sdk.get_current_scope().fork()
163
+ else:
164
+ isolation_scope = None
165
+ current_scope = None
166
+
167
+ def wrapped_fn(*args, **kwargs):
168
+ # type: (*Any, **Any) -> Any
169
+ if isolation_scope is not None and current_scope is not None:
170
+ with use_isolation_scope(isolation_scope):
171
+ with use_scope(current_scope):
172
+ return fn(*args, **kwargs)
173
+
174
+ return fn(*args, **kwargs)
175
+
176
+ return func(self, wrapped_fn, *args, **kwargs)
177
+
178
+ return sentry_submit
179
+
180
+
137
181
  def _capture_exception():
138
182
  # type: () -> ExcInfo
139
183
  exc_info = sys.exc_info()
sentry_sdk/logger.py CHANGED
@@ -46,7 +46,7 @@ def _capture_log(severity_text, severity_number, template, **kwargs):
46
46
  }
47
47
 
48
48
  # noinspection PyProtectedMember
49
- client._capture_experimental_log(
49
+ client._capture_log(
50
50
  {
51
51
  "severity_text": severity_text,
52
52
  "severity_number": severity_number,
sentry_sdk/tracing.py CHANGED
@@ -276,7 +276,6 @@ class Span:
276
276
  "hub",
277
277
  "_context_manager_state",
278
278
  "_containing_transaction",
279
- "_local_aggregator",
280
279
  "scope",
281
280
  "origin",
282
281
  "name",
@@ -345,7 +344,6 @@ class Span:
345
344
  self.timestamp = None # type: Optional[datetime]
346
345
 
347
346
  self._span_recorder = None # type: Optional[_SpanRecorder]
348
- self._local_aggregator = None # type: Optional[LocalAggregator]
349
347
 
350
348
  self.update_active_thread()
351
349
  self.set_profiler_id(get_profiler_id())
@@ -383,13 +381,6 @@ class Span:
383
381
  # type: (str) -> None
384
382
  self._span_id = value
385
383
 
386
- def _get_local_aggregator(self):
387
- # type: (...) -> LocalAggregator
388
- rv = self._local_aggregator
389
- if rv is None:
390
- rv = self._local_aggregator = LocalAggregator()
391
- return rv
392
-
393
384
  def __repr__(self):
394
385
  # type: () -> str
395
386
  return (
@@ -741,11 +732,6 @@ class Span:
741
732
  if self.status:
742
733
  self._tags["status"] = self.status
743
734
 
744
- if self._local_aggregator is not None:
745
- metrics_summary = self._local_aggregator.to_json()
746
- if metrics_summary:
747
- rv["_metrics_summary"] = metrics_summary
748
-
749
735
  if len(self._measurements) > 0:
750
736
  rv["measurements"] = self._measurements
751
737
 
@@ -1122,13 +1108,6 @@ class Transaction(Span):
1122
1108
 
1123
1109
  event["measurements"] = self._measurements
1124
1110
 
1125
- # This is here since `to_json` is not invoked. This really should
1126
- # be gone when we switch to onlyspans.
1127
- if self._local_aggregator is not None:
1128
- metrics_summary = self._local_aggregator.to_json()
1129
- if metrics_summary:
1130
- event["_metrics_summary"] = metrics_summary
1131
-
1132
1111
  return scope.capture_event(event)
1133
1112
 
1134
1113
  def set_measurement(self, name, value, unit=""):
@@ -1505,8 +1484,3 @@ from sentry_sdk.tracing_utils import (
1505
1484
  has_tracing_enabled,
1506
1485
  maybe_create_breadcrumbs_from_span,
1507
1486
  )
1508
-
1509
- with warnings.catch_warnings():
1510
- # The code in this file which uses `LocalAggregator` is only called from the deprecated `metrics` module.
1511
- warnings.simplefilter("ignore", DeprecationWarning)
1512
- from sentry_sdk.metrics import LocalAggregator
@@ -218,33 +218,11 @@ def _should_be_included(
218
218
  )
219
219
 
220
220
 
221
- def add_query_source(span):
222
- # type: (sentry_sdk.tracing.Span) -> None
221
+ def add_source(span, project_root, in_app_include, in_app_exclude):
222
+ # type: (sentry_sdk.tracing.Span, Optional[str], Optional[list[str]], Optional[list[str]]) -> None
223
223
  """
224
224
  Adds OTel compatible source code information to the span
225
225
  """
226
- client = sentry_sdk.get_client()
227
- if not client.is_active():
228
- return
229
-
230
- if span.timestamp is None or span.start_timestamp is None:
231
- return
232
-
233
- should_add_query_source = client.options.get("enable_db_query_source", True)
234
- if not should_add_query_source:
235
- return
236
-
237
- duration = span.timestamp - span.start_timestamp
238
- threshold = client.options.get("db_query_source_threshold_ms", 0)
239
- slow_query = duration / timedelta(milliseconds=1) > threshold
240
-
241
- if not slow_query:
242
- return
243
-
244
- project_root = client.options["project_root"]
245
- in_app_include = client.options.get("in_app_include")
246
- in_app_exclude = client.options.get("in_app_exclude")
247
-
248
226
  # Find the correct frame
249
227
  frame = sys._getframe() # type: Union[FrameType, None]
250
228
  while frame is not None:
@@ -309,6 +287,68 @@ def add_query_source(span):
309
287
  span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name)
310
288
 
311
289
 
290
+ def add_query_source(span):
291
+ # type: (sentry_sdk.tracing.Span) -> None
292
+ """
293
+ Adds OTel compatible source code information to a database query span
294
+ """
295
+ client = sentry_sdk.get_client()
296
+ if not client.is_active():
297
+ return
298
+
299
+ if span.timestamp is None or span.start_timestamp is None:
300
+ return
301
+
302
+ should_add_query_source = client.options.get("enable_db_query_source", True)
303
+ if not should_add_query_source:
304
+ return
305
+
306
+ duration = span.timestamp - span.start_timestamp
307
+ threshold = client.options.get("db_query_source_threshold_ms", 0)
308
+ slow_query = duration / timedelta(milliseconds=1) > threshold
309
+
310
+ if not slow_query:
311
+ return
312
+
313
+ add_source(
314
+ span=span,
315
+ project_root=client.options["project_root"],
316
+ in_app_include=client.options.get("in_app_include"),
317
+ in_app_exclude=client.options.get("in_app_exclude"),
318
+ )
319
+
320
+
321
+ def add_http_request_source(span):
322
+ # type: (sentry_sdk.tracing.Span) -> None
323
+ """
324
+ Adds OTel compatible source code information to a span for an outgoing HTTP request
325
+ """
326
+ client = sentry_sdk.get_client()
327
+ if not client.is_active():
328
+ return
329
+
330
+ if span.timestamp is None or span.start_timestamp is None:
331
+ return
332
+
333
+ should_add_request_source = client.options.get("enable_http_request_source", False)
334
+ if not should_add_request_source:
335
+ return
336
+
337
+ duration = span.timestamp - span.start_timestamp
338
+ threshold = client.options.get("http_request_source_threshold_ms", 0)
339
+ slow_query = duration / timedelta(milliseconds=1) > threshold
340
+
341
+ if not slow_query:
342
+ return
343
+
344
+ add_source(
345
+ span=span,
346
+ project_root=client.options["project_root"],
347
+ in_app_include=client.options.get("in_app_include"),
348
+ in_app_exclude=client.options.get("in_app_exclude"),
349
+ )
350
+
351
+
312
352
  def extract_sentrytrace_data(header):
313
353
  # type: (Optional[str]) -> Optional[Dict[str, Union[str, bool, None]]]
314
354
  """
sentry_sdk/transport.py CHANGED
@@ -171,17 +171,7 @@ def _parse_rate_limits(header, now=None):
171
171
 
172
172
  retry_after = now + timedelta(seconds=int(retry_after_val))
173
173
  for category in categories and categories.split(";") or (None,):
174
- if category == "metric_bucket":
175
- try:
176
- namespaces = parameters[4].split(";")
177
- except IndexError:
178
- namespaces = []
179
-
180
- if not namespaces or "custom" in namespaces:
181
- yield category, retry_after # type: ignore
182
-
183
- else:
184
- yield category, retry_after # type: ignore
174
+ yield category, retry_after # type: ignore
185
175
  except (LookupError, ValueError):
186
176
  continue
187
177
 
@@ -417,12 +407,6 @@ class BaseHttpTransport(Transport):
417
407
  # type: (str) -> bool
418
408
  def _disabled(bucket):
419
409
  # type: (Any) -> bool
420
-
421
- # The envelope item type used for metrics is statsd
422
- # whereas the rate limit category is metric_bucket
423
- if bucket == "statsd":
424
- bucket = "metric_bucket"
425
-
426
410
  ts = self._disabled_until.get(bucket)
427
411
  return ts is not None and ts > datetime.now(timezone.utc)
428
412
 
sentry_sdk/types.py CHANGED
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
21
21
  Log,
22
22
  MonitorConfig,
23
23
  SamplingContext,
24
+ Metric,
24
25
  )
25
26
  else:
26
27
  from typing import Any
@@ -35,6 +36,7 @@ else:
35
36
  Log = Any
36
37
  MonitorConfig = Any
37
38
  SamplingContext = Any
39
+ Metric = Any
38
40
 
39
41
 
40
42
  __all__ = (
@@ -46,4 +48,5 @@ __all__ = (
46
48
  "Log",
47
49
  "MonitorConfig",
48
50
  "SamplingContext",
51
+ "Metric",
49
52
  )
sentry_sdk/utils.py CHANGED
@@ -59,7 +59,7 @@ if TYPE_CHECKING:
59
59
 
60
60
  from gevent.hub import Hub
61
61
 
62
- from sentry_sdk._types import Event, ExcInfo, Log, Hint
62
+ from sentry_sdk._types import Event, ExcInfo, Log, Hint, Metric
63
63
 
64
64
  P = ParamSpec("P")
65
65
  R = TypeVar("R")
@@ -2013,3 +2013,19 @@ def get_before_send_log(options):
2013
2013
  return options.get("before_send_log") or options["_experiments"].get(
2014
2014
  "before_send_log"
2015
2015
  )
2016
+
2017
+
2018
+ def has_metrics_enabled(options):
2019
+ # type: (Optional[dict[str, Any]]) -> bool
2020
+ if options is None:
2021
+ return False
2022
+
2023
+ return bool(options["_experiments"].get("enable_metrics", False))
2024
+
2025
+
2026
+ def get_before_send_metric(options):
2027
+ # type: (Optional[dict[str, Any]]) -> Optional[Callable[[Metric, Hint], Optional[Metric]]]
2028
+ if options is None:
2029
+ return None
2030
+
2031
+ return options["_experiments"].get("before_send_metric")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sentry-sdk
3
- Version: 2.40.0
3
+ Version: 2.42.0
4
4
  Summary: Python client for Sentry (https://sentry.io)
5
5
  Home-page: https://github.com/getsentry/sentry-python
6
6
  Author: Sentry Team and Contributors
@@ -118,6 +118,8 @@ Provides-Extra: tornado
118
118
  Requires-Dist: tornado>=6; extra == "tornado"
119
119
  Provides-Extra: unleash
120
120
  Requires-Dist: UnleashClient>=6.0.1; extra == "unleash"
121
+ Provides-Extra: google-genai
122
+ Requires-Dist: google-genai>=1.29.0; extra == "google-genai"
121
123
  Dynamic: author
122
124
  Dynamic: author-email
123
125
  Dynamic: classifier