diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index fc2b305716..9e12a2c94c 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -16,7 +16,7 @@ from typing import Union from sentry_sdk._types import Event, Hint, Breadcrumb, BreadcrumbHint, ExcInfo - from sentry_sdk.tracing import Span + from sentry_sdk.tracing import Span, Transaction T = TypeVar("T") F = TypeVar("F", bound=Callable[..., Any]) @@ -37,6 +37,7 @@ def overload(x): "flush", "last_event_id", "start_span", + "start_transaction", "set_tag", "set_context", "set_extra", @@ -201,3 +202,12 @@ def start_span( ): # type: (...) -> Span return Hub.current.start_span(span=span, **kwargs) + + +@hubmethod +def start_transaction( + transaction=None, # type: Optional[Transaction] + **kwargs # type: Any +): + # type: (...) -> Transaction + return Hub.current.start_transaction(transaction, **kwargs) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 18558761cf..8bd9e14c77 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -8,7 +8,7 @@ from sentry_sdk._compat import with_metaclass from sentry_sdk.scope import Scope from sentry_sdk.client import Client -from sentry_sdk.tracing import Span +from sentry_sdk.tracing import Span, Transaction from sentry_sdk.sessions import Session from sentry_sdk.utils import ( exc_info_from_error, @@ -445,10 +445,9 @@ def start_span( span, if any. The return value is the span object that can be used as a context manager to start and stop timing. - Note that you will not see any span that is not contained - within a transaction. Create a transaction with - ``start_span(transaction="my transaction")`` if an - integration doesn't already do this for you. + Note that you will not see any span that is not contained within a + transaction. Most integrations already do this for you, but create a + transaction with `start_transaction` otherwise. """ client, scope = self._stack[-1] @@ -462,17 +461,63 @@ def start_span( else: span = Span(**kwargs) - if span.sampled is None and span.transaction is not None: + elif isinstance(span, Transaction): + raise ValueError("Pass transactions to start_transaction instead") + + return span + + def start_transaction( + self, + transaction=None, # type: Optional[Transaction] + **kwargs # type: Any + ): + # type: (...) -> Transaction + """ + Start and return a transaction. + + Start an existing transaction if given, otherwise create and start a new + transaction with kwargs. + """ + # XXX: should we always set transaction.hub = self? + # In a multi-hub program, what does it mean to write + # hub1.start_transaction(Transaction(hub=hub2)) + # ? Should the transaction be on hub1 or hub2? + + # XXX: it is strange that both start_span and start_transaction take + # kwargs, but those are ignored if span/transaction are not None. + # Code such as: + # hub.start_transaction(Transaction(name="foo"), name="bar") + # + # is clearly weird, but not so weird if we intentionally want to rename + # a transaction we got from somewhere else: + # hub.start_transaction(transaction, name="new_name") + # + # Would be clearer if Hub was not involved: + # transaction.name = "new_name" + # with transaction: # __enter__ => start, __exit__ => finish + # with transaction.start_child(...): + # pass + # # alternatively, rely on transaction being in the current scope + # with Span(...): + # pass + + if transaction is None: + kwargs.setdefault("hub", self) + transaction = Transaction(**kwargs) + + client, scope = self._stack[-1] + + if transaction.sampled is None: sample_rate = client and client.options["traces_sample_rate"] or 0 - span.sampled = random.random() < sample_rate + transaction.sampled = random.random() < sample_rate - if span.sampled: + if transaction.sampled: max_spans = ( client and client.options["_experiments"].get("max_spans") or 1000 ) - span.init_finished_spans(maxlen=max_spans) + transaction.init_span_recorder(maxlen=max_spans) - return span + return transaction @overload # noqa def push_scope( diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 8bbb1670ee..61973ee9b6 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -9,7 +9,7 @@ _filter_headers, request_body_within_bounds, ) -from sentry_sdk.tracing import Span +from sentry_sdk.tracing import Transaction from sentry_sdk.utils import ( capture_internal_exceptions, event_from_exception, @@ -87,27 +87,29 @@ async def sentry_app_handle(self, request, *args, **kwargs): scope.clear_breadcrumbs() scope.add_event_processor(_make_request_processor(weak_request)) - span = Span.continue_from_headers(request.headers) - span.op = "http.server" - # If this transaction name makes it to the UI, AIOHTTP's - # URL resolver did not find a route or died trying. - span.transaction = "generic AIOHTTP request" + transaction = Transaction.continue_from_headers( + request.headers, + op="http.server", + # If this transaction name makes it to the UI, AIOHTTP's + # URL resolver did not find a route or died trying. + name="generic AIOHTTP request", + ) - with hub.start_span(span): + with hub.start_transaction(transaction): try: response = await old_handle(self, request) except HTTPException as e: - span.set_http_status(e.status_code) + transaction.set_http_status(e.status_code) raise except asyncio.CancelledError: - span.set_status("cancelled") + transaction.set_status("cancelled") raise except Exception: # This will probably map to a 500 but seems like we # have no way to tell. Do not set span status. reraise(*_capture_exception(hub)) - span.set_http_status(response.status) + transaction.set_http_status(response.status) return response Application._handle = sentry_app_handle diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 202c49025a..3db4fa1dfc 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -19,7 +19,7 @@ HAS_REAL_CONTEXTVARS, CONTEXTVARS_ERROR_MESSAGE, ) -from sentry_sdk.tracing import Span +from sentry_sdk.tracing import Transaction if MYPY: from typing import Dict @@ -123,16 +123,19 @@ async def _run_app(self, scope, callback): ty = scope["type"] if ty in ("http", "websocket"): - span = Span.continue_from_headers(dict(scope["headers"])) - span.op = "{}.server".format(ty) + transaction = Transaction.continue_from_headers( + dict(scope["headers"]), + name=_DEFAULT_TRANSACTION_NAME, + op="{}.server".format(ty), + ) else: - span = Span() - span.op = "asgi.server" + transaction = Transaction( + name=_DEFAULT_TRANSACTION_NAME, op="asgi.server" + ) - span.set_tag("asgi.type", ty) - span.transaction = _DEFAULT_TRANSACTION_NAME + transaction.set_tag("asgi.type", ty) - with hub.start_span(span) as span: + with hub.start_transaction(transaction): # XXX: Would be cool to have correct span status, but we # would have to wrap send(). That is a bit hard to do with # the current abstraction over ASGI 2/3. diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 5ac0d32f40..86714e2111 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -4,7 +4,7 @@ from sentry_sdk.hub import Hub from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from sentry_sdk.tracing import Span +from sentry_sdk.tracing import Transaction from sentry_sdk._compat import reraise from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger @@ -130,19 +130,21 @@ def _inner(*args, **kwargs): scope.clear_breadcrumbs() scope.add_event_processor(_make_event_processor(task, *args, **kwargs)) - span = Span.continue_from_headers(args[3].get("headers") or {}) - span.op = "celery.task" - span.transaction = "unknown celery task" + transaction = Transaction.continue_from_headers( + args[3].get("headers") or {}, + op="celery.task", + name="unknown celery task", + ) # Could possibly use a better hook than this one - span.set_status("ok") + transaction.set_status("ok") with capture_internal_exceptions(): # Celery task objects are not a thing to be trusted. Even # something such as attribute access can fail. - span.transaction = task.name + transaction.name = task.name - with hub.start_span(span): + with hub.start_transaction(transaction): return f(*args, **kwargs) return _inner # type: ignore diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py index fbe8cdda3d..1e51ec50cf 100644 --- a/sentry_sdk/integrations/rq.py +++ b/sentry_sdk/integrations/rq.py @@ -4,7 +4,7 @@ from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration, DidNotEnable -from sentry_sdk.tracing import Span +from sentry_sdk.tracing import Transaction from sentry_sdk.utils import capture_internal_exceptions, event_from_exception @@ -61,15 +61,16 @@ def sentry_patched_perform_job(self, job, *args, **kwargs): scope.clear_breadcrumbs() scope.add_event_processor(_make_event_processor(weakref.ref(job))) - span = Span.continue_from_headers( - job.meta.get("_sentry_trace_headers") or {} + transaction = Transaction.continue_from_headers( + job.meta.get("_sentry_trace_headers") or {}, + op="rq.task", + name="unknown RQ task", ) - span.op = "rq.task" with capture_internal_exceptions(): - span.transaction = job.func_name + transaction.name = job.func_name - with hub.start_span(span): + with hub.start_transaction(transaction): rv = old_perform_job(self, job, *args, **kwargs) if self.is_horse: diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 2ac9f2f191..505cdd6bea 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -8,7 +8,7 @@ event_from_exception, ) from sentry_sdk._compat import PY2, reraise, iteritems -from sentry_sdk.tracing import Span +from sentry_sdk.tracing import Transaction from sentry_sdk.sessions import auto_session_tracking from sentry_sdk.integrations._wsgi_common import _filter_headers @@ -113,15 +113,17 @@ def __call__(self, environ, start_response): _make_wsgi_event_processor(environ) ) - span = Span.continue_from_environ(environ) - span.op = "http.server" - span.transaction = "generic WSGI request" + transaction = Transaction.continue_from_environ( + environ, op="http.server", name="generic WSGI request" + ) - with hub.start_span(span) as span: + with hub.start_transaction(transaction) as transaction: try: rv = self.app( environ, - partial(_sentry_start_response, start_response, span), + partial( + _sentry_start_response, start_response, transaction + ), ) except BaseException: reraise(*_capture_exception(hub)) @@ -133,7 +135,7 @@ def __call__(self, environ, start_response): def _sentry_start_response( old_start_response, # type: StartResponse - span, # type: Span + span, # type: Transaction status, # type: str response_headers, # type: WsgiResponseHeaders exc_info=None, # type: Optional[WsgiExcInfo] diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index c721b56505..614661f438 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -5,6 +5,7 @@ from sentry_sdk._functools import wraps from sentry_sdk._types import MYPY from sentry_sdk.utils import logger, capture_internal_exceptions +from sentry_sdk.tracing import Transaction if MYPY: from typing import Any @@ -140,8 +141,8 @@ def transaction(self, value): """When set this forces a specific transaction name to be set.""" self._transaction = value span = self._span - if span: - span.transaction = value + if span and isinstance(span, Transaction) and value: + span.name = value @_attr_setter def user(self, value): @@ -166,8 +167,8 @@ def span(self): def span(self, span): # type: (Optional[Span]) -> None self._span = span - if span is not None: - span_transaction = span.transaction + if isinstance(span, Transaction): + span_transaction = span.name if span_transaction: self._transaction = span_transaction diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index b3dbde6f65..2cb6e1e0ab 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -25,6 +25,10 @@ from typing import Dict from typing import List from typing import Tuple + from typing import TypeVar + from typing import Type + + SpanTy = TypeVar("SpanTy", bound="Span") _traceparent_header_format_re = re.compile( "^[ \t]*" # whitespace @@ -67,28 +71,20 @@ def __iter__(self): class _SpanRecorder(object): - __slots__ = ("maxlen", "finished_spans", "open_span_count") + __slots__ = ("maxlen", "spans") def __init__(self, maxlen): # type: (int) -> None self.maxlen = maxlen - self.open_span_count = 0 # type: int - self.finished_spans = [] # type: List[Span] + self.spans = [] # type: List[Span] - def start_span(self, span): + def add(self, span): # type: (Span) -> None - # This is just so that we don't run out of memory while recording a lot - # of spans. At some point we just stop and flush out the start of the - # trace tree (i.e. the first n spans with the smallest - # start_timestamp). - self.open_span_count += 1 - if self.open_span_count > self.maxlen: + if len(self.spans) >= self.maxlen: span._span_recorder = None - - def finish_span(self, span): - # type: (Span) -> None - self.finished_spans.append(span) + else: + self.spans.append(span) class Span(object): @@ -98,7 +94,6 @@ class Span(object): "parent_span_id", "same_process_as_parent", "sampled", - "transaction", "op", "description", "start_timestamp", @@ -119,7 +114,6 @@ def __init__( parent_span_id=None, # type: Optional[str] same_process_as_parent=True, # type: bool sampled=None, # type: Optional[bool] - transaction=None, # type: Optional[str] op=None, # type: Optional[str] description=None, # type: Optional[str] hub=None, # type: Optional[sentry_sdk.Hub] @@ -130,9 +124,8 @@ def __init__( 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.transaction = transaction self.op = op + self.sampled = sampled self.description = description self.status = status self.hub = hub @@ -153,19 +146,19 @@ def __init__( self._span_recorder = None # type: Optional[_SpanRecorder] - def init_finished_spans(self, maxlen): + def init_span_recorder(self, maxlen): # type: (int) -> None if self._span_recorder is None: self._span_recorder = _SpanRecorder(maxlen) - self._span_recorder.start_span(self) + self._span_recorder.add(self) def __repr__(self): # type: () -> str return ( - "<%s(transaction=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r)>" + "<%s(name=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r)>" % ( self.__class__.__name__, - self.transaction, + getattr(self, "name", None), self.trace_id, self.span_id, self.parent_span_id, @@ -173,8 +166,10 @@ def __repr__(self): ) ) - def __enter__(self): - # type: () -> Span + def __enter__( + self, # type: SpanTy + ): + # type: (...) -> SpanTy hub = self.hub or sentry_sdk.Hub.current _, scope = hub._stack[-1] @@ -194,25 +189,45 @@ def __exit__(self, ty, value, tb): self.finish(hub) scope.span = old_span - def new_span(self, **kwargs): + 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`. No + attributes other than the sample rate are inherited. + """ kwargs.setdefault("sampled", self.sampled) - rv = type(self)( + + rv = Span( trace_id=self.trace_id, span_id=None, parent_span_id=self.span_id, **kwargs ) - rv._span_recorder = self._span_recorder + rv._span_recorder = recorder = self._span_recorder + if recorder: + recorder.add(rv) return rv + # deprecated + new_span = start_child + @classmethod - def continue_from_environ(cls, environ): - # type: (typing.Mapping[str, str]) -> Span - return cls.continue_from_headers(EnvironHeaders(environ)) + def continue_from_environ( + cls, # type: Type[SpanTy] + environ, # type: typing.Mapping[str, str] + **kwargs # type: Any + ): + # type: (...) -> SpanTy + return cls.continue_from_headers(EnvironHeaders(environ), **kwargs) @classmethod - def continue_from_headers(cls, headers): - # type: (typing.Mapping[str, str]) -> Span - parent = cls.from_traceparent(headers.get("sentry-trace")) + def continue_from_headers( + cls, # type: Type[SpanTy] + headers, # type: typing.Mapping[str, str] + **kwargs # type: Any + ): + # type: (...) -> SpanTy + parent = cls.from_traceparent(headers.get("sentry-trace"), **kwargs) if parent is None: return cls() parent.same_process_as_parent = False @@ -223,8 +238,12 @@ def iter_headers(self): yield "sentry-trace", self.to_traceparent() @classmethod - def from_traceparent(cls, traceparent): - # type: (Optional[str]) -> Optional[Span] + def from_traceparent( + cls, # type: Type[SpanTy] + traceparent, # type: Optional[str] + **kwargs # type: Any + ): + # type: (...) -> Optional[SpanTy] if not traceparent: return None @@ -247,7 +266,7 @@ def from_traceparent(cls, traceparent): else: sampled = None - return cls(trace_id=trace_id, parent_span_id=span_id, sampled=sampled) + return cls(trace_id=trace_id, parent_span_id=span_id, sampled=sampled, **kwargs) def to_traceparent(self): # type: () -> str @@ -326,49 +345,7 @@ def finish(self, hub=None): self.timestamp = datetime.utcnow() _maybe_create_breadcrumbs_from_span(hub, self) - - if self._span_recorder is None: - return None - - self._span_recorder.finish_span(self) - - if self.transaction is None: - # If this has no transaction set we assume there's a parent - # transaction for this span that would be flushed out eventually. - return None - - client = hub.client - - if client is None: - # We have no client and therefore nowhere to send this transaction - # event. - return None - - if not self.sampled: - # At this point a `sampled = None` should have already been - # resolved to a concrete decision. If `sampled` is `None`, it's - # likely that somebody used `with sentry_sdk.Hub.start_span(..)` on a - # non-transaction span and later decided to make it a transaction. - if self.sampled is None: - logger.warning("Discarding transaction Span without sampling decision") - - return None - - return hub.capture_event( - { - "type": "transaction", - "transaction": self.transaction, - "contexts": {"trace": self.get_trace_context()}, - "tags": self._tags, - "timestamp": self.timestamp, - "start_timestamp": self.start_timestamp, - "spans": [ - s.to_json(client) - for s in self._span_recorder.finished_spans - if s is not self - ], - } - ) + return None def to_json(self, client): # type: (Optional[sentry_sdk.Client]) -> Dict[str, Any] @@ -383,10 +360,6 @@ def to_json(self, client): "timestamp": self.timestamp, } # type: Dict[str, Any] - transaction = self.transaction - if transaction: - rv["transaction"] = transaction - if self.status: self._tags["status"] = self.status @@ -415,6 +388,64 @@ def get_trace_context(self): return rv +class Transaction(Span): + __slots__ = ("name",) + + def __init__( + self, + name="", # type: str + **kwargs # type: Any + ): + # type: (...) -> None + self.name = name + + Span.__init__(self, **kwargs) + + def finish(self, hub=None): + # type: (Optional[sentry_sdk.Hub]) -> Optional[str] + if not self.name: + logger.warning( + "Transaction has no name, falling back to ``." + ) + self.name = "" + + Span.finish(self, hub) + + hub = hub or self.hub or sentry_sdk.Hub.current + client = hub.client + + if client is None: + # We have no client and therefore nowhere to send this transaction + # event. + return None + + if not self.sampled: + # At this point a `sampled = None` should have already been + # resolved to a concrete decision. If `sampled` is `None`, it's + # likely that somebody used `with sentry_sdk.Hub.start_span(..)` on a + # non-transaction span and later decided to make it a transaction. + if self.sampled is None: + logger.warning("Discarding transaction Span without sampling decision") + + return None + + return hub.capture_event( + { + "type": "transaction", + "transaction": self.name, + "contexts": {"trace": self.get_trace_context()}, + "tags": self._tags, + "timestamp": self.timestamp, + "start_timestamp": self.start_timestamp, + "spans": [ + s.to_json(client) + for s in (self._span_recorder and self._span_recorder.spans or ()) + if s is not self and s.timestamp is not None + ], + } + ) + + def _format_sql(cursor, sql): # type: (Any, str) -> Optional[str] diff --git a/tests/integrations/stdlib/test_subprocess.py b/tests/integrations/stdlib/test_subprocess.py index ee6e7c8c60..4dc2137255 100644 --- a/tests/integrations/stdlib/test_subprocess.py +++ b/tests/integrations/stdlib/test_subprocess.py @@ -5,7 +5,7 @@ import pytest -from sentry_sdk import Hub, capture_message +from sentry_sdk import capture_message, start_transaction from sentry_sdk._compat import PY2 from sentry_sdk.integrations.stdlib import StdlibIntegration @@ -63,7 +63,7 @@ def test_subprocess_basic( sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0) events = capture_events() - with Hub.current.start_span(transaction="foo", op="foo") as span: + with start_transaction(name="foo") as transaction: args = [ sys.executable, "-c", @@ -114,7 +114,7 @@ def test_subprocess_basic( assert os.environ == old_environ - assert span.trace_id in str(output) + assert transaction.trace_id in str(output) capture_message("hi") diff --git a/tests/test_tracing.py b/tests/test_tracing.py index d68f815bd2..9a3d41460e 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -3,8 +3,14 @@ import pytest -from sentry_sdk import Hub, capture_message -from sentry_sdk.tracing import Span +from sentry_sdk import ( + Hub, + capture_message, + start_span, + start_transaction, + configure_scope, +) +from sentry_sdk.tracing import Transaction @pytest.mark.parametrize("sample_rate", [0.0, 1.0]) @@ -12,13 +18,13 @@ def test_basic(sentry_init, capture_events, sample_rate): sentry_init(traces_sample_rate=sample_rate) events = capture_events() - with Hub.current.start_span(transaction="hi") as span: - span.set_status("ok") + with start_transaction(name="hi") as transaction: + transaction.set_status("ok") with pytest.raises(ZeroDivisionError): - with Hub.current.start_span(op="foo", description="foodesc"): + with start_span(op="foo", description="foodesc"): 1 / 0 - with Hub.current.start_span(op="bar", description="bardesc"): + with start_span(op="bar", description="bardesc"): pass if sample_rate: @@ -44,8 +50,8 @@ def test_continue_from_headers(sentry_init, capture_events, sampled): sentry_init(traces_sample_rate=1.0, traceparent_v2=True) events = capture_events() - with Hub.current.start_span(transaction="hi"): - with Hub.current.start_span() as old_span: + with start_transaction(name="hi"): + with start_span() as old_span: old_span.sampled = sampled headers = dict(Hub.current.iter_trace_propagation_headers()) @@ -57,17 +63,16 @@ def test_continue_from_headers(sentry_init, capture_events, sampled): if sampled is None: assert header.endswith("-") - span = Span.continue_from_headers(headers) - span.transaction = "WRONG" - assert span is not None - assert span.sampled == sampled - assert span.trace_id == old_span.trace_id - assert span.same_process_as_parent is False - assert span.parent_span_id == old_span.span_id - assert span.span_id != old_span.span_id - - with Hub.current.start_span(span): - with Hub.current.configure_scope() as scope: + transaction = Transaction.continue_from_headers(headers, name="WRONG") + assert transaction is not None + assert transaction.sampled == sampled + assert transaction.trace_id == old_span.trace_id + assert transaction.same_process_as_parent is False + assert transaction.parent_span_id == old_span.span_id + assert transaction.span_id != old_span.span_id + + with start_transaction(transaction): + with configure_scope() as scope: scope.transaction = "ho" capture_message("hello") @@ -84,7 +89,7 @@ def test_continue_from_headers(sentry_init, capture_events, sampled): assert ( trace1["contexts"]["trace"]["trace_id"] == trace2["contexts"]["trace"]["trace_id"] - == span.trace_id + == transaction.trace_id == message["contexts"]["trace"]["trace_id"] ) @@ -94,13 +99,13 @@ def test_continue_from_headers(sentry_init, capture_events, sampled): def test_sampling_decided_only_for_transactions(sentry_init, capture_events): sentry_init(traces_sample_rate=0.5) - with Hub.current.start_span(transaction="hi") as trace: - assert trace.sampled is not None + with start_transaction(name="hi") as transaction: + assert transaction.sampled is not None - with Hub.current.start_span() as span: - assert span.sampled == trace.sampled + with start_span() as span: + assert span.sampled == transaction.sampled - with Hub.current.start_span() as span: + with start_span() as span: assert span.sampled is None @@ -113,11 +118,9 @@ def test_memory_usage(sentry_init, capture_events, args, expected_refcount): references = weakref.WeakSet() - with Hub.current.start_span(transaction="hi"): + with start_transaction(name="hi"): for i in range(100): - with Hub.current.start_span( - op="helloworld", description="hi {}".format(i) - ) as span: + with start_span(op="helloworld", description="hi {}".format(i)) as span: def foo(): pass @@ -139,9 +142,9 @@ def test_span_trimming(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0, _experiments={"max_spans": 3}) events = capture_events() - with Hub.current.start_span(transaction="hi"): + with start_transaction(name="hi"): for i in range(10): - with Hub.current.start_span(op="foo{}".format(i)): + with start_span(op="foo{}".format(i)): pass (event,) = events @@ -150,8 +153,57 @@ def test_span_trimming(sentry_init, capture_events): assert span2["op"] == "foo1" -def test_nested_span_sampling_override(): - with Hub.current.start_span(transaction="outer", sampled=True) as span: - assert span.sampled is True - with Hub.current.start_span(transaction="inner", sampled=False) as span: - assert span.sampled is False +def test_nested_transaction_sampling_override(): + with start_transaction(name="outer", sampled=True) as outer_transaction: + assert outer_transaction.sampled is True + with start_transaction(name="inner", sampled=False) as inner_transaction: + assert inner_transaction.sampled is False + assert outer_transaction.sampled is True + + +def test_transaction_method_signature(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + with pytest.raises(TypeError): + start_span(name="foo") + assert len(events) == 0 + + with start_transaction() as transaction: + pass + assert transaction.name == "" + assert len(events) == 1 + + with start_transaction() as transaction: + transaction.name = "name-known-after-transaction-started" + assert len(events) == 2 + + with start_transaction(name="a"): + pass + assert len(events) == 3 + + with start_transaction(Transaction(name="c")): + pass + assert len(events) == 4 + + +def test_nested_spans_in_scope(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + # XXX: nesting of spans may not work when async code is involved (data race + # accessing scope._span) + with start_transaction(name="/status/") as transaction: + assert Hub.current.scope._span is transaction + with start_span(op="check1", description="desc1") as span1: + assert Hub.current.scope._span is span1 + with start_span(op="check1_1", description="desc1_1") as span1_1: + assert Hub.current.scope._span is span1_1 + assert Hub.current.scope._span is span1 + with start_span(op="check2", description="desc2") as span2: + assert Hub.current.scope._span is span2 + assert Hub.current.scope._span is transaction + transaction.set_status("ok") + + assert len(events) == 1 + assert len(events[0]["spans"]) == 3