client.py 40 KB

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