You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1081 line
38 KiB

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