client.py 37 KB

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