diff --git a/docs/apidocs.rst b/docs/apidocs.rst
index dc4117e559..855778484d 100644
--- a/docs/apidocs.rst
+++ b/docs/apidocs.rst
@@ -11,6 +11,9 @@ API Docs
.. autoclass:: sentry_sdk.Client
:members:
+.. autoclass:: sentry_sdk.client._Client
+ :members:
+
.. autoclass:: sentry_sdk.Transport
:members:
diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py
index f0c6a87432..ffa525ca66 100644
--- a/sentry_sdk/api.py
+++ b/sentry_sdk/api.py
@@ -82,10 +82,10 @@ def capture_event(
event, # type: Event
hint=None, # type: Optional[Hint]
scope=None, # type: Optional[Any]
- **scope_args # type: Any
+ **scope_kwargs # type: Any
):
# type: (...) -> Optional[str]
- return Hub.current.capture_event(event, hint, scope=scope, **scope_args)
+ return Hub.current.capture_event(event, hint, scope=scope, **scope_kwargs)
@hubmethod
@@ -93,20 +93,20 @@ def capture_message(
message, # type: str
level=None, # type: Optional[str]
scope=None, # type: Optional[Any]
- **scope_args # type: Any
+ **scope_kwargs # type: Any
):
# type: (...) -> Optional[str]
- return Hub.current.capture_message(message, level, scope=scope, **scope_args)
+ return Hub.current.capture_message(message, level, scope=scope, **scope_kwargs)
@hubmethod
def capture_exception(
error=None, # type: Optional[Union[BaseException, ExcInfo]]
scope=None, # type: Optional[Any]
- **scope_args # type: Any
+ **scope_kwargs # type: Any
):
# type: (...) -> Optional[str]
- return Hub.current.capture_exception(error, scope=scope, **scope_args)
+ return Hub.current.capture_exception(error, scope=scope, **scope_kwargs)
@hubmethod
diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py
index aeaa8fa518..70ffdbe2aa 100644
--- a/sentry_sdk/client.py
+++ b/sentry_sdk/client.py
@@ -43,7 +43,10 @@
from typing import Dict
from typing import Optional
from typing import Sequence
+ from typing import Type
+ from typing import Union
+ from sentry_sdk.integrations import Integration
from sentry_sdk.scope import Scope
from sentry_sdk._types import Event, Hint
from sentry_sdk.session import Session
@@ -153,6 +156,8 @@ class _Client(object):
forwarding them to sentry through the configured transport. It takes
the client options as keyword arguments and optionally the DSN as first
argument.
+
+ Alias of :py:class:`Client`. (Was created for better intelisense support)
"""
def __init__(self, *args, **kwargs):
@@ -557,8 +562,8 @@ def capture_event(
:param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object.
- :param scope: An optional scope to use for determining whether this event
- should be captured.
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
:returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help.
"""
@@ -661,6 +666,22 @@ def capture_session(
else:
self.session_flusher.add_session(session)
+ def get_integration(
+ self, name_or_class # type: Union[str, Type[Integration]]
+ ):
+ # type: (...) -> Any
+ """Returns the integration for this client by name or class.
+ If the client does not have that integration then `None` is returned.
+ """
+ if isinstance(name_or_class, str):
+ integration_name = name_or_class
+ elif name_or_class.identifier is not None:
+ integration_name = name_or_class.identifier
+ else:
+ raise ValueError("Integration has no name")
+
+ return self.integrations.get(integration_name)
+
def close(
self,
timeout=None, # type: Optional[float]
diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py
index 2525dc56f1..45afb56cc9 100644
--- a/sentry_sdk/hub.py
+++ b/sentry_sdk/hub.py
@@ -3,27 +3,17 @@
from contextlib import contextmanager
-from sentry_sdk._compat import datetime_utcnow, with_metaclass
+from sentry_sdk._compat import with_metaclass
from sentry_sdk.consts import INSTRUMENTER
from sentry_sdk.scope import Scope
from sentry_sdk.client import Client
-from sentry_sdk.profiler import Profile
from sentry_sdk.tracing import (
NoOpSpan,
Span,
Transaction,
- BAGGAGE_HEADER_NAME,
- SENTRY_TRACE_HEADER_NAME,
-)
-from sentry_sdk.session import Session
-from sentry_sdk.tracing_utils import (
- has_tracing_enabled,
- normalize_incoming_data,
)
from sentry_sdk.utils import (
- exc_info_from_error,
- event_from_exception,
logger,
ContextVar,
)
@@ -31,18 +21,18 @@
from sentry_sdk._types import TYPE_CHECKING
if TYPE_CHECKING:
- from typing import Union
from typing import Any
- from typing import Optional
- from typing import Tuple
- from typing import Dict
- from typing import List
from typing import Callable
+ from typing import ContextManager
+ from typing import Dict
from typing import Generator
+ from typing import List
+ from typing import Optional
+ from typing import overload
+ from typing import Tuple
from typing import Type
from typing import TypeVar
- from typing import overload
- from typing import ContextManager
+ from typing import Union
from sentry_sdk.integrations import Integration
from sentry_sdk._types import (
@@ -66,24 +56,6 @@ def overload(x):
_local = ContextVar("sentry_current_hub")
-def _update_scope(base, scope_change, scope_kwargs):
- # type: (Scope, Optional[Any], Dict[str, Any]) -> Scope
- if scope_change and scope_kwargs:
- raise TypeError("cannot provide scope and kwargs")
- if scope_change is not None:
- final_scope = copy.copy(base)
- if callable(scope_change):
- scope_change(final_scope)
- else:
- final_scope.update_from_scope(scope_change)
- elif scope_kwargs:
- final_scope = copy.copy(base)
- final_scope.update_from_kwargs(**scope_kwargs)
- else:
- final_scope = base
- return final_scope
-
-
def _should_send_default_pii():
# type: () -> bool
client = Hub.current.client
@@ -294,18 +266,9 @@ def get_integration(
If the return value is not `None` the hub is guaranteed to have a
client attached.
"""
- if isinstance(name_or_class, str):
- integration_name = name_or_class
- elif name_or_class.identifier is not None:
- integration_name = name_or_class.identifier
- else:
- raise ValueError("Integration has no name")
-
client = self.client
if client is not None:
- rv = client.integrations.get(integration_name)
- if rv is not None:
- return rv
+ return client.get_integration(name_or_class)
@property
def client(self):
@@ -332,76 +295,100 @@ def bind_client(
top = self._stack[-1]
self._stack[-1] = (new, top[1])
- def capture_event(self, event, hint=None, scope=None, **scope_args):
+ def capture_event(self, event, hint=None, scope=None, **scope_kwargs):
# type: (Event, Optional[Hint], Optional[Scope], Any) -> Optional[str]
"""
Captures an event.
- Alias of :py:meth:`sentry_sdk.Client.capture_event`.
+ Alias of :py:meth:`sentry_sdk.Scope.capture_event`.
+
+ :param event: A ready-made event that can be directly sent to Sentry.
+
+ :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object.
- :param scope_args: For supported `**scope_args` see
- :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
+
+ :param scope_kwargs: Optional data to apply to event.
+ For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
"""
client, top_scope = self._stack[-1]
- scope = _update_scope(top_scope, scope, scope_args)
- if client is not None:
- is_transaction = event.get("type") == "transaction"
- rv = client.capture_event(event, hint, scope)
- if rv is not None and not is_transaction:
- self._last_event_id = rv
- return rv
- return None
+ if client is None:
+ return None
- def capture_message(self, message, level=None, scope=None, **scope_args):
+ last_event_id = top_scope.capture_event(
+ event, hint, client=client, scope=scope, **scope_kwargs
+ )
+
+ is_transaction = event.get("type") == "transaction"
+ if last_event_id is not None and not is_transaction:
+ self._last_event_id = last_event_id
+
+ return last_event_id
+
+ def capture_message(self, message, level=None, scope=None, **scope_kwargs):
# type: (str, Optional[str], Optional[Scope], Any) -> Optional[str]
"""
Captures a message.
- :param message: The string to send as the message.
+ Alias of :py:meth:`sentry_sdk.Scope.capture_message`.
+
+ :param message: The string to send as the message to Sentry.
:param level: If no level is provided, the default level is `info`.
- :param scope: An optional :py:class:`sentry_sdk.Scope` to use.
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
- :param scope_args: For supported `**scope_args` see
- :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ :param scope_kwargs: Optional data to apply to event.
+ For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
:returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
"""
- if self.client is None:
+ client, top_scope = self._stack[-1]
+ if client is None:
return None
- if level is None:
- level = "info"
- return self.capture_event(
- {"message": message, "level": level}, scope=scope, **scope_args
+
+ last_event_id = top_scope.capture_message(
+ message, level=level, client=client, scope=scope, **scope_kwargs
)
- def capture_exception(self, error=None, scope=None, **scope_args):
+ if last_event_id is not None:
+ self._last_event_id = last_event_id
+
+ return last_event_id
+
+ def capture_exception(self, error=None, scope=None, **scope_kwargs):
# type: (Optional[Union[BaseException, ExcInfo]], Optional[Scope], Any) -> Optional[str]
"""Captures an exception.
- :param error: An exception to catch. If `None`, `sys.exc_info()` will be used.
+ Alias of :py:meth:`sentry_sdk.Scope.capture_exception`.
+
+ :param error: An exception to capture. If `None`, `sys.exc_info()` will be used.
+
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
- :param scope_args: For supported `**scope_args` see
- :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ :param scope_kwargs: Optional data to apply to event.
+ For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
:returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
"""
- client = self.client
+ client, top_scope = self._stack[-1]
if client is None:
return None
- if error is not None:
- exc_info = exc_info_from_error(error)
- else:
- exc_info = sys.exc_info()
- event, hint = event_from_exception(exc_info, client_options=client.options)
- try:
- return self.capture_event(event, hint=hint, scope=scope, **scope_args)
- except Exception:
- self._capture_internal_exception(sys.exc_info())
+ last_event_id = top_scope.capture_exception(
+ error, client=client, scope=scope, **scope_kwargs
+ )
- return None
+ if last_event_id is not None:
+ self._last_event_id = last_event_id
+
+ return last_event_id
def _capture_internal_exception(
self, exc_info # type: Any
@@ -411,6 +398,8 @@ def _capture_internal_exception(
Capture an exception that is likely caused by a bug in the SDK
itself.
+ Duplicated in :py:meth:`sentry_sdk.Client._capture_internal_exception`.
+
These exceptions do not end up in Sentry and are just logged instead.
"""
logger.error("Internal error in sentry_sdk", exc_info=exc_info)
@@ -430,31 +419,9 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs):
logger.info("Dropped breadcrumb because no client bound")
return
- crumb = dict(crumb or ()) # type: Breadcrumb
- crumb.update(kwargs)
- if not crumb:
- return
-
- hint = dict(hint or ()) # type: Hint
-
- if crumb.get("timestamp") is None:
- crumb["timestamp"] = datetime_utcnow()
- if crumb.get("type") is None:
- crumb["type"] = "default"
+ kwargs["client"] = client
- if client.options["before_breadcrumb"] is not None:
- new_crumb = client.options["before_breadcrumb"](crumb, hint)
- else:
- new_crumb = crumb
-
- if new_crumb is not None:
- scope._breadcrumbs.append(new_crumb)
- else:
- logger.info("before breadcrumb dropped breadcrumb (%s)", crumb)
-
- max_breadcrumbs = client.options["max_breadcrumbs"] # type: int
- while len(scope._breadcrumbs) > max_breadcrumbs:
- scope._breadcrumbs.popleft()
+ scope.add_breadcrumb(crumb, hint, **kwargs)
def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
# type: (Optional[Span], str, Any) -> Span
@@ -473,54 +440,12 @@ def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`.
"""
- configuration_instrumenter = self.client and self.client.options["instrumenter"]
-
- if instrumenter != configuration_instrumenter:
- return NoOpSpan()
-
- # THIS BLOCK IS DEPRECATED
- # TODO: consider removing this in a future release.
- # This is for backwards compatibility with releases before
- # start_transaction existed, to allow for a smoother transition.
- if isinstance(span, Transaction) or "transaction" in kwargs:
- deprecation_msg = (
- "Deprecated: use start_transaction to start transactions and "
- "Transaction.start_child to start spans."
- )
-
- if isinstance(span, Transaction):
- logger.warning(deprecation_msg)
- return self.start_transaction(span)
-
- if "transaction" in kwargs:
- logger.warning(deprecation_msg)
- name = kwargs.pop("transaction")
- return self.start_transaction(name=name, **kwargs)
-
- # THIS BLOCK IS DEPRECATED
- # We do not pass a span into start_span in our code base, so I deprecate this.
- if span is not None:
- deprecation_msg = "Deprecated: passing a span into `start_span` is deprecated and will be removed in the future."
- logger.warning(deprecation_msg)
- return span
-
- kwargs.setdefault("hub", self)
-
- active_span = self.scope.span
- if active_span is not None:
- new_child_span = active_span.start_child(**kwargs)
- return new_child_span
+ client, scope = self._stack[-1]
- # If there is already a trace_id in the propagation context, use it.
- # This does not need to be done for `start_child` above because it takes
- # the trace_id from the parent span.
- if "trace_id" not in kwargs:
- traceparent = self.get_traceparent()
- trace_id = traceparent.split("-")[0] if traceparent else None
- if trace_id is not None:
- kwargs["trace_id"] = trace_id
+ kwargs["hub"] = self
+ kwargs["client"] = client
- return Span(**kwargs)
+ return scope.start_span(span=span, instrumenter=instrumenter, **kwargs)
def start_transaction(
self, transaction=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs
@@ -550,55 +475,25 @@ def start_transaction(
For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`.
"""
- configuration_instrumenter = self.client and self.client.options["instrumenter"]
-
- if instrumenter != configuration_instrumenter:
- return NoOpSpan()
-
- custom_sampling_context = kwargs.pop("custom_sampling_context", {})
-
- # if we haven't been given a transaction, make one
- if transaction is None:
- kwargs.setdefault("hub", self)
- transaction = Transaction(**kwargs)
-
- # use traces_sample_rate, traces_sampler, and/or inheritance to make a
- # sampling decision
- sampling_context = {
- "transaction_context": transaction.to_json(),
- "parent_sampled": transaction.parent_sampled,
- }
- sampling_context.update(custom_sampling_context)
- transaction._set_initial_sampling_decision(sampling_context=sampling_context)
-
- profile = Profile(transaction, hub=self)
- profile._set_initial_sampling_decision(sampling_context=sampling_context)
+ client, scope = self._stack[-1]
- # we don't bother to keep spans if we already know we're not going to
- # send the transaction
- if transaction.sampled:
- max_spans = (
- self.client and self.client.options["_experiments"].get("max_spans")
- ) or 1000
- transaction.init_span_recorder(maxlen=max_spans)
+ kwargs["hub"] = self
+ kwargs["client"] = client
- return transaction
+ return scope.start_transaction(
+ transaction=transaction, instrumenter=instrumenter, **kwargs
+ )
def continue_trace(self, environ_or_headers, op=None, name=None, source=None):
# type: (Dict[str, Any], Optional[str], Optional[str], Optional[str]) -> Transaction
"""
Sets the propagation context from environment or headers and returns a transaction.
"""
- with self.configure_scope() as scope:
- scope.generate_propagation_context(environ_or_headers)
+ scope = self._stack[-1][1]
- transaction = Transaction.continue_from_headers(
- normalize_incoming_data(environ_or_headers),
- op=op,
- name=name,
- source=source,
+ return scope.continue_trace(
+ environ_or_headers=environ_or_headers, op=op, name=name, source=source
)
- return transaction
@overload
def push_scope(
@@ -712,12 +607,9 @@ def start_session(
):
# type: (...) -> None
"""Starts a new session."""
- self.end_session()
client, scope = self._stack[-1]
- scope._session = Session(
- release=client.options["release"] if client else None,
- environment=client.options["environment"] if client else None,
- user=scope._user,
+ scope.start_session(
+ client=client,
session_mode=session_mode,
)
@@ -725,13 +617,7 @@ def end_session(self):
# type: (...) -> None
"""Ends the current session if there is one."""
client, scope = self._stack[-1]
- session = scope._session
- self.scope._session = None
-
- if session is not None:
- session.close()
- if client is not None:
- client.capture_session(session)
+ scope.end_session(client=client)
def stop_auto_session_tracking(self):
# type: (...) -> None
@@ -740,9 +626,8 @@ def stop_auto_session_tracking(self):
This temporarily session tracking for the current scope when called.
To resume session tracking call `resume_auto_session_tracking`.
"""
- self.end_session()
client, scope = self._stack[-1]
- scope._force_auto_session_tracking = False
+ scope.stop_auto_session_tracking(client=client)
def resume_auto_session_tracking(self):
# type: (...) -> None
@@ -750,8 +635,8 @@ def resume_auto_session_tracking(self):
disabled earlier. This requires that generally automatic session
tracking is enabled.
"""
- client, scope = self._stack[-1]
- scope._force_auto_session_tracking = None
+ scope = self._stack[-1][1]
+ scope.resume_auto_session_tracking()
def flush(
self,
@@ -771,25 +656,16 @@ def get_traceparent(self):
"""
Returns the traceparent either from the active span or from the scope.
"""
- if self.client is not None:
- if has_tracing_enabled(self.client.options) and self.scope.span is not None:
- return self.scope.span.to_traceparent()
-
- return self.scope.get_traceparent()
+ client, scope = self._stack[-1]
+ return scope.get_traceparent(client=client)
def get_baggage(self):
# type: () -> Optional[str]
"""
Returns Baggage either from the active span or from the scope.
"""
- if (
- self.client is not None
- and has_tracing_enabled(self.client.options)
- and self.scope.span is not None
- ):
- baggage = self.scope.span.to_baggage()
- else:
- baggage = self.scope.get_baggage()
+ client, scope = self._stack[-1]
+ baggage = scope.get_baggage(client=client)
if baggage is not None:
return baggage.serialize()
@@ -803,19 +679,9 @@ def iter_trace_propagation_headers(self, span=None):
from the span representing the request, if available, or the current
span on the scope if not.
"""
- client = self._stack[-1][0]
- propagate_traces = client and client.options["propagate_traces"]
- if not propagate_traces:
- return
-
- span = span or self.scope.span
+ client, scope = self._stack[-1]
- if client and has_tracing_enabled(client.options) and span is not None:
- for header in span.iter_headers():
- yield header
- else:
- for header in self.scope.iter_headers():
- yield header
+ return scope.iter_trace_propagation_headers(span=span, client=client)
def trace_propagation_meta(self, span=None):
# type: (Optional[Span]) -> str
@@ -828,23 +694,8 @@ def trace_propagation_meta(self, span=None):
"The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future."
)
- meta = ""
-
- sentry_trace = self.get_traceparent()
- if sentry_trace is not None:
- meta += '' % (
- SENTRY_TRACE_HEADER_NAME,
- sentry_trace,
- )
-
- baggage = self.get_baggage()
- if baggage is not None:
- meta += '' % (
- BAGGAGE_HEADER_NAME,
- baggage,
- )
-
- return meta
+ client, scope = self._stack[-1]
+ return scope.trace_propagation_meta(span=span, client=client)
GLOBAL_HUB = Hub()
diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py
index 5096eccce0..424e2bbb7d 100644
--- a/sentry_sdk/scope.py
+++ b/sentry_sdk/scope.py
@@ -2,10 +2,15 @@
from collections import deque
from itertools import chain
import os
+import sys
import uuid
from sentry_sdk.attachments import Attachment
+from sentry_sdk._compat import datetime_utcnow
+from sentry_sdk.consts import FALSE_VALUES, INSTRUMENTER
from sentry_sdk._functools import wraps
+from sentry_sdk.profiler import Profile
+from sentry_sdk.session import Session
from sentry_sdk.tracing_utils import (
Baggage,
extract_sentrytrace_data,
@@ -15,38 +20,43 @@
from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
SENTRY_TRACE_HEADER_NAME,
+ NoOpSpan,
+ Span,
Transaction,
)
from sentry_sdk._types import TYPE_CHECKING
-from sentry_sdk.utils import logger, capture_internal_exceptions
-
-from sentry_sdk.consts import FALSE_VALUES
-
+from sentry_sdk.utils import (
+ event_from_exception,
+ exc_info_from_error,
+ logger,
+ capture_internal_exceptions,
+)
if TYPE_CHECKING:
from typing import Any
+ from typing import Callable
+ from typing import Deque
from typing import Dict
+ from typing import Generator
from typing import Iterator
- from typing import Optional
- from typing import Deque
from typing import List
- from typing import Callable
+ from typing import Optional
from typing import Tuple
from typing import TypeVar
+ from typing import Union
from sentry_sdk._types import (
Breadcrumb,
+ BreadcrumbHint,
+ ErrorProcessor,
Event,
EventProcessor,
- ErrorProcessor,
ExcInfo,
Hint,
Type,
)
- from sentry_sdk.profiler import Profile
- from sentry_sdk.tracing import Span
- from sentry_sdk.session import Session
+ import sentry_sdk
F = TypeVar("F", bound=Callable[..., Any])
T = TypeVar("T")
@@ -81,6 +91,28 @@ def wrapper(self, *args, **kwargs):
return wrapper # type: ignore
+def _merge_scopes(base, scope_change, scope_kwargs):
+ # type: (Scope, Optional[Any], Dict[str, Any]) -> Scope
+ if scope_change and scope_kwargs:
+ raise TypeError("cannot provide scope and kwargs")
+
+ if scope_change is not None:
+ final_scope = copy(base)
+ if callable(scope_change):
+ scope_change(final_scope)
+ else:
+ final_scope.update_from_scope(scope_change)
+
+ elif scope_kwargs:
+ final_scope = copy(base)
+ final_scope.update_from_kwargs(**scope_kwargs)
+
+ else:
+ final_scope = base
+
+ return final_scope
+
+
class Scope(object):
"""The scope holds extra information that should be sent with all
events that belong to it.
@@ -244,11 +276,22 @@ def get_dynamic_sampling_context(self):
return self._propagation_context["dynamic_sampling_context"]
- def get_traceparent(self):
- # type: () -> Optional[str]
+ def get_traceparent(self, *args, **kwargs):
+ # type: (Any, Any) -> Optional[str]
"""
- Returns the Sentry "sentry-trace" header (aka the traceparent) from the Propagation Context.
+ Returns the Sentry "sentry-trace" header (aka the traceparent) from the
+ currently active span or the scopes Propagation Context.
"""
+ client = kwargs.pop("client", None)
+
+ # If we have an active span, return traceparent from there
+ if (
+ client is not None
+ and has_tracing_enabled(client.options)
+ and self.span is not None
+ ):
+ return self.span.to_traceparent()
+
if self._propagation_context is None:
return None
@@ -258,8 +301,18 @@ def get_traceparent(self):
)
return traceparent
- def get_baggage(self):
- # type: () -> Optional[Baggage]
+ def get_baggage(self, *args, **kwargs):
+ # type: (Any, Any) -> Optional[Baggage]
+ client = kwargs.pop("client", None)
+
+ # If we have an active span, return baggage from there
+ if (
+ client is not None
+ and has_tracing_enabled(client.options)
+ and self.span is not None
+ ):
+ return self.span.to_baggage()
+
if self._propagation_context is None:
return None
@@ -288,6 +341,38 @@ def get_trace_context(self):
return trace_context
+ def trace_propagation_meta(self, *args, **kwargs):
+ # type: (*Any, **Any) -> str
+ """
+ Return meta tags which should be injected into HTML templates
+ to allow propagation of trace information.
+ """
+ span = kwargs.pop("span", None)
+ if span is not None:
+ logger.warning(
+ "The parameter `span` in trace_propagation_meta() is deprecated and will be removed in the future."
+ )
+
+ client = kwargs.pop("client", None)
+
+ meta = ""
+
+ sentry_trace = self.get_traceparent(client=client)
+ if sentry_trace is not None:
+ meta += '' % (
+ SENTRY_TRACE_HEADER_NAME,
+ sentry_trace,
+ )
+
+ baggage = self.get_baggage(client=client)
+ if baggage is not None:
+ meta += '' % (
+ BAGGAGE_HEADER_NAME,
+ baggage.serialize(),
+ )
+
+ return meta
+
def iter_headers(self):
# type: () -> Iterator[Tuple[str, str]]
"""
@@ -303,6 +388,29 @@ def iter_headers(self):
baggage = Baggage(dsc).serialize()
yield BAGGAGE_HEADER_NAME, baggage
+ def iter_trace_propagation_headers(self, *args, **kwargs):
+ # type: (Any, Any) -> Generator[Tuple[str, str], None, None]
+ """
+ Return HTTP headers which allow propagation of trace data. Data taken
+ from the span representing the request, if available, or the current
+ span on the scope if not.
+ """
+ span = kwargs.pop("span", None)
+ client = kwargs.pop("client", None)
+
+ propagate_traces = client and client.options["propagate_traces"]
+ if not propagate_traces:
+ return
+
+ span = span or self.span
+
+ if client and has_tracing_enabled(client.options) and span is not None:
+ for header in span.iter_headers():
+ yield header
+ else:
+ for header in self.iter_headers():
+ yield header
+
def clear(self):
# type: () -> None
"""Clears the entire scope."""
@@ -517,6 +625,359 @@ def add_attachment(
)
)
+ def add_breadcrumb(self, crumb=None, hint=None, **kwargs):
+ # type: (Optional[Breadcrumb], Optional[BreadcrumbHint], Any) -> None
+ """
+ Adds a breadcrumb.
+
+ :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects.
+
+ :param hint: An optional value that can be used by `before_breadcrumb`
+ to customize the breadcrumbs that are emitted.
+ """
+ client = kwargs.pop("client", None)
+ if client is None:
+ return
+
+ before_breadcrumb = client.options.get("before_breadcrumb")
+ max_breadcrumbs = client.options.get("max_breadcrumbs")
+
+ crumb = dict(crumb or ()) # type: Breadcrumb
+ crumb.update(kwargs)
+ if not crumb:
+ return
+
+ hint = dict(hint or ()) # type: Hint
+
+ if crumb.get("timestamp") is None:
+ crumb["timestamp"] = datetime_utcnow()
+ if crumb.get("type") is None:
+ crumb["type"] = "default"
+
+ if before_breadcrumb is not None:
+ new_crumb = before_breadcrumb(crumb, hint)
+ else:
+ new_crumb = crumb
+
+ if new_crumb is not None:
+ self._breadcrumbs.append(new_crumb)
+ else:
+ logger.info("before breadcrumb dropped breadcrumb (%s)", crumb)
+
+ while len(self._breadcrumbs) > max_breadcrumbs:
+ self._breadcrumbs.popleft()
+
+ def capture_event(self, event, hint=None, client=None, scope=None, **scope_kwargs):
+ # type: (Event, Optional[Hint], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str]
+ """
+ Captures an event.
+
+ Merges given scope data and calls :py:meth:`sentry_sdk.Client.capture_event`.
+
+ :param event: A ready-made event that can be directly sent to Sentry.
+
+ :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object.
+
+ :param client: The client to use for sending the event to Sentry.
+
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
+
+ :param scope_kwargs: Optional data to apply to event.
+ For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
+
+ :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
+ """
+ if client is None:
+ return None
+
+ if scope_kwargs is not None:
+ scope = _merge_scopes(self, scope, scope_kwargs)
+
+ return client.capture_event(event=event, hint=hint, scope=scope)
+
+ def capture_message(
+ self, message, level=None, client=None, scope=None, **scope_kwargs
+ ):
+ # type: (str, Optional[str], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str]
+ """
+ Captures a message.
+
+ :param message: The string to send as the message.
+
+ :param level: If no level is provided, the default level is `info`.
+
+ :param client: The client to use for sending the event to Sentry.
+
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
+
+ :param scope_kwargs: Optional data to apply to event.
+ For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
+
+ :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
+ """
+ if client is None:
+ return None
+
+ if level is None:
+ level = "info"
+
+ event = {
+ "message": message,
+ "level": level,
+ }
+
+ return self.capture_event(event, client=client, scope=scope, **scope_kwargs)
+
+ def capture_exception(self, error=None, client=None, scope=None, **scope_kwargs):
+ # type: (Optional[Union[BaseException, ExcInfo]], Optional[sentry_sdk.Client], Optional[Scope], Any) -> Optional[str]
+ """Captures an exception.
+
+ :param error: An exception to capture. If `None`, `sys.exc_info()` will be used.
+
+ :param client: The client to use for sending the event to Sentry.
+
+ :param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
+
+ :param scope_kwargs: Optional data to apply to event.
+ For supported `**scope_kwargs` see :py:meth:`sentry_sdk.Scope.update_from_kwargs`.
+ The `scope` and `scope_kwargs` parameters are mutually exclusive.
+
+ :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
+ """
+ if client is None:
+ return None
+
+ if error is not None:
+ exc_info = exc_info_from_error(error)
+ else:
+ exc_info = sys.exc_info()
+
+ event, hint = event_from_exception(exc_info, client_options=client.options)
+
+ try:
+ return self.capture_event(
+ event, hint=hint, client=client, scope=scope, **scope_kwargs
+ )
+ except Exception:
+ self._capture_internal_exception(sys.exc_info())
+
+ return None
+
+ def _capture_internal_exception(
+ self, exc_info # type: Any
+ ):
+ # type: (...) -> Any
+ """
+ Capture an exception that is likely caused by a bug in the SDK
+ itself.
+
+ These exceptions do not end up in Sentry and are just logged instead.
+ """
+ logger.error("Internal error in sentry_sdk", exc_info=exc_info)
+
+ def start_transaction(
+ self, transaction=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs
+ ):
+ # type: (Optional[Transaction], str, Any) -> Union[Transaction, NoOpSpan]
+ """
+ Start and return a transaction.
+
+ Start an existing transaction if given, otherwise create and start a new
+ transaction with kwargs.
+
+ This is the entry point to manual tracing instrumentation.
+
+ A tree structure can be built by adding child spans to the transaction,
+ and child spans to other spans. To start a new child span within the
+ transaction or any span, call the respective `.start_child()` method.
+
+ Every child span must be finished before the transaction is finished,
+ otherwise the unfinished spans are discarded.
+
+ When used as context managers, spans and transactions are automatically
+ finished at the end of the `with` block. If not using context managers,
+ call the `.finish()` method.
+
+ When the transaction is finished, it will be sent to Sentry with all its
+ finished child spans.
+
+ For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Transaction`.
+ """
+ hub = kwargs.pop("hub", None)
+ client = kwargs.pop("client", None)
+
+ configuration_instrumenter = client and client.options["instrumenter"]
+
+ if instrumenter != configuration_instrumenter:
+ return NoOpSpan()
+
+ custom_sampling_context = kwargs.pop("custom_sampling_context", {})
+
+ # if we haven't been given a transaction, make one
+ if transaction is None:
+ kwargs.setdefault("hub", hub)
+ transaction = Transaction(**kwargs)
+
+ # use traces_sample_rate, traces_sampler, and/or inheritance to make a
+ # sampling decision
+ sampling_context = {
+ "transaction_context": transaction.to_json(),
+ "parent_sampled": transaction.parent_sampled,
+ }
+ sampling_context.update(custom_sampling_context)
+ transaction._set_initial_sampling_decision(sampling_context=sampling_context)
+
+ profile = Profile(transaction, hub=hub)
+ profile._set_initial_sampling_decision(sampling_context=sampling_context)
+
+ # we don't bother to keep spans if we already know we're not going to
+ # send the transaction
+ if transaction.sampled:
+ max_spans = (
+ client and client.options["_experiments"].get("max_spans")
+ ) or 1000
+ transaction.init_span_recorder(maxlen=max_spans)
+
+ return transaction
+
+ def start_span(self, span=None, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
+ # type: (Optional[Span], str, Any) -> Span
+ """
+ Start a span whose parent is the currently active span or transaction, if any.
+
+ The return value is a :py:class:`sentry_sdk.tracing.Span` instance,
+ typically used as a context manager to start and stop timing in a `with`
+ block.
+
+ Only spans contained in a transaction are sent to Sentry. Most
+ integrations start a transaction at the appropriate time, for example
+ for every incoming HTTP request. Use
+ :py:meth:`sentry_sdk.start_transaction` to start a new transaction when
+ one is not already in progress.
+
+ For supported `**kwargs` see :py:class:`sentry_sdk.tracing.Span`.
+ """
+ client = kwargs.get("client", None)
+
+ configuration_instrumenter = client and client.options["instrumenter"]
+
+ if instrumenter != configuration_instrumenter:
+ return NoOpSpan()
+
+ # THIS BLOCK IS DEPRECATED
+ # TODO: consider removing this in a future release.
+ # This is for backwards compatibility with releases before
+ # start_transaction existed, to allow for a smoother transition.
+ if isinstance(span, Transaction) or "transaction" in kwargs:
+ deprecation_msg = (
+ "Deprecated: use start_transaction to start transactions and "
+ "Transaction.start_child to start spans."
+ )
+
+ if isinstance(span, Transaction):
+ logger.warning(deprecation_msg)
+ return self.start_transaction(span, **kwargs)
+
+ if "transaction" in kwargs:
+ logger.warning(deprecation_msg)
+ name = kwargs.pop("transaction")
+ return self.start_transaction(name=name, **kwargs)
+
+ # THIS BLOCK IS DEPRECATED
+ # We do not pass a span into start_span in our code base, so I deprecate this.
+ if span is not None:
+ deprecation_msg = "Deprecated: passing a span into `start_span` is deprecated and will be removed in the future."
+ logger.warning(deprecation_msg)
+ return span
+
+ kwargs.pop("client")
+
+ active_span = self.span
+ if active_span is not None:
+ new_child_span = active_span.start_child(**kwargs)
+ return new_child_span
+
+ # If there is already a trace_id in the propagation context, use it.
+ # This does not need to be done for `start_child` above because it takes
+ # the trace_id from the parent span.
+ if "trace_id" not in kwargs:
+ traceparent = self.get_traceparent()
+ trace_id = traceparent.split("-")[0] if traceparent else None
+ if trace_id is not None:
+ kwargs["trace_id"] = trace_id
+
+ return Span(**kwargs)
+
+ def continue_trace(self, environ_or_headers, op=None, name=None, source=None):
+ # type: (Dict[str, Any], Optional[str], Optional[str], Optional[str]) -> Transaction
+ """
+ Sets the propagation context from environment or headers and returns a transaction.
+ """
+ self.generate_propagation_context(environ_or_headers)
+
+ transaction = Transaction.continue_from_headers(
+ normalize_incoming_data(environ_or_headers),
+ op=op,
+ name=name,
+ source=source,
+ )
+
+ return transaction
+
+ def start_session(self, *args, **kwargs):
+ # type: (*Any, **Any) -> None
+ """Starts a new session."""
+ client = kwargs.pop("client", None)
+ session_mode = kwargs.pop("session_mode", "application")
+
+ self.end_session(client=client)
+
+ self._session = Session(
+ release=client.options["release"] if client else None,
+ environment=client.options["environment"] if client else None,
+ user=self._user,
+ session_mode=session_mode,
+ )
+
+ def end_session(self, *args, **kwargs):
+ # type: (*Any, **Any) -> None
+ """Ends the current session if there is one."""
+ client = kwargs.pop("client", None)
+
+ session = self._session
+ self._session = None
+
+ if session is not None:
+ session.close()
+ if client is not None:
+ client.capture_session(session)
+
+ def stop_auto_session_tracking(self, *args, **kwargs):
+ # type: (*Any, **Any) -> None
+ """Stops automatic session tracking.
+
+ This temporarily session tracking for the current scope when called.
+ To resume session tracking call `resume_auto_session_tracking`.
+ """
+ client = kwargs.pop("client", None)
+
+ self.end_session(client=client)
+
+ self._force_auto_session_tracking = False
+
+ def resume_auto_session_tracking(self):
+ # type: (...) -> None
+ """Resumes automatic session tracking for the current scope if
+ disabled earlier. This requires that generally automatic session
+ tracking is enabled.
+ """
+ self._force_auto_session_tracking = None
+
def add_event_processor(
self, func # type: EventProcessor
):
diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py
index d547e363b6..9d1c3cd9f5 100644
--- a/sentry_sdk/utils.py
+++ b/sentry_sdk/utils.py
@@ -4,6 +4,7 @@
import logging
import math
import os
+import random
import re
import subprocess
import sys
@@ -1248,24 +1249,49 @@ def _make_threadlocal_contextvars(local):
class ContextVar(object):
# Super-limited impl of ContextVar
- def __init__(self, name):
- # type: (str) -> None
+ def __init__(self, name, default=None):
+ # type: (str, Any) -> None
self._name = name
+ self._default = default
self._local = local()
+ self._original_local = local()
- def get(self, default):
+ def get(self, default=None):
# type: (Any) -> Any
- return getattr(self._local, "value", default)
+ return getattr(self._local, "value", default or self._default)
def set(self, value):
- # type: (Any) -> None
+ # type: (Any) -> Any
+ token = str(random.getrandbits(64))
+ original_value = self.get()
+ setattr(self._original_local, token, original_value)
self._local.value = value
+ return token
+
+ def reset(self, token):
+ # type: (Any) -> None
+ self._local.value = getattr(self._original_local, token)
+ setattr(self._original_local, token, None)
return ContextVar
+def _make_noop_copy_context():
+ # type: () -> Callable[[], Any]
+ class NoOpContext:
+ def run(self, func, *args, **kwargs):
+ # type: (Callable[..., Any], *Any, **Any) -> Any
+ return func(*args, **kwargs)
+
+ def copy_context():
+ # type: () -> NoOpContext
+ return NoOpContext()
+
+ return copy_context
+
+
def _get_contextvars():
- # type: () -> Tuple[bool, type]
+ # type: () -> Tuple[bool, type, Callable[[], Any]]
"""
Figure out the "right" contextvars installation to use. Returns a
`contextvars.ContextVar`-like class with a limited API.
@@ -1281,17 +1307,17 @@ def _get_contextvars():
# `aiocontextvars` is absolutely required for functional
# contextvars on Python 3.6.
try:
- from aiocontextvars import ContextVar
+ from aiocontextvars import ContextVar, copy_context
- return True, ContextVar
+ return True, ContextVar, copy_context
except ImportError:
pass
else:
# On Python 3.7 contextvars are functional.
try:
- from contextvars import ContextVar
+ from contextvars import ContextVar, copy_context
- return True, ContextVar
+ return True, ContextVar, copy_context
except ImportError:
pass
@@ -1299,10 +1325,10 @@ def _get_contextvars():
from threading import local
- return False, _make_threadlocal_contextvars(local)
+ return False, _make_threadlocal_contextvars(local), _make_noop_copy_context()
-HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
+HAS_REAL_CONTEXTVARS, ContextVar, copy_context = _get_contextvars()
CONTEXTVARS_ERROR_MESSAGE = """
diff --git a/tests/test_client.py b/tests/test_client.py
index 5a7a5cff16..fa55c1111a 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -20,7 +20,7 @@
)
from sentry_sdk.integrations.executing import ExecutingIntegration
from sentry_sdk.transport import Transport
-from sentry_sdk._compat import reraise, text_type, PY2
+from sentry_sdk._compat import text_type, PY2
from sentry_sdk.utils import HAS_CHAINED_EXCEPTIONS
from sentry_sdk.utils import logger
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
@@ -358,24 +358,27 @@ def test_simple_transport(sentry_init):
def test_ignore_errors(sentry_init, capture_events):
- class MyDivisionError(ZeroDivisionError):
- pass
+ with mock.patch(
+ "sentry_sdk.scope.Scope._capture_internal_exception"
+ ) as mock_capture_internal_exception:
- def raise_it(exc_info):
- reraise(*exc_info)
+ class MyDivisionError(ZeroDivisionError):
+ pass
- sentry_init(ignore_errors=[ZeroDivisionError], transport=_TestTransport())
- Hub.current._capture_internal_exception = raise_it
+ sentry_init(ignore_errors=[ZeroDivisionError], transport=_TestTransport())
- def e(exc):
- try:
- raise exc
- except Exception:
- capture_exception()
+ def e(exc):
+ try:
+ raise exc
+ except Exception:
+ capture_exception()
+
+ e(ZeroDivisionError())
+ e(MyDivisionError())
+ e(ValueError())
- e(ZeroDivisionError())
- e(MyDivisionError())
- pytest.raises(EventCapturedError, lambda: e(ValueError()))
+ assert mock_capture_internal_exception.call_count == 1
+ assert mock_capture_internal_exception.call_args[0][0][0] == EventCapturedError
def test_with_locals_deprecation_enabled(sentry_init):
diff --git a/tests/utils/test_contextvars.py b/tests/utils/test_contextvars.py
index a6d296bb1f..faf33e8580 100644
--- a/tests/utils/test_contextvars.py
+++ b/tests/utils/test_contextvars.py
@@ -12,7 +12,7 @@ def test_leaks(maybe_monkeypatched_threading):
from sentry_sdk import utils
- _, ContextVar = utils._get_contextvars() # noqa: N806
+ _, ContextVar, _ = utils._get_contextvars() # noqa: N806
ts = []