Skip to content

Commit 7c94965

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. Co-authored-by: Markus Unterwaditzer <markus@unterwaditzer.net>
1 parent 2c0b5ec commit 7c94965

File tree

14 files changed

+461
-205
lines changed

14 files changed

+461
-205
lines changed

sentry_sdk/_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Optional
1313
from typing import Tuple
1414
from typing import Type
15+
1516
from typing_extensions import Literal
1617

1718
ExcInfo = Tuple[

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: 122 additions & 22 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,138 @@ 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
"""
453-
454-
client, scope = self._stack[-1]
454+
# XXX: if the only way to use start_span correctly is when there
455+
# is already an existing transaction/span in the scope, then
456+
# this is a hard to use API. We don't document nor support
457+
# appending an existing span to a new transaction created to
458+
# contain the span.
459+
460+
# TODO: consider removing this in a future release.
461+
# This is for backwards compatibility with releases before
462+
# start_transaction existed, to allow for a smoother transition.
463+
if isinstance(span, Transaction) or "transaction" in kwargs:
464+
deprecation_msg = (
465+
"Deprecated: use start_transaction to start transactions and "
466+
"Transaction.start_child to start spans."
467+
)
468+
if isinstance(span, Transaction):
469+
logger.warning(deprecation_msg)
470+
return self.start_transaction(span)
471+
if "transaction" in kwargs:
472+
logger.warning(deprecation_msg)
473+
name = kwargs.pop("transaction")
474+
return self.start_transaction(name=name, **kwargs)
475+
476+
# XXX: this is not very useful because
477+
#
478+
# with hub.start_span(Span(...)):
479+
# pass
480+
#
481+
# is equivalent to the simpler
482+
#
483+
# with Span(...):
484+
# pass
485+
if span is not None:
486+
return span
455487

456488
kwargs.setdefault("hub", self)
457489

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)
490+
span = self.scope.span
491+
if span is not None:
492+
return span.start_child(**kwargs)
493+
494+
# XXX: returning a detached span here means whatever span tree built
495+
# from it will be eventually discarded.
496+
#
497+
# This is also not very useful because
498+
#
499+
# with hub.start_span(op="..."):
500+
# pass
501+
#
502+
# is equivalent, when there is no span in the scope, to the simpler
503+
#
504+
# with Span(op="..."):
505+
# pass
506+
return Span(**kwargs)
507+
508+
def start_transaction(
509+
self,
510+
transaction=None, # type: Optional[Transaction]
511+
**kwargs # type: Any
512+
):
513+
# type: (...) -> Transaction
514+
"""
515+
Start and return a transaction.
516+
517+
Start an existing transaction if given, otherwise create and start a new
518+
transaction with kwargs.
519+
520+
This is the entry point to manual tracing instrumentation.
521+
522+
A tree structure can be built by adding child spans to the transaction,
523+
and child spans to other spans. To start a new child span within the
524+
transaction or any span, call the respective `.start_child()` method.
525+
526+
Every child span must be finished before the transaction is finished,
527+
otherwise the unfinished spans are discarded.
528+
529+
When used as context managers, spans and transactions are automatically
530+
finished at the end of the `with` block. If not using context managers,
531+
call the `.finish()` method.
532+
533+
When the transaction is finished, it will be sent to Sentry with all its
534+
finished child spans.
535+
"""
536+
# XXX: should we always set transaction.hub = self?
537+
# In a multi-hub program, what does it mean to write
538+
# hub1.start_transaction(Transaction(hub=hub2))
539+
# ? Should the transaction be on hub1 or hub2?
540+
541+
# XXX: it is strange that both start_span and start_transaction take
542+
# kwargs, but those are ignored if span/transaction are not None.
543+
# Code such as:
544+
# hub.start_transaction(Transaction(name="foo"), name="bar")
545+
#
546+
# is clearly weird, but not so weird if we intentionally want to rename
547+
# a transaction we got from somewhere else:
548+
# hub.start_transaction(transaction, name="new_name")
549+
#
550+
# Would be clearer if Hub was not involved:
551+
# transaction.name = "new_name"
552+
# with transaction: # __enter__ => start, __exit__ => finish
553+
# with transaction.start_child(...):
554+
# pass
555+
# # alternatively, rely on transaction being in the current scope
556+
# with Span(...):
557+
# pass
558+
559+
if transaction is None:
560+
kwargs.setdefault("hub", self)
561+
transaction = Transaction(**kwargs)
562+
563+
client, scope = self._stack[-1]
464564

465-
if span.sampled is None and span.transaction is not None:
565+
if transaction.sampled is None:
466566
sample_rate = client and client.options["traces_sample_rate"] or 0
467-
span.sampled = random.random() < sample_rate
567+
transaction.sampled = random.random() < sample_rate
468568

469-
if span.sampled:
569+
if transaction.sampled:
470570
max_spans = (
471571
client and client.options["_experiments"].get("max_spans") or 1000
472572
)
473-
span.init_finished_spans(maxlen=max_spans)
573+
transaction.init_span_recorder(maxlen=max_spans)
474574

475-
return span
575+
return transaction
476576

477577
@overload # noqa
478578
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:

0 commit comments

Comments
 (0)