88
99from sentry_sdk .utils import logger
1010from sentry_sdk .tracing_utils import (
11- SENTRY_TRACE_REGEX ,
1211 EnvironHeaders ,
12+ compute_tracestate_entry ,
13+ extract_sentrytrace_data ,
14+ extract_tracestate_data ,
1315 has_tracing_enabled ,
1416 is_valid_sample_rate ,
1517 maybe_create_breadcrumbs_from_span ,
@@ -210,11 +212,12 @@ def continue_from_environ(
210212 # type: (...) -> Transaction
211213 """
212214 Create a Transaction with the given params, then add in data pulled from
213- the 'sentry-trace' header in the environ (if any) before returning the
214- Transaction.
215+ the 'sentry-trace' and 'tracestate' headers from the environ (if any)
216+ before returning the Transaction.
215217
216- If the 'sentry-trace' header is malformed or missing, just create and
217- return a Transaction instance with the given params.
218+ This is different from `continue_from_headers` in that it assumes header
219+ names in the form "HTTP_HEADER_NAME" - such as you would get from a wsgi
220+ environ - rather than the form "header-name".
218221 """
219222 if cls is Span :
220223 logger .warning (
@@ -231,28 +234,30 @@ def continue_from_headers(
231234 ):
232235 # type: (...) -> Transaction
233236 """
234- Create a Transaction with the given params, then add in data pulled from
235- the 'sentry-trace' header (if any) before returning the Transaction.
236-
237- If the 'sentry-trace' header is malformed or missing, just create and
238- return a Transaction instance with the given params.
237+ Create a transaction with the given params (including any data pulled from
238+ the 'sentry-trace' and 'tracestate' headers).
239239 """
240+ # TODO move this to the Transaction class
240241 if cls is Span :
241242 logger .warning (
242243 "Deprecated: use Transaction.continue_from_headers "
243244 "instead of Span.continue_from_headers."
244245 )
245- transaction = Transaction . from_traceparent (
246- headers .get ("sentry-trace" ), ** kwargs
247- )
248- if transaction is None :
249- transaction = Transaction (** kwargs )
246+
247+ kwargs . update ( extract_sentrytrace_data ( headers .get ("sentry-trace" )))
248+ kwargs . update ( extract_tracestate_data ( headers . get ( "tracestate" )) )
249+
250+ transaction = Transaction (** kwargs )
250251 transaction .same_process_as_parent = False
252+
251253 return transaction
252254
253255 def iter_headers (self ):
254256 # type: () -> Generator[Tuple[str, str], None, None]
255257 yield "sentry-trace" , self .to_traceparent ()
258+ tracestate = self .to_tracestate ()
259+ if tracestate :
260+ yield "tracestate" , tracestate
256261
257262 @classmethod
258263 def from_traceparent (
@@ -262,46 +267,21 @@ def from_traceparent(
262267 ):
263268 # type: (...) -> Optional[Transaction]
264269 """
270+ DEPRECATED: Use Transaction.continue_from_headers(headers, **kwargs)
271+
265272 Create a Transaction with the given params, then add in data pulled from
266273 the given 'sentry-trace' header value before returning the Transaction.
267274
268- If the header value is malformed or missing, just create and return a
269- Transaction instance with the given params.
270275 """
271- if cls is Span :
272- logger .warning (
273- "Deprecated: use Transaction.from_traceparent "
274- "instead of Span.from_traceparent."
275- )
276+ logger .warning (
277+ "Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) "
278+ "instead of from_traceparent(traceparent, **kwargs)"
279+ )
276280
277281 if not traceparent :
278282 return None
279283
280- if traceparent .startswith ("00-" ) and traceparent .endswith ("-00" ):
281- traceparent = traceparent [3 :- 3 ]
282-
283- match = SENTRY_TRACE_REGEX .match (str (traceparent ))
284- if match is None :
285- return None
286-
287- trace_id , parent_span_id , sampled_str = match .groups ()
288-
289- if trace_id is not None :
290- trace_id = "{:032x}" .format (int (trace_id , 16 ))
291- if parent_span_id is not None :
292- parent_span_id = "{:016x}" .format (int (parent_span_id , 16 ))
293-
294- if sampled_str :
295- parent_sampled = sampled_str != "0" # type: Optional[bool]
296- else :
297- parent_sampled = None
298-
299- return Transaction (
300- trace_id = trace_id ,
301- parent_span_id = parent_span_id ,
302- parent_sampled = parent_sampled ,
303- ** kwargs
304- )
284+ return cls .continue_from_headers ({"sentry-trace" : traceparent }, ** kwargs )
305285
306286 def to_traceparent (self ):
307287 # type: () -> str
@@ -312,6 +292,34 @@ def to_traceparent(self):
312292 sampled = "0"
313293 return "%s-%s-%s" % (self .trace_id , self .span_id , sampled )
314294
295+ def to_tracestate (self ):
296+ # type: () -> Optional[str]
297+ """
298+ Generates the `tracestate` header value to attach to outgoing requests.
299+ """
300+ header_value = None
301+
302+ if isinstance (self , Transaction ):
303+ transaction = self # type: Optional[Transaction]
304+ else :
305+ transaction = self ._containing_transaction
306+
307+ # we should have the relevant values stored on the transaction, but if
308+ # this is an orphan span, make a new value
309+ if transaction :
310+ sentry_tracestate = transaction ._sentry_tracestate
311+ third_party_tracestate = transaction ._third_party_tracestate
312+ else :
313+ sentry_tracestate = compute_tracestate_entry (self )
314+ third_party_tracestate = None
315+
316+ header_value = sentry_tracestate
317+
318+ if third_party_tracestate :
319+ header_value = header_value + "," + third_party_tracestate
320+
321+ return header_value
322+
315323 def set_tag (self , key , value ):
316324 # type: (str, Any) -> None
317325 self ._tags [key ] = value
@@ -418,16 +426,36 @@ def get_trace_context(self):
418426 if self .status :
419427 rv ["status" ] = self .status
420428
429+ if isinstance (self , Transaction ):
430+ transaction = self # type: Optional[Transaction]
431+ else :
432+ transaction = self ._containing_transaction
433+
434+ if transaction :
435+ rv ["tracestate" ] = transaction ._sentry_tracestate
436+
421437 return rv
422438
423439
424440class Transaction (Span ):
425- __slots__ = ("name" , "parent_sampled" )
441+ __slots__ = (
442+ "name" ,
443+ "parent_sampled" ,
444+ # the sentry portion of the `tracestate` header used to transmit
445+ # correlation context for server-side dynamic sampling, of the form
446+ # `sentry=xxxxx`, where `xxxxx` is the base64-encoded json of the
447+ # correlation context data, missing trailing any =
448+ "_sentry_tracestate" ,
449+ # tracestate data from other vendors, of the form `dogs=yes,cats=maybe`
450+ "_third_party_tracestate" ,
451+ )
426452
427453 def __init__ (
428454 self ,
429455 name = "" , # type: str
430456 parent_sampled = None , # type: Optional[bool]
457+ sentry_tracestate = None , # type: Optional[str]
458+ third_party_tracestate = None , # type: Optional[str]
431459 ** kwargs # type: Any
432460 ):
433461 # type: (...) -> None
@@ -443,6 +471,8 @@ def __init__(
443471 Span .__init__ (self , ** kwargs )
444472 self .name = name
445473 self .parent_sampled = parent_sampled
474+ self ._sentry_tracestate = sentry_tracestate or compute_tracestate_entry (self )
475+ self ._third_party_tracestate = third_party_tracestate
446476
447477 def __repr__ (self ):
448478 # type: () -> str
0 commit comments