2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -1,12 +1,13 @@
import uuid
import random
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.consts import INSTRUMENTER, SPANSTATUS, SPANDATA, SPANTEMPLATE
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
from sentry_sdk.utils import (
capture_internal_exceptions,
get_current_thread_meta,
is_valid_sample_rate,
logger,
@@ -16,6 +17,7 @@ from sentry_sdk.utils import (
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable, Mapping, MutableMapping
from typing import Any
@@ -28,6 +30,7 @@ if TYPE_CHECKING:
from typing import Tuple
from typing import Union
from typing import TypeVar
from typing import Set
from typing_extensions import TypedDict, Unpack
@@ -126,30 +129,37 @@ if TYPE_CHECKING:
BAGGAGE_HEADER_NAME = "baggage"
SENTRY_TRACE_HEADER_NAME = "sentry-trace"
# Transaction source
# see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
TRANSACTION_SOURCE_CUSTOM = "custom"
TRANSACTION_SOURCE_URL = "url"
TRANSACTION_SOURCE_ROUTE = "route"
TRANSACTION_SOURCE_VIEW = "view"
TRANSACTION_SOURCE_COMPONENT = "component"
TRANSACTION_SOURCE_TASK = "task"
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 = [
TRANSACTION_SOURCE_URL,
TransactionSource.URL,
]
SOURCE_FOR_STYLE = {
"endpoint": TRANSACTION_SOURCE_COMPONENT,
"function_name": TRANSACTION_SOURCE_COMPONENT,
"handler_name": TRANSACTION_SOURCE_COMPONENT,
"method_and_path_pattern": TRANSACTION_SOURCE_ROUTE,
"path": TRANSACTION_SOURCE_URL,
"route_name": TRANSACTION_SOURCE_COMPONENT,
"route_pattern": TRANSACTION_SOURCE_ROUTE,
"uri_template": TRANSACTION_SOURCE_ROUTE,
"url": TRANSACTION_SOURCE_ROUTE,
"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,
}
@@ -248,8 +258,8 @@ class Span:
"""
__slots__ = (
"trace_id",
"span_id",
"_trace_id",
"_span_id",
"parent_span_id",
"same_process_as_parent",
"sampled",
@@ -266,10 +276,11 @@ class Span:
"hub",
"_context_manager_state",
"_containing_transaction",
"_local_aggregator",
"scope",
"origin",
"name",
"_flags",
"_flags_capacity",
)
def __init__(
@@ -290,8 +301,8 @@ class Span:
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._trace_id = trace_id
self._span_id = span_id
self.parent_span_id = parent_span_id
self.same_process_as_parent = same_process_as_parent
self.sampled = sampled
@@ -305,6 +316,8 @@ class Span:
self._tags = {} # type: MutableMapping[str, str]
self._data = {} # type: Dict[str, Any]
self._containing_transaction = containing_transaction
self._flags = {} # type: Dict[str, bool]
self._flags_capacity = 10
if hub is not None:
warnings.warn(
@@ -331,7 +344,6 @@ class 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())
@@ -343,12 +355,31 @@ class Span:
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
@property
def trace_id(self):
# type: () -> str
if not self._trace_id:
self._trace_id = uuid.uuid4().hex
return self._trace_id
@trace_id.setter
def trace_id(self, value):
# type: (str) -> None
self._trace_id = value
@property
def span_id(self):
# type: () -> str
if not self._span_id:
self._span_id = uuid.uuid4().hex[16:]
return self._span_id
@span_id.setter
def span_id(self, value):
# type: (str) -> None
self._span_id = value
def __repr__(self):
# type: () -> str
@@ -377,12 +408,14 @@ class Span:
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)
if self.status != SPANSTATUS.ERROR:
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
with capture_internal_exceptions():
scope, old_span = self._context_manager_state
del self._context_manager_state
self.finish(scope)
scope.span = old_span
@property
def containing_transaction(self):
@@ -468,6 +501,8 @@ class Span:
def continue_from_headers(
cls,
headers, # type: Mapping[str, str]
*,
_sample_rand=None, # type: Optional[str]
**kwargs, # type: Any
):
# type: (...) -> Transaction
@@ -476,6 +511,8 @@ class Span:
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:
@@ -486,7 +523,9 @@ class Span:
# 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))
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(
@@ -583,12 +622,31 @@ class Span:
# type: (str, Any) -> None
self._data[key] = value
def update_data(self, data):
# type: (Dict[str, Any]) -> None
self._data.update(data)
def set_flag(self, flag, result):
# type: (str, bool) -> None
if len(self._flags) < self._flags_capacity:
self._flags[flag] = result
def set_status(self, value):
# type: (str) -> None
self.status = value
def set_measurement(self, name, value, unit=""):
# type: (str, float, MeasurementUnit) -> None
"""
.. deprecated:: 2.28.0
This function is deprecated and will be removed in the next major release.
"""
warnings.warn(
"`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.",
DeprecationWarning,
stacklevel=2,
)
self._measurements[name] = {"value": value, "unit": unit}
def set_thread(self, thread_id, thread_name):
@@ -674,11 +732,6 @@ class Span:
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
@@ -686,7 +739,9 @@ class Span:
if tags:
rv["tags"] = tags
data = self._data
data = {}
data.update(self._flags)
data.update(self._data)
if data:
rv["data"] = data
@@ -770,6 +825,7 @@ class Transaction(Span):
"_profile",
"_continuous_profile",
"_baggage",
"_sample_rand",
)
def __init__( # type: ignore[misc]
@@ -777,11 +833,10 @@ class Transaction(Span):
name="", # type: str
parent_sampled=None, # type: Optional[bool]
baggage=None, # type: Optional[Baggage]
source=TRANSACTION_SOURCE_CUSTOM, # type: str
source=TransactionSource.CUSTOM, # type: str
**kwargs, # type: Unpack[SpanKwargs]
):
# type: (...) -> None
super().__init__(**kwargs)
self.name = name
@@ -794,6 +849,14 @@ class Transaction(Span):
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 (
@@ -894,6 +957,12 @@ class Transaction(Span):
return scope_or_hub
def _get_log_representation(self):
# type: () -> str
return "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
def finish(
self,
scope=None, # type: Optional[sentry_sdk.Scope]
@@ -922,9 +991,7 @@ class Transaction(Span):
# 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 = 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()
@@ -965,6 +1032,32 @@ class Transaction(Span):
super().finish(scope, end_timestamp)
status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE)
if (
status_code is not None
and status_code in client.options["trace_ignore_status_codes"]
):
logger.debug(
"[Tracing] Discarding {transaction_description} because the HTTP status code {status_code} is matched by trace_ignore_status_codes: {trace_ignore_status_codes}".format(
transaction_description=self._get_log_representation(),
status_code=self._data[SPANDATA.HTTP_STATUS_CODE],
trace_ignore_status_codes=client.options[
"trace_ignore_status_codes"
],
)
)
if client.transport:
client.transport.record_lost_event(
"event_processor", data_category="transaction"
)
num_spans = len(self._span_recorder.spans) + 1
client.transport.record_lost_event(
"event_processor", data_category="span", quantity=num_spans
)
self.sampled = False
if not self.sampled:
# At this point a `sampled = None` should have already been resolved
# to a concrete decision.
@@ -1015,21 +1108,24 @@ class Transaction(Span):
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
"""
.. deprecated:: 2.28.0
This function is deprecated and will be removed in the next major release.
"""
warnings.warn(
"`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.",
DeprecationWarning,
stacklevel=2,
)
self._measurements[name] = {"value": value, "unit": unit}
def set_context(self, key, value):
# type: (str, Any) -> None
# 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.
@@ -1102,9 +1198,7 @@ class Transaction(Span):
"""
client = sentry_sdk.get_client()
transaction_description = "{op}transaction <{name}>".format(
op=("<" + self.op + "> " if self.op else ""), name=self.name
)
transaction_description = self._get_log_representation()
# nothing to do if tracing is disabled
if not has_tracing_enabled(client.options):
@@ -1123,8 +1217,8 @@ class Transaction(Span):
sample_rate = (
client.options["traces_sampler"](sampling_context)
if callable(client.options.get("traces_sampler"))
# default inheritance behavior
else (
# default inheritance behavior
sampling_context["parent_sampled"]
if sampling_context["parent_sampled"] is not None
else client.options["traces_sample_rate"]
@@ -1164,10 +1258,8 @@ class Transaction(Span):
self.sampled = False
return
# Now we roll the dice. random.random 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 = random.random() < self.sample_rate
# Now we roll the dice.
self.sampled = self._sample_rand < self.sample_rate
if self.sampled:
logger.debug(
@@ -1222,6 +1314,10 @@ class NoOpSpan(Span):
# type: (str, Any) -> None
pass
def update_data(self, data):
# type: (Dict[str, Any]) -> None
pass
def set_status(self, value):
# type: (str) -> None
pass
@@ -1264,7 +1360,7 @@ class NoOpSpan(Span):
pass
def set_context(self, key, value):
# type: (str, Any) -> None
# type: (str, dict[str, Any]) -> None
pass
def init_span_recorder(self, maxlen):
@@ -1279,43 +1375,103 @@ class NoOpSpan(Span):
if TYPE_CHECKING:
@overload
def trace(func=None):
# type: (None) -> Callable[[Callable[P, R]], Callable[P, R]]
def trace(
func=None, *, op=None, name=None, attributes=None, template=SPANTEMPLATE.DEFAULT
):
# type: (None, Optional[str], Optional[str], Optional[dict[str, Any]], SPANTEMPLATE) -> Callable[[Callable[P, R]], Callable[P, R]]
# Handles: @trace() and @trace(op="custom")
pass
@overload
def trace(func):
# type: (Callable[P, R]) -> Callable[P, R]
# Handles: @trace
pass
def trace(func=None):
# type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
def trace(
func=None, *, op=None, name=None, attributes=None, template=SPANTEMPLATE.DEFAULT
):
# type: (Optional[Callable[P, R]], Optional[str], Optional[str], Optional[dict[str, Any]], SPANTEMPLATE) -> 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.
Decorator to start a child span around a function call.
.. code-block::
:caption: Usage
This decorator automatically creates a new span when the decorated function
is called, and finishes the span when the function returns or raises an exception.
:param func: The function to trace. When used as a decorator without parentheses,
this is the function being decorated. When used with parameters (e.g.,
``@trace(op="custom")``, this should be None.
:type func: Callable or None
:param op: The operation name for the span. This is a high-level description
of what the span represents (e.g., "http.client", "db.query").
You can use predefined constants from :py:class:`sentry_sdk.consts.OP`
or provide your own string. If not provided, a default operation will
be assigned based on the template.
:type op: str or None
:param name: The human-readable name/description for the span. If not provided,
defaults to the function name. This provides more specific details about
what the span represents (e.g., "GET /api/users", "process_user_data").
:type name: str or None
:param attributes: A dictionary of key-value pairs to add as attributes to the span.
Attribute values must be strings, integers, floats, or booleans. These
attributes provide additional context about the span's execution.
:type attributes: dict[str, Any] or None
:param template: The type of span to create. This determines what kind of
span instrumentation and data collection will be applied. Use predefined
constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`.
The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most
use cases.
:type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE`
:returns: When used as ``@trace``, returns the decorated function. When used as
``@trace(...)`` with parameters, returns a decorator function.
:rtype: Callable or decorator function
Example::
import sentry_sdk
from sentry_sdk.consts import OP, SPANTEMPLATE
# Simple usage with default values
@sentry_sdk.trace
def my_function():
...
def process_data():
# Function implementation
pass
@sentry_sdk.trace
async def my_async_function():
...
# With custom parameters
@sentry_sdk.trace(
op=OP.DB_QUERY,
name="Get user data",
attributes={"postgres": True}
)
def make_db_query(sql):
# Function implementation
pass
# With a custom template
@sentry_sdk.trace(template=SPANTEMPLATE.AI_TOOL)
def calculate_interest_rate(amount, rate, years):
# Function implementation
pass
"""
from sentry_sdk.tracing_utils import start_child_span_decorator
from sentry_sdk.tracing_utils import create_span_decorator
decorator = create_span_decorator(
op=op,
name=name,
attributes=attributes,
template=template,
)
# 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)
return decorator(func)
else:
return start_child_span_decorator
return decorator
# Circular imports
@@ -1324,11 +1480,7 @@ 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