about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/sentry_sdk/tracing.py
diff options
context:
space:
mode:
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.py1358
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