sentry-sdk 2.39.0__py2.py3-none-any.whl → 2.41.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 (52) 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 +87 -36
  7. sentry_sdk/consts.py +15 -9
  8. sentry_sdk/envelope.py +31 -17
  9. sentry_sdk/feature_flags.py +0 -1
  10. sentry_sdk/hub.py +17 -9
  11. sentry_sdk/integrations/__init__.py +1 -0
  12. sentry_sdk/integrations/anthropic.py +10 -2
  13. sentry_sdk/integrations/asgi.py +3 -2
  14. sentry_sdk/integrations/dramatiq.py +89 -31
  15. sentry_sdk/integrations/grpc/aio/client.py +2 -1
  16. sentry_sdk/integrations/grpc/client.py +3 -4
  17. sentry_sdk/integrations/langchain.py +29 -5
  18. sentry_sdk/integrations/langgraph.py +5 -3
  19. sentry_sdk/integrations/launchdarkly.py +0 -1
  20. sentry_sdk/integrations/litellm.py +251 -0
  21. sentry_sdk/integrations/litestar.py +4 -4
  22. sentry_sdk/integrations/logging.py +1 -1
  23. sentry_sdk/integrations/loguru.py +1 -1
  24. sentry_sdk/integrations/openai.py +3 -2
  25. sentry_sdk/integrations/openai_agents/spans/ai_client.py +4 -1
  26. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +10 -2
  27. sentry_sdk/integrations/openai_agents/utils.py +60 -19
  28. sentry_sdk/integrations/pure_eval.py +3 -1
  29. sentry_sdk/integrations/spark/spark_driver.py +2 -1
  30. sentry_sdk/integrations/sqlalchemy.py +2 -6
  31. sentry_sdk/integrations/starlette.py +1 -3
  32. sentry_sdk/integrations/starlite.py +4 -4
  33. sentry_sdk/integrations/threading.py +52 -8
  34. sentry_sdk/integrations/wsgi.py +3 -2
  35. sentry_sdk/logger.py +1 -1
  36. sentry_sdk/profiler/utils.py +2 -6
  37. sentry_sdk/scope.py +6 -3
  38. sentry_sdk/serializer.py +1 -3
  39. sentry_sdk/session.py +4 -2
  40. sentry_sdk/sessions.py +4 -2
  41. sentry_sdk/tracing.py +36 -33
  42. sentry_sdk/tracing_utils.py +1 -3
  43. sentry_sdk/transport.py +9 -26
  44. sentry_sdk/types.py +3 -0
  45. sentry_sdk/utils.py +22 -4
  46. {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/METADATA +3 -1
  47. {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/RECORD +51 -49
  48. sentry_sdk/metrics.py +0 -965
  49. {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/WHEEL +0 -0
  50. {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/entry_points.txt +0 -0
  51. {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/licenses/LICENSE +0 -0
  52. {sentry_sdk-2.39.0.dist-info → sentry_sdk-2.41.0.dist-info}/top_level.txt +0 -0
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)
@@ -0,0 +1,156 @@
1
+ import os
2
+ import random
3
+ import threading
4
+ from datetime import datetime, timezone
5
+ from typing import Optional, List, Callable, TYPE_CHECKING, Any, Union
6
+
7
+ from sentry_sdk.utils import format_timestamp, safe_repr
8
+ from sentry_sdk.envelope import Envelope, Item, PayloadRef
9
+
10
+ if TYPE_CHECKING:
11
+ from sentry_sdk._types import Metric
12
+
13
+
14
+ class MetricsBatcher:
15
+ MAX_METRICS_BEFORE_FLUSH = 100
16
+ FLUSH_WAIT_TIME = 5.0
17
+
18
+ def __init__(
19
+ self,
20
+ capture_func, # type: Callable[[Envelope], None]
21
+ ):
22
+ # type: (...) -> None
23
+ self._metric_buffer = [] # type: List[Metric]
24
+ self._capture_func = capture_func
25
+ self._running = True
26
+ self._lock = threading.Lock()
27
+
28
+ self._flush_event = threading.Event() # type: threading.Event
29
+
30
+ self._flusher = None # type: Optional[threading.Thread]
31
+ self._flusher_pid = None # type: Optional[int]
32
+
33
+ def _ensure_thread(self):
34
+ # type: (...) -> bool
35
+ if not self._running:
36
+ return False
37
+
38
+ pid = os.getpid()
39
+ if self._flusher_pid == pid:
40
+ return True
41
+
42
+ with self._lock:
43
+ if self._flusher_pid == pid:
44
+ return True
45
+
46
+ self._flusher_pid = pid
47
+
48
+ self._flusher = threading.Thread(target=self._flush_loop)
49
+ self._flusher.daemon = True
50
+
51
+ try:
52
+ self._flusher.start()
53
+ except RuntimeError:
54
+ self._running = False
55
+ return False
56
+
57
+ return True
58
+
59
+ def _flush_loop(self):
60
+ # type: (...) -> None
61
+ while self._running:
62
+ self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random())
63
+ self._flush_event.clear()
64
+ self._flush()
65
+
66
+ def add(
67
+ self,
68
+ metric, # type: Metric
69
+ ):
70
+ # type: (...) -> None
71
+ if not self._ensure_thread() or self._flusher is None:
72
+ return None
73
+
74
+ with self._lock:
75
+ self._metric_buffer.append(metric)
76
+ if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_FLUSH:
77
+ self._flush_event.set()
78
+
79
+ def kill(self):
80
+ # type: (...) -> None
81
+ if self._flusher is None:
82
+ return
83
+
84
+ self._running = False
85
+ self._flush_event.set()
86
+ self._flusher = None
87
+
88
+ def flush(self):
89
+ # type: (...) -> None
90
+ self._flush()
91
+
92
+ @staticmethod
93
+ def _metric_to_transport_format(metric):
94
+ # type: (Metric) -> Any
95
+ def format_attribute(val):
96
+ # type: (Union[int, float, str, bool]) -> Any
97
+ if isinstance(val, bool):
98
+ return {"value": val, "type": "boolean"}
99
+ if isinstance(val, int):
100
+ return {"value": val, "type": "integer"}
101
+ if isinstance(val, float):
102
+ return {"value": val, "type": "double"}
103
+ if isinstance(val, str):
104
+ return {"value": val, "type": "string"}
105
+ return {"value": safe_repr(val), "type": "string"}
106
+
107
+ res = {
108
+ "timestamp": metric["timestamp"],
109
+ "trace_id": metric["trace_id"],
110
+ "name": metric["name"],
111
+ "type": metric["type"],
112
+ "value": metric["value"],
113
+ "attributes": {
114
+ k: format_attribute(v) for (k, v) in metric["attributes"].items()
115
+ },
116
+ }
117
+
118
+ if metric.get("span_id") is not None:
119
+ res["span_id"] = metric["span_id"]
120
+
121
+ if metric.get("unit") is not None:
122
+ res["unit"] = metric["unit"]
123
+
124
+ return res
125
+
126
+ def _flush(self):
127
+ # type: (...) -> Optional[Envelope]
128
+
129
+ envelope = Envelope(
130
+ headers={"sent_at": format_timestamp(datetime.now(timezone.utc))}
131
+ )
132
+ with self._lock:
133
+ if len(self._metric_buffer) == 0:
134
+ return None
135
+
136
+ envelope.add_item(
137
+ Item(
138
+ type="trace_metric",
139
+ content_type="application/vnd.sentry.items.trace-metric+json",
140
+ headers={
141
+ "item_count": len(self._metric_buffer),
142
+ },
143
+ payload=PayloadRef(
144
+ json={
145
+ "items": [
146
+ self._metric_to_transport_format(metric)
147
+ for metric in self._metric_buffer
148
+ ]
149
+ }
150
+ ),
151
+ )
152
+ )
153
+ self._metric_buffer.clear()
154
+
155
+ self._capture_func(envelope)
156
+ return envelope
sentry_sdk/_types.py CHANGED
@@ -210,7 +210,6 @@ if TYPE_CHECKING:
210
210
  "type": Literal["check_in", "transaction"],
211
211
  "user": dict[str, object],
212
212
  "_dropped_spans": int,
213
- "_metrics_summary": dict[str, object],
214
213
  },
215
214
  total=False,
216
215
  )
@@ -235,6 +234,32 @@ if TYPE_CHECKING:
235
234
  },
236
235
  )
237
236
 
237
+ MetricType = Literal["counter", "gauge", "distribution"]
238
+
239
+ MetricAttributeValue = TypedDict(
240
+ "MetricAttributeValue",
241
+ {
242
+ "value": Union[str, bool, float, int],
243
+ "type": Literal["string", "boolean", "double", "integer"],
244
+ },
245
+ )
246
+
247
+ Metric = TypedDict(
248
+ "Metric",
249
+ {
250
+ "timestamp": float,
251
+ "trace_id": Optional[str],
252
+ "span_id": Optional[str],
253
+ "name": str,
254
+ "type": MetricType,
255
+ "value": float,
256
+ "unit": Optional[str],
257
+ "attributes": dict[str, str | bool | float | int],
258
+ },
259
+ )
260
+
261
+ MetricProcessor = Callable[[Metric, Hint], Optional[Metric]]
262
+
238
263
  # TODO: Make a proper type definition for this (PRs welcome!)
239
264
  Breadcrumb = Dict[str, Any]
240
265
 
@@ -266,36 +291,16 @@ if TYPE_CHECKING:
266
291
  "internal",
267
292
  "profile",
268
293
  "profile_chunk",
269
- "metric_bucket",
270
294
  "monitor",
271
295
  "span",
272
296
  "log_item",
297
+ "trace_metric",
273
298
  ]
274
299
  SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
275
300
 
276
301
  ContinuousProfilerMode = Literal["thread", "gevent", "unknown"]
277
302
  ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]]
278
303
 
279
- # Type of the metric.
280
- MetricType = Literal["d", "s", "g", "c"]
281
-
282
- # Value of the metric.
283
- MetricValue = Union[int, float, str]
284
-
285
- # Internal representation of tags as a tuple of tuples (this is done in order to allow for the same key to exist
286
- # multiple times).
287
- MetricTagsInternal = Tuple[Tuple[str, str], ...]
288
-
289
- # External representation of tags as a dictionary.
290
- MetricTagValue = Union[str, int, float, None]
291
- MetricTags = Mapping[str, MetricTagValue]
292
-
293
- # Value inside the generator for the metric value.
294
- FlushedMetricValue = Union[int, float]
295
-
296
- BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal]
297
- MetricMetaKey = Tuple[MetricType, str, MeasurementUnit]
298
-
299
304
  MonitorConfigScheduleType = Literal["crontab", "interval"]
300
305
  MonitorConfigScheduleUnit = Literal[
301
306
  "year",
sentry_sdk/ai/__init__.py CHANGED
@@ -0,0 +1,7 @@
1
+ from .utils import (
2
+ set_data_normalized,
3
+ GEN_AI_MESSAGE_ROLE_MAPPING,
4
+ GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING,
5
+ normalize_message_role,
6
+ normalize_message_roles,
7
+ ) # noqa: F401
sentry_sdk/ai/utils.py CHANGED
@@ -10,6 +10,26 @@ import sentry_sdk
10
10
  from sentry_sdk.utils import logger
11
11
 
12
12
 
13
+ class GEN_AI_ALLOWED_MESSAGE_ROLES:
14
+ SYSTEM = "system"
15
+ USER = "user"
16
+ ASSISTANT = "assistant"
17
+ TOOL = "tool"
18
+
19
+
20
+ GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING = {
21
+ GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM: ["system"],
22
+ GEN_AI_ALLOWED_MESSAGE_ROLES.USER: ["user", "human"],
23
+ GEN_AI_ALLOWED_MESSAGE_ROLES.ASSISTANT: ["assistant", "ai"],
24
+ GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL: ["tool", "tool_call"],
25
+ }
26
+
27
+ GEN_AI_MESSAGE_ROLE_MAPPING = {}
28
+ for target_role, source_roles in GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING.items():
29
+ for source_role in source_roles:
30
+ GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role
31
+
32
+
13
33
  def _normalize_data(data, unpack=True):
14
34
  # type: (Any, bool) -> Any
15
35
  # convert pydantic data (e.g. OpenAI v1+) to json compatible format
@@ -40,6 +60,34 @@ def set_data_normalized(span, key, value, unpack=True):
40
60
  span.set_data(key, json.dumps(normalized))
41
61
 
42
62
 
63
+ def normalize_message_role(role):
64
+ # type: (str) -> str
65
+ """
66
+ Normalize a message role to one of the 4 allowed gen_ai role values.
67
+ Maps "ai" -> "assistant" and keeps other standard roles unchanged.
68
+ """
69
+ return GEN_AI_MESSAGE_ROLE_MAPPING.get(role, role)
70
+
71
+
72
+ def normalize_message_roles(messages):
73
+ # type: (list[dict[str, Any]]) -> list[dict[str, Any]]
74
+ """
75
+ Normalize roles in a list of messages to use standard gen_ai role values.
76
+ Creates a deep copy to avoid modifying the original messages.
77
+ """
78
+ normalized_messages = []
79
+ for message in messages:
80
+ if not isinstance(message, dict):
81
+ normalized_messages.append(message)
82
+ continue
83
+ normalized_message = message.copy()
84
+ if "role" in message:
85
+ normalized_message["role"] = normalize_message_role(message["role"])
86
+ normalized_messages.append(normalized_message)
87
+
88
+ return normalized_messages
89
+
90
+
43
91
  def get_start_span_function():
44
92
  # type: () -> Callable[..., Any]
45
93
  current_span = sentry_sdk.get_current_span()
sentry_sdk/client.py CHANGED
@@ -24,7 +24,9 @@ from sentry_sdk.utils import (
24
24
  is_gevent,
25
25
  logger,
26
26
  get_before_send_log,
27
+ get_before_send_metric,
27
28
  has_logs_enabled,
29
+ has_metrics_enabled,
28
30
  )
29
31
  from sentry_sdk.serializer import serialize
30
32
  from sentry_sdk.tracing import trace
@@ -59,14 +61,14 @@ if TYPE_CHECKING:
59
61
  from typing import Union
60
62
  from typing import TypeVar
61
63
 
62
- from sentry_sdk._types import Event, Hint, SDKInfo, Log
64
+ from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric
63
65
  from sentry_sdk.integrations import Integration
64
- from sentry_sdk.metrics import MetricsAggregator
65
66
  from sentry_sdk.scope import Scope
66
67
  from sentry_sdk.session import Session
67
68
  from sentry_sdk.spotlight import SpotlightClient
68
69
  from sentry_sdk.transport import Transport
69
70
  from sentry_sdk._log_batcher import LogBatcher
71
+ from sentry_sdk._metrics_batcher import MetricsBatcher
70
72
 
71
73
  I = TypeVar("I", bound=Integration) # noqa: E741
72
74
 
@@ -178,14 +180,12 @@ class BaseClient:
178
180
 
179
181
  def __init__(self, options=None):
180
182
  # type: (Optional[Dict[str, Any]]) -> None
181
- self.options = (
182
- options if options is not None else DEFAULT_OPTIONS
183
- ) # type: Dict[str, Any]
183
+ self.options = options if options is not None else DEFAULT_OPTIONS # type: Dict[str, Any]
184
184
 
185
185
  self.transport = None # type: Optional[Transport]
186
186
  self.monitor = None # type: Optional[Monitor]
187
- self.metrics_aggregator = None # type: Optional[MetricsAggregator]
188
187
  self.log_batcher = None # type: Optional[LogBatcher]
188
+ self.metrics_batcher = None # type: Optional[MetricsBatcher]
189
189
 
190
190
  def __getstate__(self, *args, **kwargs):
191
191
  # type: (*Any, **Any) -> Any
@@ -217,10 +217,14 @@ class BaseClient:
217
217
  # type: (*Any, **Any) -> Optional[str]
218
218
  return None
219
219
 
220
- def _capture_experimental_log(self, log):
220
+ def _capture_log(self, log):
221
221
  # type: (Log) -> None
222
222
  pass
223
223
 
224
+ def _capture_metric(self, metric):
225
+ # type: (Metric) -> None
226
+ pass
227
+
224
228
  def capture_session(self, *args, **kwargs):
225
229
  # type: (*Any, **Any) -> None
226
230
  return None
@@ -363,26 +367,6 @@ class _Client(BaseClient):
363
367
 
364
368
  self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
365
369
 
366
- self.metrics_aggregator = None # type: Optional[MetricsAggregator]
367
- experiments = self.options.get("_experiments", {})
368
- if experiments.get("enable_metrics", True):
369
- # Context vars are not working correctly on Python <=3.6
370
- # with gevent.
371
- metrics_supported = not is_gevent() or PY37
372
- if metrics_supported:
373
- from sentry_sdk.metrics import MetricsAggregator
374
-
375
- self.metrics_aggregator = MetricsAggregator(
376
- capture_func=_capture_envelope,
377
- enable_code_locations=bool(
378
- experiments.get("metric_code_locations", True)
379
- ),
380
- )
381
- else:
382
- logger.info(
383
- "Metrics not supported on Python 3.6 and lower with gevent."
384
- )
385
-
386
370
  self.log_batcher = None
387
371
 
388
372
  if has_logs_enabled(self.options):
@@ -390,6 +374,13 @@ class _Client(BaseClient):
390
374
 
391
375
  self.log_batcher = LogBatcher(capture_func=_capture_envelope)
392
376
 
377
+ self.metrics_batcher = None
378
+
379
+ if has_metrics_enabled(self.options):
380
+ from sentry_sdk._metrics_batcher import MetricsBatcher
381
+
382
+ self.metrics_batcher = MetricsBatcher(capture_func=_capture_envelope)
383
+
393
384
  max_request_body_size = ("always", "never", "small", "medium")
394
385
  if self.options["max_request_body_size"] not in max_request_body_size:
395
386
  raise ValueError(
@@ -469,7 +460,6 @@ class _Client(BaseClient):
469
460
 
470
461
  if (
471
462
  self.monitor
472
- or self.metrics_aggregator
473
463
  or self.log_batcher
474
464
  or has_profiling_enabled(self.options)
475
465
  or isinstance(self.transport, BaseHttpTransport)
@@ -902,7 +892,7 @@ class _Client(BaseClient):
902
892
 
903
893
  return return_value
904
894
 
905
- def _capture_experimental_log(self, log):
895
+ def _capture_log(self, log):
906
896
  # type: (Optional[Log]) -> None
907
897
  if not has_logs_enabled(self.options) or log is None:
908
898
  return
@@ -956,7 +946,7 @@ class _Client(BaseClient):
956
946
  debug = self.options.get("debug", False)
957
947
  if debug:
958
948
  logger.debug(
959
- f'[Sentry Logs] [{log.get("severity_text")}] {log.get("body")}'
949
+ f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}"
960
950
  )
961
951
 
962
952
  before_send_log = get_before_send_log(self.options)
@@ -969,8 +959,68 @@ class _Client(BaseClient):
969
959
  if self.log_batcher:
970
960
  self.log_batcher.add(log)
971
961
 
962
+ def _capture_metric(self, metric):
963
+ # type: (Optional[Metric]) -> None
964
+ if not has_metrics_enabled(self.options) or metric is None:
965
+ return
966
+
967
+ isolation_scope = sentry_sdk.get_isolation_scope()
968
+
969
+ metric["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
970
+ metric["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
971
+
972
+ environment = self.options.get("environment")
973
+ if environment is not None and "sentry.environment" not in metric["attributes"]:
974
+ metric["attributes"]["sentry.environment"] = environment
975
+
976
+ release = self.options.get("release")
977
+ if release is not None and "sentry.release" not in metric["attributes"]:
978
+ metric["attributes"]["sentry.release"] = release
979
+
980
+ span = sentry_sdk.get_current_span()
981
+ metric["trace_id"] = "00000000-0000-0000-0000-000000000000"
982
+
983
+ if span:
984
+ metric["trace_id"] = span.trace_id
985
+ metric["span_id"] = span.span_id
986
+ else:
987
+ propagation_context = isolation_scope.get_active_propagation_context()
988
+ if propagation_context and propagation_context.trace_id:
989
+ metric["trace_id"] = propagation_context.trace_id
990
+
991
+ if isolation_scope._user is not None:
992
+ for metric_attribute, user_attribute in (
993
+ ("user.id", "id"),
994
+ ("user.name", "username"),
995
+ ("user.email", "email"),
996
+ ):
997
+ if (
998
+ user_attribute in isolation_scope._user
999
+ and metric_attribute not in metric["attributes"]
1000
+ ):
1001
+ metric["attributes"][metric_attribute] = isolation_scope._user[
1002
+ user_attribute
1003
+ ]
1004
+
1005
+ debug = self.options.get("debug", False)
1006
+ if debug:
1007
+ logger.debug(
1008
+ f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}"
1009
+ )
1010
+
1011
+ before_send_metric = get_before_send_metric(self.options)
1012
+ if before_send_metric is not None:
1013
+ metric = before_send_metric(metric, {})
1014
+
1015
+ if metric is None:
1016
+ return
1017
+
1018
+ if self.metrics_batcher:
1019
+ self.metrics_batcher.add(metric)
1020
+
972
1021
  def capture_session(
973
- self, session # type: Session
1022
+ self,
1023
+ session, # type: Session
974
1024
  ):
975
1025
  # type: (...) -> None
976
1026
  if not session.release:
@@ -991,7 +1041,8 @@ class _Client(BaseClient):
991
1041
  ...
992
1042
 
993
1043
  def get_integration(
994
- self, name_or_class # type: Union[str, Type[Integration]]
1044
+ self,
1045
+ name_or_class, # type: Union[str, Type[Integration]]
995
1046
  ):
996
1047
  # type: (...) -> Optional[Integration]
997
1048
  """Returns the integration for this client by name or class.
@@ -1019,10 +1070,10 @@ class _Client(BaseClient):
1019
1070
  if self.transport is not None:
1020
1071
  self.flush(timeout=timeout, callback=callback)
1021
1072
  self.session_flusher.kill()
1022
- if self.metrics_aggregator is not None:
1023
- self.metrics_aggregator.kill()
1024
1073
  if self.log_batcher is not None:
1025
1074
  self.log_batcher.kill()
1075
+ if self.metrics_batcher is not None:
1076
+ self.metrics_batcher.kill()
1026
1077
  if self.monitor:
1027
1078
  self.monitor.kill()
1028
1079
  self.transport.kill()
@@ -1045,10 +1096,10 @@ class _Client(BaseClient):
1045
1096
  if timeout is None:
1046
1097
  timeout = self.options["shutdown_timeout"]
1047
1098
  self.session_flusher.flush()
1048
- if self.metrics_aggregator is not None:
1049
- self.metrics_aggregator.flush()
1050
1099
  if self.log_batcher is not None:
1051
1100
  self.log_batcher.flush()
1101
+ if self.metrics_batcher is not None:
1102
+ self.metrics_batcher.flush()
1052
1103
  self.transport.flush(timeout=timeout, callback=callback)
1053
1104
 
1054
1105
  def __enter__(self):
sentry_sdk/consts.py CHANGED
@@ -40,6 +40,7 @@ if TYPE_CHECKING:
40
40
  from typing import Any
41
41
  from typing import Sequence
42
42
  from typing import Tuple
43
+ from typing import AbstractSet
43
44
  from typing_extensions import Literal
44
45
  from typing_extensions import TypedDict
45
46
 
@@ -51,11 +52,10 @@ if TYPE_CHECKING:
51
52
  Hint,
52
53
  Log,
53
54
  MeasurementUnit,
55
+ Metric,
54
56
  ProfilerMode,
55
57
  TracesSampler,
56
58
  TransactionProcessor,
57
- MetricTags,
58
- MetricValue,
59
59
  )
60
60
 
61
61
  # Experiments are feature flags to enable and disable certain unstable SDK
@@ -76,13 +76,10 @@ if TYPE_CHECKING:
76
76
  "transport_compression_algo": Optional[CompressionAlgo],
77
77
  "transport_num_pools": Optional[int],
78
78
  "transport_http2": Optional[bool],
79
- "enable_metrics": Optional[bool],
80
- "before_emit_metric": Optional[
81
- Callable[[str, MetricValue, MeasurementUnit, MetricTags], bool]
82
- ],
83
- "metric_code_locations": Optional[bool],
84
79
  "enable_logs": Optional[bool],
85
80
  "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]],
81
+ "enable_metrics": Optional[bool],
82
+ "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]],
86
83
  },
87
84
  total=False,
88
85
  )
@@ -838,6 +835,7 @@ class OP:
838
835
  QUEUE_TASK_HUEY = "queue.task.huey"
839
836
  QUEUE_SUBMIT_RAY = "queue.submit.ray"
840
837
  QUEUE_TASK_RAY = "queue.task.ray"
838
+ QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq"
841
839
  SUBPROCESS = "subprocess"
842
840
  SUBPROCESS_WAIT = "subprocess.wait"
843
841
  SUBPROCESS_COMMUNICATE = "subprocess.communicate"
@@ -852,7 +850,6 @@ class OP:
852
850
  # This type exists to trick mypy and PyCharm into thinking `init` and `Client`
853
851
  # take these arguments (even though they take opaque **kwargs)
854
852
  class ClientConstructor:
855
-
856
853
  def __init__(
857
854
  self,
858
855
  dsn=None, # type: Optional[str]
@@ -920,6 +917,7 @@ class ClientConstructor:
920
917
  max_stack_frames=DEFAULT_MAX_STACK_FRAMES, # type: Optional[int]
921
918
  enable_logs=False, # type: bool
922
919
  before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
920
+ trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
923
921
  ):
924
922
  # type: (...) -> None
925
923
  """Initialize the Sentry SDK with the given parameters. All parameters described here can be used in a call to `sentry_sdk.init()`.
@@ -1308,6 +1306,14 @@ class ClientConstructor:
1308
1306
  function will be retained. If the function returns None, the log will
1309
1307
  not be sent to Sentry.
1310
1308
 
1309
+ :param trace_ignore_status_codes: An optional property that disables tracing for
1310
+ HTTP requests with certain status codes.
1311
+
1312
+ Requests are not traced if the status code is contained in the provided set.
1313
+
1314
+ If `trace_ignore_status_codes` is not provided, requests with any status code
1315
+ may be traced.
1316
+
1311
1317
  :param _experiments:
1312
1318
  """
1313
1319
  pass
@@ -1333,4 +1339,4 @@ DEFAULT_OPTIONS = _get_default_options()
1333
1339
  del _get_default_options
1334
1340
 
1335
1341
 
1336
- VERSION = "2.39.0"
1342
+ VERSION = "2.41.0"