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
sentry_sdk/client.py CHANGED
@@ -1,88 +1,291 @@
1
1
  import os
2
2
  import uuid
3
3
  import random
4
- from datetime import datetime
5
- from itertools import islice
6
4
  import socket
7
-
8
- from sentry_sdk._compat import string_types, text_type, iteritems
5
+ from collections.abc import Mapping
6
+ from datetime import datetime, timezone
7
+ from importlib import import_module
8
+ from typing import TYPE_CHECKING, List, Dict, cast, overload
9
+ import warnings
10
+
11
+ import sentry_sdk
12
+ from sentry_sdk._compat import PY37, check_uwsgi_thread_support
13
+ from sentry_sdk._metrics_batcher import MetricsBatcher
9
14
  from sentry_sdk.utils import (
15
+ AnnotatedValue,
16
+ ContextVar,
10
17
  capture_internal_exceptions,
11
18
  current_stacktrace,
12
- disable_capture_event,
19
+ env_to_bool,
13
20
  format_timestamp,
21
+ get_sdk_name,
14
22
  get_type_name,
23
+ get_default_release,
15
24
  handle_in_app,
25
+ is_gevent,
16
26
  logger,
27
+ get_before_send_log,
28
+ get_before_send_metric,
29
+ has_logs_enabled,
30
+ has_metrics_enabled,
17
31
  )
18
32
  from sentry_sdk.serializer import serialize
19
- from sentry_sdk.transport import make_transport
20
- from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO, ClientConstructor
21
- from sentry_sdk.integrations import setup_integrations
22
- from sentry_sdk.utils import ContextVar
33
+ from sentry_sdk.tracing import trace
34
+ from sentry_sdk.transport import BaseHttpTransport, make_transport
35
+ from sentry_sdk.consts import (
36
+ SPANDATA,
37
+ DEFAULT_MAX_VALUE_LENGTH,
38
+ DEFAULT_OPTIONS,
39
+ INSTRUMENTER,
40
+ VERSION,
41
+ ClientConstructor,
42
+ )
43
+ from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations
44
+ from sentry_sdk.integrations.dedupe import DedupeIntegration
23
45
  from sentry_sdk.sessions import SessionFlusher
24
- from sentry_sdk.envelope import Envelope, Item, PayloadRef
25
-
26
- from sentry_sdk._types import MYPY
46
+ from sentry_sdk.envelope import Envelope
47
+ from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler
48
+ from sentry_sdk.profiler.transaction_profiler import (
49
+ has_profiling_enabled,
50
+ Profile,
51
+ setup_profiler,
52
+ )
53
+ from sentry_sdk.scrubber import EventScrubber
54
+ from sentry_sdk.monitor import Monitor
27
55
 
28
- if MYPY:
56
+ if TYPE_CHECKING:
29
57
  from typing import Any
30
58
  from typing import Callable
31
- from typing import Dict
32
- from typing import List
33
59
  from typing import Optional
60
+ from typing import Sequence
61
+ from typing import Type
62
+ from typing import Union
63
+ from typing import TypeVar
34
64
 
65
+ from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory
66
+ from sentry_sdk.integrations import Integration
35
67
  from sentry_sdk.scope import Scope
36
- from sentry_sdk._types import Event, Hint
37
- from sentry_sdk.sessions import Session
68
+ from sentry_sdk.session import Session
69
+ from sentry_sdk.spotlight import SpotlightClient
70
+ from sentry_sdk.transport import Transport
71
+ from sentry_sdk._log_batcher import LogBatcher
72
+ from sentry_sdk._metrics_batcher import MetricsBatcher
38
73
 
74
+ I = TypeVar("I", bound=Integration) # noqa: E741
39
75
 
40
76
  _client_init_debug = ContextVar("client_init_debug")
41
77
 
42
78
 
79
+ SDK_INFO = {
80
+ "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations()
81
+ "version": VERSION,
82
+ "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
83
+ } # type: SDKInfo
84
+
85
+
43
86
  def _get_options(*args, **kwargs):
44
87
  # type: (*Optional[str], **Any) -> Dict[str, Any]
45
- if args and (isinstance(args[0], (text_type, bytes, str)) or args[0] is None):
88
+ if args and (isinstance(args[0], (bytes, str)) or args[0] is None):
46
89
  dsn = args[0] # type: Optional[str]
47
90
  args = args[1:]
48
91
  else:
49
92
  dsn = None
50
93
 
94
+ if len(args) > 1:
95
+ raise TypeError("Only single positional argument is expected")
96
+
51
97
  rv = dict(DEFAULT_OPTIONS)
52
98
  options = dict(*args, **kwargs)
53
99
  if dsn is not None and options.get("dsn") is None:
54
100
  options["dsn"] = dsn
55
101
 
56
- for key, value in iteritems(options):
102
+ for key, value in options.items():
57
103
  if key not in rv:
58
104
  raise TypeError("Unknown option %r" % (key,))
105
+
59
106
  rv[key] = value
60
107
 
61
108
  if rv["dsn"] is None:
62
109
  rv["dsn"] = os.environ.get("SENTRY_DSN")
63
110
 
64
111
  if rv["release"] is None:
65
- rv["release"] = os.environ.get("SENTRY_RELEASE")
112
+ rv["release"] = get_default_release()
66
113
 
67
114
  if rv["environment"] is None:
68
- rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT")
115
+ rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production"
116
+
117
+ if rv["debug"] is None:
118
+ rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False
69
119
 
70
120
  if rv["server_name"] is None and hasattr(socket, "gethostname"):
71
121
  rv["server_name"] = socket.gethostname()
72
122
 
123
+ if rv["instrumenter"] is None:
124
+ rv["instrumenter"] = INSTRUMENTER.SENTRY
125
+
126
+ if rv["project_root"] is None:
127
+ try:
128
+ project_root = os.getcwd()
129
+ except Exception:
130
+ project_root = None
131
+
132
+ rv["project_root"] = project_root
133
+
134
+ if rv["enable_tracing"] is True and rv["traces_sample_rate"] is None:
135
+ rv["traces_sample_rate"] = 1.0
136
+
137
+ if rv["event_scrubber"] is None:
138
+ rv["event_scrubber"] = EventScrubber(
139
+ send_default_pii=(
140
+ False if rv["send_default_pii"] is None else rv["send_default_pii"]
141
+ )
142
+ )
143
+
144
+ if rv["socket_options"] and not isinstance(rv["socket_options"], list):
145
+ logger.warning(
146
+ "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format."
147
+ )
148
+ rv["socket_options"] = None
149
+
150
+ if rv["keep_alive"] is None:
151
+ rv["keep_alive"] = (
152
+ env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False
153
+ )
154
+
155
+ if rv["enable_tracing"] is not None:
156
+ warnings.warn(
157
+ "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.",
158
+ DeprecationWarning,
159
+ stacklevel=2,
160
+ )
161
+
73
162
  return rv
74
163
 
75
164
 
76
- class _Client(object):
77
- """The client is internally responsible for capturing the events and
165
+ try:
166
+ # Python 3.6+
167
+ module_not_found_error = ModuleNotFoundError
168
+ except Exception:
169
+ # Older Python versions
170
+ module_not_found_error = ImportError # type: ignore
171
+
172
+
173
+ class BaseClient:
174
+ """
175
+ .. versionadded:: 2.0.0
176
+
177
+ The basic definition of a client that is used for sending data to Sentry.
178
+ """
179
+
180
+ spotlight = None # type: Optional[SpotlightClient]
181
+
182
+ def __init__(self, options=None):
183
+ # type: (Optional[Dict[str, Any]]) -> None
184
+ self.options = options if options is not None else DEFAULT_OPTIONS # type: Dict[str, Any]
185
+
186
+ self.transport = None # type: Optional[Transport]
187
+ self.monitor = None # type: Optional[Monitor]
188
+ self.log_batcher = None # type: Optional[LogBatcher]
189
+ self.metrics_batcher = None # type: Optional[MetricsBatcher]
190
+
191
+ def __getstate__(self, *args, **kwargs):
192
+ # type: (*Any, **Any) -> Any
193
+ return {"options": {}}
194
+
195
+ def __setstate__(self, *args, **kwargs):
196
+ # type: (*Any, **Any) -> None
197
+ pass
198
+
199
+ @property
200
+ def dsn(self):
201
+ # type: () -> Optional[str]
202
+ return None
203
+
204
+ def should_send_default_pii(self):
205
+ # type: () -> bool
206
+ return False
207
+
208
+ def is_active(self):
209
+ # type: () -> bool
210
+ """
211
+ .. versionadded:: 2.0.0
212
+
213
+ Returns whether the client is active (able to send data to Sentry)
214
+ """
215
+ return False
216
+
217
+ def capture_event(self, *args, **kwargs):
218
+ # type: (*Any, **Any) -> Optional[str]
219
+ return None
220
+
221
+ def _capture_log(self, log):
222
+ # type: (Log) -> None
223
+ pass
224
+
225
+ def _capture_metric(self, metric):
226
+ # type: (Metric) -> None
227
+ pass
228
+
229
+ def capture_session(self, *args, **kwargs):
230
+ # type: (*Any, **Any) -> None
231
+ return None
232
+
233
+ if TYPE_CHECKING:
234
+
235
+ @overload
236
+ def get_integration(self, name_or_class):
237
+ # type: (str) -> Optional[Integration]
238
+ ...
239
+
240
+ @overload
241
+ def get_integration(self, name_or_class):
242
+ # type: (type[I]) -> Optional[I]
243
+ ...
244
+
245
+ def get_integration(self, name_or_class):
246
+ # type: (Union[str, type[Integration]]) -> Optional[Integration]
247
+ return None
248
+
249
+ def close(self, *args, **kwargs):
250
+ # type: (*Any, **Any) -> None
251
+ return None
252
+
253
+ def flush(self, *args, **kwargs):
254
+ # type: (*Any, **Any) -> None
255
+ return None
256
+
257
+ def __enter__(self):
258
+ # type: () -> BaseClient
259
+ return self
260
+
261
+ def __exit__(self, exc_type, exc_value, tb):
262
+ # type: (Any, Any, Any) -> None
263
+ return None
264
+
265
+
266
+ class NonRecordingClient(BaseClient):
267
+ """
268
+ .. versionadded:: 2.0.0
269
+
270
+ A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized.
271
+ """
272
+
273
+ pass
274
+
275
+
276
+ class _Client(BaseClient):
277
+ """
278
+ The client is internally responsible for capturing the events and
78
279
  forwarding them to sentry through the configured transport. It takes
79
280
  the client options as keyword arguments and optionally the DSN as first
80
281
  argument.
282
+
283
+ Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support)
81
284
  """
82
285
 
83
286
  def __init__(self, *args, **kwargs):
84
287
  # type: (*Any, **Any) -> None
85
- self.options = get_options(*args, **kwargs) # type: Dict[str, Any]
288
+ super(_Client, self).__init__(options=get_options(*args, **kwargs))
86
289
  self._init_impl()
87
290
 
88
291
  def __getstate__(self):
@@ -94,47 +297,213 @@ class _Client(object):
94
297
  self.options = state["options"]
95
298
  self._init_impl()
96
299
 
300
+ def _setup_instrumentation(self, functions_to_trace):
301
+ # type: (Sequence[Dict[str, str]]) -> None
302
+ """
303
+ Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator.
304
+ """
305
+ for function in functions_to_trace:
306
+ class_name = None
307
+ function_qualname = function["qualified_name"]
308
+ module_name, function_name = function_qualname.rsplit(".", 1)
309
+
310
+ try:
311
+ # Try to import module and function
312
+ # ex: "mymodule.submodule.funcname"
313
+
314
+ module_obj = import_module(module_name)
315
+ function_obj = getattr(module_obj, function_name)
316
+ setattr(module_obj, function_name, trace(function_obj))
317
+ logger.debug("Enabled tracing for %s", function_qualname)
318
+ except module_not_found_error:
319
+ try:
320
+ # Try to import a class
321
+ # ex: "mymodule.submodule.MyClassName.member_function"
322
+
323
+ module_name, class_name = module_name.rsplit(".", 1)
324
+ module_obj = import_module(module_name)
325
+ class_obj = getattr(module_obj, class_name)
326
+ function_obj = getattr(class_obj, function_name)
327
+ function_type = type(class_obj.__dict__[function_name])
328
+ traced_function = trace(function_obj)
329
+
330
+ if function_type in (staticmethod, classmethod):
331
+ traced_function = staticmethod(traced_function)
332
+
333
+ setattr(class_obj, function_name, traced_function)
334
+ setattr(module_obj, class_name, class_obj)
335
+ logger.debug("Enabled tracing for %s", function_qualname)
336
+
337
+ except Exception as e:
338
+ logger.warning(
339
+ "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
340
+ function_qualname,
341
+ e,
342
+ )
343
+
344
+ except Exception as e:
345
+ logger.warning(
346
+ "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
347
+ function_qualname,
348
+ e,
349
+ )
350
+
97
351
  def _init_impl(self):
98
352
  # type: () -> None
99
353
  old_debug = _client_init_debug.get(False)
100
354
 
101
- def _send_sessions(sessions):
102
- # type: (List[Any]) -> None
103
- transport = self.transport
104
- if not transport or not sessions:
105
- return
106
- sessions_iter = iter(sessions)
107
- while True:
108
- envelope = Envelope()
109
- for session in islice(sessions_iter, 100):
110
- envelope.add_session(session)
111
- if not envelope.items:
112
- break
113
- transport.capture_envelope(envelope)
355
+ def _capture_envelope(envelope):
356
+ # type: (Envelope) -> None
357
+ if self.transport is not None:
358
+ self.transport.capture_envelope(envelope)
359
+
360
+ def _record_lost_event(
361
+ reason, # type: str
362
+ data_category, # type: EventDataCategory
363
+ quantity=1, # type: int
364
+ ):
365
+ # type: (...) -> None
366
+ if self.transport is not None:
367
+ self.transport.record_lost_event(
368
+ reason=reason,
369
+ data_category=data_category,
370
+ quantity=quantity,
371
+ )
114
372
 
115
373
  try:
116
374
  _client_init_debug.set(self.options["debug"])
117
375
  self.transport = make_transport(self.options)
118
- self.session_flusher = SessionFlusher(flush_func=_send_sessions)
119
376
 
120
- request_bodies = ("always", "never", "small", "medium")
121
- if self.options["request_bodies"] not in request_bodies:
377
+ self.monitor = None
378
+ if self.transport:
379
+ if self.options["enable_backpressure_handling"]:
380
+ self.monitor = Monitor(self.transport)
381
+
382
+ self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
383
+
384
+ self.log_batcher = None
385
+
386
+ if has_logs_enabled(self.options):
387
+ from sentry_sdk._log_batcher import LogBatcher
388
+
389
+ self.log_batcher = LogBatcher(
390
+ capture_func=_capture_envelope,
391
+ record_lost_func=_record_lost_event,
392
+ )
393
+
394
+ self.metrics_batcher = None
395
+ if has_metrics_enabled(self.options):
396
+ self.metrics_batcher = MetricsBatcher(
397
+ capture_func=_capture_envelope,
398
+ record_lost_func=_record_lost_event,
399
+ )
400
+
401
+ max_request_body_size = ("always", "never", "small", "medium")
402
+ if self.options["max_request_body_size"] not in max_request_body_size:
122
403
  raise ValueError(
123
- "Invalid value for request_bodies. Must be one of {}".format(
124
- request_bodies
404
+ "Invalid value for max_request_body_size. Must be one of {}".format(
405
+ max_request_body_size
125
406
  )
126
407
  )
127
408
 
409
+ if self.options["_experiments"].get("otel_powered_performance", False):
410
+ logger.debug(
411
+ "[OTel] Enabling experimental OTel-powered performance monitoring."
412
+ )
413
+ self.options["instrumenter"] = INSTRUMENTER.OTEL
414
+ if (
415
+ "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration"
416
+ not in _DEFAULT_INTEGRATIONS
417
+ ):
418
+ _DEFAULT_INTEGRATIONS.append(
419
+ "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration",
420
+ )
421
+
128
422
  self.integrations = setup_integrations(
129
423
  self.options["integrations"],
130
424
  with_defaults=self.options["default_integrations"],
131
- with_auto_enabling_integrations=self.options["_experiments"].get(
132
- "auto_enabling_integrations", False
133
- ),
425
+ with_auto_enabling_integrations=self.options[
426
+ "auto_enabling_integrations"
427
+ ],
428
+ disabled_integrations=self.options["disabled_integrations"],
429
+ options=self.options,
134
430
  )
431
+
432
+ spotlight_config = self.options.get("spotlight")
433
+ if spotlight_config is None and "SENTRY_SPOTLIGHT" in os.environ:
434
+ spotlight_env_value = os.environ["SENTRY_SPOTLIGHT"]
435
+ spotlight_config = env_to_bool(spotlight_env_value, strict=True)
436
+ self.options["spotlight"] = (
437
+ spotlight_config
438
+ if spotlight_config is not None
439
+ else spotlight_env_value
440
+ )
441
+
442
+ if self.options.get("spotlight"):
443
+ # This is intentionally here to prevent setting up spotlight
444
+ # stuff we don't need unless spotlight is explicitly enabled
445
+ from sentry_sdk.spotlight import setup_spotlight
446
+
447
+ self.spotlight = setup_spotlight(self.options)
448
+ if not self.options["dsn"]:
449
+ sample_all = lambda *_args, **_kwargs: 1.0
450
+ self.options["send_default_pii"] = True
451
+ self.options["error_sampler"] = sample_all
452
+ self.options["traces_sampler"] = sample_all
453
+ self.options["profiles_sampler"] = sample_all
454
+
455
+ sdk_name = get_sdk_name(list(self.integrations.keys()))
456
+ SDK_INFO["name"] = sdk_name
457
+ logger.debug("Setting SDK name to '%s'", sdk_name)
458
+
459
+ if has_profiling_enabled(self.options):
460
+ try:
461
+ setup_profiler(self.options)
462
+ except Exception as e:
463
+ logger.debug("Can not set up profiler. (%s)", e)
464
+ else:
465
+ try:
466
+ setup_continuous_profiler(
467
+ self.options,
468
+ sdk_info=SDK_INFO,
469
+ capture_func=_capture_envelope,
470
+ )
471
+ except Exception as e:
472
+ logger.debug("Can not set up continuous profiler. (%s)", e)
473
+
135
474
  finally:
136
475
  _client_init_debug.set(old_debug)
137
476
 
477
+ self._setup_instrumentation(self.options.get("functions_to_trace", []))
478
+
479
+ if (
480
+ self.monitor
481
+ or self.log_batcher
482
+ or has_profiling_enabled(self.options)
483
+ or isinstance(self.transport, BaseHttpTransport)
484
+ ):
485
+ # If we have anything on that could spawn a background thread, we
486
+ # need to check if it's safe to use them.
487
+ check_uwsgi_thread_support()
488
+
489
+ def is_active(self):
490
+ # type: () -> bool
491
+ """
492
+ .. versionadded:: 2.0.0
493
+
494
+ Returns whether the client is active (able to send data to Sentry)
495
+ """
496
+ return True
497
+
498
+ def should_send_default_pii(self):
499
+ # type: () -> bool
500
+ """
501
+ .. versionadded:: 2.0.0
502
+
503
+ Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry.
504
+ """
505
+ return self.options.get("send_default_pii") or False
506
+
138
507
  @property
139
508
  def dsn(self):
140
509
  # type: () -> Optional[str]
@@ -144,24 +513,64 @@ class _Client(object):
144
513
  def _prepare_event(
145
514
  self,
146
515
  event, # type: Event
147
- hint, # type: Optional[Hint]
516
+ hint, # type: Hint
148
517
  scope, # type: Optional[Scope]
149
518
  ):
150
519
  # type: (...) -> Optional[Event]
151
520
 
521
+ previous_total_spans = None # type: Optional[int]
522
+ previous_total_breadcrumbs = None # type: Optional[int]
523
+
152
524
  if event.get("timestamp") is None:
153
- event["timestamp"] = datetime.utcnow()
525
+ event["timestamp"] = datetime.now(timezone.utc)
154
526
 
155
- hint = dict(hint or ()) # type: Hint
527
+ is_transaction = event.get("type") == "transaction"
156
528
 
157
529
  if scope is not None:
158
- event_ = scope.apply_to_event(event, hint)
530
+ spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
531
+ event_ = scope.apply_to_event(event, hint, self.options)
532
+
533
+ # one of the event/error processors returned None
159
534
  if event_ is None:
535
+ if self.transport:
536
+ self.transport.record_lost_event(
537
+ "event_processor",
538
+ data_category=("transaction" if is_transaction else "error"),
539
+ )
540
+ if is_transaction:
541
+ self.transport.record_lost_event(
542
+ "event_processor",
543
+ data_category="span",
544
+ quantity=spans_before + 1, # +1 for the transaction itself
545
+ )
160
546
  return None
547
+
161
548
  event = event_
549
+ spans_delta = spans_before - len(
550
+ cast(List[Dict[str, object]], event.get("spans", []))
551
+ )
552
+ if is_transaction and spans_delta > 0 and self.transport is not None:
553
+ self.transport.record_lost_event(
554
+ "event_processor", data_category="span", quantity=spans_delta
555
+ )
556
+
557
+ dropped_spans = event.pop("_dropped_spans", 0) + spans_delta # type: int
558
+ if dropped_spans > 0:
559
+ previous_total_spans = spans_before + dropped_spans
560
+ if scope._n_breadcrumbs_truncated > 0:
561
+ breadcrumbs = event.get("breadcrumbs", {})
562
+ values = (
563
+ breadcrumbs.get("values", [])
564
+ if not isinstance(breadcrumbs, AnnotatedValue)
565
+ else []
566
+ )
567
+ previous_total_breadcrumbs = (
568
+ len(values) + scope._n_breadcrumbs_truncated
569
+ )
162
570
 
163
571
  if (
164
- self.options["attach_stacktrace"]
572
+ not is_transaction
573
+ and self.options["attach_stacktrace"]
165
574
  and "exception" not in event
166
575
  and "stacktrace" not in event
167
576
  and "threads" not in event
@@ -171,7 +580,12 @@ class _Client(object):
171
580
  "values": [
172
581
  {
173
582
  "stacktrace": current_stacktrace(
174
- self.options["with_locals"]
583
+ include_local_variables=self.options.get(
584
+ "include_local_variables", True
585
+ ),
586
+ max_value_length=self.options.get(
587
+ "max_value_length", DEFAULT_MAX_VALUE_LENGTH
588
+ ),
175
589
  ),
176
590
  "crashed": False,
177
591
  "current": True,
@@ -181,7 +595,7 @@ class _Client(object):
181
595
 
182
596
  for key in "release", "environment", "server_name", "dist":
183
597
  if event.get(key) is None and self.options[key] is not None:
184
- event[key] = text_type(self.options[key]).strip()
598
+ event[key] = str(self.options[key]).strip()
185
599
  if event.get("sdk") is None:
186
600
  sdk_info = dict(SDK_INFO)
187
601
  sdk_info["integrations"] = sorted(self.integrations.keys())
@@ -191,27 +605,109 @@ class _Client(object):
191
605
  event["platform"] = "python"
192
606
 
193
607
  event = handle_in_app(
194
- event, self.options["in_app_exclude"], self.options["in_app_include"]
608
+ event,
609
+ self.options["in_app_exclude"],
610
+ self.options["in_app_include"],
611
+ self.options["project_root"],
195
612
  )
196
613
 
614
+ if event is not None:
615
+ event_scrubber = self.options["event_scrubber"]
616
+ if event_scrubber:
617
+ event_scrubber.scrub_event(event)
618
+
619
+ if scope is not None and scope._gen_ai_original_message_count:
620
+ spans = event.get("spans", []) # type: List[Dict[str, Any]] | AnnotatedValue
621
+ if isinstance(spans, list):
622
+ for span in spans:
623
+ span_id = span.get("span_id", None)
624
+ span_data = span.get("data", {})
625
+ if (
626
+ span_id
627
+ and span_id in scope._gen_ai_original_message_count
628
+ and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data
629
+ ):
630
+ span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue(
631
+ span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES],
632
+ {"len": scope._gen_ai_original_message_count[span_id]},
633
+ )
634
+ if previous_total_spans is not None:
635
+ event["spans"] = AnnotatedValue(
636
+ event.get("spans", []), {"len": previous_total_spans}
637
+ )
638
+ if previous_total_breadcrumbs is not None:
639
+ event["breadcrumbs"] = AnnotatedValue(
640
+ event.get("breadcrumbs", {"values": []}),
641
+ {"len": previous_total_breadcrumbs},
642
+ )
643
+
197
644
  # Postprocess the event here so that annotated types do
198
645
  # generally not surface in before_send
199
646
  if event is not None:
200
- event = serialize(
201
- event,
202
- smart_transaction_trimming=self.options["_experiments"].get(
203
- "smart_transaction_trimming"
647
+ event = cast(
648
+ "Event",
649
+ serialize(
650
+ cast("Dict[str, Any]", event),
651
+ max_request_body_size=self.options.get("max_request_body_size"),
652
+ max_value_length=self.options.get("max_value_length"),
653
+ custom_repr=self.options.get("custom_repr"),
204
654
  ),
205
655
  )
206
656
 
207
657
  before_send = self.options["before_send"]
208
- if before_send is not None and event.get("type") != "transaction":
658
+ if (
659
+ before_send is not None
660
+ and event is not None
661
+ and event.get("type") != "transaction"
662
+ ):
209
663
  new_event = None
210
664
  with capture_internal_exceptions():
211
665
  new_event = before_send(event, hint or {})
212
666
  if new_event is None:
213
- logger.info("before send dropped event (%s)", event)
214
- event = new_event # type: ignore
667
+ logger.info("before send dropped event")
668
+ if self.transport:
669
+ self.transport.record_lost_event(
670
+ "before_send", data_category="error"
671
+ )
672
+
673
+ # If this is an exception, reset the DedupeIntegration. It still
674
+ # remembers the dropped exception as the last exception, meaning
675
+ # that if the same exception happens again and is not dropped
676
+ # in before_send, it'd get dropped by DedupeIntegration.
677
+ if event.get("exception"):
678
+ DedupeIntegration.reset_last_seen()
679
+
680
+ event = new_event
681
+
682
+ before_send_transaction = self.options["before_send_transaction"]
683
+ if (
684
+ before_send_transaction is not None
685
+ and event is not None
686
+ and event.get("type") == "transaction"
687
+ ):
688
+ new_event = None
689
+ spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
690
+ with capture_internal_exceptions():
691
+ new_event = before_send_transaction(event, hint or {})
692
+ if new_event is None:
693
+ logger.info("before send transaction dropped event")
694
+ if self.transport:
695
+ self.transport.record_lost_event(
696
+ reason="before_send", data_category="transaction"
697
+ )
698
+ self.transport.record_lost_event(
699
+ reason="before_send",
700
+ data_category="span",
701
+ quantity=spans_before + 1, # +1 for the transaction itself
702
+ )
703
+ else:
704
+ spans_delta = spans_before - len(new_event.get("spans", []))
705
+ if spans_delta > 0 and self.transport is not None:
706
+ self.transport.record_lost_event(
707
+ reason="before_send", data_category="span", quantity=spans_delta
708
+ )
709
+
710
+ event = new_event
215
711
 
216
712
  return event
217
713
 
@@ -221,17 +717,18 @@ class _Client(object):
221
717
  if exc_info is None:
222
718
  return False
223
719
 
224
- type_name = get_type_name(exc_info[0])
225
- full_name = "%s.%s" % (exc_info[0].__module__, type_name)
720
+ error = exc_info[0]
721
+ error_type_name = get_type_name(exc_info[0])
722
+ error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name)
226
723
 
227
- for errcls in self.options["ignore_errors"]:
724
+ for ignored_error in self.options["ignore_errors"]:
228
725
  # String types are matched against the type name in the
229
726
  # exception only
230
- if isinstance(errcls, string_types):
231
- if errcls == full_name or errcls == type_name:
727
+ if isinstance(ignored_error, str):
728
+ if ignored_error == error_full_name or ignored_error == error_type_name:
232
729
  return True
233
730
  else:
234
- if issubclass(exc_info[0], errcls):
731
+ if issubclass(error, ignored_error):
235
732
  return True
236
733
 
237
734
  return False
@@ -243,20 +740,65 @@ class _Client(object):
243
740
  scope=None, # type: Optional[Scope]
244
741
  ):
245
742
  # type: (...) -> bool
246
- if event.get("type") == "transaction":
247
- # Transactions are sampled independent of error events.
743
+ # Transactions are sampled independent of error events.
744
+ is_transaction = event.get("type") == "transaction"
745
+ if is_transaction:
248
746
  return True
249
747
 
250
- if scope is not None and not scope._should_capture:
748
+ ignoring_prevents_recursion = scope is not None and not scope._should_capture
749
+ if ignoring_prevents_recursion:
251
750
  return False
252
751
 
253
- if (
254
- self.options["sample_rate"] < 1.0
255
- and random.random() >= self.options["sample_rate"]
256
- ):
752
+ ignored_by_config_option = self._is_ignored_error(event, hint)
753
+ if ignored_by_config_option:
257
754
  return False
258
755
 
259
- if self._is_ignored_error(event, hint):
756
+ return True
757
+
758
+ def _should_sample_error(
759
+ self,
760
+ event, # type: Event
761
+ hint, # type: Hint
762
+ ):
763
+ # type: (...) -> bool
764
+ error_sampler = self.options.get("error_sampler", None)
765
+
766
+ if callable(error_sampler):
767
+ with capture_internal_exceptions():
768
+ sample_rate = error_sampler(event, hint)
769
+ else:
770
+ sample_rate = self.options["sample_rate"]
771
+
772
+ try:
773
+ not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate
774
+ except NameError:
775
+ logger.warning(
776
+ "The provided error_sampler raised an error. Defaulting to sampling the event."
777
+ )
778
+
779
+ # If the error_sampler raised an error, we should sample the event, since the default behavior
780
+ # (when no sample_rate or error_sampler is provided) is to sample all events.
781
+ not_in_sample_rate = False
782
+ except TypeError:
783
+ parameter, verb = (
784
+ ("error_sampler", "returned")
785
+ if callable(error_sampler)
786
+ else ("sample_rate", "contains")
787
+ )
788
+ logger.warning(
789
+ "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event."
790
+ % (parameter, verb, repr(sample_rate))
791
+ )
792
+
793
+ # If the sample_rate has an invalid value, we should sample the event, since the default behavior
794
+ # (when no sample_rate or error_sampler is provided) is to sample all events.
795
+ not_in_sample_rate = False
796
+
797
+ if not_in_sample_rate:
798
+ # because we will not sample this event, record a "lost event".
799
+ if self.transport:
800
+ self.transport.record_lost_event("sample_rate", data_category="error")
801
+
260
802
  return False
261
803
 
262
804
  return True
@@ -276,8 +818,10 @@ class _Client(object):
276
818
  if exceptions:
277
819
  errored = True
278
820
  for error in exceptions:
821
+ if isinstance(error, AnnotatedValue):
822
+ error = error.value or {}
279
823
  mechanism = error.get("mechanism")
280
- if mechanism and mechanism.get("handled") is False:
824
+ if isinstance(mechanism, Mapping) and mechanism.get("handled") is False:
281
825
  crashed = True
282
826
  break
283
827
 
@@ -285,7 +829,8 @@ class _Client(object):
285
829
 
286
830
  if session.user_agent is None:
287
831
  headers = (event.get("request") or {}).get("headers")
288
- for (k, v) in iteritems(headers or {}):
832
+ headers_dict = headers if isinstance(headers, dict) else {}
833
+ for k, v in headers_dict.items():
289
834
  if k.lower() == "user-agent":
290
835
  user_agent = v
291
836
  break
@@ -310,20 +855,20 @@ class _Client(object):
310
855
 
311
856
  :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object.
312
857
 
858
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
859
+
313
860
  :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help.
314
861
  """
315
- if disable_capture_event.get(False):
316
- return None
862
+ hint = dict(hint or ()) # type: Hint
317
863
 
318
- if self.transport is None:
864
+ if not self._should_capture(event, hint, scope):
319
865
  return None
320
- if hint is None:
321
- hint = {}
866
+
867
+ profile = event.pop("profile", None)
868
+
322
869
  event_id = event.get("event_id")
323
870
  if event_id is None:
324
871
  event["event_id"] = event_id = uuid.uuid4().hex
325
- if not self._should_capture(event, hint, scope):
326
- return None
327
872
  event_opt = self._prepare_event(event, hint, scope)
328
873
  if event_opt is None:
329
874
  return None
@@ -334,26 +879,189 @@ class _Client(object):
334
879
  if session:
335
880
  self._update_session_from_event(session, event)
336
881
 
337
- if event_opt.get("type") == "transaction":
338
- # Transactions should go to the /envelope/ endpoint.
339
- self.transport.capture_envelope(
340
- Envelope(
341
- headers={
342
- "event_id": event_opt["event_id"],
343
- "sent_at": format_timestamp(datetime.utcnow()),
344
- },
345
- items=[
346
- Item(payload=PayloadRef(json=event_opt), type="transaction"),
347
- ],
348
- )
349
- )
882
+ is_transaction = event_opt.get("type") == "transaction"
883
+ is_checkin = event_opt.get("type") == "check_in"
884
+
885
+ if (
886
+ not is_transaction
887
+ and not is_checkin
888
+ and not self._should_sample_error(event, hint)
889
+ ):
890
+ return None
891
+
892
+ attachments = hint.get("attachments")
893
+
894
+ trace_context = event_opt.get("contexts", {}).get("trace") or {}
895
+ dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {})
896
+
897
+ headers = {
898
+ "event_id": event_opt["event_id"],
899
+ "sent_at": format_timestamp(datetime.now(timezone.utc)),
900
+ } # type: dict[str, object]
901
+
902
+ if dynamic_sampling_context:
903
+ headers["trace"] = dynamic_sampling_context
904
+
905
+ envelope = Envelope(headers=headers)
906
+
907
+ if is_transaction:
908
+ if isinstance(profile, Profile):
909
+ envelope.add_profile(profile.to_json(event_opt, self.options))
910
+ envelope.add_transaction(event_opt)
911
+ elif is_checkin:
912
+ envelope.add_checkin(event_opt)
350
913
  else:
351
- # All other events go to the /store/ endpoint.
352
- self.transport.capture_event(event_opt)
353
- return event_id
914
+ envelope.add_event(event_opt)
915
+
916
+ for attachment in attachments or ():
917
+ envelope.add_item(attachment.to_envelope_item())
918
+
919
+ return_value = None
920
+ if self.spotlight:
921
+ self.spotlight.capture_envelope(envelope)
922
+ return_value = event_id
923
+
924
+ if self.transport is not None:
925
+ self.transport.capture_envelope(envelope)
926
+ return_value = event_id
927
+
928
+ return return_value
929
+
930
+ def _capture_log(self, log):
931
+ # type: (Optional[Log]) -> None
932
+ if not has_logs_enabled(self.options) or log is None:
933
+ return
934
+
935
+ current_scope = sentry_sdk.get_current_scope()
936
+ isolation_scope = sentry_sdk.get_isolation_scope()
937
+
938
+ log["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
939
+ log["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
940
+
941
+ server_name = self.options.get("server_name")
942
+ if server_name is not None and SPANDATA.SERVER_ADDRESS not in log["attributes"]:
943
+ log["attributes"][SPANDATA.SERVER_ADDRESS] = server_name
944
+
945
+ environment = self.options.get("environment")
946
+ if environment is not None and "sentry.environment" not in log["attributes"]:
947
+ log["attributes"]["sentry.environment"] = environment
948
+
949
+ release = self.options.get("release")
950
+ if release is not None and "sentry.release" not in log["attributes"]:
951
+ log["attributes"]["sentry.release"] = release
952
+
953
+ trace_context = current_scope.get_trace_context()
954
+ trace_id = trace_context.get("trace_id")
955
+ span_id = trace_context.get("span_id")
956
+
957
+ if trace_id is not None and log.get("trace_id") is None:
958
+ log["trace_id"] = trace_id
959
+
960
+ if (
961
+ span_id is not None
962
+ and "sentry.trace.parent_span_id" not in log["attributes"]
963
+ ):
964
+ log["attributes"]["sentry.trace.parent_span_id"] = span_id
965
+
966
+ # The user, if present, is always set on the isolation scope.
967
+ if isolation_scope._user is not None:
968
+ for log_attribute, user_attribute in (
969
+ ("user.id", "id"),
970
+ ("user.name", "username"),
971
+ ("user.email", "email"),
972
+ ):
973
+ if (
974
+ user_attribute in isolation_scope._user
975
+ and log_attribute not in log["attributes"]
976
+ ):
977
+ log["attributes"][log_attribute] = isolation_scope._user[
978
+ user_attribute
979
+ ]
980
+
981
+ # If debug is enabled, log the log to the console
982
+ debug = self.options.get("debug", False)
983
+ if debug:
984
+ logger.debug(
985
+ f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}"
986
+ )
987
+
988
+ before_send_log = get_before_send_log(self.options)
989
+ if before_send_log is not None:
990
+ log = before_send_log(log, {})
991
+
992
+ if log is None:
993
+ return
994
+
995
+ if self.log_batcher:
996
+ self.log_batcher.add(log)
997
+
998
+ def _capture_metric(self, metric):
999
+ # type: (Optional[Metric]) -> None
1000
+ if not has_metrics_enabled(self.options) or metric is None:
1001
+ return
1002
+
1003
+ current_scope = sentry_sdk.get_current_scope()
1004
+ isolation_scope = sentry_sdk.get_isolation_scope()
1005
+
1006
+ metric["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
1007
+ metric["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
1008
+
1009
+ server_name = self.options.get("server_name")
1010
+ if (
1011
+ server_name is not None
1012
+ and SPANDATA.SERVER_ADDRESS not in metric["attributes"]
1013
+ ):
1014
+ metric["attributes"][SPANDATA.SERVER_ADDRESS] = server_name
1015
+
1016
+ environment = self.options.get("environment")
1017
+ if environment is not None and "sentry.environment" not in metric["attributes"]:
1018
+ metric["attributes"]["sentry.environment"] = environment
1019
+
1020
+ release = self.options.get("release")
1021
+ if release is not None and "sentry.release" not in metric["attributes"]:
1022
+ metric["attributes"]["sentry.release"] = release
1023
+
1024
+ trace_context = current_scope.get_trace_context()
1025
+ trace_id = trace_context.get("trace_id")
1026
+ span_id = trace_context.get("span_id")
1027
+
1028
+ metric["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000"
1029
+ if span_id is not None:
1030
+ metric["span_id"] = span_id
1031
+
1032
+ if isolation_scope._user is not None:
1033
+ for metric_attribute, user_attribute in (
1034
+ ("user.id", "id"),
1035
+ ("user.name", "username"),
1036
+ ("user.email", "email"),
1037
+ ):
1038
+ if (
1039
+ user_attribute in isolation_scope._user
1040
+ and metric_attribute not in metric["attributes"]
1041
+ ):
1042
+ metric["attributes"][metric_attribute] = isolation_scope._user[
1043
+ user_attribute
1044
+ ]
1045
+
1046
+ debug = self.options.get("debug", False)
1047
+ if debug:
1048
+ logger.debug(
1049
+ f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}"
1050
+ )
1051
+
1052
+ before_send_metric = get_before_send_metric(self.options)
1053
+ if before_send_metric is not None:
1054
+ metric = before_send_metric(metric, {})
1055
+
1056
+ if metric is None:
1057
+ return
1058
+
1059
+ if self.metrics_batcher:
1060
+ self.metrics_batcher.add(metric)
354
1061
 
355
1062
  def capture_session(
356
- self, session # type: Session
1063
+ self,
1064
+ session, # type: Session
357
1065
  ):
358
1066
  # type: (...) -> None
359
1067
  if not session.release:
@@ -361,6 +1069,35 @@ class _Client(object):
361
1069
  else:
362
1070
  self.session_flusher.add_session(session)
363
1071
 
1072
+ if TYPE_CHECKING:
1073
+
1074
+ @overload
1075
+ def get_integration(self, name_or_class):
1076
+ # type: (str) -> Optional[Integration]
1077
+ ...
1078
+
1079
+ @overload
1080
+ def get_integration(self, name_or_class):
1081
+ # type: (type[I]) -> Optional[I]
1082
+ ...
1083
+
1084
+ def get_integration(
1085
+ self,
1086
+ name_or_class, # type: Union[str, Type[Integration]]
1087
+ ):
1088
+ # type: (...) -> Optional[Integration]
1089
+ """Returns the integration for this client by name or class.
1090
+ If the client does not have that integration then `None` is returned.
1091
+ """
1092
+ if isinstance(name_or_class, str):
1093
+ integration_name = name_or_class
1094
+ elif name_or_class.identifier is not None:
1095
+ integration_name = name_or_class.identifier
1096
+ else:
1097
+ raise ValueError("Integration has no name")
1098
+
1099
+ return self.integrations.get(integration_name)
1100
+
364
1101
  def close(
365
1102
  self,
366
1103
  timeout=None, # type: Optional[float]
@@ -374,6 +1111,12 @@ class _Client(object):
374
1111
  if self.transport is not None:
375
1112
  self.flush(timeout=timeout, callback=callback)
376
1113
  self.session_flusher.kill()
1114
+ if self.log_batcher is not None:
1115
+ self.log_batcher.kill()
1116
+ if self.metrics_batcher is not None:
1117
+ self.metrics_batcher.kill()
1118
+ if self.monitor:
1119
+ self.monitor.kill()
377
1120
  self.transport.kill()
378
1121
  self.transport = None
379
1122
 
@@ -394,6 +1137,10 @@ class _Client(object):
394
1137
  if timeout is None:
395
1138
  timeout = self.options["shutdown_timeout"]
396
1139
  self.session_flusher.flush()
1140
+ if self.log_batcher is not None:
1141
+ self.log_batcher.flush()
1142
+ if self.metrics_batcher is not None:
1143
+ self.metrics_batcher.flush()
397
1144
  self.transport.flush(timeout=timeout, callback=callback)
398
1145
 
399
1146
  def __enter__(self):
@@ -405,9 +1152,9 @@ class _Client(object):
405
1152
  self.close()
406
1153
 
407
1154
 
408
- from sentry_sdk._types import MYPY
1155
+ from typing import TYPE_CHECKING
409
1156
 
410
- if MYPY:
1157
+ if TYPE_CHECKING:
411
1158
  # Make mypy, PyCharm and other static analyzers think `get_options` is a
412
1159
  # type to have nicer autocompletion for params.
413
1160
  #
@@ -420,7 +1167,6 @@ if MYPY:
420
1167
  class Client(ClientConstructor, _Client):
421
1168
  pass
422
1169
 
423
-
424
1170
  else:
425
1171
  # Alias `get_options` for actual usage. Go through the lambda indirection
426
1172
  # to throw PyCharm off of the weakly typed signature (it would otherwise