| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167 |
- import os
- import uuid
- import random
- import socket
- from collections.abc import Mapping
- from datetime import datetime, timezone
- from importlib import import_module
- from typing import TYPE_CHECKING, List, Dict, cast, overload
- import warnings
- from sentry_sdk._compat import check_uwsgi_thread_support
- from sentry_sdk._metrics_batcher import MetricsBatcher
- from sentry_sdk._span_batcher import SpanBatcher
- from sentry_sdk.utils import (
- AnnotatedValue,
- ContextVar,
- capture_internal_exceptions,
- current_stacktrace,
- env_to_bool,
- format_timestamp,
- get_sdk_name,
- get_type_name,
- get_default_release,
- handle_in_app,
- logger,
- get_before_send_log,
- get_before_send_metric,
- has_logs_enabled,
- has_metrics_enabled,
- )
- from sentry_sdk.serializer import serialize
- from sentry_sdk.tracing import trace
- from sentry_sdk.tracing_utils import has_span_streaming_enabled
- from sentry_sdk.transport import (
- HttpTransportCore,
- make_transport,
- AsyncHttpTransport,
- )
- from sentry_sdk.consts import (
- SPANDATA,
- DEFAULT_MAX_VALUE_LENGTH,
- DEFAULT_OPTIONS,
- INSTRUMENTER,
- VERSION,
- ClientConstructor,
- )
- from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations
- from sentry_sdk.integrations.dedupe import DedupeIntegration
- from sentry_sdk.sessions import SessionFlusher
- from sentry_sdk.envelope import Envelope
- from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler
- from sentry_sdk.profiler.transaction_profiler import (
- has_profiling_enabled,
- Profile,
- setup_profiler,
- )
- from sentry_sdk.scrubber import EventScrubber
- from sentry_sdk.monitor import Monitor
- if TYPE_CHECKING:
- from typing import Any
- from typing import Callable
- from typing import Optional
- from typing import Sequence
- from typing import Type
- from typing import Union
- from typing import TypeVar
- from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory
- from sentry_sdk.integrations import Integration
- from sentry_sdk.scope import Scope
- from sentry_sdk.session import Session
- from sentry_sdk.spotlight import SpotlightClient
- from sentry_sdk.traces import StreamedSpan
- from sentry_sdk.transport import Transport, Item
- from sentry_sdk._log_batcher import LogBatcher
- from sentry_sdk._metrics_batcher import MetricsBatcher
- from sentry_sdk.utils import Dsn
- I = TypeVar("I", bound=Integration) # noqa: E741
- _client_init_debug = ContextVar("client_init_debug")
- SDK_INFO: "SDKInfo" = {
- "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations()
- "version": VERSION,
- "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
- }
- def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]":
- if args and (isinstance(args[0], (bytes, str)) or args[0] is None):
- dsn: "Optional[str]" = args[0]
- args = args[1:]
- else:
- dsn = None
- if len(args) > 1:
- raise TypeError("Only single positional argument is expected")
- rv = dict(DEFAULT_OPTIONS)
- options = dict(*args, **kwargs)
- if dsn is not None and options.get("dsn") is None:
- options["dsn"] = dsn
- for key, value in options.items():
- if key not in rv:
- raise TypeError("Unknown option %r" % (key,))
- rv[key] = value
- if rv["dsn"] is None:
- rv["dsn"] = os.environ.get("SENTRY_DSN")
- if rv["release"] is None:
- rv["release"] = get_default_release()
- if rv["environment"] is None:
- rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production"
- if rv["debug"] is None:
- rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False
- if rv["server_name"] is None and hasattr(socket, "gethostname"):
- rv["server_name"] = socket.gethostname()
- if rv["instrumenter"] is None:
- rv["instrumenter"] = INSTRUMENTER.SENTRY
- if rv["project_root"] is None:
- try:
- project_root = os.getcwd()
- except Exception:
- project_root = None
- rv["project_root"] = project_root
- if rv["enable_tracing"] is True and rv["traces_sample_rate"] is None:
- rv["traces_sample_rate"] = 1.0
- if rv["event_scrubber"] is None:
- rv["event_scrubber"] = EventScrubber(
- send_default_pii=(
- False if rv["send_default_pii"] is None else rv["send_default_pii"]
- )
- )
- if rv["socket_options"] and not isinstance(rv["socket_options"], list):
- logger.warning(
- "Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format."
- )
- rv["socket_options"] = None
- if rv["keep_alive"] is None:
- rv["keep_alive"] = (
- env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False
- )
- if rv["enable_tracing"] is not None:
- warnings.warn(
- "The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return rv
- try:
- # Python 3.6+
- module_not_found_error = ModuleNotFoundError
- except Exception:
- # Older Python versions
- module_not_found_error = ImportError # type: ignore
- class BaseClient:
- """
- .. versionadded:: 2.0.0
- The basic definition of a client that is used for sending data to Sentry.
- """
- spotlight: "Optional[SpotlightClient]" = None
- def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None:
- self.options: "Dict[str, Any]" = (
- options if options is not None else DEFAULT_OPTIONS
- )
- self.transport: "Optional[Transport]" = None
- self.monitor: "Optional[Monitor]" = None
- self.log_batcher: "Optional[LogBatcher]" = None
- self.metrics_batcher: "Optional[MetricsBatcher]" = None
- self.span_batcher: "Optional[SpanBatcher]" = None
- self.integrations: "dict[str, Integration]" = {}
- def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any":
- return {"options": {}}
- def __setstate__(self, *args: "Any", **kwargs: "Any") -> None:
- pass
- @property
- def dsn(self) -> "Optional[str]":
- return None
- @property
- def parsed_dsn(self) -> "Optional[Dsn]":
- return None
- def should_send_default_pii(self) -> bool:
- return False
- def is_active(self) -> bool:
- """
- .. versionadded:: 2.0.0
- Returns whether the client is active (able to send data to Sentry)
- """
- return False
- def capture_event(self, *args: "Any", **kwargs: "Any") -> "Optional[str]":
- return None
- def _capture_log(self, log: "Log", scope: "Scope") -> None:
- pass
- def _capture_metric(self, metric: "Metric", scope: "Scope") -> None:
- pass
- def _capture_span(self, span: "StreamedSpan", scope: "Scope") -> None:
- pass
- def capture_session(self, *args: "Any", **kwargs: "Any") -> None:
- return None
- if TYPE_CHECKING:
- @overload
- def get_integration(self, name_or_class: str) -> "Optional[Integration]": ...
- @overload
- def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ...
- def get_integration(
- self, name_or_class: "Union[str, type[Integration]]"
- ) -> "Optional[Integration]":
- return None
- def close(self, *args: "Any", **kwargs: "Any") -> None:
- return None
- def flush(self, *args: "Any", **kwargs: "Any") -> None:
- return None
- async def close_async(self, *args: "Any", **kwargs: "Any") -> None:
- return None
- async def flush_async(self, *args: "Any", **kwargs: "Any") -> None:
- return None
- def __enter__(self) -> "BaseClient":
- return self
- def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
- return None
- class NonRecordingClient(BaseClient):
- """
- .. versionadded:: 2.0.0
- A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized.
- """
- pass
- class _Client(BaseClient):
- """
- The client is internally responsible for capturing the events and
- forwarding them to sentry through the configured transport. It takes
- the client options as keyword arguments and optionally the DSN as first
- argument.
- Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support)
- """
- def __init__(self, *args: "Any", **kwargs: "Any") -> None:
- super(_Client, self).__init__(options=get_options(*args, **kwargs))
- self._init_impl()
- def __getstate__(self) -> "Any":
- return {"options": self.options}
- def __setstate__(self, state: "Any") -> None:
- self.options = state["options"]
- self._init_impl()
- def _setup_instrumentation(
- self, functions_to_trace: "Sequence[Dict[str, str]]"
- ) -> None:
- """
- Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator.
- """
- for function in functions_to_trace:
- class_name = None
- function_qualname = function["qualified_name"]
- module_name, function_name = function_qualname.rsplit(".", 1)
- try:
- # Try to import module and function
- # ex: "mymodule.submodule.funcname"
- module_obj = import_module(module_name)
- function_obj = getattr(module_obj, function_name)
- setattr(module_obj, function_name, trace(function_obj))
- logger.debug("Enabled tracing for %s", function_qualname)
- except module_not_found_error:
- try:
- # Try to import a class
- # ex: "mymodule.submodule.MyClassName.member_function"
- module_name, class_name = module_name.rsplit(".", 1)
- module_obj = import_module(module_name)
- class_obj = getattr(module_obj, class_name)
- function_obj = getattr(class_obj, function_name)
- function_type = type(class_obj.__dict__[function_name])
- traced_function = trace(function_obj)
- if function_type in (staticmethod, classmethod):
- traced_function = staticmethod(traced_function)
- setattr(class_obj, function_name, traced_function)
- setattr(module_obj, class_name, class_obj)
- logger.debug("Enabled tracing for %s", function_qualname)
- except Exception as e:
- logger.warning(
- "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
- function_qualname,
- e,
- )
- except Exception as e:
- logger.warning(
- "Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
- function_qualname,
- e,
- )
- def _init_impl(self) -> None:
- old_debug = _client_init_debug.get(False)
- def _capture_envelope(envelope: "Envelope") -> None:
- if self.spotlight is not None:
- self.spotlight.capture_envelope(envelope)
- if self.transport is not None:
- self.transport.capture_envelope(envelope)
- def _record_lost_event(
- reason: str,
- data_category: "EventDataCategory",
- item: "Optional[Item]" = None,
- quantity: int = 1,
- ) -> None:
- if self.transport is not None:
- self.transport.record_lost_event(
- reason=reason,
- data_category=data_category,
- item=item,
- quantity=quantity,
- )
- try:
- _client_init_debug.set(self.options["debug"])
- self.transport = make_transport(self.options)
- self.monitor = None
- if self.transport:
- if self.options["enable_backpressure_handling"]:
- self.monitor = Monitor(self.transport)
- # Setup Spotlight before creating batchers so _capture_envelope can use it.
- # setup_spotlight handles all config/env var resolution per the SDK spec.
- from sentry_sdk.spotlight import setup_spotlight
- self.spotlight = setup_spotlight(self.options)
- if self.spotlight is not None and not self.options["dsn"]:
- sample_all = lambda *_args, **_kwargs: 1.0
- self.options["send_default_pii"] = True
- self.options["error_sampler"] = sample_all
- self.options["traces_sampler"] = sample_all
- self.options["profiles_sampler"] = sample_all
- self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
- self.log_batcher = None
- if has_logs_enabled(self.options):
- from sentry_sdk._log_batcher import LogBatcher
- self.log_batcher = LogBatcher(
- capture_func=_capture_envelope,
- record_lost_func=_record_lost_event,
- )
- self.metrics_batcher = None
- if has_metrics_enabled(self.options):
- self.metrics_batcher = MetricsBatcher(
- capture_func=_capture_envelope,
- record_lost_func=_record_lost_event,
- )
- self.span_batcher = None
- if has_span_streaming_enabled(self.options):
- self.span_batcher = SpanBatcher(
- capture_func=_capture_envelope,
- record_lost_func=_record_lost_event,
- )
- max_request_body_size = ("always", "never", "small", "medium")
- if self.options["max_request_body_size"] not in max_request_body_size:
- raise ValueError(
- "Invalid value for max_request_body_size. Must be one of {}".format(
- max_request_body_size
- )
- )
- if self.options["_experiments"].get("otel_powered_performance", False):
- logger.debug(
- "[OTel] Enabling experimental OTel-powered performance monitoring."
- )
- self.options["instrumenter"] = INSTRUMENTER.OTEL
- if (
- "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration"
- not in _DEFAULT_INTEGRATIONS
- ):
- _DEFAULT_INTEGRATIONS.append(
- "sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration",
- )
- self.integrations = setup_integrations(
- self.options["integrations"],
- with_defaults=self.options["default_integrations"],
- with_auto_enabling_integrations=self.options[
- "auto_enabling_integrations"
- ],
- disabled_integrations=self.options["disabled_integrations"],
- options=self.options,
- )
- sdk_name = get_sdk_name(list(self.integrations.keys()))
- SDK_INFO["name"] = sdk_name
- logger.debug("Setting SDK name to '%s'", sdk_name)
- if has_profiling_enabled(self.options):
- try:
- setup_profiler(self.options)
- except Exception as e:
- logger.debug("Can not set up profiler. (%s)", e)
- else:
- try:
- setup_continuous_profiler(
- self.options,
- sdk_info=SDK_INFO,
- capture_func=_capture_envelope,
- )
- except Exception as e:
- logger.debug("Can not set up continuous profiler. (%s)", e)
- finally:
- _client_init_debug.set(old_debug)
- self._setup_instrumentation(self.options.get("functions_to_trace", []))
- if (
- self.monitor
- or self.log_batcher
- or self.metrics_batcher
- or self.span_batcher
- or has_profiling_enabled(self.options)
- or isinstance(self.transport, HttpTransportCore)
- ):
- # If we have anything on that could spawn a background thread, we
- # need to check if it's safe to use them.
- check_uwsgi_thread_support()
- def is_active(self) -> bool:
- """
- .. versionadded:: 2.0.0
- Returns whether the client is active (able to send data to Sentry)
- """
- return True
- def should_send_default_pii(self) -> bool:
- """
- .. versionadded:: 2.0.0
- Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry.
- """
- return self.options.get("send_default_pii") or False
- @property
- def dsn(self) -> "Optional[str]":
- """Returns the configured DSN as string."""
- return self.options["dsn"]
- @property
- def parsed_dsn(self) -> "Optional[Dsn]":
- """Returns the configured parsed DSN object."""
- return self.transport.parsed_dsn if self.transport else None
- def _prepare_event(
- self,
- event: "Event",
- hint: "Hint",
- scope: "Optional[Scope]",
- ) -> "Optional[Event]":
- previous_total_spans: "Optional[int]" = None
- previous_total_breadcrumbs: "Optional[int]" = None
- if event.get("timestamp") is None:
- event["timestamp"] = datetime.now(timezone.utc)
- is_transaction = event.get("type") == "transaction"
- if scope is not None:
- spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
- event_ = scope.apply_to_event(event, hint, self.options)
- # one of the event/error processors returned None
- if event_ is None:
- if self.transport:
- self.transport.record_lost_event(
- "event_processor",
- data_category=("transaction" if is_transaction else "error"),
- )
- if is_transaction:
- self.transport.record_lost_event(
- "event_processor",
- data_category="span",
- quantity=spans_before + 1, # +1 for the transaction itself
- )
- return None
- event = event_
- spans_delta = spans_before - len(
- cast(List[Dict[str, object]], event.get("spans", []))
- )
- span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0)
- if is_transaction and self.transport is not None:
- if spans_delta > 0:
- self.transport.record_lost_event(
- "event_processor", data_category="span", quantity=spans_delta
- )
- if span_recorder_dropped_spans > 0:
- self.transport.record_lost_event(
- "buffer_overflow",
- data_category="span",
- quantity=span_recorder_dropped_spans,
- )
- dropped_spans: int = span_recorder_dropped_spans + spans_delta
- if dropped_spans > 0:
- previous_total_spans = spans_before + dropped_spans
- if scope._n_breadcrumbs_truncated > 0:
- breadcrumbs = event.get("breadcrumbs", {})
- values = (
- breadcrumbs.get("values", [])
- if not isinstance(breadcrumbs, AnnotatedValue)
- else []
- )
- previous_total_breadcrumbs = (
- len(values) + scope._n_breadcrumbs_truncated
- )
- if (
- not is_transaction
- and self.options["attach_stacktrace"]
- and "exception" not in event
- and "stacktrace" not in event
- and "threads" not in event
- ):
- with capture_internal_exceptions():
- event["threads"] = {
- "values": [
- {
- "stacktrace": current_stacktrace(
- include_local_variables=self.options.get(
- "include_local_variables", True
- ),
- max_value_length=self.options.get(
- "max_value_length", DEFAULT_MAX_VALUE_LENGTH
- ),
- ),
- "crashed": False,
- "current": True,
- }
- ]
- }
- for key in "release", "environment", "server_name", "dist":
- if event.get(key) is None and self.options[key] is not None:
- event[key] = str(self.options[key]).strip()
- if event.get("sdk") is None:
- sdk_info = dict(SDK_INFO)
- sdk_info["integrations"] = sorted(self.integrations.keys())
- event["sdk"] = sdk_info
- if event.get("platform") is None:
- event["platform"] = "python"
- event = handle_in_app(
- event,
- self.options["in_app_exclude"],
- self.options["in_app_include"],
- self.options["project_root"],
- )
- if event is not None:
- event_scrubber = self.options["event_scrubber"]
- if event_scrubber:
- event_scrubber.scrub_event(event)
- if scope is not None and scope._gen_ai_original_message_count:
- spans: "List[Dict[str, Any]] | AnnotatedValue" = event.get("spans", [])
- if isinstance(spans, list):
- for span in spans:
- span_id = span.get("span_id", None)
- span_data = span.get("data", {})
- if (
- span_id
- and span_id in scope._gen_ai_original_message_count
- and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data
- ):
- span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue(
- span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES],
- {"len": scope._gen_ai_original_message_count[span_id]},
- )
- if previous_total_spans is not None:
- event["spans"] = AnnotatedValue(
- event.get("spans", []), {"len": previous_total_spans}
- )
- if previous_total_breadcrumbs is not None:
- event["breadcrumbs"] = AnnotatedValue(
- event.get("breadcrumbs", {"values": []}),
- {"len": previous_total_breadcrumbs},
- )
- # Postprocess the event here so that annotated types do
- # generally not surface in before_send
- if event is not None:
- event = cast(
- "Event",
- serialize(
- cast("Dict[str, Any]", event),
- max_request_body_size=self.options.get("max_request_body_size"),
- max_value_length=self.options.get("max_value_length"),
- custom_repr=self.options.get("custom_repr"),
- ),
- )
- before_send = self.options["before_send"]
- if (
- before_send is not None
- and event is not None
- and event.get("type") != "transaction"
- ):
- new_event = None
- with capture_internal_exceptions():
- new_event = before_send(event, hint or {})
- if new_event is None:
- logger.info("before send dropped event")
- if self.transport:
- self.transport.record_lost_event(
- "before_send", data_category="error"
- )
- # If this is an exception, reset the DedupeIntegration. It still
- # remembers the dropped exception as the last exception, meaning
- # that if the same exception happens again and is not dropped
- # in before_send, it'd get dropped by DedupeIntegration.
- if event.get("exception"):
- DedupeIntegration.reset_last_seen()
- event = new_event
- before_send_transaction = self.options["before_send_transaction"]
- if (
- before_send_transaction is not None
- and event is not None
- and event.get("type") == "transaction"
- ):
- new_event = None
- spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
- with capture_internal_exceptions():
- new_event = before_send_transaction(event, hint or {})
- if new_event is None:
- logger.info("before send transaction dropped event")
- if self.transport:
- self.transport.record_lost_event(
- reason="before_send", data_category="transaction"
- )
- self.transport.record_lost_event(
- reason="before_send",
- data_category="span",
- quantity=spans_before + 1, # +1 for the transaction itself
- )
- else:
- spans_delta = spans_before - len(new_event.get("spans", []))
- if spans_delta > 0 and self.transport is not None:
- self.transport.record_lost_event(
- reason="before_send", data_category="span", quantity=spans_delta
- )
- event = new_event
- return event
- def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool:
- exc_info = hint.get("exc_info")
- if exc_info is None:
- return False
- error = exc_info[0]
- error_type_name = get_type_name(exc_info[0])
- error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name)
- for ignored_error in self.options["ignore_errors"]:
- # String types are matched against the type name in the
- # exception only
- if isinstance(ignored_error, str):
- if ignored_error == error_full_name or ignored_error == error_type_name:
- return True
- else:
- if issubclass(error, ignored_error):
- return True
- return False
- def _should_capture(
- self,
- event: "Event",
- hint: "Hint",
- scope: "Optional[Scope]" = None,
- ) -> bool:
- # Transactions are sampled independent of error events.
- is_transaction = event.get("type") == "transaction"
- if is_transaction:
- return True
- ignoring_prevents_recursion = scope is not None and not scope._should_capture
- if ignoring_prevents_recursion:
- return False
- ignored_by_config_option = self._is_ignored_error(event, hint)
- if ignored_by_config_option:
- return False
- return True
- def _should_sample_error(
- self,
- event: "Event",
- hint: "Hint",
- ) -> bool:
- error_sampler = self.options.get("error_sampler", None)
- if callable(error_sampler):
- with capture_internal_exceptions():
- sample_rate = error_sampler(event, hint)
- else:
- sample_rate = self.options["sample_rate"]
- try:
- not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate
- except NameError:
- logger.warning(
- "The provided error_sampler raised an error. Defaulting to sampling the event."
- )
- # If the error_sampler raised an error, we should sample the event, since the default behavior
- # (when no sample_rate or error_sampler is provided) is to sample all events.
- not_in_sample_rate = False
- except TypeError:
- parameter, verb = (
- ("error_sampler", "returned")
- if callable(error_sampler)
- else ("sample_rate", "contains")
- )
- logger.warning(
- "The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event."
- % (parameter, verb, repr(sample_rate))
- )
- # If the sample_rate has an invalid value, we should sample the event, since the default behavior
- # (when no sample_rate or error_sampler is provided) is to sample all events.
- not_in_sample_rate = False
- if not_in_sample_rate:
- # because we will not sample this event, record a "lost event".
- if self.transport:
- self.transport.record_lost_event("sample_rate", data_category="error")
- return False
- return True
- def _update_session_from_event(
- self,
- session: "Session",
- event: "Event",
- ) -> None:
- crashed = False
- errored = False
- user_agent = None
- exceptions = (event.get("exception") or {}).get("values")
- if exceptions:
- errored = True
- for error in exceptions:
- if isinstance(error, AnnotatedValue):
- error = error.value or {}
- mechanism = error.get("mechanism")
- if isinstance(mechanism, Mapping) and mechanism.get("handled") is False:
- crashed = True
- break
- user = event.get("user")
- if session.user_agent is None:
- headers = (event.get("request") or {}).get("headers")
- headers_dict = headers if isinstance(headers, dict) else {}
- for k, v in headers_dict.items():
- if k.lower() == "user-agent":
- user_agent = v
- break
- session.update(
- status="crashed" if crashed else None,
- user=user,
- user_agent=user_agent,
- errors=session.errors + (errored or crashed),
- )
- def capture_event(
- self,
- event: "Event",
- hint: "Optional[Hint]" = None,
- scope: "Optional[Scope]" = None,
- ) -> "Optional[str]":
- """Captures an event.
- :param event: A ready-made event that can be directly sent to Sentry.
- :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.
- :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
- :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.
- """
- hint: "Hint" = dict(hint or ())
- if not self._should_capture(event, hint, scope):
- return None
- profile = event.pop("profile", None)
- event_id = event.get("event_id")
- if event_id is None:
- event["event_id"] = event_id = uuid.uuid4().hex
- event_opt = self._prepare_event(event, hint, scope)
- if event_opt is None:
- return None
- # whenever we capture an event we also check if the session needs
- # to be updated based on that information.
- session = scope._session if scope else None
- if session:
- self._update_session_from_event(session, event)
- is_transaction = event_opt.get("type") == "transaction"
- is_checkin = event_opt.get("type") == "check_in"
- if (
- not is_transaction
- and not is_checkin
- and not self._should_sample_error(event, hint)
- ):
- return None
- attachments = hint.get("attachments")
- trace_context = event_opt.get("contexts", {}).get("trace") or {}
- dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {})
- headers: "dict[str, object]" = {
- "event_id": event_opt["event_id"],
- "sent_at": format_timestamp(datetime.now(timezone.utc)),
- }
- if dynamic_sampling_context:
- headers["trace"] = dynamic_sampling_context
- envelope = Envelope(headers=headers)
- if is_transaction:
- if isinstance(profile, Profile):
- envelope.add_profile(profile.to_json(event_opt, self.options))
- envelope.add_transaction(event_opt)
- elif is_checkin:
- envelope.add_checkin(event_opt)
- else:
- envelope.add_event(event_opt)
- for attachment in attachments or ():
- envelope.add_item(attachment.to_envelope_item())
- return_value = None
- if self.spotlight:
- self.spotlight.capture_envelope(envelope)
- return_value = event_id
- if self.transport is not None:
- self.transport.capture_envelope(envelope)
- return_value = event_id
- return return_value
- def _capture_telemetry(
- self,
- telemetry: "Optional[Union[Log, Metric, StreamedSpan]]",
- ty: str,
- scope: "Scope",
- ) -> None:
- # Capture attributes-based telemetry (logs, metrics, spansV2)
- if telemetry is None:
- return
- scope.apply_to_telemetry(telemetry)
- before_send = None
- if ty == "log":
- before_send = get_before_send_log(self.options)
- elif ty == "metric":
- before_send = get_before_send_metric(self.options) # type: ignore
- if before_send is not None:
- telemetry = before_send(telemetry, {}) # type: ignore
- if telemetry is None:
- return
- batcher = None
- if ty == "log":
- batcher = self.log_batcher
- elif ty == "metric":
- batcher = self.metrics_batcher # type: ignore
- elif ty == "span":
- batcher = self.span_batcher # type: ignore
- if batcher is not None:
- batcher.add(telemetry) # type: ignore
- def _capture_log(self, log: "Optional[Log]", scope: "Scope") -> None:
- self._capture_telemetry(log, "log", scope)
- def _capture_metric(self, metric: "Optional[Metric]", scope: "Scope") -> None:
- self._capture_telemetry(metric, "metric", scope)
- def _capture_span(self, span: "Optional[StreamedSpan]", scope: "Scope") -> None:
- self._capture_telemetry(span, "span", scope)
- def capture_session(
- self,
- session: "Session",
- ) -> None:
- if not session.release:
- logger.info("Discarded session update because of missing release")
- else:
- self.session_flusher.add_session(session)
- if TYPE_CHECKING:
- @overload
- def get_integration(self, name_or_class: str) -> "Optional[Integration]": ...
- @overload
- def get_integration(self, name_or_class: "type[I]") -> "Optional[I]": ...
- def get_integration(
- self,
- name_or_class: "Union[str, Type[Integration]]",
- ) -> "Optional[Integration]":
- """Returns the integration for this client by name or class.
- If the client does not have that integration then `None` is returned.
- """
- if isinstance(name_or_class, str):
- integration_name = name_or_class
- elif name_or_class.identifier is not None:
- integration_name = name_or_class.identifier
- else:
- raise ValueError("Integration has no name")
- return self.integrations.get(integration_name)
- def _has_async_transport(self) -> bool:
- """Check if the current transport is async."""
- return isinstance(self.transport, AsyncHttpTransport)
- @property
- def _batchers(self) -> "tuple[Any, ...]":
- return tuple(
- b
- for b in (self.log_batcher, self.metrics_batcher, self.span_batcher)
- if b is not None
- )
- def _close_components(self) -> None:
- """Kill all client components in the correct order."""
- self.session_flusher.kill()
- for b in self._batchers:
- b.kill()
- if self.monitor:
- self.monitor.kill()
- def _flush_components(self) -> None:
- """Flush all client components."""
- self.session_flusher.flush()
- for b in self._batchers:
- b.flush()
- def close(
- self,
- timeout: "Optional[float]" = None,
- callback: "Optional[Callable[[int, float], None]]" = None,
- ) -> None:
- """
- Close the client and shut down the transport. Arguments have the same
- semantics as :py:meth:`Client.flush`.
- """
- if self.transport is not None:
- if self._has_async_transport():
- warnings.warn(
- "close() used with AsyncHttpTransport. Use close_async() instead.",
- stacklevel=2,
- )
- self._flush_components()
- else:
- self.flush(timeout=timeout, callback=callback)
- self._close_components()
- self.transport.kill()
- self.transport = None
- async def close_async(
- self,
- timeout: "Optional[float]" = None,
- callback: "Optional[Callable[[int, float], None]]" = None,
- ) -> None:
- """
- Asynchronously close the client and shut down the transport. Arguments have the same
- semantics as :py:meth:`Client.flush_async`.
- """
- if self.transport is not None:
- if not self._has_async_transport():
- logger.debug(
- "close_async() used with non-async transport, aborting. Please use close() instead."
- )
- return
- await self.flush_async(timeout=timeout, callback=callback)
- self._close_components()
- kill_task = self.transport.kill() # type: ignore
- if kill_task is not None:
- await kill_task
- self.transport = None
- def flush(
- self,
- timeout: "Optional[float]" = None,
- callback: "Optional[Callable[[int, float], None]]" = None,
- ) -> None:
- """
- Wait for the current events to be sent.
- :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
- :param callback: Is invoked with the number of pending events and the configured timeout.
- """
- if self.transport is not None:
- if self._has_async_transport():
- warnings.warn(
- "flush() used with AsyncHttpTransport. Use flush_async() instead.",
- stacklevel=2,
- )
- return
- if timeout is None:
- timeout = self.options["shutdown_timeout"]
- self._flush_components()
- self.transport.flush(timeout=timeout, callback=callback)
- async def flush_async(
- self,
- timeout: "Optional[float]" = None,
- callback: "Optional[Callable[[int, float], None]]" = None,
- ) -> None:
- """
- Asynchronously wait for the current events to be sent.
- :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
- :param callback: Is invoked with the number of pending events and the configured timeout.
- """
- if self.transport is not None:
- if not self._has_async_transport():
- logger.debug(
- "flush_async() used with non-async transport, aborting. Please use flush() instead."
- )
- return
- if timeout is None:
- timeout = self.options["shutdown_timeout"]
- self._flush_components()
- flush_task = self.transport.flush(timeout=timeout, callback=callback) # type: ignore
- if flush_task is not None:
- await flush_task
- def __enter__(self) -> "_Client":
- return self
- def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
- self.close()
- async def __aenter__(self) -> "_Client":
- return self
- async def __aexit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
- await self.close_async()
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- # Make mypy, PyCharm and other static analyzers think `get_options` is a
- # type to have nicer autocompletion for params.
- #
- # Use `ClientConstructor` to define the argument types of `init` and
- # `Dict[str, Any]` to tell static analyzers about the return type.
- class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801
- pass
- class Client(ClientConstructor, _Client):
- pass
- else:
- # Alias `get_options` for actual usage. Go through the lambda indirection
- # to throw PyCharm off of the weakly typed signature (it would otherwise
- # discover both the weakly typed signature of `_init` and our faked `init`
- # type).
- get_options = (lambda: _get_options)()
- Client = (lambda: _Client)()
|