From 0ce05237fd03325372e8c23dc017702b14305d1c Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 3 Jul 2024 18:06:31 +0200 Subject: [PATCH] feat(tracing): Record lost spans in client reports Resolves #3229 --- sentry_sdk/_types.py | 1 + sentry_sdk/client.py | 22 +++++++++++--------- sentry_sdk/tracing.py | 5 ++--- sentry_sdk/transport.py | 45 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index bd229977a5..14fa8d08c2 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -155,6 +155,7 @@ "profile_chunk", "metric_bucket", "monitor", + "span", ] SessionStatus = Literal["ok", "exited", "crashed", "abnormal"] diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 07cd39029b..235b495453 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -61,12 +61,11 @@ from sentry_sdk.session import Session from sentry_sdk.transport import Transport - _client_init_debug = ContextVar("client_init_debug") - SDK_INFO = { - "name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() + "name": "sentry.python", + # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations() "version": VERSION, "packages": [{"name": "pypi:sentry-sdk", "version": VERSION}], } @@ -453,10 +452,15 @@ def _prepare_event( # one of the event/error processors returned None if event_ is None: if self.transport: - self.transport.record_lost_event( - "event_processor", - data_category=("transaction" if is_transaction else "error"), - ) + if is_transaction: + self.transport.record_lost_transaction( + "event_processor", len(event.get("spans", [])) + ) + else: + self.transport.record_lost_event( + "event_processor", + data_category="error", + ) return None event = event_ @@ -546,8 +550,8 @@ def _prepare_event( if new_event is None: logger.info("before send transaction dropped event") if self.transport: - self.transport.record_lost_event( - "before_send", data_category="transaction" + self.transport.record_lost_transaction( + "before_send", len(event.get("spans", [])) ) event = new_event # type: ignore diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 96ef81496f..89acb5b5d9 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -119,11 +119,9 @@ class TransactionKwargs(SpanKwargs, total=False): }, ) - BAGGAGE_HEADER_NAME = "baggage" SENTRY_TRACE_HEADER_NAME = "sentry-trace" - # Transaction source # see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations TRANSACTION_SOURCE_CUSTOM = "custom" @@ -856,7 +854,8 @@ def finish(self, hub=None, end_timestamp=None): else: reason = "sample_rate" - client.transport.record_lost_event(reason, data_category="transaction") + # No spans are discarded here because they were never recorded in the first place + client.transport.record_lost_transaction(reason, span_count=0) return None diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 293dfc0e97..0856361956 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -137,6 +137,22 @@ def record_lost_event( # type: (...) -> None """This increments a counter for event loss by reason and data category. + + data_category="transaction" should use record_lost_transaction, + instead. + """ + return None + + def record_lost_transaction( + self, + reason, # type: str + span_count, # type: int + ): + # type: (...) -> None + """This increments a counter for loss of a transaction and the transaction's spans. + + The span_count should only include the number of spans contained in the transaction, EXCLUDING the transaction + itself. """ return None @@ -229,10 +245,24 @@ def record_lost_event( if not self.options["send_client_reports"]: return + if data_category == "transaction": + warnings.warn( + "Use record_lost_transaction for transactions to ensure lost spans are counted.", + stacklevel=2, + ) + quantity = 1 if item is not None: data_category = item.data_category - if data_category == "attachment": + + if data_category == "transaction": + # Handle transactions through record_lost_transaction. + event = item.get_event() or {} + span_count = len(event.get("spans") or []) + self.record_lost_transaction(reason, span_count) + return + + elif data_category == "attachment": # quantity of 0 is actually 1 as we do not want to count # empty attachments as actually empty. quantity = len(item.get_bytes()) or 1 @@ -242,6 +272,19 @@ def record_lost_event( self._discarded_events[data_category, reason] += quantity + def record_lost_transaction( + self, + reason, # type: str + span_count, # type: int + ): # type: (...) -> None + if not self.options["send_client_reports"]: + return + + self._discarded_events["transaction", reason] += 1 + self._discarded_events["span", reason] += ( + span_count + 1 + ) # Also count the transaction itself + def _update_rate_limits(self, response): # type: (urllib3.BaseHTTPResponse) -> None