)
)
- def __enter__(self):
- # type: () -> Transaction
- super().__enter__()
+ def _possibly_started(self):
+ # type: () -> bool
+ """Returns whether the transaction might have been started.
- if self._profile is not None:
- self._profile.__enter__()
-
- return self
+ If this returns False, we know that the transaction was not started
+ with sentry_sdk.start_transaction, and therefore the transaction will
+ be discarded.
+ """
- def __exit__(self, ty, value, tb):
- # type: (Optional[Any], Optional[Any], Optional[Any]) -> None
- if self._profile is not None:
- self._profile.__exit__(ty, value, tb)
-
- super().__exit__(ty, value, tb)
-
- @property
- def containing_transaction(self):
- # type: () -> Transaction
- """The root element of the span tree.
- In the case of a transaction it is the transaction itself.
- """
+ # We must explicitly check self.sampled is False since self.sampled can be None
+ return self._span_recorder is not None or self.sampled is False
+
+ def __enter__(self):
+ # type: () -> Transaction
+ if not self._possibly_started():
+ logger.warning(
+ "Transaction was entered without being started with sentry_sdk.start_transaction."
+ "The transaction will not be sent to Sentry. To fix, start the transaction by"
+ "passing it to sentry_sdk.start_transaction."
+ )
+
+ super().__enter__()
- # Transactions (as spans) belong to themselves (as transactions). This
- # is a getter rather than a regular attribute to avoid having a circular
- # reference.
+ if self._profile is not None:
+ self._profile.__enter__()
+
return self
-
-
[docs]
-
def finish(self, hub=None, end_timestamp=None):
- # type: (Optional[Union[sentry_sdk.Hub, sentry_sdk.Scope]], Optional[Union[float, datetime]]) -> Optional[str]
- """Finishes the transaction and sends it to Sentry.
- All finished spans in the transaction will also be sent to Sentry.
+ def __exit__(self, ty, value, tb):
+ # type: (Optional[Any], Optional[Any], Optional[Any]) -> None
+ if self._profile is not None:
+ self._profile.__exit__(ty, value, tb)
- :param hub: The hub to use for this transaction.
- If not provided, the current hub will be used.
- :param end_timestamp: Optional timestamp that should
- be used as timestamp instead of the current time.
-
- :return: The event ID if the transaction was sent to Sentry,
- otherwise None.
+ super().__exit__(ty, value, tb)
+
+ @property
+ def containing_transaction(self):
+ # type: () -> Transaction
+ """The root element of the span tree.
+ In the case of a transaction it is the transaction itself.
"""
- if self.timestamp is not None:
- # This transaction is already finished, ignore.
- return None
-
- hub = hub or self.hub or sentry_sdk.Hub.current
- client = sentry_sdk.Scope.get_client()
-
- if not client.is_active():
- # We have no active client and therefore nowhere to send this transaction.
- return None
+
+ # Transactions (as spans) belong to themselves (as transactions). This
+ # is a getter rather than a regular attribute to avoid having a circular
+ # reference.
+ return self
+
+
+
[docs]
+
def finish(self, hub=None, end_timestamp=None):
+ # type: (Optional[Union[sentry_sdk.Hub, sentry_sdk.Scope]], Optional[Union[float, datetime]]) -> Optional[str]
+ """Finishes the transaction and sends it to Sentry.
+ All finished spans in the transaction will also be sent to Sentry.
- if self._span_recorder is None:
- # Explicit check against False needed because self.sampled might be None
- if self.sampled is False:
- logger.debug("Discarding transaction because sampled = False")
- else:
- logger.debug(
- "Discarding transaction because it was not started with sentry_sdk.start_transaction"
- )
-
- # This is not entirely accurate because discards here are not
- # exclusively based on sample rate but also traces sampler, but
- # we handle this the same here.
- if client.transport and has_tracing_enabled(client.options):
- if client.monitor and client.monitor.downsample_factor > 0:
- reason = "backpressure"
- else:
- reason = "sample_rate"
-
- client.transport.record_lost_event(reason, data_category="transaction")
-
- return None
-
- if not self.name:
- logger.warning(
- "Transaction has no name, falling back to `<unlabeled transaction>`."
- )
- self.name = "<unlabeled transaction>"
+ :param hub: The hub to use for this transaction.
+ If not provided, the current hub will be used.
+ :param end_timestamp: Optional timestamp that should
+ be used as timestamp instead of the current time.
+
+ :return: The event ID if the transaction was sent to Sentry,
+ otherwise None.
+ """
+ if self.timestamp is not None:
+ # This transaction is already finished, ignore.
+ return None
+
+ hub = hub or self.hub or sentry_sdk.Hub.current
+ client = sentry_sdk.Scope.get_client()
+
+ if not client.is_active():
+ # We have no active client and therefore nowhere to send this transaction.
+ return None
+
+ if self._span_recorder is None:
+ # Explicit check against False needed because self.sampled might be None
+ if self.sampled is False:
+ logger.debug("Discarding transaction because sampled = False")
+ else:
+ logger.debug(
+ "Discarding transaction because it was not started with sentry_sdk.start_transaction"
+ )
- super().finish(hub, end_timestamp)
-
- if not self.sampled:
- # At this point a `sampled = None` should have already been resolved
- # to a concrete decision.
- if self.sampled is None:
- logger.warning("Discarding transaction without sampling decision.")
-
- return None
-
- finished_spans = [
- span.to_json()
- for span in self._span_recorder.spans
- if span.timestamp is not None
- ]
-
- # we do this to break the circular reference of transaction -> span
- # recorder -> span -> containing transaction (which is where we started)
- # before either the spans or the transaction goes out of scope and has
- # to be garbage collected
- self._span_recorder = None
-
- contexts = {}
- contexts.update(self._contexts)
- contexts.update({"trace": self.get_trace_context()})
-
- event = {
- "type": "transaction",
- "transaction": self.name,
- "transaction_info": {"source": self.source},
- "contexts": contexts,
- "tags": self._tags,
- "timestamp": self.timestamp,
- "start_timestamp": self.start_timestamp,
- "spans": finished_spans,
- } # type: Event
-
- if self._profile is not None and self._profile.valid():
- event["profile"] = self._profile
- self._profile = None
+ # This is not entirely accurate because discards here are not
+ # exclusively based on sample rate but also traces sampler, but
+ # we handle this the same here.
+ if client.transport and has_tracing_enabled(client.options):
+ if client.monitor and client.monitor.downsample_factor > 0:
+ reason = "backpressure"
+ else:
+ reason = "sample_rate"
+
+ client.transport.record_lost_event(reason, data_category="transaction")
+
+ return None
+
+ if not self.name:
+ logger.warning(
+ "Transaction has no name, falling back to `<unlabeled transaction>`."
+ )
+ self.name = "<unlabeled transaction>"
+
+ super().finish(hub, end_timestamp)
+
+ if not self.sampled:
+ # At this point a `sampled = None` should have already been resolved
+ # to a concrete decision.
+ if self.sampled is None:
+ logger.warning("Discarding transaction without sampling decision.")
+
+ return None
+
+ finished_spans = [
+ span.to_json()
+ for span in self._span_recorder.spans
+ if span.timestamp is not None
+ ]
+
+ # we do this to break the circular reference of transaction -> span
+ # recorder -> span -> containing transaction (which is where we started)
+ # before either the spans or the transaction goes out of scope and has
+ # to be garbage collected
+ self._span_recorder = None
- event["measurements"] = self._measurements
-
- # This is here since `to_json` is not invoked. This really should
- # be gone when we switch to onlyspans.
- if self._local_aggregator is not None:
- metrics_summary = self._local_aggregator.to_json()
- if metrics_summary:
- event["_metrics_summary"] = metrics_summary
-
- return hub.capture_event(event)
-
-
- def set_measurement(self, name, value, unit=""):
- # type: (str, float, MeasurementUnit) -> None
- self._measurements[name] = {"value": value, "unit": unit}
+ contexts = {}
+ contexts.update(self._contexts)
+ contexts.update({"trace": self.get_trace_context()})
+
+ event = {
+ "type": "transaction",
+ "transaction": self.name,
+ "transaction_info": {"source": self.source},
+ "contexts": contexts,
+ "tags": self._tags,
+ "timestamp": self.timestamp,
+ "start_timestamp": self.start_timestamp,
+ "spans": finished_spans,
+ } # type: Event
+ if self._profile is not None and self._profile.valid():
+ event["profile"] = self._profile
+ self._profile = None
+
+ event["measurements"] = self._measurements
+
+ # This is here since `to_json` is not invoked. This really should
+ # be gone when we switch to onlyspans.
+ if self._local_aggregator is not None:
+ metrics_summary = self._local_aggregator.to_json()
+ if metrics_summary:
+ event["_metrics_summary"] = metrics_summary
+
+ return hub.capture_event(event)
+
+
+ def set_measurement(self, name, value, unit=""):
+ # type: (str, float, MeasurementUnit) -> None
+ self._measurements[name] = {"value": value, "unit": unit}
+
[docs]
-
def set_context(self, key, value):
- # type: (str, Any) -> None
- """Sets a context. Transactions can have multiple contexts
- and they should follow the format described in the "Contexts Interface"
- documentation.
-
- :param key: The name of the context.
- :param value: The information about the context.
- """
- self._contexts[key] = value
+ def set_context(self, key, value):
+ # type: (str, Any) -> None
+ """Sets a context. Transactions can have multiple contexts
+ and they should follow the format described in the "Contexts Interface"
+ documentation.
+
+ :param key: The name of the context.
+ :param value: The information about the context.
+ """
+ self._contexts[key] = value
-