client.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. import os
  2. import uuid
  3. import random
  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
  10. from sentry_sdk._compat import check_uwsgi_thread_support
  11. from sentry_sdk._metrics_batcher import MetricsBatcher
  12. from sentry_sdk._span_batcher import SpanBatcher
  13. from sentry_sdk.utils import (
  14. AnnotatedValue,
  15. ContextVar,
  16. capture_internal_exceptions,
  17. current_stacktrace,
  18. env_to_bool,
  19. format_timestamp,
  20. get_sdk_name,
  21. get_type_name,
  22. get_default_release,
  23. handle_in_app,
  24. logger,
  25. get_before_send_log,
  26. get_before_send_metric,
  27. has_logs_enabled,
  28. has_metrics_enabled,
  29. )
  30. from sentry_sdk.serializer import serialize
  31. from sentry_sdk.tracing import trace
  32. from sentry_sdk.tracing_utils import has_span_streaming_enabled
  33. from sentry_sdk.transport import (
  34. HttpTransportCore,
  35. make_transport,
  36. AsyncHttpTransport,
  37. )
  38. from sentry_sdk.consts import (
  39. SPANDATA,
  40. DEFAULT_MAX_VALUE_LENGTH,
  41. DEFAULT_OPTIONS,
  42. INSTRUMENTER,
  43. VERSION,
  44. ClientConstructor,
  45. )
  46. from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations
  47. from sentry_sdk.integrations.dedupe import DedupeIntegration
  48. from sentry_sdk.sessions import SessionFlusher
  49. from sentry_sdk.envelope import Envelope
  50. from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler
  51. from sentry_sdk.profiler.transaction_profiler import (
  52. has_profiling_enabled,
  53. Profile,
  54. setup_profiler,
  55. )
  56. from sentry_sdk.scrubber import EventScrubber
  57. from sentry_sdk.monitor import Monitor
  58. if TYPE_CHECKING:
  59. from typing import Any
  60. from typing import Callable
  61. from typing import Optional
  62. from typing import Sequence
  63. from typing import Type
  64. from typing import Union
  65. from typing import TypeVar
  66. from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory
  67. from sentry_sdk.integrations import Integration
  68. from sentry_sdk.scope import Scope
  69. from sentry_sdk.session import Session
  70. from sentry_sdk.spotlight import SpotlightClient
  71. from sentry_sdk.traces import StreamedSpan
  72. from sentry_sdk.transport import Transport, Item
  73. from sentry_sdk._log_batcher import LogBatcher
  74. from sentry_sdk._metrics_batcher import MetricsBatcher
  75. from sentry_sdk.utils import Dsn
  76. I = TypeVar("I", bound=Integration) # noqa: E741
  77. _client_init_debug = ContextVar("client_init_debug")
  78. SDK_INFO: "SDKInfo" = {
  79. "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations()
  80. "version": VERSION,
  81. "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
  82. }
  83. def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]":
  84. if args and (isinstance(args[0], (bytes, str)) or args[0] is None):
  85. dsn: "Optional[str]" = args[0]
  86. args = args[1:]
  87. else:
  88. dsn = None
  89. if len(args) > 1:
  90. raise TypeError("Only single positional argument is expected")
  91. rv = dict(DEFAULT_OPTIONS)
  92. options = dict(*args, **kwargs)
  93. if dsn is not None and options.get("dsn") is None:
  94. options["dsn"] = dsn
  95. for key, value in options.items():
  96. if key not in rv:
  97. raise TypeError("Unknown option %r" % (key,))
  98. rv[key] = value
  99. if rv["dsn"] is None:
  100. rv["dsn"] = os.environ.get("SENTRY_DSN")
  101. if rv["release"] is None:
  102. rv["release"] = get_default_release()
  103. if rv["environment"] is None:
  104. rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production"
  105. if rv["debug"] is None:
  106. rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False
  107. if rv["server_name"] is None and hasattr(socket, "gethostname"):
  108. rv["server_name"] = socket.gethostname()
  109. if rv["instrumenter"] is None:
  110. rv["instrumenter"] = INSTRUMENTER.SENTRY
  111. if rv["project_root"] is None:
  112. try:
  113. project_root = os.getcwd()
  114. except Exception:
  115. project_root = None
  116. rv["project_root"] = project_root
  117. if rv["enable_tracing"] is True and rv["traces_sample_rate"] is None:
  118. rv["traces_sample_rate"] = 1.0
  119. if rv["event_scrubber"] is None:
  120. rv["event_scrubber"] = EventScrubber(
  121. send_default_pii=(
  122. False if rv["send_default_pii"] is None else rv["send_default_pii"]
  123. )
  124. )
  125. if rv["socket_options"] and not isinstance(rv["socket_options"], list):
  126. logger.warning(
  127. "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format."
  128. )
  129. rv["socket_options"] = None
  130. if rv["keep_alive"] is None:
  131. rv["keep_alive"] = (
  132. env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False
  133. )
  134. if rv["enable_tracing"] is not None:
  135. warnings.warn(
  136. "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.",
  137. DeprecationWarning,
  138. stacklevel=2,
  139. )
  140. return rv
  141. try:
  142. # Python 3.6+
  143. module_not_found_error = ModuleNotFoundError
  144. except Exception:
  145. # Older Python versions
  146. module_not_found_error = ImportError # type: ignore
  147. class BaseClient:
  148. """
  149. .. versionadded:: 2.0.0
  150. The basic definition of a client that is used for sending data to Sentry.
  151. """
  152. spotlight: "Optional[SpotlightClient]" = None
  153. def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None:
  154. self.options: "Dict[str, Any]" = (
  155. options if options is not None else DEFAULT_OPTIONS
  156. )
  157. self.transport: "Optional[Transport]" = None
  158. self.monitor: "Optional[Monitor]" = None
  159. self.log_batcher: "Optional[LogBatcher]" = None
  160. self.metrics_batcher: "Optional[MetricsBatcher]" = None
  161. self.span_batcher: "Optional[SpanBatcher]" = None
  162. self.integrations: "dict[str, Integration]" = {}
  163. def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any":
  164. return {"options": {}}
  165. def __setstate__(self, *args: "Any", **kwargs: "Any") -> None:
  166. pass
  167. @property
  168. def dsn(self) -> "Optional[str]":
  169. return None
  170. @property
  171. def parsed_dsn(self) -> "Optional[Dsn]":
  172. return None
  173. def should_send_default_pii(self) -> bool:
  174. return False
  175. def is_active(self) -> bool:
  176. """
  177. .. versionadded:: 2.0.0
  178. Returns whether the client is active (able to send data to Sentry)
  179. """
  180. return False
  181. def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]":
  182. return None
  183. def _capture_log(self, log: "Log", scope: "Scope") -> None:
  184. pass
  185. def _capture_metric(self, metric: "Metric", scope: "Scope") -> None:
  186. pass
  187. def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None:
  188. pass
  189. def capture_session(self, *args: "Any", **kwargs: "Any") -> None:
  190. return None
  191. if TYPE_CHECKING:
  192. @overload
  193. def get_integration(self, name_or_class: str) -> "Optional[Integration]": ...
  194. @overload
  195. def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ...
  196. def get_integration(
  197. self, name_or_class: "Union[str, type[Integration]]"
  198. ) -> "Optional[Integration]":
  199. return None
  200. def close(self, *args: "Any", **kwargs: "Any") -> None:
  201. return None
  202. def flush(self, *args: "Any", **kwargs: "Any") -> None:
  203. return None
  204. async def close_async(self, *args: "Any", **kwargs: "Any") -> None:
  205. return None
  206. async def flush_async(self, *args: "Any", **kwargs: "Any") -> None:
  207. return None
  208. def __enter__(self) -> "BaseClient":
  209. return self
  210. def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
  211. return None
  212. class NonRecordingClient(BaseClient):
  213. """
  214. .. versionadded:: 2.0.0
  215. A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized.
  216. """
  217. pass
  218. class _Client(BaseClient):
  219. """
  220. The client is internally responsible for capturing the events and
  221. forwarding them to sentry through the configured transport. It takes
  222. the client options as keyword arguments and optionally the DSN as first
  223. argument.
  224. Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support)
  225. """
  226. def __init__(self, *args: "Any", **kwargs: "Any") -> None:
  227. super(_Client, self).__init__(options=get_options(*args, **kwargs))
  228. self._init_impl()
  229. def __getstate__(self) -> "Any":
  230. return {"options": self.options}
  231. def __setstate__(self, state: "Any") -> None:
  232. self.options = state["options"]
  233. self._init_impl()
  234. def _setup_instrumentation(
  235. self, functions_to_trace: "Sequence[Dict[str, str]]"
  236. ) -> None:
  237. """
  238. Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator.
  239. """
  240. for function in functions_to_trace:
  241. class_name = None
  242. function_qualname = function["qualified_name"]
  243. module_name, function_name = function_qualname.rsplit(".", 1)
  244. try:
  245. # Try to import module and function
  246. # ex: "mymodule.submodule.funcname"
  247. module_obj = import_module(module_name)
  248. function_obj = getattr(module_obj, function_name)
  249. setattr(module_obj, function_name, trace(function_obj))
  250. logger.debug("Enabled tracing for %s", function_qualname)
  251. except module_not_found_error:
  252. try:
  253. # Try to import a class
  254. # ex: "mymodule.submodule.MyClassName.member_function"
  255. module_name, class_name = module_name.rsplit(".", 1)
  256. module_obj = import_module(module_name)
  257. class_obj = getattr(module_obj, class_name)
  258. function_obj = getattr(class_obj, function_name)
  259. function_type = type(class_obj.__dict__[function_name])
  260. traced_function = trace(function_obj)
  261. if function_type in (staticmethod, classmethod):
  262. traced_function = staticmethod(traced_function)
  263. setattr(class_obj, function_name, traced_function)
  264. setattr(module_obj, class_name, class_obj)
  265. logger.debug("Enabled tracing for %s", function_qualname)
  266. except Exception as e:
  267. logger.warning(
  268. "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
  269. function_qualname,
  270. e,
  271. )
  272. except Exception as e:
  273. logger.warning(
  274. "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
  275. function_qualname,
  276. e,
  277. )
  278. def _init_impl(self) -> None:
  279. old_debug = _client_init_debug.get(False)
  280. def _capture_envelope(envelope: "Envelope") -> None:
  281. if self.spotlight is not None:
  282. self.spotlight.capture_envelope(envelope)
  283. if self.transport is not None:
  284. self.transport.capture_envelope(envelope)
  285. def _record_lost_event(
  286. reason: str,
  287. data_category: "EventDataCategory",
  288. item: "Optional[Item]" = None,
  289. quantity: int = 1,
  290. ) -> None:
  291. if self.transport is not None:
  292. self.transport.record_lost_event(
  293. reason=reason,
  294. data_category=data_category,
  295. item=item,
  296. quantity=quantity,
  297. )
  298. try:
  299. _client_init_debug.set(self.options["debug"])
  300. self.transport = make_transport(self.options)
  301. self.monitor = None
  302. if self.transport:
  303. if self.options["enable_backpressure_handling"]:
  304. self.monitor = Monitor(self.transport)
  305. # Setup Spotlight before creating batchers so _capture_envelope can use it.
  306. # setup_spotlight handles all config/env var resolution per the SDK spec.
  307. from sentry_sdk.spotlight import setup_spotlight
  308. self.spotlight = setup_spotlight(self.options)
  309. if self.spotlight is not None and not self.options["dsn"]:
  310. sample_all = lambda *_args, **_kwargs: 1.0
  311. self.options["send_default_pii"] = True
  312. self.options["error_sampler"] = sample_all
  313. self.options["traces_sampler"] = sample_all
  314. self.options["profiles_sampler"] = sample_all
  315. self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
  316. self.log_batcher = None
  317. if has_logs_enabled(self.options):
  318. from sentry_sdk._log_batcher import LogBatcher
  319. self.log_batcher = LogBatcher(
  320. capture_func=_capture_envelope,
  321. record_lost_func=_record_lost_event,
  322. )
  323. self.metrics_batcher = None
  324. if has_metrics_enabled(self.options):
  325. self.metrics_batcher = MetricsBatcher(
  326. capture_func=_capture_envelope,
  327. record_lost_func=_record_lost_event,
  328. )
  329. self.span_batcher = None
  330. if has_span_streaming_enabled(self.options):
  331. self.span_batcher = SpanBatcher(
  332. capture_func=_capture_envelope,
  333. record_lost_func=_record_lost_event,
  334. )
  335. max_request_body_size = ("always", "never", "small", "medium")
  336. if self.options["max_request_body_size"] not in max_request_body_size:
  337. raise ValueError(
  338. "Invalid value for max_request_body_size. Must be one of {}".format(
  339. max_request_body_size
  340. )
  341. )
  342. if self.options["_experiments"].get("otel_powered_performance", False):
  343. logger.debug(
  344. "[OTel] Enabling experimental OTel-powered performance monitoring."
  345. )
  346. self.options["instrumenter"] = INSTRUMENTER.OTEL
  347. if (
  348. "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration"
  349. not in _DEFAULT_INTEGRATIONS
  350. ):
  351. _DEFAULT_INTEGRATIONS.append(
  352. "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration",
  353. )
  354. self.integrations = setup_integrations(
  355. self.options["integrations"],
  356. with_defaults=self.options["default_integrations"],
  357. with_auto_enabling_integrations=self.options[
  358. "auto_enabling_integrations"
  359. ],
  360. disabled_integrations=self.options["disabled_integrations"],
  361. options=self.options,
  362. )
  363. sdk_name = get_sdk_name(list(self.integrations.keys()))
  364. SDK_INFO["name"] = sdk_name
  365. logger.debug("Setting SDK name to '%s'", sdk_name)
  366. if has_profiling_enabled(self.options):
  367. try:
  368. setup_profiler(self.options)
  369. except Exception as e:
  370. logger.debug("Can not set up profiler. (%s)", e)
  371. else:
  372. try:
  373. setup_continuous_profiler(
  374. self.options,
  375. sdk_info=SDK_INFO,
  376. capture_func=_capture_envelope,
  377. )
  378. except Exception as e:
  379. logger.debug("Can not set up continuous profiler. (%s)", e)
  380. finally:
  381. _client_init_debug.set(old_debug)
  382. self._setup_instrumentation(self.options.get("functions_to_trace", []))
  383. if (
  384. self.monitor
  385. or self.log_batcher
  386. or self.metrics_batcher
  387. or self.span_batcher
  388. or has_profiling_enabled(self.options)
  389. or isinstance(self.transport, HttpTransportCore)
  390. ):
  391. # If we have anything on that could spawn a background thread, we
  392. # need to check if it's safe to use them.
  393. check_uwsgi_thread_support()
  394. def is_active(self) -> bool:
  395. """
  396. .. versionadded:: 2.0.0
  397. Returns whether the client is active (able to send data to Sentry)
  398. """
  399. return True
  400. def should_send_default_pii(self) -> bool:
  401. """
  402. .. versionadded:: 2.0.0
  403. Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry.
  404. """
  405. return self.options.get("send_default_pii") or False
  406. @property
  407. def dsn(self) -> "Optional[str]":
  408. """Returns the configured DSN as string."""
  409. return self.options["dsn"]
  410. @property
  411. def parsed_dsn(self) -> "Optional[Dsn]":
  412. """Returns the configured parsed DSN object."""
  413. return self.transport.parsed_dsn if self.transport else None
  414. def _prepare_event(
  415. self,
  416. event: "Event",
  417. hint: "Hint",
  418. scope: "Optional[Scope]",
  419. ) -> "Optional[Event]":
  420. previous_total_spans: "Optional[int]" = None
  421. previous_total_breadcrumbs: "Optional[int]" = None
  422. if event.get("timestamp") is None:
  423. event["timestamp"] = datetime.now(timezone.utc)
  424. is_transaction = event.get("type") == "transaction"
  425. if scope is not None:
  426. spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
  427. event_ = scope.apply_to_event(event, hint, self.options)
  428. # one of the event/error processors returned None
  429. if event_ is None:
  430. if self.transport:
  431. self.transport.record_lost_event(
  432. "event_processor",
  433. data_category=("transaction" if is_transaction else "error"),
  434. )
  435. if is_transaction:
  436. self.transport.record_lost_event(
  437. "event_processor",
  438. data_category="span",
  439. quantity=spans_before + 1, # +1 for the transaction itself
  440. )
  441. return None
  442. event = event_
  443. spans_delta = spans_before - len(
  444. cast(List[Dict[str, object]], event.get("spans", []))
  445. )
  446. span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0)
  447. if is_transaction and self.transport is not None:
  448. if spans_delta > 0:
  449. self.transport.record_lost_event(
  450. "event_processor", data_category="span", quantity=spans_delta
  451. )
  452. if span_recorder_dropped_spans > 0:
  453. self.transport.record_lost_event(
  454. "buffer_overflow",
  455. data_category="span",
  456. quantity=span_recorder_dropped_spans,
  457. )
  458. dropped_spans: int = span_recorder_dropped_spans + spans_delta
  459. if dropped_spans > 0:
  460. previous_total_spans = spans_before + dropped_spans
  461. if scope._n_breadcrumbs_truncated > 0:
  462. breadcrumbs = event.get("breadcrumbs", {})
  463. values = (
  464. breadcrumbs.get("values", [])
  465. if not isinstance(breadcrumbs, AnnotatedValue)
  466. else []
  467. )
  468. previous_total_breadcrumbs = (
  469. len(values) + scope._n_breadcrumbs_truncated
  470. )
  471. if (
  472. not is_transaction
  473. and self.options["attach_stacktrace"]
  474. and "exception" not in event
  475. and "stacktrace" not in event
  476. and "threads" not in event
  477. ):
  478. with capture_internal_exceptions():
  479. event["threads"] = {
  480. "values": [
  481. {
  482. "stacktrace": current_stacktrace(
  483. include_local_variables=self.options.get(
  484. "include_local_variables", True
  485. ),
  486. max_value_length=self.options.get(
  487. "max_value_length", DEFAULT_MAX_VALUE_LENGTH
  488. ),
  489. ),
  490. "crashed": False,
  491. "current": True,
  492. }
  493. ]
  494. }
  495. for key in "release", "environment", "server_name", "dist":
  496. if event.get(key) is None and self.options[key] is not None:
  497. event[key] = str(self.options[key]).strip()
  498. if event.get("sdk") is None:
  499. sdk_info = dict(SDK_INFO)
  500. sdk_info["integrations"] = sorted(self.integrations.keys())
  501. event["sdk"] = sdk_info
  502. if event.get("platform") is None:
  503. event["platform"] = "python"
  504. event = handle_in_app(
  505. event,
  506. self.options["in_app_exclude"],
  507. self.options["in_app_include"],
  508. self.options["project_root"],
  509. )
  510. if event is not None:
  511. event_scrubber = self.options["event_scrubber"]
  512. if event_scrubber:
  513. event_scrubber.scrub_event(event)
  514. if scope is not None and scope._gen_ai_original_message_count:
  515. spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", [])
  516. if isinstance(spans, list):
  517. for span in spans:
  518. span_id = span.get("span_id", None)
  519. span_data = span.get("data", {})
  520. if (
  521. span_id
  522. and span_id in scope._gen_ai_original_message_count
  523. and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data
  524. ):
  525. span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue(
  526. span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES],
  527. {"len": scope._gen_ai_original_message_count[span_id]},
  528. )
  529. if previous_total_spans is not None:
  530. event["spans"] = AnnotatedValue(
  531. event.get("spans", []), {"len": previous_total_spans}
  532. )
  533. if previous_total_breadcrumbs is not None:
  534. event["breadcrumbs"] = AnnotatedValue(
  535. event.get("breadcrumbs", {"values": []}),
  536. {"len": previous_total_breadcrumbs},
  537. )
  538. # Postprocess the event here so that annotated types do
  539. # generally not surface in before_send
  540. if event is not None:
  541. event = cast(
  542. "Event",
  543. serialize(
  544. cast("Dict[str, Any]", event),
  545. max_request_body_size=self.options.get("max_request_body_size"),
  546. max_value_length=self.options.get("max_value_length"),
  547. custom_repr=self.options.get("custom_repr"),
  548. ),
  549. )
  550. before_send = self.options["before_send"]
  551. if (
  552. before_send is not None
  553. and event is not None
  554. and event.get("type") != "transaction"
  555. ):
  556. new_event = None
  557. with capture_internal_exceptions():
  558. new_event = before_send(event, hint or {})
  559. if new_event is None:
  560. logger.info("before send dropped event")
  561. if self.transport:
  562. self.transport.record_lost_event(
  563. "before_send", data_category="error"
  564. )
  565. # If this is an exception, reset the DedupeIntegration. It still
  566. # remembers the dropped exception as the last exception, meaning
  567. # that if the same exception happens again and is not dropped
  568. # in before_send, it'd get dropped by DedupeIntegration.
  569. if event.get("exception"):
  570. DedupeIntegration.reset_last_seen()
  571. event = new_event
  572. before_send_transaction = self.options["before_send_transaction"]
  573. if (
  574. before_send_transaction is not None
  575. and event is not None
  576. and event.get("type") == "transaction"
  577. ):
  578. new_event = None
  579. spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
  580. with capture_internal_exceptions():
  581. new_event = before_send_transaction(event, hint or {})
  582. if new_event is None:
  583. logger.info("before send transaction dropped event")
  584. if self.transport:
  585. self.transport.record_lost_event(
  586. reason="before_send", data_category="transaction"
  587. )
  588. self.transport.record_lost_event(
  589. reason="before_send",
  590. data_category="span",
  591. quantity=spans_before + 1, # +1 for the transaction itself
  592. )
  593. else:
  594. spans_delta = spans_before - len(new_event.get("spans", []))
  595. if spans_delta > 0 and self.transport is not None:
  596. self.transport.record_lost_event(
  597. reason="before_send", data_category="span", quantity=spans_delta
  598. )
  599. event = new_event
  600. return event
  601. def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool:
  602. exc_info = hint.get("exc_info")
  603. if exc_info is None:
  604. return False
  605. error = exc_info[0]
  606. error_type_name = get_type_name(exc_info[0])
  607. error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name)
  608. for ignored_error in self.options["ignore_errors"]:
  609. # String types are matched against the type name in the
  610. # exception only
  611. if isinstance(ignored_error, str):
  612. if ignored_error == error_full_name or ignored_error == error_type_name:
  613. return True
  614. else:
  615. if issubclass(error, ignored_error):
  616. return True
  617. return False
  618. def _should_capture(
  619. self,
  620. event: "Event",
  621. hint: "Hint",
  622. scope: "Optional[Scope]" = None,
  623. ) -> bool:
  624. # Transactions are sampled independent of error events.
  625. is_transaction = event.get("type") == "transaction"
  626. if is_transaction:
  627. return True
  628. ignoring_prevents_recursion = scope is not None and not scope._should_capture
  629. if ignoring_prevents_recursion:
  630. return False
  631. ignored_by_config_option = self._is_ignored_error(event, hint)
  632. if ignored_by_config_option:
  633. return False
  634. return True
  635. def _should_sample_error(
  636. self,
  637. event: "Event",
  638. hint: "Hint",
  639. ) -> bool:
  640. error_sampler = self.options.get("error_sampler", None)
  641. if callable(error_sampler):
  642. with capture_internal_exceptions():
  643. sample_rate = error_sampler(event, hint)
  644. else:
  645. sample_rate = self.options["sample_rate"]
  646. try:
  647. not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate
  648. except NameError:
  649. logger.warning(
  650. "The provided error_sampler raised an error. Defaulting to sampling the event."
  651. )
  652. # If the error_sampler raised an error, we should sample the event, since the default behavior
  653. # (when no sample_rate or error_sampler is provided) is to sample all events.
  654. not_in_sample_rate = False
  655. except TypeError:
  656. parameter, verb = (
  657. ("error_sampler", "returned")
  658. if callable(error_sampler)
  659. else ("sample_rate", "contains")
  660. )
  661. logger.warning(
  662. "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event."
  663. % (parameter, verb, repr(sample_rate))
  664. )
  665. # If the sample_rate has an invalid value, we should sample the event, since the default behavior
  666. # (when no sample_rate or error_sampler is provided) is to sample all events.
  667. not_in_sample_rate = False
  668. if not_in_sample_rate:
  669. # because we will not sample this event, record a "lost event".
  670. if self.transport:
  671. self.transport.record_lost_event("sample_rate", data_category="error")
  672. return False
  673. return True
  674. def _update_session_from_event(
  675. self,
  676. session: "Session",
  677. event: "Event",
  678. ) -> None:
  679. crashed = False
  680. errored = False
  681. user_agent = None
  682. exceptions = (event.get("exception") or {}).get("values")
  683. if exceptions:
  684. errored = True
  685. for error in exceptions:
  686. if isinstance(error, AnnotatedValue):
  687. error = error.value or {}
  688. mechanism = error.get("mechanism")
  689. if isinstance(mechanism, Mapping) and mechanism.get("handled") is False:
  690. crashed = True
  691. break
  692. user = event.get("user")
  693. if session.user_agent is None:
  694. headers = (event.get("request") or {}).get("headers")
  695. headers_dict = headers if isinstance(headers, dict) else {}
  696. for k, v in headers_dict.items():
  697. if k.lower() == "user-agent":
  698. user_agent = v
  699. break
  700. session.update(
  701. status="crashed" if crashed else None,
  702. user=user,
  703. user_agent=user_agent,
  704. errors=session.errors + (errored or crashed),
  705. )
  706. def capture_event(
  707. self,
  708. event: "Event",
  709. hint: "Optional[Hint]" = None,
  710. scope: "Optional[Scope]" = None,
  711. ) -> "Optional[str]":
  712. """Captures an event.
  713. :param event: A ready-made event that can be directly sent to Sentry.
  714. :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.
  715. :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
  716. :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.
  717. """
  718. hint: "Hint" = dict(hint or ())
  719. if not self._should_capture(event, hint, scope):
  720. return None
  721. profile = event.pop("profile", None)
  722. event_id = event.get("event_id")
  723. if event_id is None:
  724. event["event_id"] = event_id = uuid.uuid4().hex
  725. event_opt = self._prepare_event(event, hint, scope)
  726. if event_opt is None:
  727. return None
  728. # whenever we capture an event we also check if the session needs
  729. # to be updated based on that information.
  730. session = scope._session if scope else None
  731. if session:
  732. self._update_session_from_event(session, event)
  733. is_transaction = event_opt.get("type") == "transaction"
  734. is_checkin = event_opt.get("type") == "check_in"
  735. if (
  736. not is_transaction
  737. and not is_checkin
  738. and not self._should_sample_error(event, hint)
  739. ):
  740. return None
  741. attachments = hint.get("attachments")
  742. trace_context = event_opt.get("contexts", {}).get("trace") or {}
  743. dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {})
  744. headers: "dict[str, object]" = {
  745. "event_id": event_opt["event_id"],
  746. "sent_at": format_timestamp(datetime.now(timezone.utc)),
  747. }
  748. if dynamic_sampling_context:
  749. headers["trace"] = dynamic_sampling_context
  750. envelope = Envelope(headers=headers)
  751. if is_transaction:
  752. if isinstance(profile, Profile):
  753. envelope.add_profile(profile.to_json(event_opt, self.options))
  754. envelope.add_transaction(event_opt)
  755. elif is_checkin:
  756. envelope.add_checkin(event_opt)
  757. else:
  758. envelope.add_event(event_opt)
  759. for attachment in attachments or ():
  760. envelope.add_item(attachment.to_envelope_item())
  761. return_value = None
  762. if self.spotlight:
  763. self.spotlight.capture_envelope(envelope)
  764. return_value = event_id
  765. if self.transport is not None:
  766. self.transport.capture_envelope(envelope)
  767. return_value = event_id
  768. return return_value
  769. def _capture_telemetry(
  770. self,
  771. telemetry: "Optional[Union[Log, Metric, StreamedSpan]]",
  772. ty: str,
  773. scope: "Scope",
  774. ) -> None:
  775. # Capture attributes-based telemetry (logs, metrics, spansV2)
  776. if telemetry is None:
  777. return
  778. scope.apply_to_telemetry(telemetry)
  779. before_send = None
  780. if ty == "log":
  781. before_send = get_before_send_log(self.options)
  782. elif ty == "metric":
  783. before_send = get_before_send_metric(self.options) # type: ignore
  784. if before_send is not None:
  785. telemetry = before_send(telemetry, {}) # type: ignore
  786. if telemetry is None:
  787. return
  788. batcher = None
  789. if ty == "log":
  790. batcher = self.log_batcher
  791. elif ty == "metric":
  792. batcher = self.metrics_batcher # type: ignore
  793. elif ty == "span":
  794. batcher = self.span_batcher # type: ignore
  795. if batcher is not None:
  796. batcher.add(telemetry) # type: ignore
  797. def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None:
  798. self._capture_telemetry(log, "log", scope)
  799. def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None:
  800. self._capture_telemetry(metric, "metric", scope)
  801. def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None:
  802. self._capture_telemetry(span, "span", scope)
  803. def capture_session(
  804. self,
  805. session: "Session",
  806. ) -> None:
  807. if not session.release:
  808. logger.info("Discarded session update because of missing release")
  809. else:
  810. self.session_flusher.add_session(session)
  811. if TYPE_CHECKING:
  812. @overload
  813. def get_integration(self, name_or_class: str) -> "Optional[Integration]": ...
  814. @overload
  815. def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ...
  816. def get_integration(
  817. self,
  818. name_or_class: "Union[str, Type[Integration]]",
  819. ) -> "Optional[Integration]":
  820. """Returns the integration for this client by name or class.
  821. If the client does not have that integration then `None` is returned.
  822. """
  823. if isinstance(name_or_class, str):
  824. integration_name = name_or_class
  825. elif name_or_class.identifier is not None:
  826. integration_name = name_or_class.identifier
  827. else:
  828. raise ValueError("Integration has no name")
  829. return self.integrations.get(integration_name)
  830. def _has_async_transport(self) -> bool:
  831. """Check if the current transport is async."""
  832. return isinstance(self.transport, AsyncHttpTransport)
  833. @property
  834. def _batchers(self) -> "tuple[Any, ...]":
  835. return tuple(
  836. b
  837. for b in (self.log_batcher, self.metrics_batcher, self.span_batcher)
  838. if b is not None
  839. )
  840. def _close_components(self) -> None:
  841. """Kill all client components in the correct order."""
  842. self.session_flusher.kill()
  843. for b in self._batchers:
  844. b.kill()
  845. if self.monitor:
  846. self.monitor.kill()
  847. def _flush_components(self) -> None:
  848. """Flush all client components."""
  849. self.session_flusher.flush()
  850. for b in self._batchers:
  851. b.flush()
  852. def close(
  853. self,
  854. timeout: "Optional[float]" = None,
  855. callback: "Optional[Callable[[int, float], None]]" = None,
  856. ) -> None:
  857. """
  858. Close the client and shut down the transport. Arguments have the same
  859. semantics as :py:meth:`Client.flush`.
  860. """
  861. if self.transport is not None:
  862. if self._has_async_transport():
  863. warnings.warn(
  864. "close() used with AsyncHttpTransport. Use close_async() instead.",
  865. stacklevel=2,
  866. )
  867. self._flush_components()
  868. else:
  869. self.flush(timeout=timeout, callback=callback)
  870. self._close_components()
  871. self.transport.kill()
  872. self.transport = None
  873. async def close_async(
  874. self,
  875. timeout: "Optional[float]" = None,
  876. callback: "Optional[Callable[[int, float], None]]" = None,
  877. ) -> None:
  878. """
  879. Asynchronously close the client and shut down the transport. Arguments have the same
  880. semantics as :py:meth:`Client.flush_async`.
  881. """
  882. if self.transport is not None:
  883. if not self._has_async_transport():
  884. logger.debug(
  885. "close_async() used with non-async transport, aborting. Please use close() instead."
  886. )
  887. return
  888. await self.flush_async(timeout=timeout, callback=callback)
  889. self._close_components()
  890. kill_task = self.transport.kill() # type: ignore
  891. if kill_task is not None:
  892. await kill_task
  893. self.transport = None
  894. def flush(
  895. self,
  896. timeout: "Optional[float]" = None,
  897. callback: "Optional[Callable[[int, float], None]]" = None,
  898. ) -> None:
  899. """
  900. Wait for the current events to be sent.
  901. :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
  902. :param callback: Is invoked with the number of pending events and the configured timeout.
  903. """
  904. if self.transport is not None:
  905. if self._has_async_transport():
  906. warnings.warn(
  907. "flush() used with AsyncHttpTransport. Use flush_async() instead.",
  908. stacklevel=2,
  909. )
  910. return
  911. if timeout is None:
  912. timeout = self.options["shutdown_timeout"]
  913. self._flush_components()
  914. self.transport.flush(timeout=timeout, callback=callback)
  915. async def flush_async(
  916. self,
  917. timeout: "Optional[float]" = None,
  918. callback: "Optional[Callable[[int, float], None]]" = None,
  919. ) -> None:
  920. """
  921. Asynchronously wait for the current events to be sent.
  922. :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
  923. :param callback: Is invoked with the number of pending events and the configured timeout.
  924. """
  925. if self.transport is not None:
  926. if not self._has_async_transport():
  927. logger.debug(
  928. "flush_async() used with non-async transport, aborting. Please use flush() instead."
  929. )
  930. return
  931. if timeout is None:
  932. timeout = self.options["shutdown_timeout"]
  933. self._flush_components()
  934. flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore
  935. if flush_task is not None:
  936. await flush_task
  937. def __enter__(self) -> "_Client":
  938. return self
  939. def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
  940. self.close()
  941. async def __aenter__(self) -> "_Client":
  942. return self
  943. async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
  944. await self.close_async()
  945. from typing import TYPE_CHECKING
  946. if TYPE_CHECKING:
  947. # Make mypy, PyCharm and other static analyzers think `get_options` is a
  948. # type to have nicer autocompletion for params.
  949. #
  950. # Use `ClientConstructor` to define the argument types of `init` and
  951. # `Dict[str, Any]` to tell static analyzers about the return type.
  952. class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801
  953. pass
  954. class Client(ClientConstructor, _Client):
  955. pass
  956. else:
  957. # Alias `get_options` for actual usage. Go through the lambda indirection
  958. # to throw PyCharm off of the weakly typed signature (it would otherwise
  959. # discover both the weakly typed signature of `_init` and our faked `init`
  960. # type).
  961. get_options = (lambda: _get_options)()
  962. Client = (lambda: _Client)()