diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/tracing.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/sentry_sdk/tracing.py | 1358 |
1 files changed, 1358 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/tracing.py b/.venv/lib/python3.12/site-packages/sentry_sdk/tracing.py new file mode 100644 index 00000000..13d9f63d --- /dev/null +++ b/.venv/lib/python3.12/site-packages/sentry_sdk/tracing.py @@ -0,0 +1,1358 @@ +import uuid +import warnings +from datetime import datetime, timedelta, timezone +from enum import Enum + +import sentry_sdk +from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA +from sentry_sdk.profiler.continuous_profiler import get_profiler_id +from sentry_sdk.utils import ( + get_current_thread_meta, + is_valid_sample_rate, + logger, + nanosecond_time, + should_be_treated_as_error, +) + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, MutableMapping + from typing import Any + from typing import Dict + from typing import Iterator + from typing import List + from typing import Optional + from typing import overload + from typing import ParamSpec + from typing import Tuple + from typing import Union + from typing import TypeVar + + from typing_extensions import TypedDict, Unpack + + P = ParamSpec("P") + R = TypeVar("R") + + from sentry_sdk.profiler.continuous_profiler import ContinuousProfile + from sentry_sdk.profiler.transaction_profiler import Profile + from sentry_sdk._types import ( + Event, + MeasurementUnit, + SamplingContext, + MeasurementValue, + ) + + class SpanKwargs(TypedDict, total=False): + trace_id: str + """ + The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + """ + + span_id: str + """The span ID of this span. If omitted, a new span ID will be generated.""" + + parent_span_id: str + """The span ID of the parent span, if applicable.""" + + same_process_as_parent: bool + """Whether this span is in the same process as the parent span.""" + + sampled: bool + """ + Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + """ + + op: str + """ + The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + """ + + description: str + """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead.""" + + hub: Optional["sentry_sdk.Hub"] + """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead.""" + + status: str + """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/""" + + containing_transaction: Optional["Transaction"] + """The transaction that this span belongs to.""" + + start_timestamp: Optional[Union[datetime, float]] + """ + The timestamp when the span started. If omitted, the current time + will be used. + """ + + scope: "sentry_sdk.Scope" + """The scope to use for this span. If not provided, we use the current scope.""" + + origin: str + """ + The origin of the span. + See https://develop.sentry.dev/sdk/performance/trace-origin/ + Default "manual". + """ + + name: str + """A string describing what operation is being performed within the span/transaction.""" + + class TransactionKwargs(SpanKwargs, total=False): + source: str + """ + A string describing the source of the transaction name. This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information. + Default "custom". + """ + + parent_sampled: bool + """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded.""" + + baggage: "Baggage" + """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)""" + + ProfileContext = TypedDict( + "ProfileContext", + { + "profiler_id": str, + }, + ) + +BAGGAGE_HEADER_NAME = "baggage" +SENTRY_TRACE_HEADER_NAME = "sentry-trace" + + +# Transaction source +# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations +class TransactionSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self): + # type: () -> str + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_TRANSACTION_SOURCES = [ + TransactionSource.URL, +] + +SOURCE_FOR_STYLE = { + "endpoint": TransactionSource.COMPONENT, + "function_name": TransactionSource.COMPONENT, + "handler_name": TransactionSource.COMPONENT, + "method_and_path_pattern": TransactionSource.ROUTE, + "path": TransactionSource.URL, + "route_name": TransactionSource.COMPONENT, + "route_pattern": TransactionSource.ROUTE, + "uri_template": TransactionSource.ROUTE, + "url": TransactionSource.ROUTE, +} + + +def get_span_status_from_http_code(http_status_code): + # type: (int) -> str + """ + Returns the Sentry status corresponding to the given HTTP status code. + + See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context + """ + if http_status_code < 400: + return SPANSTATUS.OK + + elif 400 <= http_status_code < 500: + if http_status_code == 403: + return SPANSTATUS.PERMISSION_DENIED + elif http_status_code == 404: + return SPANSTATUS.NOT_FOUND + elif http_status_code == 429: + return SPANSTATUS.RESOURCE_EXHAUSTED + elif http_status_code == 413: + return SPANSTATUS.FAILED_PRECONDITION + elif http_status_code == 401: + return SPANSTATUS.UNAUTHENTICATED + elif http_status_code == 409: + return SPANSTATUS.ALREADY_EXISTS + else: + return SPANSTATUS.INVALID_ARGUMENT + + elif 500 <= http_status_code < 600: + if http_status_code == 504: + return SPANSTATUS.DEADLINE_EXCEEDED + elif http_status_code == 501: + return SPANSTATUS.UNIMPLEMENTED + elif http_status_code == 503: + return SPANSTATUS.UNAVAILABLE + else: + return SPANSTATUS.INTERNAL_ERROR + + return SPANSTATUS.UNKNOWN_ERROR + + +class _SpanRecorder: + """Limits the number of spans recorded in a transaction.""" + + __slots__ = ("maxlen", "spans", "dropped_spans") + + def __init__(self, maxlen): + # type: (int) -> None + # FIXME: this is `maxlen - 1` only to preserve historical behavior + # enforced by tests. + # Either this should be changed to `maxlen` or the JS SDK implementation + # should be changed to match a consistent interpretation of what maxlen + # limits: either transaction+spans or only child spans. + self.maxlen = maxlen - 1 + self.spans = [] # type: List[Span] + self.dropped_spans = 0 # type: int + + def add(self, span): + # type: (Span) -> None + if len(self.spans) > self.maxlen: + span._span_recorder = None + self.dropped_spans += 1 + else: + self.spans.append(span) + + +class Span: + """A span holds timing information of a block of code. + Spans can have multiple child spans thus forming a span tree. + + :param trace_id: The trace ID of the root span. If this new span is to be the root span, + omit this parameter, and a new trace ID will be generated. + :param span_id: The span ID of this span. If omitted, a new span ID will be generated. + :param parent_span_id: The span ID of the parent span, if applicable. + :param same_process_as_parent: Whether this span is in the same process as the parent span. + :param sampled: Whether the span should be sampled. Overrides the default sampling decision + for this span when provided. + :param op: The span's operation. A list of recommended values is available here: + https://develop.sentry.dev/sdk/performance/span-operations/ + :param description: A description of what operation is being performed within the span. + + .. deprecated:: 2.15.0 + Please use the `name` parameter, instead. + :param name: A string describing what operation is being performed within the span. + :param hub: The hub to use for this span. + + .. deprecated:: 2.0.0 + Please use the `scope` parameter, instead. + :param status: The span's status. Possible values are listed at + https://develop.sentry.dev/sdk/event-payloads/span/ + :param containing_transaction: The transaction that this span belongs to. + :param start_timestamp: The timestamp when the span started. If omitted, the current time + will be used. + :param scope: The scope to use for this span. If not provided, we use the current scope. + """ + + __slots__ = ( + "trace_id", + "span_id", + "parent_span_id", + "same_process_as_parent", + "sampled", + "op", + "description", + "_measurements", + "start_timestamp", + "_start_timestamp_monotonic_ns", + "status", + "timestamp", + "_tags", + "_data", + "_span_recorder", + "hub", + "_context_manager_state", + "_containing_transaction", + "_local_aggregator", + "scope", + "origin", + "name", + ) + + def __init__( + self, + trace_id=None, # type: Optional[str] + span_id=None, # type: Optional[str] + parent_span_id=None, # type: Optional[str] + same_process_as_parent=True, # type: bool + sampled=None, # type: Optional[bool] + op=None, # type: Optional[str] + description=None, # type: Optional[str] + hub=None, # type: Optional[sentry_sdk.Hub] # deprecated + status=None, # type: Optional[str] + containing_transaction=None, # type: Optional[Transaction] + start_timestamp=None, # type: Optional[Union[datetime, float]] + scope=None, # type: Optional[sentry_sdk.Scope] + origin="manual", # type: str + name=None, # type: Optional[str] + ): + # type: (...) -> None + self.trace_id = trace_id or uuid.uuid4().hex + self.span_id = span_id or uuid.uuid4().hex[16:] + self.parent_span_id = parent_span_id + self.same_process_as_parent = same_process_as_parent + self.sampled = sampled + self.op = op + self.description = name or description + self.status = status + self.hub = hub # backwards compatibility + self.scope = scope + self.origin = origin + self._measurements = {} # type: Dict[str, MeasurementValue] + self._tags = {} # type: MutableMapping[str, str] + self._data = {} # type: Dict[str, Any] + self._containing_transaction = containing_transaction + + if hub is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use `scope` instead.", + DeprecationWarning, + stacklevel=2, + ) + + self.scope = self.scope or hub.scope + + if start_timestamp is None: + start_timestamp = datetime.now(timezone.utc) + elif isinstance(start_timestamp, float): + start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc) + self.start_timestamp = start_timestamp + try: + # profiling depends on this value and requires that + # it is measured in nanoseconds + self._start_timestamp_monotonic_ns = nanosecond_time() + except AttributeError: + pass + + #: End timestamp of span + self.timestamp = None # type: Optional[datetime] + + self._span_recorder = None # type: Optional[_SpanRecorder] + self._local_aggregator = None # type: Optional[LocalAggregator] + + self.update_active_thread() + self.set_profiler_id(get_profiler_id()) + + # TODO this should really live on the Transaction class rather than the Span + # class + def init_span_recorder(self, maxlen): + # type: (int) -> None + if self._span_recorder is None: + self._span_recorder = _SpanRecorder(maxlen) + + def _get_local_aggregator(self): + # type: (...) -> LocalAggregator + rv = self._local_aggregator + if rv is None: + rv = self._local_aggregator = LocalAggregator() + return rv + + def __repr__(self): + # type: () -> str + return ( + "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.op, + self.description, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.origin, + ) + ) + + def __enter__(self): + # type: () -> Span + scope = self.scope or sentry_sdk.get_current_scope() + old_span = scope.span + scope.span = self + self._context_manager_state = (scope, old_span) + return self + + def __exit__(self, ty, value, tb): + # type: (Optional[Any], Optional[Any], Optional[Any]) -> None + if value is not None and should_be_treated_as_error(ty, value): + self.set_status(SPANSTATUS.INTERNAL_ERROR) + + scope, old_span = self._context_manager_state + del self._context_manager_state + self.finish(scope) + scope.span = old_span + + @property + def containing_transaction(self): + # type: () -> Optional[Transaction] + """The ``Transaction`` that this span belongs to. + The ``Transaction`` is the root of the span tree, + so one could also think of this ``Transaction`` as the "root span".""" + + # this is a getter rather than a regular attribute so that transactions + # can return `self` here instead (as a way to prevent them circularly + # referencing themselves) + return self._containing_transaction + + def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs): + # type: (str, **Any) -> Span + """ + Start a sub-span from the current span or transaction. + + Takes the same arguments as the initializer of :py:class:`Span`. The + trace id, sampling decision, transaction pointer, and span recorder are + inherited from the current span/transaction. + + The instrumenter parameter is deprecated for user code, and it will + be removed in the next major version. Going forward, it should only + be used by the SDK itself. + """ + if kwargs.get("description") is not None: + warnings.warn( + "The `description` parameter is deprecated. Please use `name` instead.", + DeprecationWarning, + stacklevel=2, + ) + + configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"] + + if instrumenter != configuration_instrumenter: + return NoOpSpan() + + kwargs.setdefault("sampled", self.sampled) + + child = Span( + trace_id=self.trace_id, + parent_span_id=self.span_id, + containing_transaction=self.containing_transaction, + **kwargs, + ) + + span_recorder = ( + self.containing_transaction and self.containing_transaction._span_recorder + ) + if span_recorder: + span_recorder.add(child) + + return child + + @classmethod + def continue_from_environ( + cls, + environ, # type: Mapping[str, str] + **kwargs, # type: Any + ): + # type: (...) -> Transaction + """ + Create a Transaction with the given params, then add in data pulled from + the ``sentry-trace`` and ``baggage`` headers from the environ (if any) + before returning the Transaction. + + This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers` + in that it assumes header names in the form ``HTTP_HEADER_NAME`` - + such as you would get from a WSGI/ASGI environ - + rather than the form ``header-name``. + + :param environ: The ASGI/WSGI environ to pull information from. + """ + if cls is Span: + logger.warning( + "Deprecated: use Transaction.continue_from_environ " + "instead of Span.continue_from_environ." + ) + return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs) + + @classmethod + def continue_from_headers( + cls, + headers, # type: Mapping[str, str] + *, + _sample_rand=None, # type: Optional[str] + **kwargs, # type: Any + ): + # type: (...) -> Transaction + """ + Create a transaction with the given params (including any data pulled from + the ``sentry-trace`` and ``baggage`` headers). + + :param headers: The dictionary with the HTTP headers to pull information from. + :param _sample_rand: If provided, we override the sample_rand value from the + incoming headers with this value. (internal use only) + """ + # TODO move this to the Transaction class + if cls is Span: + logger.warning( + "Deprecated: use Transaction.continue_from_headers " + "instead of Span.continue_from_headers." + ) + + # TODO-neel move away from this kwargs stuff, it's confusing and opaque + # make more explicit + baggage = Baggage.from_incoming_header( + headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand + ) + kwargs.update({BAGGAGE_HEADER_NAME: baggage}) + + sentrytrace_kwargs = extract_sentrytrace_data( + headers.get(SENTRY_TRACE_HEADER_NAME) + ) + + if sentrytrace_kwargs is not None: + kwargs.update(sentrytrace_kwargs) + + # If there's an incoming sentry-trace but no incoming baggage header, + # for instance in traces coming from older SDKs, + # baggage will be empty and immutable and won't be populated as head SDK. + baggage.freeze() + + transaction = Transaction(**kwargs) + transaction.same_process_as_parent = False + + return transaction + + def iter_headers(self): + # type: () -> Iterator[Tuple[str, str]] + """ + Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers. + If the span's containing transaction doesn't yet have a ``baggage`` value, + this will cause one to be generated and stored. + """ + if not self.containing_transaction: + # Do not propagate headers if there is no containing transaction. Otherwise, this + # span ends up being the root span of a new trace, and since it does not get sent + # to Sentry, the trace will be missing a root transaction. The dynamic sampling + # context will also be missing, breaking dynamic sampling & traces. + return + + yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent() + + baggage = self.containing_transaction.get_baggage().serialize() + if baggage: + yield BAGGAGE_HEADER_NAME, baggage + + @classmethod + def from_traceparent( + cls, + traceparent, # type: Optional[str] + **kwargs, # type: Any + ): + # type: (...) -> Optional[Transaction] + """ + DEPRECATED: Use :py:meth:`sentry_sdk.tracing.Span.continue_from_headers`. + + Create a ``Transaction`` with the given params, then add in data pulled from + the given ``sentry-trace`` header value before returning the ``Transaction``. + """ + logger.warning( + "Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) " + "instead of from_traceparent(traceparent, **kwargs)" + ) + + if not traceparent: + return None + + return cls.continue_from_headers( + {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs + ) + + def to_traceparent(self): + # type: () -> str + if self.sampled is True: + sampled = "1" + elif self.sampled is False: + sampled = "0" + else: + sampled = None + + traceparent = "%s-%s" % (self.trace_id, self.span_id) + if sampled is not None: + traceparent += "-%s" % (sampled,) + + return traceparent + + def to_baggage(self): + # type: () -> Optional[Baggage] + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with this ``Span``, if any. (Taken from the root of the span tree.) + """ + if self.containing_transaction: + return self.containing_transaction.get_baggage() + return None + + def set_tag(self, key, value): + # type: (str, Any) -> None + self._tags[key] = value + + def set_data(self, key, value): + # type: (str, Any) -> None + self._data[key] = value + + def set_status(self, value): + # type: (str) -> None + self.status = value + + def set_measurement(self, name, value, unit=""): + # type: (str, float, MeasurementUnit) -> None + self._measurements[name] = {"value": value, "unit": unit} + + def set_thread(self, thread_id, thread_name): + # type: (Optional[int], Optional[str]) -> None + + if thread_id is not None: + self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + + if thread_name is not None: + self.set_data(SPANDATA.THREAD_NAME, thread_name) + + def set_profiler_id(self, profiler_id): + # type: (Optional[str]) -> None + if profiler_id is not None: + self.set_data(SPANDATA.PROFILER_ID, profiler_id) + + def set_http_status(self, http_status): + # type: (int) -> None + self.set_tag( + "http.status_code", str(http_status) + ) # we keep this for backwards compatibility + self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status) + self.set_status(get_span_status_from_http_code(http_status)) + + def is_success(self): + # type: () -> bool + return self.status == "ok" + + def finish(self, scope=None, end_timestamp=None): + # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] + """ + Sets the end timestamp of the span. + + Additionally it also creates a breadcrumb from the span, + if the span represents a database or HTTP request. + + :param scope: The scope to use for this transaction. + If not provided, the current scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + + :return: Always ``None``. The type is ``Optional[str]`` to match + the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. + """ + if self.timestamp is not None: + # This span is already finished, ignore. + return None + + try: + if end_timestamp: + if isinstance(end_timestamp, float): + end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) + self.timestamp = end_timestamp + else: + elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns + self.timestamp = self.start_timestamp + timedelta( + microseconds=elapsed / 1000 + ) + except AttributeError: + self.timestamp = datetime.now(timezone.utc) + + scope = scope or sentry_sdk.get_current_scope() + maybe_create_breadcrumbs_from_span(scope, self) + + return None + + def to_json(self): + # type: () -> Dict[str, Any] + """Returns a JSON-compatible representation of the span.""" + + rv = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "same_process_as_parent": self.same_process_as_parent, + "op": self.op, + "description": self.description, + "start_timestamp": self.start_timestamp, + "timestamp": self.timestamp, + "origin": self.origin, + } # type: Dict[str, Any] + + if self.status: + self._tags["status"] = self.status + + if self._local_aggregator is not None: + metrics_summary = self._local_aggregator.to_json() + if metrics_summary: + rv["_metrics_summary"] = metrics_summary + + if len(self._measurements) > 0: + rv["measurements"] = self._measurements + + tags = self._tags + if tags: + rv["tags"] = tags + + data = self._data + if data: + rv["data"] = data + + return rv + + def get_trace_context(self): + # type: () -> Any + rv = { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "op": self.op, + "description": self.description, + "origin": self.origin, + } # type: Dict[str, Any] + if self.status: + rv["status"] = self.status + + if self.containing_transaction: + rv["dynamic_sampling_context"] = ( + self.containing_transaction.get_baggage().dynamic_sampling_context() + ) + + data = {} + + thread_id = self._data.get(SPANDATA.THREAD_ID) + if thread_id is not None: + data["thread.id"] = thread_id + + thread_name = self._data.get(SPANDATA.THREAD_NAME) + if thread_name is not None: + data["thread.name"] = thread_name + + if data: + rv["data"] = data + + return rv + + def get_profile_context(self): + # type: () -> Optional[ProfileContext] + profiler_id = self._data.get(SPANDATA.PROFILER_ID) + if profiler_id is None: + return None + + return { + "profiler_id": profiler_id, + } + + def update_active_thread(self): + # type: () -> None + thread_id, thread_name = get_current_thread_meta() + self.set_thread(thread_id, thread_name) + + +class Transaction(Span): + """The Transaction is the root element that holds all the spans + for Sentry performance instrumentation. + + :param name: Identifier of the transaction. + Will show up in the Sentry UI. + :param parent_sampled: Whether the parent transaction was sampled. + If True this transaction will be kept, if False it will be discarded. + :param baggage: The W3C baggage header value. + (see https://www.w3.org/TR/baggage/) + :param source: A string describing the source of the transaction name. + This will be used to determine the transaction's type. + See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations + for more information. Default "custom". + :param kwargs: Additional arguments to be passed to the Span constructor. + See :py:class:`sentry_sdk.tracing.Span` for available arguments. + """ + + __slots__ = ( + "name", + "source", + "parent_sampled", + # used to create baggage value for head SDKs in dynamic sampling + "sample_rate", + "_measurements", + "_contexts", + "_profile", + "_continuous_profile", + "_baggage", + "_sample_rand", + ) + + def __init__( # type: ignore[misc] + self, + name="", # type: str + parent_sampled=None, # type: Optional[bool] + baggage=None, # type: Optional[Baggage] + source=TransactionSource.CUSTOM, # type: str + **kwargs, # type: Unpack[SpanKwargs] + ): + # type: (...) -> None + + super().__init__(**kwargs) + + self.name = name + self.source = source + self.sample_rate = None # type: Optional[float] + self.parent_sampled = parent_sampled + self._measurements = {} # type: Dict[str, MeasurementValue] + self._contexts = {} # type: Dict[str, Any] + self._profile = None # type: Optional[Profile] + self._continuous_profile = None # type: Optional[ContinuousProfile] + self._baggage = baggage + + baggage_sample_rand = ( + None if self._baggage is None else self._baggage._sample_rand() + ) + if baggage_sample_rand is not None: + self._sample_rand = baggage_sample_rand + else: + self._sample_rand = _generate_sample_rand(self.trace_id) + + def __repr__(self): + # type: () -> str + return ( + "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>" + % ( + self.__class__.__name__, + self.name, + self.op, + self.trace_id, + self.span_id, + self.parent_span_id, + self.sampled, + self.source, + self.origin, + ) + ) + + def _possibly_started(self): + # type: () -> bool + """Returns whether the transaction might have been started. + + If this returns False, we know that the transaction was not started + with sentry_sdk.start_transaction, and therefore the transaction will + be discarded. + """ + + # We must explicitly check self.sampled is False since self.sampled can be None + return self._span_recorder is not None or self.sampled is False + + def __enter__(self): + # type: () -> Transaction + if not self._possibly_started(): + logger.debug( + "Transaction was entered without being started with sentry_sdk.start_transaction." + "The transaction will not be sent to Sentry. To fix, start the transaction by" + "passing it to sentry_sdk.start_transaction." + ) + + super().__enter__() + + if self._profile is not None: + self._profile.__enter__() + + return self + + def __exit__(self, ty, value, tb): + # type: (Optional[Any], Optional[Any], Optional[Any]) -> None + if self._profile is not None: + self._profile.__exit__(ty, value, tb) + + if self._continuous_profile is not None: + self._continuous_profile.stop() + + super().__exit__(ty, value, tb) + + @property + def containing_transaction(self): + # type: () -> Transaction + """The root element of the span tree. + In the case of a transaction it is the transaction itself. + """ + + # Transactions (as spans) belong to themselves (as transactions). This + # is a getter rather than a regular attribute to avoid having a circular + # reference. + return self + + def _get_scope_from_finish_args( + self, + scope_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] + hub_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]] + ): + # type: (...) -> Optional[sentry_sdk.Scope] + """ + Logic to get the scope from the arguments passed to finish. This + function exists for backwards compatibility with the old finish. + + TODO: Remove this function in the next major version. + """ + scope_or_hub = scope_arg + if hub_arg is not None: + warnings.warn( + "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.", + DeprecationWarning, + stacklevel=3, + ) + + scope_or_hub = hub_arg + + if isinstance(scope_or_hub, sentry_sdk.Hub): + warnings.warn( + "Passing a Hub to finish is deprecated. Please pass a Scope, instead.", + DeprecationWarning, + stacklevel=3, + ) + + return scope_or_hub.scope + + return scope_or_hub + + def finish( + self, + scope=None, # type: Optional[sentry_sdk.Scope] + end_timestamp=None, # type: Optional[Union[float, datetime]] + *, + hub=None, # type: Optional[sentry_sdk.Hub] + ): + # type: (...) -> Optional[str] + """Finishes the transaction and sends it to Sentry. + All finished spans in the transaction will also be sent to Sentry. + + :param scope: The Scope to use for this transaction. + If not provided, the current Scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + :param hub: The hub to use for this transaction. + This argument is DEPRECATED. Please use the `scope` + parameter, instead. + + :return: The event ID if the transaction was sent to Sentry, + otherwise None. + """ + if self.timestamp is not None: + # This transaction is already finished, ignore. + return None + + # For backwards compatibility, we must handle the case where `scope` + # or `hub` could both either be a `Scope` or a `Hub`. + scope = self._get_scope_from_finish_args( + scope, hub + ) # type: Optional[sentry_sdk.Scope] + + scope = scope or self.scope or sentry_sdk.get_current_scope() + client = sentry_sdk.get_client() + + if not client.is_active(): + # We have no active client and therefore nowhere to send this transaction. + return None + + if self._span_recorder is None: + # Explicit check against False needed because self.sampled might be None + if self.sampled is False: + logger.debug("Discarding transaction because sampled = False") + else: + logger.debug( + "Discarding transaction because it was not started with sentry_sdk.start_transaction" + ) + + # This is not entirely accurate because discards here are not + # exclusively based on sample rate but also traces sampler, but + # we handle this the same here. + if client.transport and has_tracing_enabled(client.options): + if client.monitor and client.monitor.downsample_factor > 0: + reason = "backpressure" + else: + reason = "sample_rate" + + client.transport.record_lost_event(reason, data_category="transaction") + + # Only one span (the transaction itself) is discarded, since we did not record any spans here. + client.transport.record_lost_event(reason, data_category="span") + return None + + if not self.name: + logger.warning( + "Transaction has no name, falling back to `<unlabeled transaction>`." + ) + self.name = "<unlabeled transaction>" + + super().finish(scope, end_timestamp) + + if not self.sampled: + # At this point a `sampled = None` should have already been resolved + # to a concrete decision. + if self.sampled is None: + logger.warning("Discarding transaction without sampling decision.") + + return None + + finished_spans = [ + span.to_json() + for span in self._span_recorder.spans + if span.timestamp is not None + ] + + len_diff = len(self._span_recorder.spans) - len(finished_spans) + dropped_spans = len_diff + self._span_recorder.dropped_spans + + # we do this to break the circular reference of transaction -> span + # recorder -> span -> containing transaction (which is where we started) + # before either the spans or the transaction goes out of scope and has + # to be garbage collected + self._span_recorder = None + + contexts = {} + contexts.update(self._contexts) + contexts.update({"trace": self.get_trace_context()}) + profile_context = self.get_profile_context() + if profile_context is not None: + contexts.update({"profile": profile_context}) + + event = { + "type": "transaction", + "transaction": self.name, + "transaction_info": {"source": self.source}, + "contexts": contexts, + "tags": self._tags, + "timestamp": self.timestamp, + "start_timestamp": self.start_timestamp, + "spans": finished_spans, + } # type: Event + + if dropped_spans > 0: + event["_dropped_spans"] = dropped_spans + + if self._profile is not None and self._profile.valid(): + event["profile"] = self._profile + self._profile = None + + event["measurements"] = self._measurements + + # This is here since `to_json` is not invoked. This really should + # be gone when we switch to onlyspans. + if self._local_aggregator is not None: + metrics_summary = self._local_aggregator.to_json() + if metrics_summary: + event["_metrics_summary"] = metrics_summary + + return scope.capture_event(event) + + def set_measurement(self, name, value, unit=""): + # type: (str, float, MeasurementUnit) -> None + self._measurements[name] = {"value": value, "unit": unit} + + def set_context(self, key, value): + # type: (str, dict[str, Any]) -> None + """Sets a context. Transactions can have multiple contexts + and they should follow the format described in the "Contexts Interface" + documentation. + + :param key: The name of the context. + :param value: The information about the context. + """ + self._contexts[key] = value + + def set_http_status(self, http_status): + # type: (int) -> None + """Sets the status of the Transaction according to the given HTTP status. + + :param http_status: The HTTP status code.""" + super().set_http_status(http_status) + self.set_context("response", {"status_code": http_status}) + + def to_json(self): + # type: () -> Dict[str, Any] + """Returns a JSON-compatible representation of the transaction.""" + rv = super().to_json() + + rv["name"] = self.name + rv["source"] = self.source + rv["sampled"] = self.sampled + + return rv + + def get_trace_context(self): + # type: () -> Any + trace_context = super().get_trace_context() + + if self._data: + trace_context["data"] = self._data + + return trace_context + + def get_baggage(self): + # type: () -> Baggage + """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage` + associated with the Transaction. + + The first time a new baggage with Sentry items is made, + it will be frozen.""" + if not self._baggage or self._baggage.mutable: + self._baggage = Baggage.populate_from_transaction(self) + + return self._baggage + + def _set_initial_sampling_decision(self, sampling_context): + # type: (SamplingContext) -> None + """ + Sets the transaction's sampling decision, according to the following + precedence rules: + + 1. If a sampling decision is passed to `start_transaction` + (`start_transaction(name: "my transaction", sampled: True)`), that + decision will be used, regardless of anything else + + 2. If `traces_sampler` is defined, its decision will be used. It can + choose to keep or ignore any parent sampling decision, or use the + sampling context data to make its own decision or to choose a sample + rate for the transaction. + + 3. If `traces_sampler` is not defined, but there's a parent sampling + decision, the parent sampling decision will be used. + + 4. If `traces_sampler` is not defined and there's no parent sampling + decision, `traces_sample_rate` will be used. + """ + client = sentry_sdk.get_client() + + transaction_description = "{op}transaction <{name}>".format( + op=("<" + self.op + "> " if self.op else ""), name=self.name + ) + + # nothing to do if tracing is disabled + if not has_tracing_enabled(client.options): + self.sampled = False + return + + # if the user has forced a sampling decision by passing a `sampled` + # value when starting the transaction, go with that + if self.sampled is not None: + self.sample_rate = float(self.sampled) + return + + # we would have bailed already if neither `traces_sampler` nor + # `traces_sample_rate` were defined, so one of these should work; prefer + # the hook if so + sample_rate = ( + client.options["traces_sampler"](sampling_context) + if callable(client.options.get("traces_sampler")) + else ( + # default inheritance behavior + sampling_context["parent_sampled"] + if sampling_context["parent_sampled"] is not None + else client.options["traces_sample_rate"] + ) + ) + + # Since this is coming from the user (or from a function provided by the + # user), who knows what we might get. (The only valid values are + # booleans or numbers between 0 and 1.) + if not is_valid_sample_rate(sample_rate, source="Tracing"): + logger.warning( + "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format( + transaction_description=transaction_description, + ) + ) + self.sampled = False + return + + self.sample_rate = float(sample_rate) + + if client.monitor: + self.sample_rate /= 2**client.monitor.downsample_factor + + # if the function returned 0 (or false), or if `traces_sample_rate` is + # 0, it's a sign the transaction should be dropped + if not self.sample_rate: + logger.debug( + "[Tracing] Discarding {transaction_description} because {reason}".format( + transaction_description=transaction_description, + reason=( + "traces_sampler returned 0 or False" + if callable(client.options.get("traces_sampler")) + else "traces_sample_rate is set to 0" + ), + ) + ) + self.sampled = False + return + + # Now we roll the dice. self._sample_rand is inclusive of 0, but not of 1, + # so strict < is safe here. In case sample_rate is a boolean, cast it + # to a float (True becomes 1.0 and False becomes 0.0) + self.sampled = self._sample_rand < self.sample_rate + + if self.sampled: + logger.debug( + "[Tracing] Starting {transaction_description}".format( + transaction_description=transaction_description, + ) + ) + else: + logger.debug( + "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format( + transaction_description=transaction_description, + sample_rate=self.sample_rate, + ) + ) + + +class NoOpSpan(Span): + def __repr__(self): + # type: () -> str + return "<%s>" % self.__class__.__name__ + + @property + def containing_transaction(self): + # type: () -> Optional[Transaction] + return None + + def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs): + # type: (str, **Any) -> NoOpSpan + return NoOpSpan() + + def to_traceparent(self): + # type: () -> str + return "" + + def to_baggage(self): + # type: () -> Optional[Baggage] + return None + + def get_baggage(self): + # type: () -> Optional[Baggage] + return None + + def iter_headers(self): + # type: () -> Iterator[Tuple[str, str]] + return iter(()) + + def set_tag(self, key, value): + # type: (str, Any) -> None + pass + + def set_data(self, key, value): + # type: (str, Any) -> None + pass + + def set_status(self, value): + # type: (str) -> None + pass + + def set_http_status(self, http_status): + # type: (int) -> None + pass + + def is_success(self): + # type: () -> bool + return True + + def to_json(self): + # type: () -> Dict[str, Any] + return {} + + def get_trace_context(self): + # type: () -> Any + return {} + + def get_profile_context(self): + # type: () -> Any + return {} + + def finish( + self, + scope=None, # type: Optional[sentry_sdk.Scope] + end_timestamp=None, # type: Optional[Union[float, datetime]] + *, + hub=None, # type: Optional[sentry_sdk.Hub] + ): + # type: (...) -> Optional[str] + """ + The `hub` parameter is deprecated. Please use the `scope` parameter, instead. + """ + pass + + def set_measurement(self, name, value, unit=""): + # type: (str, float, MeasurementUnit) -> None + pass + + def set_context(self, key, value): + # type: (str, dict[str, Any]) -> None + pass + + def init_span_recorder(self, maxlen): + # type: (int) -> None + pass + + def _set_initial_sampling_decision(self, sampling_context): + # type: (SamplingContext) -> None + pass + + +if TYPE_CHECKING: + + @overload + def trace(func=None): + # type: (None) -> Callable[[Callable[P, R]], Callable[P, R]] + pass + + @overload + def trace(func): + # type: (Callable[P, R]) -> Callable[P, R] + pass + + +def trace(func=None): + # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]] + """ + Decorator to start a child span under the existing current transaction. + If there is no current transaction, then nothing will be traced. + + .. code-block:: + :caption: Usage + + import sentry_sdk + + @sentry_sdk.trace + def my_function(): + ... + + @sentry_sdk.trace + async def my_async_function(): + ... + """ + from sentry_sdk.tracing_utils import start_child_span_decorator + + # This patterns allows usage of both @sentry_traced and @sentry_traced(...) + # See https://stackoverflow.com/questions/52126071/decorator-with-arguments-avoid-parenthesis-when-no-arguments/52126278 + if func: + return start_child_span_decorator(func) + else: + return start_child_span_decorator + + +# Circular imports + +from sentry_sdk.tracing_utils import ( + Baggage, + EnvironHeaders, + extract_sentrytrace_data, + _generate_sample_rand, + has_tracing_enabled, + maybe_create_breadcrumbs_from_span, +) + +with warnings.catch_warnings(): + # The code in this file which uses `LocalAggregator` is only called from the deprecated `metrics` module. + warnings.simplefilter("ignore", DeprecationWarning) + from sentry_sdk.metrics import LocalAggregator |