Skip to content

Commit 9389f49

Browse files
rhcarvalhountitaker
andcommitted
feat: Introduce Transaction and Hub.start_transaction
This aligns the tracing implementation with the current JS tracing implementation, up to a certain extent. Hub.start_transaction or start_transaction are meant to be used when starting transactions, replacing most uses of Hub.start_span / start_span. Spans are typically created from their parent transactions via transaction.start_child, or start_span relying on the transaction being in the current scope. It is okay to start a transaction without a name and set it later. Sometimes the proper name is not known until after the transaction has started. We could fail the transaction if it has no name when calling the finish method. Instead, set a default name that will prompt users to give a name to their transactions. This is the same behavior as implemented in JS. Span.continue_from_headers, Span.continue_from_environ, Span.from_traceparent and the equivalent methods on Transaction always return a Transaction and take kwargs to set attributes on the new Transaction. Rename Span.new_span to Span.start_child (and Transaction.start_child)a, aligning with JS / tracing API spec. The old name is kept for backwards compatibility. Co-authored-by: Markus Unterwaditzer <[email protected]>
1 parent 2c0b5ec commit 9389f49

File tree

13 files changed

+408
-204
lines changed

13 files changed

+408
-204
lines changed

sentry_sdk/api.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing import Union
1717

1818
from sentry_sdk._types import Event, Hint, Breadcrumb, BreadcrumbHint, ExcInfo
19-
from sentry_sdk.tracing import Span
19+
from sentry_sdk.tracing import Span, Transaction
2020

2121
T = TypeVar("T")
2222
F = TypeVar("F", bound=Callable[..., Any])
@@ -37,6 +37,7 @@ def overload(x):
3737
"flush",
3838
"last_event_id",
3939
"start_span",
40+
"start_transaction",
4041
"set_tag",
4142
"set_context",
4243
"set_extra",
@@ -201,3 +202,12 @@ def start_span(
201202
):
202203
# type: (...) -> Span
203204
return Hub.current.start_span(span=span, **kwargs)
205+
206+
207+
@hubmethod
208+
def start_transaction(
209+
transaction=None, # type: Optional[Transaction]
210+
**kwargs # type: Any
211+
):
212+
# type: (...) -> Transaction
213+
return Hub.current.start_transaction(transaction, **kwargs)

sentry_sdk/hub.py

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sentry_sdk._compat import with_metaclass
99
from sentry_sdk.scope import Scope
1010
from sentry_sdk.client import Client
11-
from sentry_sdk.tracing import Span
11+
from sentry_sdk.tracing import Span, Transaction
1212
from sentry_sdk.sessions import Session
1313
from sentry_sdk.utils import (
1414
exc_info_from_error,
@@ -441,38 +441,88 @@ def start_span(
441441
):
442442
# type: (...) -> Span
443443
"""
444-
Create a new span whose parent span is the currently active
445-
span, if any. The return value is the span object that can
446-
be used as a context manager to start and stop timing.
447-
448-
Note that you will not see any span that is not contained
449-
within a transaction. Create a transaction with
450-
``start_span(transaction="my transaction")`` if an
451-
integration doesn't already do this for you.
444+
Create and start timing a new span whose parent is the currently active
445+
span or transaction, if any. The return value is a span instance,
446+
typically used as a context manager to start and stop timing in a `with`
447+
block.
448+
449+
Only spans contained in a transaction are sent to Sentry. Most
450+
integrations start a transaction at the appropriate time, for example
451+
for every incoming HTTP request. Use `start_transaction` to start a new
452+
transaction when one is not already in progress.
452453
"""
454+
# TODO: consider removing this in a future release.
455+
# This is for backwards compatibility with releases before
456+
# start_transaction existed, to allow for a smoother transition.
457+
if isinstance(span, Transaction) or "transaction" in kwargs:
458+
deprecation_msg = (
459+
"Deprecated: use start_transaction to start transactions and "
460+
"Transaction.start_child to start spans."
461+
)
462+
if isinstance(span, Transaction):
463+
logger.warning(deprecation_msg)
464+
return self.start_transaction(span)
465+
if "transaction" in kwargs:
466+
logger.warning(deprecation_msg)
467+
name = kwargs.pop("transaction")
468+
return self.start_transaction(name=name, **kwargs)
453469

454-
client, scope = self._stack[-1]
470+
if span is not None:
471+
return span
455472

456473
kwargs.setdefault("hub", self)
457474

458-
if span is None:
459-
span = scope.span
460-
if span is not None:
461-
span = span.new_span(**kwargs)
462-
else:
463-
span = Span(**kwargs)
475+
span = self.scope.span
476+
if span is not None:
477+
return span.start_child(**kwargs)
478+
479+
return Span(**kwargs)
480+
481+
def start_transaction(
482+
self,
483+
transaction=None, # type: Optional[Transaction]
484+
**kwargs # type: Any
485+
):
486+
# type: (...) -> Transaction
487+
"""
488+
Start and return a transaction.
489+
490+
Start an existing transaction if given, otherwise create and start a new
491+
transaction with kwargs.
492+
493+
This is the entry point to manual tracing instrumentation.
494+
495+
A tree structure can be built by adding child spans to the transaction,
496+
and child spans to other spans. To start a new child span within the
497+
transaction or any span, call the respective `.start_child()` method.
498+
499+
Every child span must be finished before the transaction is finished,
500+
otherwise the unfinished spans are discarded.
501+
502+
When used as context managers, spans and transactions are automatically
503+
finished at the end of the `with` block. If not using context managers,
504+
call the `.finish()` method.
505+
506+
When the transaction is finished, it will be sent to Sentry with all its
507+
finished child spans.
508+
"""
509+
if transaction is None:
510+
kwargs.setdefault("hub", self)
511+
transaction = Transaction(**kwargs)
512+
513+
client, scope = self._stack[-1]
464514

465-
if span.sampled is None and span.transaction is not None:
515+
if transaction.sampled is None:
466516
sample_rate = client and client.options["traces_sample_rate"] or 0
467-
span.sampled = random.random() < sample_rate
517+
transaction.sampled = random.random() < sample_rate
468518

469-
if span.sampled:
519+
if transaction.sampled:
470520
max_spans = (
471521
client and client.options["_experiments"].get("max_spans") or 1000
472522
)
473-
span.init_finished_spans(maxlen=max_spans)
523+
transaction.init_span_recorder(maxlen=max_spans)
474524

475-
return span
525+
return transaction
476526

477527
@overload # noqa
478528
def push_scope(

sentry_sdk/integrations/aiohttp.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
_filter_headers,
1010
request_body_within_bounds,
1111
)
12-
from sentry_sdk.tracing import Span
12+
from sentry_sdk.tracing import Transaction
1313
from sentry_sdk.utils import (
1414
capture_internal_exceptions,
1515
event_from_exception,
@@ -87,27 +87,29 @@ async def sentry_app_handle(self, request, *args, **kwargs):
8787
scope.clear_breadcrumbs()
8888
scope.add_event_processor(_make_request_processor(weak_request))
8989

90-
span = Span.continue_from_headers(request.headers)
91-
span.op = "http.server"
92-
# If this transaction name makes it to the UI, AIOHTTP's
93-
# URL resolver did not find a route or died trying.
94-
span.transaction = "generic AIOHTTP request"
90+
transaction = Transaction.continue_from_headers(
91+
request.headers,
92+
op="http.server",
93+
# If this transaction name makes it to the UI, AIOHTTP's
94+
# URL resolver did not find a route or died trying.
95+
name="generic AIOHTTP request",
96+
)
9597

96-
with hub.start_span(span):
98+
with hub.start_transaction(transaction):
9799
try:
98100
response = await old_handle(self, request)
99101
except HTTPException as e:
100-
span.set_http_status(e.status_code)
102+
transaction.set_http_status(e.status_code)
101103
raise
102104
except asyncio.CancelledError:
103-
span.set_status("cancelled")
105+
transaction.set_status("cancelled")
104106
raise
105107
except Exception:
106108
# This will probably map to a 500 but seems like we
107109
# have no way to tell. Do not set span status.
108110
reraise(*_capture_exception(hub))
109111

110-
span.set_http_status(response.status)
112+
transaction.set_http_status(response.status)
111113
return response
112114

113115
Application._handle = sentry_app_handle

sentry_sdk/integrations/asgi.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
HAS_REAL_CONTEXTVARS,
2020
CONTEXTVARS_ERROR_MESSAGE,
2121
)
22-
from sentry_sdk.tracing import Span
22+
from sentry_sdk.tracing import Transaction
2323

2424
if MYPY:
2525
from typing import Dict
@@ -123,16 +123,16 @@ async def _run_app(self, scope, callback):
123123
ty = scope["type"]
124124

125125
if ty in ("http", "websocket"):
126-
span = Span.continue_from_headers(dict(scope["headers"]))
127-
span.op = "{}.server".format(ty)
126+
transaction = Transaction.continue_from_headers(
127+
dict(scope["headers"]), op="{}.server".format(ty),
128+
)
128129
else:
129-
span = Span()
130-
span.op = "asgi.server"
130+
transaction = Transaction(op="asgi.server")
131131

132-
span.set_tag("asgi.type", ty)
133-
span.transaction = _DEFAULT_TRANSACTION_NAME
132+
transaction.name = _DEFAULT_TRANSACTION_NAME
133+
transaction.set_tag("asgi.type", ty)
134134

135-
with hub.start_span(span) as span:
135+
with hub.start_transaction(transaction):
136136
# XXX: Would be cool to have correct span status, but we
137137
# would have to wrap send(). That is a bit hard to do with
138138
# the current abstraction over ASGI 2/3.

sentry_sdk/integrations/celery.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from sentry_sdk.hub import Hub
66
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
7-
from sentry_sdk.tracing import Span
7+
from sentry_sdk.tracing import Transaction
88
from sentry_sdk._compat import reraise
99
from sentry_sdk.integrations import Integration, DidNotEnable
1010
from sentry_sdk.integrations.logging import ignore_logger
@@ -130,19 +130,21 @@ def _inner(*args, **kwargs):
130130
scope.clear_breadcrumbs()
131131
scope.add_event_processor(_make_event_processor(task, *args, **kwargs))
132132

133-
span = Span.continue_from_headers(args[3].get("headers") or {})
134-
span.op = "celery.task"
135-
span.transaction = "unknown celery task"
133+
transaction = Transaction.continue_from_headers(
134+
args[3].get("headers") or {},
135+
op="celery.task",
136+
name="unknown celery task",
137+
)
136138

137139
# Could possibly use a better hook than this one
138-
span.set_status("ok")
140+
transaction.set_status("ok")
139141

140142
with capture_internal_exceptions():
141143
# Celery task objects are not a thing to be trusted. Even
142144
# something such as attribute access can fail.
143-
span.transaction = task.name
145+
transaction.name = task.name
144146

145-
with hub.start_span(span):
147+
with hub.start_transaction(transaction):
146148
return f(*args, **kwargs)
147149

148150
return _inner # type: ignore

sentry_sdk/integrations/rq.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from sentry_sdk.hub import Hub
66
from sentry_sdk.integrations import Integration, DidNotEnable
7-
from sentry_sdk.tracing import Span
7+
from sentry_sdk.tracing import Transaction
88
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
99

1010

@@ -61,15 +61,16 @@ def sentry_patched_perform_job(self, job, *args, **kwargs):
6161
scope.clear_breadcrumbs()
6262
scope.add_event_processor(_make_event_processor(weakref.ref(job)))
6363

64-
span = Span.continue_from_headers(
65-
job.meta.get("_sentry_trace_headers") or {}
64+
transaction = Transaction.continue_from_headers(
65+
job.meta.get("_sentry_trace_headers") or {},
66+
op="rq.task",
67+
name="unknown RQ task",
6668
)
67-
span.op = "rq.task"
6869

6970
with capture_internal_exceptions():
70-
span.transaction = job.func_name
71+
transaction.name = job.func_name
7172

72-
with hub.start_span(span):
73+
with hub.start_transaction(transaction):
7374
rv = old_perform_job(self, job, *args, **kwargs)
7475

7576
if self.is_horse:

sentry_sdk/integrations/wsgi.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
event_from_exception,
99
)
1010
from sentry_sdk._compat import PY2, reraise, iteritems
11-
from sentry_sdk.tracing import Span
11+
from sentry_sdk.tracing import Transaction
1212
from sentry_sdk.sessions import auto_session_tracking
1313
from sentry_sdk.integrations._wsgi_common import _filter_headers
1414

@@ -113,15 +113,17 @@ def __call__(self, environ, start_response):
113113
_make_wsgi_event_processor(environ)
114114
)
115115

116-
span = Span.continue_from_environ(environ)
117-
span.op = "http.server"
118-
span.transaction = "generic WSGI request"
116+
transaction = Transaction.continue_from_environ(
117+
environ, op="http.server", name="generic WSGI request"
118+
)
119119

120-
with hub.start_span(span) as span:
120+
with hub.start_transaction(transaction):
121121
try:
122122
rv = self.app(
123123
environ,
124-
partial(_sentry_start_response, start_response, span),
124+
partial(
125+
_sentry_start_response, start_response, transaction
126+
),
125127
)
126128
except BaseException:
127129
reraise(*_capture_exception(hub))
@@ -133,15 +135,15 @@ def __call__(self, environ, start_response):
133135

134136
def _sentry_start_response(
135137
old_start_response, # type: StartResponse
136-
span, # type: Span
138+
transaction, # type: Transaction
137139
status, # type: str
138140
response_headers, # type: WsgiResponseHeaders
139141
exc_info=None, # type: Optional[WsgiExcInfo]
140142
):
141143
# type: (...) -> WsgiResponseIter
142144
with capture_internal_exceptions():
143145
status_int = int(status.split(" ", 1)[0])
144-
span.set_http_status(status_int)
146+
transaction.set_http_status(status_int)
145147

146148
if exc_info is None:
147149
# The Django Rest Framework WSGI test client, and likely other

0 commit comments

Comments
 (0)