diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 44472f2720..a027dcc144 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,8 +1,8 @@ import inspect -from sentry_sdk import tracing, tracing_utils, Client +from sentry_sdk import tracing_utils, Client from sentry_sdk._init_implementation import init -from sentry_sdk.tracing import POTelSpan, Transaction, trace +from sentry_sdk.tracing import Span, trace from sentry_sdk.crons import monitor # TODO-neel-potel make 2 scope strategies/impls and switch @@ -11,7 +11,6 @@ new_scope, isolation_scope, ) - from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: @@ -37,7 +36,7 @@ LogLevelStr, SamplingContext, ) - from sentry_sdk.tracing import Span, TransactionKwargs + from sentry_sdk.tracing import TransactionKwargs T = TypeVar("T") F = TypeVar("F", bound=Callable[..., Any]) @@ -233,22 +232,25 @@ def flush( def start_span( + *, + root_span=None, + custom_sampling_context=None, **kwargs, # type: Any ): - # type: (...) -> POTelSpan + # type: (...) -> Span """ - Alias for tracing.POTelSpan constructor. The method signature is the same. + Start and return a span. """ # TODO: Consider adding type hints to the method signature. - return tracing.POTelSpan(**kwargs) + return get_current_scope().start_span(root_span, custom_sampling_context, **kwargs) def start_transaction( - transaction=None, # type: Optional[Transaction] + transaction=None, # type: Optional[Span] custom_sampling_context=None, # type: Optional[SamplingContext] **kwargs, # type: Unpack[TransactionKwargs] ): - # type: (...) -> POTelSpan + # type: (...) -> Span """ .. deprecated:: 3.0.0 This function is deprecated and will be removed in a future release. @@ -282,7 +284,11 @@ def start_transaction( constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ - return start_span(**kwargs) + return get_current_scope().start_span( + root_span=transaction, + custom_sampling_context=custom_sampling_context, + **kwargs, + ) def set_measurement(name, value, unit=""): @@ -323,7 +329,7 @@ def get_baggage(): def continue_trace( environ_or_headers, op=None, name=None, source=None, origin="manual" ): - # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Transaction + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Span """ Sets the propagation context from environment or headers and returns a transaction. """ diff --git a/sentry_sdk/integrations/boto3.py b/sentry_sdk/integrations/boto3.py index 0fb997767b..3c5131e9d0 100644 --- a/sentry_sdk/integrations/boto3.py +++ b/sentry_sdk/integrations/boto3.py @@ -3,7 +3,6 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import Integration, DidNotEnable -from sentry_sdk.tracing import Span from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import ( @@ -19,6 +18,8 @@ from typing import Optional from typing import Type + from sentry_sdk.tracing import Span + try: from botocore import __version__ as BOTOCORE_VERSION # type: ignore from botocore.client import BaseClient # type: ignore diff --git a/sentry_sdk/integrations/opentelemetry/__init__.py b/sentry_sdk/integrations/opentelemetry/__init__.py index 43587f2e01..e0020204d5 100644 --- a/sentry_sdk/integrations/opentelemetry/__init__.py +++ b/sentry_sdk/integrations/opentelemetry/__init__.py @@ -1,8 +1,7 @@ -# TODO-neel-potel fix circular imports -# from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401 -# SentrySpanProcessor, -# ) +from sentry_sdk.integrations.opentelemetry.span_processor import ( # noqa: F401 + SentrySpanProcessor, +) -# from sentry_sdk.integrations.opentelemetry.propagator import ( # noqa: F401 -# SentryPropagator, -# ) +from sentry_sdk.integrations.opentelemetry.propagator import ( # noqa: F401 + SentryPropagator, +) diff --git a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py index 9604676dce..ebb5bbc17a 100644 --- a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py @@ -7,7 +7,7 @@ from sentry_sdk import capture_event from sentry_sdk.integrations.opentelemetry.utils import ( is_sentry_span, - convert_otel_timestamp, + convert_from_otel_timestamp, extract_span_data, ) from sentry_sdk.integrations.opentelemetry.consts import ( @@ -116,12 +116,12 @@ def _root_span_to_transaction_event(self, span): span_id = format_span_id(span.context.span_id) parent_span_id = format_span_id(span.parent.span_id) if span.parent else None - (op, description, status, _) = extract_span_data(span) + (op, description, status, _, origin) = extract_span_data(span) trace_context = { "trace_id": trace_id, "span_id": span_id, - "origin": SPAN_ORIGIN, + "origin": origin, "op": op, "status": status, } # type: dict[str, Any] @@ -141,8 +141,8 @@ def _root_span_to_transaction_event(self, span): # TODO-neel-potel tx source based on integration "transaction_info": {"source": "custom"}, "contexts": contexts, - "start_timestamp": convert_otel_timestamp(span.start_time), - "timestamp": convert_otel_timestamp(span.end_time), + "start_timestamp": convert_from_otel_timestamp(span.start_time), + "timestamp": convert_from_otel_timestamp(span.end_time), } # type: Event return event @@ -160,17 +160,17 @@ def _span_to_json(self, span): span_id = format_span_id(span.context.span_id) parent_span_id = format_span_id(span.parent.span_id) if span.parent else None - (op, description, status, _) = extract_span_data(span) + (op, description, status, _, origin) = extract_span_data(span) span_json = { "trace_id": trace_id, "span_id": span_id, - "origin": SPAN_ORIGIN, "op": op, "description": description, "status": status, - "start_timestamp": convert_otel_timestamp(span.start_time), - "timestamp": convert_otel_timestamp(span.end_time), + "start_timestamp": convert_from_otel_timestamp(span.start_time), + "timestamp": convert_from_otel_timestamp(span.end_time), + "origin": origin or SPAN_ORIGIN, } # type: dict[str, Any] if parent_span_id: diff --git a/sentry_sdk/integrations/opentelemetry/span_processor.py b/sentry_sdk/integrations/opentelemetry/span_processor.py index 594ccbb71f..2140b0e70b 100644 --- a/sentry_sdk/integrations/opentelemetry/span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/span_processor.py @@ -13,7 +13,6 @@ INVALID_SPAN_ID, INVALID_TRACE_ID, ) -from sentry_sdk import get_client, start_transaction from sentry_sdk.integrations.opentelemetry.consts import ( SENTRY_BAGGAGE_KEY, SENTRY_TRACE_KEY, @@ -106,6 +105,8 @@ def _prune_old_spans(self): def on_start(self, otel_span, parent_context=None): # type: (OTelSpan, Optional[context_api.Context]) -> None + from sentry_sdk import get_client, start_transaction + client = get_client() if not client.dsn: @@ -258,7 +259,7 @@ def _update_span_with_otel_data(self, sentry_span, otel_span): for key, val in otel_span.attributes.items(): sentry_span.set_data(key, val) - (op, description, status, http_status) = extract_span_data(otel_span) + (op, description, status, http_status, _) = extract_span_data(otel_span) sentry_span.op = op sentry_span.description = description @@ -269,7 +270,7 @@ def _update_span_with_otel_data(self, sentry_span, otel_span): def _update_transaction_with_otel_data(self, sentry_span, otel_span): # type: (SentrySpan, OTelSpan) -> None - (op, _, status, http_status) = extract_span_data(otel_span) + (op, _, status, http_status, _) = extract_span_data(otel_span) sentry_span.op = op if http_status: diff --git a/sentry_sdk/integrations/opentelemetry/utils.py b/sentry_sdk/integrations/opentelemetry/utils.py index cb04dd8e1a..49b0931dca 100644 --- a/sentry_sdk/integrations/opentelemetry/utils.py +++ b/sentry_sdk/integrations/opentelemetry/utils.py @@ -6,9 +6,9 @@ from opentelemetry.sdk.trace import ReadableSpan from sentry_sdk.consts import SPANSTATUS from sentry_sdk.tracing import get_span_status_from_http_code +from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute from urllib3.util import parse_url as urlparse -from sentry_sdk import get_client from sentry_sdk.utils import Dsn from sentry_sdk._types import TYPE_CHECKING @@ -43,6 +43,8 @@ def is_sentry_span(span): Break infinite loop: HTTP requests to Sentry are caught by OTel and send again to Sentry. """ + from sentry_sdk import get_client + if not span.attributes: return False @@ -70,19 +72,32 @@ def is_sentry_span(span): return False -def convert_otel_timestamp(time): +def convert_from_otel_timestamp(time): # type: (int) -> datetime + """Convert an OTel ns-level timestamp to a datetime.""" return datetime.fromtimestamp(time / 1e9, timezone.utc) +def convert_to_otel_timestamp(time): + # type: (Union[datetime.datetime, float]) -> int + """Convert a datetime to an OTel timestamp (with ns precision).""" + if isinstance(time, datetime): + return int(time.timestamp() * 1e9) + return int(time * 1e9) + + def extract_span_data(span): - # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int]] + # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int], Optional[str]] op = span.name description = span.name status, http_status = extract_span_status(span) + origin = None if span.attributes is None: - return (op, description, status, http_status) + return (op, description, status, http_status, origin) + + origin = span.attributes.get(SentrySpanAttribute.ORIGIN) + description = span.attributes.get(SentrySpanAttribute.DESCRIPTION) or description http_method = span.attributes.get(SpanAttributes.HTTP_METHOD) http_method = cast("Optional[str]", http_method) @@ -95,26 +110,21 @@ def extract_span_data(span): rpc_service = span.attributes.get(SpanAttributes.RPC_SERVICE) if rpc_service: - return ("rpc", description, status, http_status) + return ("rpc", description, status, http_status, origin) messaging_system = span.attributes.get(SpanAttributes.MESSAGING_SYSTEM) if messaging_system: - return ("message", description, status, http_status) + return ("message", description, status, http_status, origin) faas_trigger = span.attributes.get(SpanAttributes.FAAS_TRIGGER) if faas_trigger: - return ( - str(faas_trigger), - description, - status, - http_status, - ) + return (str(faas_trigger), description, status, http_status, origin) - return (op, description, status, http_status) + return (op, description, status, http_status, origin) def span_data_for_http_method(span): - # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int]] + # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int], Optional[str]] span_attributes = span.attributes or {} op = "http" @@ -150,11 +160,13 @@ def span_data_for_http_method(span): status, http_status = extract_span_status(span) - return (op, description, status, http_status) + origin = span_attributes.get(SentrySpanAttribute.ORIGIN) + + return (op, description, status, http_status, origin) def span_data_for_db_query(span): - # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int]] + # type: (ReadableSpan) -> tuple[str, str, Optional[str], Optional[int], Optional[str]] span_attributes = span.attributes or {} op = "db" @@ -163,8 +175,9 @@ def span_data_for_db_query(span): statement = cast("Optional[str]", statement) description = statement or span.name + origin = span_attributes.get(SentrySpanAttribute.ORIGIN) - return (op, description, None, None) + return (op, description, None, None, origin) def extract_span_status(span): diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 7a95611d78..4ac801ea88 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -91,7 +91,7 @@ def __call__(self, environ, start_response): ) ) - transaction = continue_trace( + root_span = continue_trace( environ, op=OP.HTTP_SERVER, name="generic WSGI request", @@ -100,13 +100,13 @@ def __call__(self, environ, start_response): ) with sentry_sdk.start_transaction( - transaction, custom_sampling_context={"wsgi_environ": environ} + root_span, custom_sampling_context={"wsgi_environ": environ} ): try: response = self.app( environ, partial( - _sentry_start_response, start_response, transaction + _sentry_start_response, start_response, root_span ), ) except BaseException: diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 342c499ee0..4382ff48bf 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -22,9 +22,7 @@ from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, SENTRY_TRACE_HEADER_NAME, - NoOpSpan, Span, - Transaction, ) from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.utils import ( @@ -168,10 +166,8 @@ class Scope(object): "_level", "_name", "_fingerprint", - # note that for legacy reasons, _transaction is the transaction *name*, - # not a Transaction object (the object is stored in _span) - "_transaction", - "_transaction_info", + "_root_span_name", + "_root_span_info", "_user", "_tags", "_contexts", @@ -223,8 +219,8 @@ def __copy__(self): rv._level = self._level rv._name = self._name rv._fingerprint = self._fingerprint - rv._transaction = self._transaction - rv._transaction_info = dict(self._transaction_info) + rv._root_span_name = self._root_span_name + rv._root_span_info = dict(self._root_span_info) rv._user = self._user rv._tags = dict(self._tags) @@ -674,8 +670,8 @@ def clear(self): """Clears the entire scope.""" self._level = None # type: Optional[LogLevelStr] self._fingerprint = None # type: Optional[List[str]] - self._transaction = None # type: Optional[str] - self._transaction_info = {} # type: MutableMapping[str, str] + self._root_span_name = None # type: Optional[str] + self._root_span_info = {} # type: MutableMapping[str, str] self._user = None # type: Optional[Dict[str, Any]] self._tags = {} # type: Dict[str, Any] @@ -697,23 +693,6 @@ def clear(self): # self._last_event_id is only applicable to isolation scopes self._last_event_id = None # type: Optional[str] - @_attr_setter - def level(self, value): - # type: (LogLevelStr) -> None - """ - When set this overrides the level. - - .. deprecated:: 1.0.0 - Use :func:`set_level` instead. - - :param value: The level to set. - """ - logger.warning( - "Deprecated: use .set_level() instead. This will be removed in the future." - ) - - self._level = value - def set_level(self, value): # type: (LogLevelStr) -> None """ @@ -732,58 +711,54 @@ def fingerprint(self, value): @property def transaction(self): # type: () -> Any - # would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004 - """Return the transaction (root span) in the scope, if any.""" + # would be type: () -> Optional[Span], see https://github.com/python/mypy/issues/3004 + """ + Return the transaction in the scope, if any. + + .. deprecated:: 3.0.0 + This function is deprecated and will be removed in a future release. Use Scope.root_span instead. + """ + + logger.warning( + "Deprecated: use Scope.root_span instead. This will be removed in the future." + ) + return self.root_span + + @property + def root_span(self): + """Return the root span in the scope, if any.""" # there is no span/transaction on the scope if self._span is None: return None # there is an orphan span on the scope - if self._span.containing_transaction is None: + if self._span.root_span is None: return None - # there is either a transaction (which is its own containing - # transaction) or a non-orphan span on the scope - return self._span.containing_transaction - - @transaction.setter - def transaction(self, value): - # type: (Any) -> None - # would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004 - """When set this forces a specific transaction name to be set. - - Deprecated: use set_transaction_name instead.""" - - # XXX: the docstring above is misleading. The implementation of - # apply_to_event prefers an existing value of event.transaction over - # anything set in the scope. - # XXX: note that with the introduction of the Scope.transaction getter, - # there is a semantic and type mismatch between getter and setter. The - # getter returns a Transaction, the setter sets a transaction name. - # Without breaking version compatibility, we could make the setter set a - # transaction name or transaction (self._span) depending on the type of - # the value argument. - - logger.warning( - "Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead." - ) - self._transaction = value - if self._span and self._span.containing_transaction: - self._span.containing_transaction.name = value + # there is either a root span (which is its own containing + # root span) or a non-orphan span on the scope + return self._span.root_span def set_transaction_name(self, name, source=None): # type: (str, Optional[str]) -> None - """Set the transaction name and optionally the transaction source.""" - self._transaction = name + """ + Set the transaction name and optionally the transaction source. - if self._span and self._span.containing_transaction: - self._span.containing_transaction.name = name - if source: - self._span.containing_transaction.source = source + .. deprecated:: 3.0.0 + This function is deprecated and will be removed in a future release. Use Scope.set_root_span_name instead. + """ + self.set_root_span_name(name, source) + def set_root_span_name(self, name, source=None): + """Set the root span name and optionally the source.""" + self._root_span_name = name + if self._span and self._span.root_span: + self._span.root_span.name = name + if source: + self._span.root_span.source = source if source: - self._transaction_info["source"] = source + self._root_span_info["source"] = source @_attr_setter def user(self, value): @@ -802,21 +777,22 @@ def set_user(self, value): @property def span(self): # type: () -> Optional[Span] - """Get/set current tracing span or transaction.""" + """Get current tracing span.""" return self._span @span.setter def span(self, span): + """Set current tracing span.""" # type: (Optional[Span]) -> None self._span = span # XXX: this differs from the implementation in JS, there Scope.setSpan - # does not set Scope._transactionName. - if isinstance(span, Transaction): - transaction = span - if transaction.name: - self._transaction = transaction.name - if transaction.source: - self._transaction_info["source"] = transaction.source + # does not set root span name. + return + if span.is_segment: + if span.name: + self._root_span_name = span.name + if span.source: + self._root_span_info["source"] = span.source @property def profile(self): @@ -977,8 +953,12 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): def start_transaction( self, transaction=None, custom_sampling_context=None, **kwargs ): - # type: (Optional[Transaction], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Transaction, NoOpSpan] + # type: (Optional[Span], Optional[SamplingContext], Unpack[TransactionKwargs]) -> Span """ + .. deprecated:: 3.0.0 + This function is deprecated and will be removed in a future release. + Use :py:meth:`sentry_sdk.start_span` instead. + Start and return a transaction. Start an existing transaction if given, otherwise create and start a new @@ -1007,32 +987,20 @@ def start_transaction( constructor. See :py:class:`sentry_sdk.tracing.Transaction` for available arguments. """ - kwargs.setdefault("scope", self) - - client = self.get_client() - try_autostart_continuous_profiler() - custom_sampling_context = custom_sampling_context or {} - # kwargs at this point has type TransactionKwargs, since we have removed - # the client and custom_sampling_context from it. - transaction_kwargs = kwargs # type: TransactionKwargs - - # if we haven't been given a transaction, make one - if transaction is None: - transaction = Transaction(**transaction_kwargs) + kwargs.setdefault("scope", self) + span = transaction or Span(**kwargs) - # use traces_sample_rate, traces_sampler, and/or inheritance to make a - # sampling decision sampling_context = { "transaction_context": transaction.to_json(), "parent_sampled": transaction.parent_sampled, } sampling_context.update(custom_sampling_context) - transaction._set_initial_sampling_decision(sampling_context=sampling_context) + span._set_initial_sampling_decision(sampling_context=sampling_context) - if transaction.sampled: + if span.sampled: profile = Profile( transaction.sampled, transaction._start_timestamp_monotonic_ns ) @@ -1042,13 +1010,15 @@ def start_transaction( # we don't bother to keep spans if we already know we're not going to # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) + max_spans = ( + self.get_client().options["_experiments"].get("max_spans") + ) or 1000 + span.init_span_recorder(maxlen=max_spans) - return transaction + return span - def start_span(self, **kwargs): - # type: (Any) -> Span + def start_span(self, root_span=None, custom_sampling_context=None, **kwargs): + # type: (Optional[Span], Optional[SamplingContext], Any) -> Span """ Start a span whose parent is the currently active span or transaction, if any. @@ -1056,48 +1026,55 @@ def start_span(self, **kwargs): typically used as a context manager to start and stop timing in a `with` block. - Only spans contained in a transaction are sent to Sentry. Most - integrations start a transaction at the appropriate time, for example - for every incoming HTTP request. Use - :py:meth:`sentry_sdk.start_transaction` to start a new transaction when - one is not already in progress. - For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`. - - 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. """ - with new_scope(): - kwargs.setdefault("scope", self) + kwargs.setdefault("scope", self) + if root_span: + return root_span - # get current span or transaction - span = self.span or self.get_isolation_scope().span + span = self.span or self.get_isolation_scope().span - if span is None: - # New spans get the `trace_id` from the scope - if "trace_id" not in kwargs: - propagation_context = self.get_active_propagation_context() - if propagation_context is not None: - kwargs["trace_id"] = propagation_context.trace_id + if span is None: + if "trace_id" not in kwargs: + propagation_context = self.get_active_propagation_context() + if propagation_context is not None: + kwargs["trace_id"] = propagation_context.trace_id - span = Span(**kwargs) - else: - # Children take `trace_id`` from the parent span. - span = span.start_child(**kwargs) + return Span(**kwargs) + else: + return span.start_child(**kwargs) + + # XXX + # with new_scope(): + # kwargs.setdefault("scope", self) + + # # get current span or transaction + # span = self.span or self.get_isolation_scope().span + + # if span is None: + # # New spans get the `trace_id` from the scope + # if "trace_id" not in kwargs: + # propagation_context = self.get_active_propagation_context() + # if propagation_context is not None: + # kwargs["trace_id"] = propagation_context.trace_id + + # span = Span(**kwargs) + # else: + # # Children take `trace_id`` from the parent span. + # span = span.start_child(**kwargs) - return span + # return span def continue_trace( self, environ_or_headers, op=None, name=None, source=None, origin="manual" ): - # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Transaction + # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str], str) -> Span """ Sets the propagation context from environment or headers and returns a transaction. """ self.generate_propagation_context(environ_or_headers) - transaction = Transaction.continue_from_headers( + root_span = Span.continue_from_headers( normalize_incoming_data(environ_or_headers), op=op, origin=origin, @@ -1105,7 +1082,7 @@ def continue_trace( source=source, ) - return transaction + return root_span def capture_event(self, event, hint=None, scope=None, **scope_kwargs): # type: (Event, Optional[Hint], Optional[Scope], Any) -> Optional[str] @@ -1309,15 +1286,15 @@ def _apply_user_to_event(self, event, hint, options): if event.get("user") is None and self._user is not None: event["user"] = self._user - def _apply_transaction_name_to_event(self, event, hint, options): + def _apply_root_span_name_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if event.get("transaction") is None and self._transaction is not None: - event["transaction"] = self._transaction + if event.get("transaction") is None and self._root_span_name is not None: + event["transaction"] = self._root_span_name - def _apply_transaction_info_to_event(self, event, hint, options): + def _apply_root_span_info_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None - if event.get("transaction_info") is None and self._transaction_info is not None: - event["transaction_info"] = self._transaction_info + if event.get("transaction_info") is None and self._root_span_info is not None: + event["transaction_info"] = self._root_span_info def _apply_fingerprint_to_event(self, event, hint, options): # type: (Event, Hint, Optional[Dict[str, Any]]) -> None @@ -1439,8 +1416,8 @@ def apply_to_event( self._apply_level_to_event(event, hint, options) self._apply_fingerprint_to_event(event, hint, options) self._apply_user_to_event(event, hint, options) - self._apply_transaction_name_to_event(event, hint, options) - self._apply_transaction_info_to_event(event, hint, options) + self._apply_root_span_name_to_event(event, hint, options) + self._apply_root_span_info_to_event(event, hint, options) self._apply_tags_to_event(event, hint, options) self._apply_extra_to_event(event, hint, options) @@ -1464,10 +1441,10 @@ def update_from_scope(self, scope): self._level = scope._level if scope._fingerprint is not None: self._fingerprint = scope._fingerprint - if scope._transaction is not None: - self._transaction = scope._transaction - if scope._transaction_info is not None: - self._transaction_info.update(scope._transaction_info) + if scope._root_span_name is not None: + self._root_span_name = scope._root_span_name + if scope._root_span_info is not None: + self._root_span_info.update(scope._root_span_info) if scope._user is not None: self._user = scope._user if scope._tags: diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 41c998cb99..3a2f3d4ae2 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -36,6 +36,7 @@ R = TypeVar("R") import sentry_sdk.profiler + from sentry_sdk.scope import Scope from sentry_sdk._types import ( Event, MeasurementUnit, @@ -217,10 +218,871 @@ def add(self, span): self.spans.append(span) +# class Span: +# __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", +# ) + +# 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 +# ): +# # type: (...) -> None + +# 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: +# 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 + +# def start_child(self, **kwargs): +# # type: (**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. +# """ +# 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] +# **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. +# """ +# # 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)) +# 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_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 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, +# } + + +# 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", +# "_baggage", +# ) + +# def __init__( +# self, +# name="", # type: str +# parent_sampled=None, # type: Optional[bool] +# baggage=None, # type: Optional[Baggage] +# source=TRANSACTION_SOURCE_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[sentry_sdk.profiler.transaction_profiler.Profile] +# self._baggage = baggage + +# 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) + +# 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 ``." +# ) +# self.name = "" + +# 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 +# ] + +# # 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 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, 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_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. 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 + +# 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, **kwargs): +# # type: (**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, 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 + + class Span: - """A span holds timing information of a block of code. + """ + A span holds timing information of a block of code. + Spans can have multiple child spans thus forming a span tree. + As of 3.0, this class is an OTel span wrapper providing compatibility + with the old span interface. The wrapper itself should have as little state + as possible. Everything persistent should be stored on the underlying OTel + span. + :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. @@ -231,10 +1093,6 @@ class Span: :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. - :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. @@ -243,77 +1101,64 @@ class Span: :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", - ) - 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] + scope=None, # type: Optional[Scope] start_timestamp=None, # type: Optional[Union[datetime, float]] - scope=None, # type: Optional[sentry_sdk.Scope] origin="manual", # type: str + **_, # type: dict[str, object] + # XXX old args: + # 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 ): # 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 + """ + For backwards compatibility with the old Span interface, this class + accepts arbitrary keyword arguments, in addition to the ones explicitly + listed in the signature. These additional arguments are ignored. + """ + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + from sentry_sdk.integrations.opentelemetry.utils import ( + convert_to_otel_timestamp, + ) + + if start_timestamp is not None: + # OTel timestamps have nanosecond precision + start_timestamp = convert_to_otel_timestamp(start_timestamp) + + self._otel_span = tracer.start_span( + description or op or "", start_time=start_timestamp + ) # XXX + + # XXX deal with _otel_span being a NonRecordingSpan + + self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, origin) + + if op is not None: + self.op = op + self.description = 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 status is not None: + self.set_status(status) + + self.scope = 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 @@ -321,30 +1166,12 @@ def __init__( 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] thread_id, thread_name = get_current_thread_meta() self.set_thread(thread_id, thread_name) 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 ( @@ -363,952 +1190,158 @@ def __repr__(self): def __enter__(self): # type: () -> Span + # XXX use_span? https://github.com/open-telemetry/opentelemetry-python/blob/3836da8543ce9751051e38a110c0468724042e62/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547 + # + # create a Context object with parent set as current span + ctx = otel_trace.set_span_in_context(self._otel_span) + # set as the implicit current context + self._ctx_token = context.attach(ctx) 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: - 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 + self._otel_span.end() + # XXX set status to error if unset and an exception occurred? + context.detach(self._ctx_token) @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, **kwargs): - # type: (**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. - """ - 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] - **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. - """ - # 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)) - 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 name(self): + pass - def set_status(self, value): - # type: (str) -> None - self.status = value + @name.setter + def name(self, value): + pass - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - self._measurements[name] = {"value": value, "unit": unit} + @property + def source(self): + pass - def set_thread(self, thread_id, thread_name): - # type: (Optional[int], Optional[str]) -> None + @source.setter + def source(self, value): + pass - if thread_id is not None: - self.set_data(SPANDATA.THREAD_ID, str(thread_id)) + @property + def description(self): + # type: () -> Optional[str] + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - if thread_name is not None: - self.set_data(SPANDATA.THREAD_NAME, thread_name) + return self._otel_span.attributes.get(SentrySpanAttribute.DESCRIPTION) - def set_profiler_id(self, profiler_id): + @description.setter + def description(self, value): # type: (Optional[str]) -> None - if profiler_id is not None: - self.set_data(SPANDATA.PROFILER_ID, profiler_id) + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - 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)) + if value is not None: + self._otel_span.set_attribute(SentrySpanAttribute.DESCRIPTION, value) - def is_success(self): - # type: () -> bool - return self.status == "ok" + @property + def origin(self): + # type: () -> Optional[str] + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - 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. + return self._otel_span.attributes.get(SentrySpanAttribute.ORIGIN) - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. + @origin.setter + def origin(self, value): + # type: (Optional[str]) -> None + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - :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. + if value is not None: + self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, value) - :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. + @property + def root_span(self): + if isinstance(self._otel_span, otel_trace.NonRecordingSpan): return None - try: - if end_timestamp: - if isinstance(end_timestamp, float): - end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc) - self.timestamp = end_timestamp + parent = None + while True: + # XXX + if self._otel_span.parent: + parent = self._otel_span.parent 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, - } - - -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", - "_baggage", - ) - - def __init__( - self, - name="", # type: str - parent_sampled=None, # type: Optional[bool] - baggage=None, # type: Optional[Baggage] - source=TRANSACTION_SOURCE_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[sentry_sdk.profiler.transaction_profiler.Profile] - self._baggage = baggage - - 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__() + break - if self._profile is not None: - self._profile.__enter__() + return parent - 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) + @property + def is_segment(self): + if isinstance(self._otel_span, otel_trace.NonRecordingSpan): + return False - super().__exit__(ty, value, tb) + return self._otel_span.parent is None @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. + # type: () -> Optional[Span] """ + Get the transaction this span is a child of. - # 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. + .. deprecated:: 3.0.0 + This will be removed in the future. """ - 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 ``." - ) - self.name = "" - - 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 - ] - - # 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 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, 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) + logger.warning("Deprecated: This will be removed in the future.") + return self.root_span - return self._baggage - - def _set_initial_sampling_decision(self, sampling_context): - # type: (SamplingContext) -> None + @containing_transaction.setter + def containing_transaction(self, value): + # type: (Span) -> 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. + Set this span's 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. + .. deprecated:: 3.0.0 + Use :func:`root_span` instead. """ - 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. 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 - - 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, **kwargs): - # type: (**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 + @property + def parent_span_id(self): + # type: () -> Optional[str] + return self._otel_span.parent if hasattr(self._otel_span, "parent") else None - def set_measurement(self, name, value, unit=""): - # type: (str, float, MeasurementUnit) -> None - pass + @property + def trace_id(self): + # type: () -> Optional[str] + return self._otel_span.get_span_context().trace_id - def set_context(self, key, value): - # type: (str, Any) -> None - pass + @property + def span_id(self): + # type: () -> Optional[str] + return self._otel_span.get_span_context().span_id - def init_span_recorder(self, maxlen): - # type: (int) -> None - pass + @property + def sampled(self): + # type: () -> Optional[bool] + return self._otel_span.get_span_context().trace_flags.sampled - def _set_initial_sampling_decision(self, sampling_context): - # type: (SamplingContext) -> None + @sampled.setter + def sampled(self, value): + # type: () -> Optional[bool] pass - -class POTelSpan: - """ - OTel span wrapper providing compatibility with the old span interface. - """ - - # XXX Maybe it makes sense to repurpose the existing Span class for this. - # For now I'm keeping this class separate to have a clean slate. - - # XXX The wrapper itself should have as little state as possible - - def __init__( - self, - *, - active=True, # type: bool - op=None, # type: Optional[str] - description=None, # type: Optional[str] - origin="manual", # type: str - **_, # type: dict[str, object] - ): - # type: (...) -> None - """ - For backwards compatibility with old the old Span interface, this class - accepts arbitrary keyword arguments, in addition to the ones explicitly - listed in the signature. These additional arguments are ignored. - """ + @property + def op(self): + # type: () -> Optional[str] from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - self._otel_span = tracer.start_span(description or op or "") # XXX - self._active = active + self._otel_span.attributes.get(SentrySpanAttribute.OP) - self._otel_span.set_attribute(SentrySpanAttribute.ORIGIN, origin) - if op is not None: - self._otel_span.set_attribute(SentrySpanAttribute.OP, op) - if description is not None: - self._otel_span.set_attribute(SentrySpanAttribute.DESCRIPTION, description) - - def __enter__(self): - # type: () -> POTelSpan - # XXX use_span? https://github.com/open-telemetry/opentelemetry-python/blob/3836da8543ce9751051e38a110c0468724042e62/opentelemetry-api/src/opentelemetry/trace/__init__.py#L547 - # - # create a Context object with parent set as current span - if self._active: - ctx = otel_trace.set_span_in_context(self._otel_span) - # set as the implicit current context - self._ctx_token = context.attach(ctx) - - return self - - def __exit__(self, ty, value, tb): - # type: (Optional[Any], Optional[Any], Optional[Any]) -> None - self._otel_span.end() - # XXX set status to error if unset and an exception occurred? - if self._active: - context.detach(self._ctx_token) + @op.setter + def op(self, value): + # type: (str) -> None + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute - @property - def containing_transaction(self): - # type: () -> Optional[Transaction] - pass + self._otel_span.set_attribute(SentrySpanAttribute.OP, value) def start_child(self, **kwargs): - # type: (str, **Any) -> POTelSpan - pass + # type: (str, **Any) -> Span + kwargs.setdefault("sampled", self.sampled) + child_span = Span(**kwargs) + return child_span @classmethod def continue_from_environ( @@ -1316,8 +1349,10 @@ def continue_from_environ( environ, # type: Mapping[str, str] **kwargs, # type: Any ): - # type: (...) -> POTelSpan - pass + # type: (...) -> Span + # XXX actually propagate + span = Span(**kwargs) + return span @classmethod def continue_from_headers( @@ -1325,8 +1360,10 @@ def continue_from_headers( headers, # type: Mapping[str, str] **kwargs, # type: Any ): - # type: (...) -> POTelSpan - pass + # type: (...) -> Span + # XXX actually propagate + span = Span(**kwargs) + return span def iter_headers(self): # type: () -> Iterator[Tuple[str, str]] @@ -1338,12 +1375,25 @@ def from_traceparent( traceparent, # type: Optional[str] **kwargs, # type: Any ): - # type: (...) -> Optional[Transaction] - pass + # type: (...) -> Optional[Span] + # XXX actually propagate + span = Span(**kwargs) + return span def to_traceparent(self): # type: () -> str - pass + 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] @@ -1359,23 +1409,41 @@ def set_data(self, key, value): def set_status(self, status): # type: (str) -> None - pass + if status == SPANSTATUS.OK: + otel_status = StatusCode.OK + otel_description = None + else: + otel_status = StatusCode.ERROR + otel_description = status.value + + self._otel_span.set_status(otel_status, otel_description) def set_measurement(self, name, value, unit=""): # type: (str, float, MeasurementUnit) -> None - pass + # XXX own namespace, e.g. sentry.measurement.xxx? + self._otel_span.set_attribute(name, (value, unit)) def set_thread(self, thread_id, thread_name): # type: (Optional[int], Optional[str]) -> None - pass + 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 - pass + if profiler_id is not None: + self.set_data(SPANDATA.PROFILER_ID, profiler_id) def set_http_status(self, http_status): # type: (int) -> None - pass + self.set_tag( + "http.status_code", str(http_status) + ) # we keep this for backwards compatibility + # XXX do we still need this? ^ + 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 @@ -1383,7 +1451,16 @@ def is_success(self): def finish(self, scope=None, end_timestamp=None): # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] - pass + # XXX check if already finished + from sentry_sdk.integrations.opentelemetry.utils import ( + convert_to_otel_timestamp, + ) + + if end_timestamp is not None: + end_timestamp = convert_to_otel_timestamp(end_timestamp) + self._otel_span.end(end_time=end_timestamp) + scope = scope or sentry_sdk.get_current_scope() + maybe_create_breadcrumbs_from_span(scope, self) def to_json(self): # type: () -> dict[str, Any] @@ -1397,6 +1474,13 @@ def get_profile_context(self): # type: () -> Optional[ProfileContext] pass + def _get_local_aggregator(self): + # type: (...) -> LocalAggregator + rv = self._local_aggregator + if rv is None: + rv = self._local_aggregator = LocalAggregator() + return rv + # transaction/root span methods def set_context(self, key, value): @@ -1408,6 +1492,14 @@ def get_baggage(self): pass +Transaction = Span + + +class NoOpSpan: + # XXX + pass + + if TYPE_CHECKING: @overload diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 0dabfbc486..a39b5d61f4 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -687,7 +687,7 @@ def func_with_tracing(*args, **kwargs): def get_current_span(scope=None): - # type: (Optional[sentry_sdk.Scope]) -> Optional[Span] + # type: (Optional[sentry_sdk.Scope]) -> Optional[sentry_sdk.tracing.Span] """ Returns the currently active span if there is one running, otherwise `None` """ @@ -702,6 +702,3 @@ def get_current_span(scope=None): LOW_QUALITY_TRANSACTION_SOURCES, SENTRY_TRACE_HEADER_NAME, ) - -if TYPE_CHECKING: - from sentry_sdk.tracing import Span diff --git a/tests/integrations/opentelemetry/test_potel.py b/tests/integrations/opentelemetry/test_potel.py index 2e094b41b5..5e44cc3888 100644 --- a/tests/integrations/opentelemetry/test_potel.py +++ b/tests/integrations/opentelemetry/test_potel.py @@ -41,7 +41,7 @@ def test_root_span_transaction_payload_started_with_otel_only(capture_envelopes) trace_context = contexts["trace"] assert "trace_id" in trace_context assert "span_id" in trace_context - assert trace_context["origin"] == "auto.otel" + assert trace_context["origin"] == "manual" assert trace_context["op"] == "request" assert trace_context["status"] == "ok" @@ -62,7 +62,7 @@ def test_child_span_payload_started_with_otel_only(capture_envelopes): assert span["op"] == "db" assert span["description"] == "db" - assert span["origin"] == "auto.otel" + assert span["origin"] == "manual" assert span["status"] == "ok" assert span["span_id"] is not None assert span["trace_id"] == payload["contexts"]["trace"]["trace_id"] @@ -124,7 +124,7 @@ def test_root_span_transaction_payload_started_with_sentry_only(capture_envelope trace_context = contexts["trace"] assert "trace_id" in trace_context assert "span_id" in trace_context - assert trace_context["origin"] == "auto.otel" + assert trace_context["origin"] == "manual" assert trace_context["op"] == "request" assert trace_context["status"] == "ok" @@ -145,7 +145,7 @@ def test_child_span_payload_started_with_sentry_only(capture_envelopes): assert span["op"] == "db" assert span["description"] == "db" - assert span["origin"] == "auto.otel" + assert span["origin"] == "manual" assert span["status"] == "ok" assert span["span_id"] is not None assert span["trace_id"] == payload["contexts"]["trace"]["trace_id"] diff --git a/tests/integrations/opentelemetry/test_utils.py b/tests/integrations/opentelemetry/test_utils.py index ceb58a58ef..66ffd7898a 100644 --- a/tests/integrations/opentelemetry/test_utils.py +++ b/tests/integrations/opentelemetry/test_utils.py @@ -23,6 +23,7 @@ "description": "OTel Span Blank", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -36,6 +37,7 @@ "description": "OTel Span RPC", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -49,6 +51,7 @@ "description": "OTel Span Messaging", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -62,6 +65,7 @@ "description": "OTel Span FaaS", "status": "ok", "http_status_code": None, + "origin": None, }, ), ], @@ -72,12 +76,13 @@ def test_extract_span_data(name, status, attributes, expected): otel_span.status = Status(StatusCode.UNSET) otel_span.attributes = attributes - op, description, status, http_status_code = extract_span_data(otel_span) + op, description, status, http_status_code, origin = extract_span_data(otel_span) result = { "op": op, "description": description, "status": status, "http_status_code": http_status_code, + "origin": origin, } assert result == expected @@ -99,6 +104,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -113,6 +119,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET /target", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -127,6 +134,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET example.com", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -142,6 +150,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET /target", "status": "ok", "http_status_code": None, + "origin": None, }, ), ( @@ -156,6 +165,7 @@ def test_extract_span_data(name, status, attributes, expected): "description": "GET https://example.com/bla/", "status": "ok", "http_status_code": None, + "origin": None, }, ), ], @@ -166,12 +176,15 @@ def test_span_data_for_http_method(kind, status, attributes, expected): otel_span.status = status otel_span.attributes = attributes - op, description, status, http_status_code = span_data_for_http_method(otel_span) + op, description, status, http_status_code, origin = span_data_for_http_method( + otel_span + ) result = { "op": op, "description": description, "status": status, "http_status_code": http_status_code, + "origin": origin, } assert result == expected @@ -181,19 +194,21 @@ def test_span_data_for_db_query(): otel_span.name = "OTel Span" otel_span.attributes = {} - op, description, status, http_status = span_data_for_db_query(otel_span) + op, description, status, http_status, origin = span_data_for_db_query(otel_span) assert op == "db" assert description == "OTel Span" assert status is None assert http_status is None + assert origin is None otel_span.attributes = {"db.statement": "SELECT * FROM table;"} - op, description, status, http_status = span_data_for_db_query(otel_span) + op, description, status, http_status, origin = span_data_for_db_query(otel_span) assert op == "db" assert description == "SELECT * FROM table;" assert status is None assert http_status is None + assert origin is None @pytest.mark.parametrize( diff --git a/tests/new_scopes_compat/__init__.py b/tests/new_scopes_compat/__init__.py deleted file mode 100644 index 45391bd9ad..0000000000 --- a/tests/new_scopes_compat/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Separate module for tests that check backwards compatibility of the Hub API with 1.x. -These tests should be removed once we remove the Hub API, likely in the next major. - -All tests in this module are run with hub isolation, provided by `isolate_hub` autouse -fixture, defined in `conftest.py`. -""" diff --git a/tests/new_scopes_compat/conftest.py b/tests/new_scopes_compat/conftest.py deleted file mode 100644 index 9f16898dea..0000000000 --- a/tests/new_scopes_compat/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest -import sentry_sdk - - -@pytest.fixture(autouse=True) -def isolate_hub(suppress_deprecation_warnings): - with sentry_sdk.Hub(None): - yield diff --git a/tests/new_scopes_compat/test_new_scopes_compat.py b/tests/new_scopes_compat/test_new_scopes_compat.py deleted file mode 100644 index 1e109ec036..0000000000 --- a/tests/new_scopes_compat/test_new_scopes_compat.py +++ /dev/null @@ -1,217 +0,0 @@ -import sentry_sdk -from sentry_sdk.hub import Hub - -""" -Those tests are meant to check the compatibility of the new scopes in SDK 2.0 with the old Hub/Scope system in SDK 1.x. - -Those tests have been run with the latest SDK 1.x versiona and the data used in the `assert` statements represents -the behvaior of the SDK 1.x. - -This makes sure that we are backwards compatible. (on a best effort basis, there will probably be some edge cases that are not covered here) -""" - - -def test_with_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B2": 1, "Z": 1} - - -def test_with_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == { - "A": 1, - "B1": 1, - "B2": 1, - "B3": 1, - "B4": 1, - "B5": 1, - "Z": 1, - } - - -def test_with_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub.current as hub: # with hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "B1": 1, "B5": 1, "Z": 1} - - -def test_with_cloned_hub_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - hub.scope.set_tag("B2", 1) - sentry_sdk.capture_message("Event B") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_cloned_hub_configure_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with configure_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.configure_scope() as scope: # configure scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} - - -def test_with_cloned_hub_push_scope_sdk1(sentry_init, capture_events): - """ - Mutate data in a `with cloned Hub:` containing a `with push_scope` block - - Checks the results of SDK 2.x against the results the same code returned in SDK 1.x. - """ - sentry_init() - - events = capture_events() - - sentry_sdk.set_tag("A", 1) - sentry_sdk.capture_message("Event A") - - with Hub(Hub.current) as hub: # clone hub - sentry_sdk.set_tag("B1", 1) - with hub.push_scope() as scope: # push scope - sentry_sdk.set_tag("B2", 1) - hub.scope.set_tag("B3", 1) - scope.set_tag("B4", 1) - sentry_sdk.capture_message("Event B") - sentry_sdk.set_tag("B5", 1) - sentry_sdk.capture_message("Event C") - - sentry_sdk.set_tag("Z", 1) - sentry_sdk.capture_message("Event Z") - - (event_a, event_b, event_c, event_z) = events - - # Check against the results the same code returned in SDK 1.x - assert event_a["tags"] == {"A": 1} - assert event_b["tags"] == {"A": 1, "B1": 1, "B2": 1, "B3": 1, "B4": 1} - assert event_c["tags"] == {"A": 1, "B1": 1, "B5": 1} - assert event_z["tags"] == {"A": 1, "Z": 1} diff --git a/tests/test_api.py b/tests/test_api.py index ffe1be756d..46fc24fd24 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -30,7 +30,7 @@ def test_get_current_span(): @pytest.mark.forked -def test_get_current_span_default_hub(sentry_init): +def test_get_current_span_current_scope(sentry_init): sentry_init() assert get_current_span() is None @@ -43,7 +43,7 @@ def test_get_current_span_default_hub(sentry_init): @pytest.mark.forked -def test_get_current_span_default_hub_with_transaction(sentry_init): +def test_get_current_span_current_scope_with_transaction(sentry_init): sentry_init() assert get_current_span() is None diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index 02966642fd..996d9c4d5d 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -258,7 +258,7 @@ def test_circular_references(monkeypatch, sentry_init, request): assert gc.collect() == 0 -def test_set_meaurement(sentry_init, capture_events): +def test_set_measurement(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0) events = capture_events() @@ -286,7 +286,7 @@ def test_set_meaurement(sentry_init, capture_events): assert event["measurements"]["metric.foobar"] == {"value": 17.99, "unit": "percent"} -def test_set_meaurement_public_api(sentry_init, capture_events): +def test_set_measurement_public_api(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0) events = capture_events() @@ -412,7 +412,7 @@ def test_transaction_dropped_debug_not_started(sentry_init, sampled): ) -def test_transaction_dropeed_sampled_false(sentry_init): +def test_transaction_dropped_sampled_false(sentry_init): sentry_init(enable_tracing=True) tx = Transaction(sampled=False)