sentry-sdk 0.7.5__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 -30
  2. sentry_sdk/_compat.py +74 -61
  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 +289 -0
  8. sentry_sdk/_types.py +338 -0
  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 +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  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 +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  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 +134 -0
  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 +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  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 +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  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 +141 -0
  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 +112 -68
  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 +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/client.py CHANGED
@@ -1,132 +1,601 @@
1
1
  import os
2
2
  import uuid
3
3
  import random
4
- from datetime import datetime
4
+ import socket
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
5
10
 
6
- from sentry_sdk._compat import string_types, text_type
11
+ import sentry_sdk
12
+ from sentry_sdk._compat import PY37, check_uwsgi_thread_support
13
+ from sentry_sdk._metrics_batcher import MetricsBatcher
7
14
  from sentry_sdk.utils import (
8
- strip_event_mut,
9
- flatten_metadata,
10
- convert_types,
11
- handle_in_app,
12
- get_type_name,
15
+ AnnotatedValue,
16
+ ContextVar,
13
17
  capture_internal_exceptions,
14
18
  current_stacktrace,
19
+ env_to_bool,
20
+ format_timestamp,
21
+ get_sdk_name,
22
+ get_type_name,
23
+ get_default_release,
24
+ handle_in_app,
25
+ is_gevent,
15
26
  logger,
27
+ get_before_send_log,
28
+ get_before_send_metric,
29
+ has_logs_enabled,
30
+ has_metrics_enabled,
31
+ )
32
+ from sentry_sdk.serializer import serialize
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,
16
42
  )
17
- from sentry_sdk.transport import make_transport
18
- from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO
19
- from sentry_sdk.integrations import setup_integrations
20
- from sentry_sdk.utils import ContextVar
43
+ from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations
44
+ from sentry_sdk.integrations.dedupe import DedupeIntegration
45
+ from sentry_sdk.sessions import SessionFlusher
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
21
55
 
22
- if False:
23
- from sentry_sdk.consts import ClientOptions
24
- from sentry_sdk.scope import Scope
56
+ if TYPE_CHECKING:
25
57
  from typing import Any
26
- from typing import Dict
58
+ from typing import Callable
27
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
64
+
65
+ from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory
66
+ from sentry_sdk.integrations import Integration
67
+ from sentry_sdk.scope import Scope
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
28
73
 
74
+ I = TypeVar("I", bound=Integration) # noqa: E741
29
75
 
30
76
  _client_init_debug = ContextVar("client_init_debug")
31
77
 
32
78
 
33
- def get_options(*args, **kwargs):
34
- # type: (*str, **ClientOptions) -> ClientOptions
35
- if args and (isinstance(args[0], string_types) or args[0] is None):
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
+
86
+ def _get_options(*args, **kwargs):
87
+ # type: (*Optional[str], **Any) -> Dict[str, Any]
88
+ if args and (isinstance(args[0], (bytes, str)) or args[0] is None):
36
89
  dsn = args[0] # type: Optional[str]
37
90
  args = args[1:]
38
91
  else:
39
92
  dsn = None
40
93
 
94
+ if len(args) > 1:
95
+ raise TypeError("Only single positional argument is expected")
96
+
41
97
  rv = dict(DEFAULT_OPTIONS)
42
- options = dict(*args, **kwargs) # type: ignore
98
+ options = dict(*args, **kwargs)
43
99
  if dsn is not None and options.get("dsn") is None:
44
- options["dsn"] = dsn # type: ignore
100
+ options["dsn"] = dsn
45
101
 
46
102
  for key, value in options.items():
47
103
  if key not in rv:
48
104
  raise TypeError("Unknown option %r" % (key,))
49
- rv[key] = value # type: ignore
105
+
106
+ rv[key] = value
50
107
 
51
108
  if rv["dsn"] is None:
52
109
  rv["dsn"] = os.environ.get("SENTRY_DSN")
53
110
 
54
111
  if rv["release"] is None:
55
- rv["release"] = os.environ.get("SENTRY_RELEASE")
112
+ rv["release"] = get_default_release()
56
113
 
57
114
  if rv["environment"] is None:
58
- 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
119
+
120
+ if rv["server_name"] is None and hasattr(socket, "gethostname"):
121
+ rv["server_name"] = socket.gethostname()
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
+
162
+ return rv
163
+
164
+
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
59
248
 
60
- return rv # type: ignore
249
+ def close(self, *args, **kwargs):
250
+ # type: (*Any, **Any) -> None
251
+ return None
61
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
62
269
 
63
- class Client(object):
64
- """The client is internally responsible for capturing the events and
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
65
279
  forwarding them to sentry through the configured transport. It takes
66
280
  the client options as keyword arguments and optionally the DSN as first
67
281
  argument.
282
+
283
+ Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support)
68
284
  """
69
285
 
70
286
  def __init__(self, *args, **kwargs):
71
- # type: (*str, **ClientOptions) -> None
287
+ # type: (*Any, **Any) -> None
288
+ super(_Client, self).__init__(options=get_options(*args, **kwargs))
289
+ self._init_impl()
290
+
291
+ def __getstate__(self):
292
+ # type: () -> Any
293
+ return {"options": self.options}
294
+
295
+ def __setstate__(self, state):
296
+ # type: (Any) -> None
297
+ self.options = state["options"]
298
+ self._init_impl()
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
+
351
+ def _init_impl(self):
352
+ # type: () -> None
72
353
  old_debug = _client_init_debug.get(False)
354
+
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
+ )
372
+
73
373
  try:
74
- self.options = options = get_options(*args, **kwargs)
75
- _client_init_debug.set(options["debug"])
76
- self.transport = make_transport(options)
374
+ _client_init_debug.set(self.options["debug"])
375
+ self.transport = make_transport(self.options)
376
+
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
+ )
77
400
 
78
- request_bodies = ("always", "never", "small", "medium")
79
- if options["request_bodies"] not in request_bodies:
401
+ max_request_body_size = ("always", "never", "small", "medium")
402
+ if self.options["max_request_body_size"] not in max_request_body_size:
80
403
  raise ValueError(
81
- "Invalid value for request_bodies. Must be one of {}".format(
82
- request_bodies
404
+ "Invalid value for max_request_body_size. Must be one of {}".format(
405
+ max_request_body_size
83
406
  )
84
407
  )
85
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
+
86
422
  self.integrations = setup_integrations(
87
- options["integrations"], with_defaults=options["default_integrations"]
423
+ self.options["integrations"],
424
+ with_defaults=self.options["default_integrations"],
425
+ with_auto_enabling_integrations=self.options[
426
+ "auto_enabling_integrations"
427
+ ],
428
+ disabled_integrations=self.options["disabled_integrations"],
429
+ options=self.options,
88
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
+
89
474
  finally:
90
475
  _client_init_debug.set(old_debug)
91
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
+
92
507
  @property
93
508
  def dsn(self):
509
+ # type: () -> Optional[str]
94
510
  """Returns the configured DSN as string."""
95
511
  return self.options["dsn"]
96
512
 
97
513
  def _prepare_event(
98
514
  self,
99
- event, # type: Dict[str, Any]
100
- hint, # type: Optional[Dict[str, Any]]
515
+ event, # type: Event
516
+ hint, # type: Hint
101
517
  scope, # type: Optional[Scope]
102
518
  ):
103
- # type: (...) -> Optional[Dict[str, Any]]
519
+ # type: (...) -> Optional[Event]
520
+
521
+ previous_total_spans = None # type: Optional[int]
522
+ previous_total_breadcrumbs = None # type: Optional[int]
523
+
104
524
  if event.get("timestamp") is None:
105
- event["timestamp"] = datetime.utcnow()
525
+ event["timestamp"] = datetime.now(timezone.utc)
526
+
527
+ is_transaction = event.get("type") == "transaction"
106
528
 
107
529
  if scope is not None:
108
- event = scope.apply_to_event(event, hint)
109
- if event is None:
110
- return
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
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
+ )
546
+ return None
547
+
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
+ )
111
570
 
112
571
  if (
113
- self.options["attach_stacktrace"]
572
+ not is_transaction
573
+ and self.options["attach_stacktrace"]
114
574
  and "exception" not in event
115
575
  and "stacktrace" not in event
116
576
  and "threads" not in event
117
577
  ):
118
578
  with capture_internal_exceptions():
119
- event["threads"] = [
120
- {
121
- "stacktrace": current_stacktrace(self.options["with_locals"]),
122
- "crashed": False,
123
- "current": True,
124
- }
125
- ]
579
+ event["threads"] = {
580
+ "values": [
581
+ {
582
+ "stacktrace": current_stacktrace(
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
+ ),
589
+ ),
590
+ "crashed": False,
591
+ "current": True,
592
+ }
593
+ ]
594
+ }
126
595
 
127
596
  for key in "release", "environment", "server_name", "dist":
128
- if event.get(key) is None and self.options[key] is not None: # type: ignore
129
- event[key] = text_type(self.options[key]).strip() # type: ignore
597
+ if event.get(key) is None and self.options[key] is not None:
598
+ event[key] = str(self.options[key]).strip()
130
599
  if event.get("sdk") is None:
131
600
  sdk_info = dict(SDK_INFO)
132
601
  sdk_info["integrations"] = sorted(self.integrations.keys())
@@ -136,122 +605,573 @@ class Client(object):
136
605
  event["platform"] = "python"
137
606
 
138
607
  event = handle_in_app(
139
- 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"],
140
612
  )
141
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
+
142
644
  # Postprocess the event here so that annotated types do
143
645
  # generally not surface in before_send
144
646
  if event is not None:
145
- strip_event_mut(event)
146
- event = flatten_metadata(event)
147
- event = convert_types(event)
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"),
654
+ ),
655
+ )
148
656
 
149
657
  before_send = self.options["before_send"]
150
- if before_send is not None:
658
+ if (
659
+ before_send is not None
660
+ and event is not None
661
+ and event.get("type") != "transaction"
662
+ ):
663
+ new_event = None
664
+ with capture_internal_exceptions():
665
+ new_event = before_send(event, hint or {})
666
+ if new_event is None:
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
+ ):
151
688
  new_event = None
689
+ spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
152
690
  with capture_internal_exceptions():
153
- new_event = before_send(event, hint)
691
+ new_event = before_send_transaction(event, hint or {})
154
692
  if new_event is None:
155
- logger.info("before send dropped event (%s)", event)
156
- event = new_event # type: ignore
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
157
711
 
158
712
  return event
159
713
 
160
714
  def _is_ignored_error(self, event, hint):
161
- # type: (Dict[str, Any], Dict[str, Any]) -> bool
715
+ # type: (Event, Hint) -> bool
162
716
  exc_info = hint.get("exc_info")
163
717
  if exc_info is None:
164
718
  return False
165
719
 
166
- type_name = get_type_name(exc_info[0])
167
- 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)
168
723
 
169
- for errcls in self.options["ignore_errors"]:
724
+ for ignored_error in self.options["ignore_errors"]:
170
725
  # String types are matched against the type name in the
171
726
  # exception only
172
- if isinstance(errcls, string_types):
173
- 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:
174
729
  return True
175
730
  else:
176
- if issubclass(exc_info[0], errcls):
731
+ if issubclass(error, ignored_error):
177
732
  return True
178
733
 
179
734
  return False
180
735
 
181
736
  def _should_capture(
182
737
  self,
183
- event, # type: Dict[str, Any]
184
- hint, # type: Dict[str, Any]
185
- scope=None, # type: Scope
738
+ event, # type: Event
739
+ hint, # type: Hint
740
+ scope=None, # type: Optional[Scope]
186
741
  ):
187
742
  # type: (...) -> bool
188
- if scope is not None and not scope._should_capture:
743
+ # Transactions are sampled independent of error events.
744
+ is_transaction = event.get("type") == "transaction"
745
+ if is_transaction:
746
+ return True
747
+
748
+ ignoring_prevents_recursion = scope is not None and not scope._should_capture
749
+ if ignoring_prevents_recursion:
189
750
  return False
190
751
 
191
- if (
192
- self.options["sample_rate"] < 1.0
193
- and random.random() >= self.options["sample_rate"]
194
- ):
752
+ ignored_by_config_option = self._is_ignored_error(event, hint)
753
+ if ignored_by_config_option:
195
754
  return False
196
755
 
197
- 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
+
198
802
  return False
199
803
 
200
804
  return True
201
805
 
202
- def capture_event(self, event, hint=None, scope=None):
203
- # type: (Dict[str, Any], Any, Scope) -> Optional[str]
806
+ def _update_session_from_event(
807
+ self,
808
+ session, # type: Session
809
+ event, # type: Event
810
+ ):
811
+ # type: (...) -> None
812
+
813
+ crashed = False
814
+ errored = False
815
+ user_agent = None
816
+
817
+ exceptions = (event.get("exception") or {}).get("values")
818
+ if exceptions:
819
+ errored = True
820
+ for error in exceptions:
821
+ if isinstance(error, AnnotatedValue):
822
+ error = error.value or {}
823
+ mechanism = error.get("mechanism")
824
+ if isinstance(mechanism, Mapping) and mechanism.get("handled") is False:
825
+ crashed = True
826
+ break
827
+
828
+ user = event.get("user")
829
+
830
+ if session.user_agent is None:
831
+ headers = (event.get("request") or {}).get("headers")
832
+ headers_dict = headers if isinstance(headers, dict) else {}
833
+ for k, v in headers_dict.items():
834
+ if k.lower() == "user-agent":
835
+ user_agent = v
836
+ break
837
+
838
+ session.update(
839
+ status="crashed" if crashed else None,
840
+ user=user,
841
+ user_agent=user_agent,
842
+ errors=session.errors + (errored or crashed),
843
+ )
844
+
845
+ def capture_event(
846
+ self,
847
+ event, # type: Event
848
+ hint=None, # type: Optional[Hint]
849
+ scope=None, # type: Optional[Scope]
850
+ ):
851
+ # type: (...) -> Optional[str]
204
852
  """Captures an event.
205
853
 
206
- This takes the ready made event and an optoinal hint and scope. The
207
- hint is internally used to further customize the representation of the
208
- error. When provided it's a dictionary of optional information such
209
- as exception info.
854
+ :param event: A ready-made event that can be directly sent to Sentry.
210
855
 
211
- If the transport is not set nothing happens, otherwise the return
212
- value of this function will be the ID of the captured event.
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.
857
+
858
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
859
+
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.
213
861
  """
214
- if self.transport is None:
215
- return None
216
- if hint is None:
217
- hint = {}
218
- rv = event.get("event_id")
219
- if rv is None:
220
- event["event_id"] = rv = uuid.uuid4().hex
862
+ hint = dict(hint or ()) # type: Hint
863
+
221
864
  if not self._should_capture(event, hint, scope):
222
865
  return None
223
- event = self._prepare_event(event, hint, scope) # type: ignore
224
- if event is None:
866
+
867
+ profile = event.pop("profile", None)
868
+
869
+ event_id = event.get("event_id")
870
+ if event_id is None:
871
+ event["event_id"] = event_id = uuid.uuid4().hex
872
+ event_opt = self._prepare_event(event, hint, scope)
873
+ if event_opt is None:
874
+ return None
875
+
876
+ # whenever we capture an event we also check if the session needs
877
+ # to be updated based on that information.
878
+ session = scope._session if scope else None
879
+ if session:
880
+ self._update_session_from_event(session, event)
881
+
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
+ ):
225
890
  return None
226
- self.transport.capture_event(event)
227
- return rv
228
891
 
229
- def close(self, timeout=None, callback=None):
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)
913
+ else:
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)
1061
+
1062
+ def capture_session(
1063
+ self,
1064
+ session, # type: Session
1065
+ ):
1066
+ # type: (...) -> None
1067
+ if not session.release:
1068
+ logger.info("Discarded session update because of missing release")
1069
+ else:
1070
+ self.session_flusher.add_session(session)
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
+
1101
+ def close(
1102
+ self,
1103
+ timeout=None, # type: Optional[float]
1104
+ callback=None, # type: Optional[Callable[[int, float], None]]
1105
+ ):
1106
+ # type: (...) -> None
230
1107
  """
231
1108
  Close the client and shut down the transport. Arguments have the same
232
- semantics as `self.flush()`.
1109
+ semantics as :py:meth:`Client.flush`.
233
1110
  """
234
1111
  if self.transport is not None:
235
1112
  self.flush(timeout=timeout, callback=callback)
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()
236
1120
  self.transport.kill()
237
1121
  self.transport = None
238
1122
 
239
- def flush(self, timeout=None, callback=None):
1123
+ def flush(
1124
+ self,
1125
+ timeout=None, # type: Optional[float]
1126
+ callback=None, # type: Optional[Callable[[int, float], None]]
1127
+ ):
1128
+ # type: (...) -> None
240
1129
  """
241
- Wait `timeout` seconds for the current events to be sent. If no
242
- `timeout` is provided, the `shutdown_timeout` option value is used.
1130
+ Wait for the current events to be sent.
243
1131
 
244
- The `callback` is invoked with two arguments: the number of pending
245
- events and the configured timeout. For instance the default atexit
246
- integration will use this to render out a message on stderr.
1132
+ :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
1133
+
1134
+ :param callback: Is invoked with the number of pending events and the configured timeout.
247
1135
  """
248
1136
  if self.transport is not None:
249
1137
  if timeout is None:
250
1138
  timeout = self.options["shutdown_timeout"]
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()
251
1144
  self.transport.flush(timeout=timeout, callback=callback)
252
1145
 
253
1146
  def __enter__(self):
1147
+ # type: () -> _Client
254
1148
  return self
255
1149
 
256
1150
  def __exit__(self, exc_type, exc_value, tb):
1151
+ # type: (Any, Any, Any) -> None
257
1152
  self.close()
1153
+
1154
+
1155
+ from typing import TYPE_CHECKING
1156
+
1157
+ if TYPE_CHECKING:
1158
+ # Make mypy, PyCharm and other static analyzers think `get_options` is a
1159
+ # type to have nicer autocompletion for params.
1160
+ #
1161
+ # Use `ClientConstructor` to define the argument types of `init` and
1162
+ # `Dict[str, Any]` to tell static analyzers about the return type.
1163
+
1164
+ class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801
1165
+ pass
1166
+
1167
+ class Client(ClientConstructor, _Client):
1168
+ pass
1169
+
1170
+ else:
1171
+ # Alias `get_options` for actual usage. Go through the lambda indirection
1172
+ # to throw PyCharm off of the weakly typed signature (it would otherwise
1173
+ # discover both the weakly typed signature of `_init` and our faked `init`
1174
+ # type).
1175
+
1176
+ get_options = (lambda: _get_options)()
1177
+ Client = (lambda: _Client)()