2209 lines
76 KiB
Python
2209 lines
76 KiB
Python
import os
|
|
import platform
|
|
import sys
|
|
import warnings
|
|
from collections import deque
|
|
from contextlib import contextmanager
|
|
from copy import copy, deepcopy
|
|
from datetime import datetime, timezone
|
|
from enum import Enum
|
|
from functools import wraps
|
|
from itertools import chain
|
|
from typing import TYPE_CHECKING, cast
|
|
|
|
import sentry_sdk
|
|
from sentry_sdk._types import AnnotatedValue
|
|
from sentry_sdk.attachments import Attachment
|
|
from sentry_sdk.consts import (
|
|
DEFAULT_MAX_BREADCRUMBS,
|
|
FALSE_VALUES,
|
|
INSTRUMENTER,
|
|
SPANDATA,
|
|
)
|
|
from sentry_sdk.feature_flags import DEFAULT_FLAG_CAPACITY, FlagBuffer
|
|
from sentry_sdk.profiler.continuous_profiler import (
|
|
get_profiler_id,
|
|
try_autostart_continuous_profiler,
|
|
try_profile_lifecycle_trace_start,
|
|
)
|
|
from sentry_sdk.profiler.transaction_profiler import Profile
|
|
from sentry_sdk.session import Session
|
|
from sentry_sdk.traces import (
|
|
_DEFAULT_PARENT_SPAN,
|
|
NoOpStreamedSpan,
|
|
StreamedSpan,
|
|
)
|
|
from sentry_sdk.tracing import (
|
|
BAGGAGE_HEADER_NAME,
|
|
SENTRY_TRACE_HEADER_NAME,
|
|
NoOpSpan,
|
|
Span,
|
|
Transaction,
|
|
)
|
|
from sentry_sdk.tracing_utils import (
|
|
Baggage,
|
|
PropagationContext,
|
|
_make_sampling_decision,
|
|
has_span_streaming_enabled,
|
|
has_tracing_enabled,
|
|
is_ignored_span,
|
|
)
|
|
from sentry_sdk.utils import (
|
|
ContextVar,
|
|
capture_internal_exception,
|
|
capture_internal_exceptions,
|
|
datetime_from_isoformat,
|
|
disable_capture_event,
|
|
event_from_exception,
|
|
exc_info_from_error,
|
|
format_attribute,
|
|
has_logs_enabled,
|
|
has_metrics_enabled,
|
|
logger,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Mapping
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Deque,
|
|
Dict,
|
|
Generator,
|
|
Iterator,
|
|
List,
|
|
Optional,
|
|
ParamSpec,
|
|
Tuple,
|
|
TypeVar,
|
|
Union,
|
|
)
|
|
|
|
from typing_extensions import Unpack
|
|
|
|
import sentry_sdk
|
|
from sentry_sdk._types import (
|
|
Attributes,
|
|
AttributeValue,
|
|
Breadcrumb,
|
|
BreadcrumbHint,
|
|
ErrorProcessor,
|
|
Event,
|
|
EventProcessor,
|
|
ExcInfo,
|
|
Hint,
|
|
Log,
|
|
LogLevelStr,
|
|
Metric,
|
|
SamplingContext,
|
|
Type,
|
|
)
|
|
from sentry_sdk.tracing import TransactionKwargs
|
|
|
|
P = ParamSpec("P")
|
|
R = TypeVar("R")
|
|
|
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
T = TypeVar("T")
|
|
|
|
|
|
# Holds data that will be added to **all** events sent by this process.
|
|
# In case this is a http server (think web framework) with multiple users
|
|
# the data will be added to events of all users.
|
|
# Typically this is used for process wide data such as the release.
|
|
_global_scope: "Optional[Scope]" = None
|
|
|
|
# Holds data for the active request.
|
|
# This is used to isolate data for different requests or users.
|
|
# The isolation scope is usually created by integrations, but may also
|
|
# be created manually
|
|
_isolation_scope = ContextVar("isolation_scope", default=None)
|
|
|
|
# Holds data for the active span.
|
|
# This can be used to manually add additional data to a span.
|
|
_current_scope = ContextVar("current_scope", default=None)
|
|
|
|
global_event_processors: "List[EventProcessor]" = []
|
|
|
|
# A function returning a (trace_id, span_id) tuple
|
|
# from an external tracing source (such as otel)
|
|
_external_propagation_context_fn: "Optional[Callable[[], Optional[Tuple[str, str]]]]" = None
|
|
|
|
|
|
class ScopeType(Enum):
|
|
CURRENT = "current"
|
|
ISOLATION = "isolation"
|
|
GLOBAL = "global"
|
|
MERGED = "merged"
|
|
|
|
|
|
class _ScopeManager:
|
|
def __init__(self, hub: "Optional[Any]" = None) -> None:
|
|
self._old_scopes: "List[Scope]" = []
|
|
|
|
def __enter__(self) -> "Scope":
|
|
isolation_scope = Scope.get_isolation_scope()
|
|
|
|
self._old_scopes.append(isolation_scope)
|
|
|
|
forked_scope = isolation_scope.fork()
|
|
_isolation_scope.set(forked_scope)
|
|
|
|
return forked_scope
|
|
|
|
def __exit__(self, exc_type: "Any", exc_value: "Any", tb: "Any") -> None:
|
|
old_scope = self._old_scopes.pop()
|
|
_isolation_scope.set(old_scope)
|
|
|
|
|
|
def add_global_event_processor(processor: "EventProcessor") -> None:
|
|
global_event_processors.append(processor)
|
|
|
|
|
|
def register_external_propagation_context(
|
|
fn: "Callable[[], Optional[Tuple[str, str]]]",
|
|
) -> None:
|
|
global _external_propagation_context_fn
|
|
_external_propagation_context_fn = fn
|
|
|
|
|
|
def remove_external_propagation_context() -> None:
|
|
global _external_propagation_context_fn
|
|
_external_propagation_context_fn = None
|
|
|
|
|
|
def get_external_propagation_context() -> "Optional[Tuple[str, str]]":
|
|
return (
|
|
_external_propagation_context_fn() if _external_propagation_context_fn else None
|
|
)
|
|
|
|
|
|
def has_external_propagation_context() -> bool:
|
|
return _external_propagation_context_fn is not None
|
|
|
|
|
|
def _attr_setter(fn: "Any") -> "Any":
|
|
return property(fset=fn, doc=fn.__doc__)
|
|
|
|
|
|
def _disable_capture(fn: "F") -> "F":
|
|
@wraps(fn)
|
|
def wrapper(self: "Any", *args: "Dict[str, Any]", **kwargs: "Any") -> "Any":
|
|
if not self._should_capture:
|
|
return
|
|
try:
|
|
self._should_capture = False
|
|
return fn(self, *args, **kwargs)
|
|
finally:
|
|
self._should_capture = True
|
|
|
|
return wrapper # type: ignore
|
|
|
|
|
|
class Scope:
|
|
"""The scope holds extra information that should be sent with all
|
|
events that belong to it.
|
|
"""
|
|
|
|
# NOTE: Even though it should not happen, the scope needs to not crash when
|
|
# accessed by multiple threads. It's fine if it's full of races, but those
|
|
# races should never make the user application crash.
|
|
#
|
|
# The same needs to hold for any accesses of the scope the SDK makes.
|
|
|
|
__slots__ = (
|
|
"_level",
|
|
"_name",
|
|
"_fingerprint",
|
|
# note that for legacy reasons, _transaction is the transaction *name*,
|
|
# not a Transaction object (the object is stored in _span)
|
|
"_transaction",
|
|
"_transaction_info",
|
|
"_user",
|
|
"_tags",
|
|
"_contexts",
|
|
"_extras",
|
|
"_breadcrumbs",
|
|
"_n_breadcrumbs_truncated",
|
|
"_gen_ai_original_message_count",
|
|
"_gen_ai_conversation_id",
|
|
"_event_processors",
|
|
"_error_processors",
|
|
"_should_capture",
|
|
"_span",
|
|
"_session",
|
|
"_attachments",
|
|
"_force_auto_session_tracking",
|
|
"_profile",
|
|
"_propagation_context",
|
|
"client",
|
|
"_type",
|
|
"_last_event_id",
|
|
"_flags",
|
|
"_attributes",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
ty: "Optional[ScopeType]" = None,
|
|
client: "Optional[sentry_sdk.Client]" = None,
|
|
) -> None:
|
|
self._type = ty
|
|
|
|
self._event_processors: "List[EventProcessor]" = []
|
|
self._error_processors: "List[ErrorProcessor]" = []
|
|
|
|
self._name: "Optional[str]" = None
|
|
self._propagation_context: "Optional[PropagationContext]" = None
|
|
self._n_breadcrumbs_truncated: int = 0
|
|
self._gen_ai_original_message_count: "Dict[str, int]" = {}
|
|
|
|
self.client: "sentry_sdk.client.BaseClient" = NonRecordingClient()
|
|
|
|
if client is not None:
|
|
self.set_client(client)
|
|
|
|
self.clear()
|
|
|
|
incoming_trace_information = self._load_trace_data_from_env()
|
|
self.generate_propagation_context(incoming_data=incoming_trace_information)
|
|
|
|
def __copy__(self) -> "Scope":
|
|
"""
|
|
Returns a copy of this scope.
|
|
This also creates a copy of all referenced data structures.
|
|
"""
|
|
rv: "Scope" = object.__new__(self.__class__)
|
|
|
|
rv._type = self._type
|
|
rv.client = self.client
|
|
rv._level = self._level
|
|
rv._name = self._name
|
|
rv._fingerprint = self._fingerprint
|
|
rv._transaction = self._transaction
|
|
rv._transaction_info = self._transaction_info.copy()
|
|
rv._user = self._user
|
|
|
|
rv._tags = self._tags.copy()
|
|
rv._contexts = self._contexts.copy()
|
|
rv._extras = self._extras.copy()
|
|
|
|
rv._breadcrumbs = copy(self._breadcrumbs)
|
|
rv._n_breadcrumbs_truncated = self._n_breadcrumbs_truncated
|
|
rv._gen_ai_original_message_count = self._gen_ai_original_message_count.copy()
|
|
rv._event_processors = self._event_processors.copy()
|
|
rv._error_processors = self._error_processors.copy()
|
|
rv._propagation_context = self._propagation_context
|
|
|
|
rv._should_capture = self._should_capture
|
|
rv._span = self._span
|
|
rv._session = self._session
|
|
rv._force_auto_session_tracking = self._force_auto_session_tracking
|
|
rv._attachments = self._attachments.copy()
|
|
|
|
rv._profile = self._profile
|
|
|
|
rv._last_event_id = self._last_event_id
|
|
|
|
rv._flags = deepcopy(self._flags)
|
|
|
|
rv._attributes = self._attributes.copy()
|
|
|
|
rv._gen_ai_conversation_id = self._gen_ai_conversation_id
|
|
|
|
return rv
|
|
|
|
@classmethod
|
|
def get_current_scope(cls) -> "Scope":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Returns the current scope.
|
|
"""
|
|
current_scope = _current_scope.get()
|
|
if current_scope is None:
|
|
current_scope = Scope(ty=ScopeType.CURRENT)
|
|
_current_scope.set(current_scope)
|
|
|
|
return current_scope
|
|
|
|
@classmethod
|
|
def set_current_scope(cls, new_current_scope: "Scope") -> None:
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Sets the given scope as the new current scope overwriting the existing current scope.
|
|
:param new_current_scope: The scope to set as the new current scope.
|
|
"""
|
|
_current_scope.set(new_current_scope)
|
|
|
|
@classmethod
|
|
def get_isolation_scope(cls) -> "Scope":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Returns the isolation scope.
|
|
"""
|
|
isolation_scope = _isolation_scope.get()
|
|
if isolation_scope is None:
|
|
isolation_scope = Scope(ty=ScopeType.ISOLATION)
|
|
_isolation_scope.set(isolation_scope)
|
|
|
|
return isolation_scope
|
|
|
|
@classmethod
|
|
def set_isolation_scope(cls, new_isolation_scope: "Scope") -> None:
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Sets the given scope as the new isolation scope overwriting the existing isolation scope.
|
|
:param new_isolation_scope: The scope to set as the new isolation scope.
|
|
"""
|
|
_isolation_scope.set(new_isolation_scope)
|
|
|
|
@classmethod
|
|
def get_global_scope(cls) -> "Scope":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Returns the global scope.
|
|
"""
|
|
global _global_scope
|
|
if _global_scope is None:
|
|
_global_scope = Scope(ty=ScopeType.GLOBAL)
|
|
|
|
return _global_scope
|
|
|
|
def set_global_attributes(self) -> None:
|
|
from sentry_sdk.client import SDK_INFO
|
|
|
|
self.set_attribute(SPANDATA.SENTRY_SDK_NAME, SDK_INFO["name"])
|
|
self.set_attribute(SPANDATA.SENTRY_SDK_VERSION, SDK_INFO["version"])
|
|
|
|
self.set_attribute(
|
|
"process.runtime.name",
|
|
platform.python_implementation(),
|
|
)
|
|
self.set_attribute(
|
|
"process.runtime.version",
|
|
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
)
|
|
|
|
options = sentry_sdk.get_client().options
|
|
|
|
server_name = options.get("server_name")
|
|
if server_name:
|
|
self.set_attribute(SPANDATA.SERVER_ADDRESS, server_name)
|
|
|
|
environment = options.get("environment")
|
|
if environment:
|
|
self.set_attribute(SPANDATA.SENTRY_ENVIRONMENT, environment)
|
|
|
|
release = options.get("release")
|
|
if release:
|
|
self.set_attribute(SPANDATA.SENTRY_RELEASE, release)
|
|
|
|
@classmethod
|
|
def last_event_id(cls) -> "Optional[str]":
|
|
"""
|
|
.. versionadded:: 2.2.0
|
|
|
|
Returns event ID of the event most recently captured by the isolation scope, or None if no event
|
|
has been captured. We do not consider events that are dropped, e.g. by a before_send hook.
|
|
Transactions also are not considered events in this context.
|
|
|
|
The event corresponding to the returned event ID is NOT guaranteed to actually be sent to Sentry;
|
|
whether the event is sent depends on the transport. The event could be sent later or not at all.
|
|
Even a sent event could fail to arrive in Sentry due to network issues, exhausted quotas, or
|
|
various other reasons.
|
|
"""
|
|
return cls.get_isolation_scope()._last_event_id
|
|
|
|
def _merge_scopes(
|
|
self,
|
|
additional_scope: "Optional[Scope]" = None,
|
|
additional_scope_kwargs: "Optional[Dict[str, Any]]" = None,
|
|
) -> "Scope":
|
|
"""
|
|
Merges global, isolation and current scope into a new scope and
|
|
adds the given additional scope or additional scope kwargs to it.
|
|
"""
|
|
if additional_scope and additional_scope_kwargs:
|
|
raise TypeError("cannot provide scope and kwargs")
|
|
|
|
final_scope = copy(_global_scope) if _global_scope is not None else Scope()
|
|
final_scope._type = ScopeType.MERGED
|
|
|
|
isolation_scope = _isolation_scope.get()
|
|
if isolation_scope is not None:
|
|
final_scope.update_from_scope(isolation_scope)
|
|
|
|
current_scope = _current_scope.get()
|
|
if current_scope is not None:
|
|
final_scope.update_from_scope(current_scope)
|
|
|
|
if self != current_scope and self != isolation_scope:
|
|
final_scope.update_from_scope(self)
|
|
|
|
if additional_scope is not None:
|
|
if callable(additional_scope):
|
|
additional_scope(final_scope)
|
|
else:
|
|
final_scope.update_from_scope(additional_scope)
|
|
|
|
elif additional_scope_kwargs:
|
|
final_scope.update_from_kwargs(**additional_scope_kwargs)
|
|
|
|
return final_scope
|
|
|
|
@classmethod
|
|
def get_client(cls) -> "sentry_sdk.client.BaseClient":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Returns the currently used :py:class:`sentry_sdk.Client`.
|
|
This checks the current scope, the isolation scope and the global scope for a client.
|
|
If no client is available a :py:class:`sentry_sdk.client.NonRecordingClient` is returned.
|
|
"""
|
|
current_scope = _current_scope.get()
|
|
try:
|
|
client = current_scope.client
|
|
except AttributeError:
|
|
client = None
|
|
|
|
if client is not None and client.is_active():
|
|
return client
|
|
|
|
isolation_scope = _isolation_scope.get()
|
|
try:
|
|
client = isolation_scope.client
|
|
except AttributeError:
|
|
client = None
|
|
|
|
if client is not None and client.is_active():
|
|
return client
|
|
|
|
try:
|
|
client = _global_scope.client # type: ignore
|
|
except AttributeError:
|
|
client = None
|
|
|
|
if client is not None and client.is_active():
|
|
return client
|
|
|
|
return NonRecordingClient()
|
|
|
|
def set_client(
|
|
self, client: "Optional[sentry_sdk.client.BaseClient]" = None
|
|
) -> None:
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Sets the client for this scope.
|
|
|
|
:param client: The client to use in this scope.
|
|
If `None` the client of the scope will be replaced by a :py:class:`sentry_sdk.NonRecordingClient`.
|
|
|
|
"""
|
|
if client is not None:
|
|
self.client = client
|
|
# We need a client to set the initial global attributes on the global
|
|
# scope since they mostly come from client options, so populate them
|
|
# as soon as a client is set
|
|
sentry_sdk.get_global_scope().set_global_attributes()
|
|
else:
|
|
self.client = NonRecordingClient()
|
|
|
|
def fork(self) -> "Scope":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Returns a fork of this scope.
|
|
"""
|
|
forked_scope = copy(self)
|
|
return forked_scope
|
|
|
|
def _load_trace_data_from_env(self) -> "Optional[Dict[str, str]]":
|
|
"""
|
|
Load Sentry trace id and baggage from environment variables.
|
|
Can be disabled by setting SENTRY_USE_ENVIRONMENT to "false".
|
|
"""
|
|
incoming_trace_information: "Optional[Dict[str, str]]" = None
|
|
|
|
sentry_use_environment = (
|
|
os.environ.get("SENTRY_USE_ENVIRONMENT") or ""
|
|
).lower()
|
|
use_environment = sentry_use_environment not in FALSE_VALUES
|
|
if use_environment:
|
|
incoming_trace_information = {}
|
|
|
|
if os.environ.get("SENTRY_TRACE"):
|
|
incoming_trace_information[SENTRY_TRACE_HEADER_NAME] = (
|
|
os.environ.get("SENTRY_TRACE") or ""
|
|
)
|
|
|
|
if os.environ.get("SENTRY_BAGGAGE"):
|
|
incoming_trace_information[BAGGAGE_HEADER_NAME] = (
|
|
os.environ.get("SENTRY_BAGGAGE") or ""
|
|
)
|
|
|
|
return incoming_trace_information or None
|
|
|
|
def set_new_propagation_context(self) -> None:
|
|
"""
|
|
Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one.
|
|
"""
|
|
self._propagation_context = PropagationContext()
|
|
|
|
def generate_propagation_context(
|
|
self, incoming_data: "Optional[Dict[str, str]]" = None
|
|
) -> None:
|
|
"""
|
|
Makes sure the propagation context is set on the scope.
|
|
If there is `incoming_data` overwrite existing propagation context.
|
|
If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing.
|
|
"""
|
|
if incoming_data is not None:
|
|
self._propagation_context = PropagationContext.from_incoming_data(
|
|
incoming_data
|
|
)
|
|
|
|
# TODO-neel this below is a BIG code smell but requires a bunch of other refactoring
|
|
if self._type != ScopeType.CURRENT:
|
|
if self._propagation_context is None:
|
|
self.set_new_propagation_context()
|
|
|
|
def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]":
|
|
"""
|
|
Returns the Dynamic Sampling Context from the Propagation Context.
|
|
If not existing, creates a new one.
|
|
|
|
Deprecated: Logic moved to PropagationContext, don't use directly.
|
|
"""
|
|
if self._propagation_context is None:
|
|
return None
|
|
|
|
return self._propagation_context.dynamic_sampling_context
|
|
|
|
def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]":
|
|
"""
|
|
Returns the Sentry "sentry-trace" header (aka the traceparent) from the
|
|
currently active span or the scopes Propagation Context.
|
|
"""
|
|
client = self.get_client()
|
|
|
|
if not has_tracing_enabled(client.options):
|
|
return self.get_active_propagation_context().to_traceparent()
|
|
|
|
span_streaming = has_span_streaming_enabled(client.options)
|
|
# If we have an active span, return traceparent from there
|
|
if span_streaming and self.streamed_span is not None:
|
|
return self.streamed_span._to_traceparent()
|
|
elif not span_streaming and self.span is not None:
|
|
return self.span._to_traceparent()
|
|
|
|
# else return traceparent from the propagation context
|
|
return self.get_active_propagation_context().to_traceparent()
|
|
|
|
def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]":
|
|
"""
|
|
Returns the Sentry "baggage" header containing trace information from the
|
|
currently active span or the scopes Propagation Context.
|
|
"""
|
|
client = self.get_client()
|
|
|
|
if not has_tracing_enabled(client.options):
|
|
return self.get_active_propagation_context().get_baggage()
|
|
|
|
span_streaming = has_span_streaming_enabled(client.options)
|
|
# If we have an active span, return baggage from there
|
|
if span_streaming and self.streamed_span is not None:
|
|
return self.streamed_span._to_baggage()
|
|
elif not span_streaming and self.span is not None:
|
|
return self.span._to_baggage()
|
|
|
|
# else return baggage from the propagation context
|
|
return self.get_active_propagation_context().get_baggage()
|
|
|
|
def get_trace_context(self) -> "Dict[str, Any]":
|
|
"""
|
|
Returns the Sentry "trace" context from the Propagation Context.
|
|
"""
|
|
if (
|
|
has_tracing_enabled(self.get_client().options)
|
|
and self._span is not None
|
|
and not isinstance(self._span, NoOpSpan)
|
|
):
|
|
return self._span._get_trace_context()
|
|
|
|
# if we are tracing externally (otel), those values take precedence
|
|
external_propagation_context = get_external_propagation_context()
|
|
if external_propagation_context:
|
|
trace_id, span_id = external_propagation_context
|
|
return {"trace_id": trace_id, "span_id": span_id}
|
|
|
|
propagation_context = self.get_active_propagation_context()
|
|
|
|
return {
|
|
"trace_id": propagation_context.trace_id,
|
|
"span_id": propagation_context.span_id,
|
|
"parent_span_id": propagation_context.parent_span_id,
|
|
"dynamic_sampling_context": propagation_context.dynamic_sampling_context,
|
|
}
|
|
|
|
def trace_propagation_meta(self, *args: "Any", **kwargs: "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."
|
|
)
|
|
|
|
meta = ""
|
|
|
|
for name, content in self.iter_trace_propagation_headers():
|
|
meta += f'<meta name="{name}" content="{content}">'
|
|
|
|
return meta
|
|
|
|
def iter_headers(self) -> "Iterator[Tuple[str, str]]":
|
|
"""
|
|
Creates a generator which returns the `sentry-trace` and `baggage` headers from the Propagation Context.
|
|
Deprecated: use PropagationContext.iter_headers instead.
|
|
"""
|
|
if self._propagation_context is not None:
|
|
yield from self._propagation_context.iter_headers()
|
|
|
|
def iter_trace_propagation_headers(
|
|
self, *args: "Any", **kwargs: "Any"
|
|
) -> "Generator[Tuple[str, str], None, None]":
|
|
"""
|
|
Return HTTP headers which allow propagation of trace data.
|
|
|
|
If a span is given, the trace data will taken from the span.
|
|
If no span is given, the trace data is taken from the scope.
|
|
"""
|
|
client = self.get_client()
|
|
if not client.options.get("propagate_traces"):
|
|
warnings.warn(
|
|
"The `propagate_traces` parameter is deprecated. Please use `trace_propagation_targets` instead.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return
|
|
|
|
span = kwargs.pop("span", None)
|
|
if not span:
|
|
span_streaming = has_span_streaming_enabled(client.options)
|
|
span = self.streamed_span if span_streaming else self.span
|
|
|
|
if (
|
|
has_tracing_enabled(client.options)
|
|
and span is not None
|
|
and not isinstance(span, NoOpSpan)
|
|
):
|
|
for header in span._iter_headers():
|
|
yield header
|
|
elif has_external_propagation_context():
|
|
# when we have an external_propagation_context (otlp)
|
|
# we leave outgoing propagation to the propagator
|
|
return
|
|
else:
|
|
for header in self.get_active_propagation_context().iter_headers():
|
|
yield header
|
|
|
|
def get_active_propagation_context(self) -> "PropagationContext":
|
|
if self._propagation_context is not None:
|
|
return self._propagation_context
|
|
|
|
current_scope = self.get_current_scope()
|
|
if current_scope._propagation_context is not None:
|
|
return current_scope._propagation_context
|
|
|
|
isolation_scope = self.get_isolation_scope()
|
|
# should actually never happen, but just in case someone calls scope.clear
|
|
if isolation_scope._propagation_context is None:
|
|
isolation_scope._propagation_context = PropagationContext()
|
|
return isolation_scope._propagation_context
|
|
|
|
@classmethod
|
|
def set_custom_sampling_context(
|
|
cls, custom_sampling_context: "dict[str, Any]"
|
|
) -> None:
|
|
cls.get_current_scope().get_active_propagation_context()._set_custom_sampling_context(
|
|
custom_sampling_context
|
|
)
|
|
|
|
def clear(self) -> None:
|
|
"""Clears the entire scope."""
|
|
self._level: "Optional[LogLevelStr]" = None
|
|
self._fingerprint: "Optional[List[str]]" = None
|
|
self._transaction: "Optional[str]" = None
|
|
self._transaction_info: "dict[str, str]" = {}
|
|
self._user: "Optional[Dict[str, Any]]" = None
|
|
|
|
self._tags: "Dict[str, Any]" = {}
|
|
self._contexts: "Dict[str, Dict[str, Any]]" = {}
|
|
self._extras: "dict[str, Any]" = {}
|
|
self._attachments: "List[Attachment]" = []
|
|
|
|
self.clear_breadcrumbs()
|
|
self._should_capture: bool = True
|
|
|
|
self._span: "Optional[Union[Span, StreamedSpan]]" = None
|
|
self._session: "Optional[Session]" = None
|
|
self._force_auto_session_tracking: "Optional[bool]" = None
|
|
|
|
self._profile: "Optional[Profile]" = None
|
|
|
|
self._propagation_context = None
|
|
|
|
# self._last_event_id is only applicable to isolation scopes
|
|
self._last_event_id: "Optional[str]" = None
|
|
self._flags: "Optional[FlagBuffer]" = None
|
|
|
|
self._attributes: "Attributes" = {}
|
|
|
|
self._gen_ai_conversation_id: "Optional[str]" = None
|
|
|
|
@_attr_setter
|
|
def level(self, value: "LogLevelStr") -> None:
|
|
"""
|
|
When set this overrides the level.
|
|
|
|
.. deprecated:: 1.0.0
|
|
Use :func:`set_level` instead.
|
|
|
|
:param value: The level to set.
|
|
"""
|
|
logger.warning(
|
|
"Deprecated: use .set_level() instead. This will be removed in the future."
|
|
)
|
|
|
|
self._level = value
|
|
|
|
def set_level(self, value: "LogLevelStr") -> None:
|
|
"""
|
|
Sets the level for the scope.
|
|
|
|
:param value: The level to set.
|
|
"""
|
|
self._level = value
|
|
|
|
@_attr_setter
|
|
def fingerprint(self, value: "Optional[List[str]]") -> None:
|
|
"""When set this overrides the default fingerprint."""
|
|
self._fingerprint = value
|
|
|
|
@property
|
|
def transaction(self) -> "Any":
|
|
# would be type: () -> Optional[Transaction], see https://github.com/python/mypy/issues/3004
|
|
"""Return the transaction (root span) in the scope, if any."""
|
|
|
|
# there is no span/transaction on the scope
|
|
if self._span is None:
|
|
return None
|
|
|
|
if isinstance(self._span, StreamedSpan):
|
|
warnings.warn(
|
|
"Scope.transaction is not available in streaming mode.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return None
|
|
|
|
# there is an orphan span on the scope
|
|
if self._span.containing_transaction is None:
|
|
return None
|
|
|
|
# there is either a transaction (which is its own containing
|
|
# transaction) or a non-orphan span on the scope
|
|
return self._span.containing_transaction
|
|
|
|
@transaction.setter
|
|
def transaction(self, value: "Any") -> None:
|
|
# would be type: (Optional[str]) -> None, see https://github.com/python/mypy/issues/3004
|
|
"""When set this forces a specific transaction name to be set.
|
|
|
|
Deprecated: use set_transaction_name instead."""
|
|
|
|
# XXX: the docstring above is misleading. The implementation of
|
|
# apply_to_event prefers an existing value of event.transaction over
|
|
# anything set in the scope.
|
|
# XXX: note that with the introduction of the Scope.transaction getter,
|
|
# there is a semantic and type mismatch between getter and setter. The
|
|
# getter returns a Transaction, the setter sets a transaction name.
|
|
# Without breaking version compatibility, we could make the setter set a
|
|
# transaction name or transaction (self._span) depending on the type of
|
|
# the value argument.
|
|
|
|
logger.warning(
|
|
"Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead."
|
|
)
|
|
self._transaction = value
|
|
if self._span:
|
|
if isinstance(self._span, StreamedSpan):
|
|
warnings.warn(
|
|
"Scope.transaction is not available in streaming mode.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return None
|
|
|
|
if self._span.containing_transaction:
|
|
self._span.containing_transaction.name = value
|
|
|
|
def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None:
|
|
"""Set the transaction name and optionally the transaction source."""
|
|
self._transaction = name
|
|
if self._span:
|
|
if isinstance(self._span, NoOpStreamedSpan):
|
|
return
|
|
|
|
elif isinstance(self._span, StreamedSpan):
|
|
self._span._segment.name = name
|
|
if source:
|
|
self._span._segment.set_attribute(
|
|
"sentry.span.source", getattr(source, "value", source)
|
|
)
|
|
|
|
elif self._span.containing_transaction:
|
|
self._span.containing_transaction.name = name
|
|
if source:
|
|
self._span.containing_transaction.source = source
|
|
|
|
if source:
|
|
self._transaction_info["source"] = source
|
|
|
|
@_attr_setter
|
|
def user(self, value: "Optional[Dict[str, Any]]") -> None:
|
|
"""When set a specific user is bound to the scope. Deprecated in favor of set_user."""
|
|
warnings.warn(
|
|
"The `Scope.user` setter is deprecated in favor of `Scope.set_user()`.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
self.set_user(value)
|
|
|
|
def set_user(self, value: "Optional[Dict[str, Any]]") -> None:
|
|
"""Sets a user for the scope."""
|
|
self._user = value
|
|
|
|
session = self.get_isolation_scope()._session
|
|
if session is not None:
|
|
session.update(user=value)
|
|
|
|
@property
|
|
def span(self) -> "Optional[Span]":
|
|
"""Get/set current tracing span or transaction."""
|
|
return self._span if isinstance(self._span, Span) else None
|
|
|
|
@span.setter
|
|
def span(self, span: "Optional[Span]") -> None:
|
|
self._span = span
|
|
# XXX: this differs from the implementation in JS, there Scope.setSpan
|
|
# does not set Scope._transactionName.
|
|
if isinstance(span, Transaction):
|
|
transaction = span
|
|
if transaction.name:
|
|
self._transaction = transaction.name
|
|
if transaction.source:
|
|
self._transaction_info["source"] = transaction.source
|
|
|
|
@property
|
|
def streamed_span(self) -> "Optional[StreamedSpan]":
|
|
"""Get/set current tracing span."""
|
|
return self._span if isinstance(self._span, StreamedSpan) else None
|
|
|
|
@streamed_span.setter
|
|
def streamed_span(self, span: "Optional[StreamedSpan]") -> None:
|
|
self._span = span
|
|
|
|
# Also set _transaction and _transaction_info in streaming mode as this
|
|
# is used for populating events and linking them to segments
|
|
if type(span) is StreamedSpan and span._is_segment():
|
|
self._transaction = span.name
|
|
if span._attributes.get("sentry.span.source"):
|
|
self._transaction_info["source"] = str(
|
|
span._attributes["sentry.span.source"]
|
|
)
|
|
|
|
@property
|
|
def profile(self) -> "Optional[Profile]":
|
|
return self._profile
|
|
|
|
@profile.setter
|
|
def profile(self, profile: "Optional[Profile]") -> None:
|
|
self._profile = profile
|
|
|
|
def set_tag(self, key: str, value: "Any") -> None:
|
|
"""
|
|
Sets a tag for a key to a specific value.
|
|
|
|
:param key: Key of the tag to set.
|
|
|
|
:param value: Value of the tag to set.
|
|
"""
|
|
self._tags[key] = value
|
|
|
|
def set_tags(self, tags: "Mapping[str, object]") -> None:
|
|
"""Sets multiple tags at once.
|
|
|
|
This method updates multiple tags at once. The tags are passed as a dictionary
|
|
or other mapping type.
|
|
|
|
Calling this method is equivalent to calling `set_tag` on each key-value pair
|
|
in the mapping. If a tag key already exists in the scope, its value will be
|
|
updated. If the tag key does not exist in the scope, the key-value pair will
|
|
be added to the scope.
|
|
|
|
This method only modifies tag keys in the `tags` mapping passed to the method.
|
|
`scope.set_tags({})` is, therefore, a no-op.
|
|
|
|
:param tags: A mapping of tag keys to tag values to set.
|
|
"""
|
|
self._tags.update(tags)
|
|
|
|
def remove_tag(self, key: str) -> None:
|
|
"""
|
|
Removes a specific tag.
|
|
|
|
:param key: Key of the tag to remove.
|
|
"""
|
|
self._tags.pop(key, None)
|
|
|
|
def set_context(
|
|
self,
|
|
key: str,
|
|
value: "Dict[str, Any]",
|
|
) -> None:
|
|
"""
|
|
Binds a context at a certain key to a specific value.
|
|
"""
|
|
self._contexts[key] = value
|
|
|
|
def remove_context(
|
|
self,
|
|
key: str,
|
|
) -> None:
|
|
"""Removes a context."""
|
|
self._contexts.pop(key, None)
|
|
|
|
def set_extra(
|
|
self,
|
|
key: str,
|
|
value: "Any",
|
|
) -> None:
|
|
"""Sets an extra key to a specific value."""
|
|
self._extras[key] = value
|
|
|
|
def remove_extra(
|
|
self,
|
|
key: str,
|
|
) -> None:
|
|
"""Removes a specific extra key."""
|
|
self._extras.pop(key, None)
|
|
|
|
def set_conversation_id(self, conversation_id: str) -> None:
|
|
"""
|
|
Sets the conversation ID for gen_ai spans.
|
|
|
|
:param conversation_id: The conversation ID to set.
|
|
"""
|
|
self._gen_ai_conversation_id = conversation_id
|
|
|
|
def get_conversation_id(self) -> "Optional[str]":
|
|
"""
|
|
Gets the conversation ID for gen_ai spans.
|
|
|
|
:returns: The conversation ID, or None if not set.
|
|
"""
|
|
return self._gen_ai_conversation_id
|
|
|
|
def remove_conversation_id(self) -> None:
|
|
"""Removes the conversation ID."""
|
|
self._gen_ai_conversation_id = None
|
|
|
|
def clear_breadcrumbs(self) -> None:
|
|
"""Clears breadcrumb buffer."""
|
|
self._breadcrumbs: "Deque[Breadcrumb]" = deque()
|
|
self._n_breadcrumbs_truncated = 0
|
|
|
|
def add_attachment(
|
|
self,
|
|
bytes: "Union[None, bytes, Callable[[], bytes]]" = None,
|
|
filename: "Optional[str]" = None,
|
|
path: "Optional[str]" = None,
|
|
content_type: "Optional[str]" = None,
|
|
add_to_transactions: bool = False,
|
|
) -> None:
|
|
"""Adds an attachment to future events sent from this scope.
|
|
|
|
The parameters are the same as for the :py:class:`sentry_sdk.attachments.Attachment` constructor.
|
|
"""
|
|
self._attachments.append(
|
|
Attachment(
|
|
bytes=bytes,
|
|
path=path,
|
|
filename=filename,
|
|
content_type=content_type,
|
|
add_to_transactions=add_to_transactions,
|
|
)
|
|
)
|
|
|
|
def add_breadcrumb(
|
|
self,
|
|
crumb: "Optional[Breadcrumb]" = None,
|
|
hint: "Optional[BreadcrumbHint]" = None,
|
|
**kwargs: "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 = self.get_client()
|
|
|
|
if not client.is_active():
|
|
logger.info("Dropped breadcrumb because no client bound")
|
|
return
|
|
|
|
before_breadcrumb = client.options.get("before_breadcrumb")
|
|
max_breadcrumbs = client.options.get("max_breadcrumbs", DEFAULT_MAX_BREADCRUMBS)
|
|
|
|
crumb = dict(crumb or ())
|
|
crumb.update(kwargs)
|
|
if not crumb:
|
|
return
|
|
|
|
hint = dict(hint or ())
|
|
|
|
if crumb.get("timestamp") is None:
|
|
crumb["timestamp"] = datetime.now(timezone.utc)
|
|
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()
|
|
self._n_breadcrumbs_truncated += 1
|
|
|
|
def start_transaction(
|
|
self,
|
|
transaction: "Optional[Transaction]" = None,
|
|
instrumenter: str = INSTRUMENTER.SENTRY,
|
|
custom_sampling_context: "Optional[SamplingContext]" = None,
|
|
**kwargs: "Unpack[TransactionKwargs]",
|
|
) -> "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.
|
|
|
|
:param transaction: The transaction to start. If omitted, we create and
|
|
start a new transaction.
|
|
:param instrumenter: This parameter is meant for internal use only. It
|
|
will be removed in the next major version.
|
|
:param custom_sampling_context: The transaction's custom sampling context.
|
|
:param kwargs: Optional keyword arguments to be passed to the Transaction
|
|
constructor. See :py:class:`sentry_sdk.tracing.Transaction` for
|
|
available arguments.
|
|
"""
|
|
kwargs.setdefault("scope", self)
|
|
|
|
client = self.get_client()
|
|
|
|
configuration_instrumenter = client.options["instrumenter"]
|
|
|
|
if instrumenter != configuration_instrumenter:
|
|
return NoOpSpan()
|
|
|
|
try_autostart_continuous_profiler()
|
|
|
|
custom_sampling_context = custom_sampling_context or {}
|
|
|
|
# kwargs at this point has type TransactionKwargs, since we have removed
|
|
# the client and custom_sampling_context from it.
|
|
transaction_kwargs: "TransactionKwargs" = kwargs
|
|
|
|
# if we haven't been given a transaction, make one
|
|
if transaction is None:
|
|
transaction = 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)
|
|
|
|
# update the sample rate in the dsc
|
|
if transaction.sample_rate is not None:
|
|
propagation_context = self.get_active_propagation_context()
|
|
baggage = propagation_context.baggage
|
|
|
|
if baggage is not None:
|
|
baggage.sentry_items["sample_rate"] = str(transaction.sample_rate)
|
|
|
|
if transaction._baggage:
|
|
transaction._baggage.sentry_items["sample_rate"] = str(
|
|
transaction.sample_rate
|
|
)
|
|
|
|
if transaction.sampled:
|
|
profile = Profile(
|
|
transaction.sampled, transaction._start_timestamp_monotonic_ns
|
|
)
|
|
profile._set_initial_sampling_decision(sampling_context=sampling_context)
|
|
|
|
transaction._profile = profile
|
|
|
|
transaction._continuous_profile = try_profile_lifecycle_trace_start()
|
|
|
|
# Typically, the profiler is set when the transaction is created. But when
|
|
# using the auto lifecycle, the profiler isn't running when the first
|
|
# transaction is started. So make sure we update the profiler id on it.
|
|
if transaction._continuous_profile is not None:
|
|
transaction.set_profiler_id(get_profiler_id())
|
|
|
|
# we don't bother to keep spans if we already know we're not going to
|
|
# send the transaction
|
|
max_spans = (client.options["_experiments"].get("max_spans")) or 1000
|
|
transaction.init_span_recorder(maxlen=max_spans)
|
|
|
|
return transaction
|
|
|
|
def start_span(
|
|
self, instrumenter: str = INSTRUMENTER.SENTRY, **kwargs: "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`.
|
|
|
|
The instrumenter parameter is deprecated for user code, and it will
|
|
be removed in the next major version. Going forward, it should only
|
|
be used by the SDK itself.
|
|
"""
|
|
client = sentry_sdk.get_client()
|
|
if has_span_streaming_enabled(client.options):
|
|
warnings.warn(
|
|
"Scope.start_span is not available in streaming mode.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return NoOpSpan()
|
|
|
|
if kwargs.get("description") is not None:
|
|
warnings.warn(
|
|
"The `description` parameter is deprecated. Please use `name` instead.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
with new_scope():
|
|
kwargs.setdefault("scope", self)
|
|
|
|
client = self.get_client()
|
|
|
|
configuration_instrumenter = client.options["instrumenter"]
|
|
|
|
if instrumenter != configuration_instrumenter:
|
|
return NoOpSpan()
|
|
|
|
# get current span or transaction
|
|
span = self.span or self.get_isolation_scope().span
|
|
if isinstance(span, StreamedSpan):
|
|
# make mypy happy
|
|
return NoOpSpan()
|
|
|
|
if span is None:
|
|
# New spans get the `trace_id` from the scope
|
|
if "trace_id" not in kwargs:
|
|
propagation_context = self.get_active_propagation_context()
|
|
kwargs["trace_id"] = propagation_context.trace_id
|
|
|
|
span = Span(**kwargs)
|
|
else:
|
|
# Children take `trace_id`` from the parent span.
|
|
span = span.start_child(**kwargs)
|
|
|
|
return span
|
|
|
|
def start_streamed_span(
|
|
self,
|
|
name: str,
|
|
attributes: "Optional[Attributes]",
|
|
parent_span: "Optional[StreamedSpan]",
|
|
active: bool,
|
|
) -> "StreamedSpan":
|
|
# TODO: rename to start_span once we drop the old API
|
|
if isinstance(parent_span, NoOpStreamedSpan):
|
|
# parent_span is only set if the user explicitly set it
|
|
logger.debug(
|
|
"Ignored parent span provided. Span will be parented to the "
|
|
"currently active span instead."
|
|
)
|
|
|
|
if parent_span is _DEFAULT_PARENT_SPAN or isinstance(
|
|
parent_span, NoOpStreamedSpan
|
|
):
|
|
parent_span = self.streamed_span
|
|
|
|
# If no eligible parent_span was provided and there is no currently
|
|
# active span, this is a new segment
|
|
if parent_span is None:
|
|
propagation_context = self.get_active_propagation_context()
|
|
|
|
if is_ignored_span(name, attributes):
|
|
return NoOpStreamedSpan(
|
|
scope=self,
|
|
segment=None,
|
|
trace_id=propagation_context.trace_id,
|
|
parent_span_id=propagation_context.parent_span_id,
|
|
parent_sampled=propagation_context.parent_sampled,
|
|
baggage=propagation_context.baggage,
|
|
unsampled_reason="ignored",
|
|
)
|
|
|
|
sampled, sample_rate, sample_rand, outcome = _make_sampling_decision(
|
|
name,
|
|
attributes,
|
|
self,
|
|
)
|
|
|
|
if sample_rate is not None:
|
|
self._update_sample_rate(sample_rate)
|
|
|
|
if sampled is False or sampled is None:
|
|
return NoOpStreamedSpan(
|
|
scope=self,
|
|
segment=None,
|
|
trace_id=propagation_context.trace_id,
|
|
parent_span_id=propagation_context.parent_span_id,
|
|
parent_sampled=propagation_context.parent_sampled,
|
|
baggage=propagation_context.baggage,
|
|
sampled=sampled,
|
|
unsampled_reason=outcome,
|
|
sample_rand=sample_rand,
|
|
sample_rate=sample_rate,
|
|
)
|
|
|
|
return StreamedSpan(
|
|
name=name,
|
|
attributes=attributes,
|
|
active=active,
|
|
scope=self,
|
|
segment=None,
|
|
trace_id=propagation_context.trace_id,
|
|
parent_span_id=propagation_context.parent_span_id,
|
|
parent_sampled=propagation_context.parent_sampled,
|
|
baggage=propagation_context.baggage,
|
|
sample_rand=sample_rand,
|
|
sample_rate=sample_rate,
|
|
)
|
|
|
|
# This is a child span; take propagation context from the parent span
|
|
with new_scope():
|
|
if is_ignored_span(name, attributes):
|
|
return NoOpStreamedSpan(
|
|
segment=parent_span._segment,
|
|
trace_id=parent_span.trace_id,
|
|
parent_span_id=parent_span.span_id,
|
|
parent_sampled=parent_span.sampled,
|
|
unsampled_reason="ignored",
|
|
)
|
|
|
|
if isinstance(parent_span, NoOpStreamedSpan):
|
|
return NoOpStreamedSpan(
|
|
segment=parent_span._segment,
|
|
trace_id=parent_span.trace_id,
|
|
parent_span_id=parent_span.span_id,
|
|
parent_sampled=parent_span.sampled,
|
|
unsampled_reason=parent_span._unsampled_reason,
|
|
)
|
|
|
|
return StreamedSpan(
|
|
name=name,
|
|
attributes=attributes,
|
|
active=active,
|
|
scope=self,
|
|
segment=parent_span._segment,
|
|
trace_id=parent_span.trace_id,
|
|
parent_span_id=parent_span.span_id,
|
|
parent_sampled=parent_span.sampled,
|
|
)
|
|
|
|
def _update_sample_rate(self, sample_rate: float) -> None:
|
|
# If we had to adjust the sample rate when setting the sampling decision
|
|
# for a span, it needs to be updated in the propagation context too
|
|
propagation_context = self.get_active_propagation_context()
|
|
baggage = propagation_context.baggage
|
|
|
|
if baggage is not None and baggage.sentry_items.get("sample_rate"):
|
|
baggage.sentry_items["sample_rate"] = str(sample_rate)
|
|
|
|
def continue_trace(
|
|
self,
|
|
environ_or_headers: "Dict[str, Any]",
|
|
op: "Optional[str]" = None,
|
|
name: "Optional[str]" = None,
|
|
source: "Optional[str]" = None,
|
|
origin: str = "manual",
|
|
) -> "Transaction":
|
|
"""
|
|
Sets the propagation context from environment or headers and returns a transaction.
|
|
"""
|
|
self.generate_propagation_context(environ_or_headers)
|
|
|
|
# generate_propagation_context ensures that the propagation_context is not None.
|
|
propagation_context = cast(PropagationContext, self._propagation_context)
|
|
|
|
optional_kwargs = {}
|
|
if name:
|
|
optional_kwargs["name"] = name
|
|
if source:
|
|
optional_kwargs["source"] = source
|
|
|
|
return Transaction(
|
|
op=op,
|
|
origin=origin,
|
|
baggage=propagation_context.baggage,
|
|
parent_sampled=propagation_context.parent_sampled,
|
|
trace_id=propagation_context.trace_id,
|
|
parent_span_id=propagation_context.parent_span_id,
|
|
same_process_as_parent=False,
|
|
**optional_kwargs,
|
|
)
|
|
|
|
def capture_event(
|
|
self,
|
|
event: "Event",
|
|
hint: "Optional[Hint]" = None,
|
|
scope: "Optional[Scope]" = None,
|
|
**scope_kwargs: "Any",
|
|
) -> "Optional[str]":
|
|
"""
|
|
Captures an event.
|
|
|
|
Merges given scope data and calls :py:meth:`sentry_sdk.client._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 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._Client.capture_event`).
|
|
"""
|
|
if disable_capture_event.get(False):
|
|
return None
|
|
|
|
scope = self._merge_scopes(scope, scope_kwargs)
|
|
|
|
event_id = self.get_client().capture_event(event=event, hint=hint, scope=scope)
|
|
|
|
if event_id is not None and event.get("type") != "transaction":
|
|
self.get_isolation_scope()._last_event_id = event_id
|
|
|
|
return event_id
|
|
|
|
def _capture_log(self, log: "Optional[Log]") -> None:
|
|
if log is None:
|
|
return
|
|
|
|
client = self.get_client()
|
|
if not has_logs_enabled(client.options):
|
|
return
|
|
|
|
merged_scope = self._merge_scopes()
|
|
|
|
debug = client.options.get("debug", False)
|
|
if debug:
|
|
logger.debug(
|
|
f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}"
|
|
)
|
|
|
|
client._capture_log(log, scope=merged_scope)
|
|
|
|
def _capture_metric(self, metric: "Optional[Metric]") -> None:
|
|
if metric is None:
|
|
return
|
|
|
|
client = self.get_client()
|
|
if not has_metrics_enabled(client.options):
|
|
return
|
|
|
|
merged_scope = self._merge_scopes()
|
|
|
|
debug = client.options.get("debug", False)
|
|
if debug:
|
|
logger.debug(
|
|
f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}"
|
|
)
|
|
|
|
client._capture_metric(metric, scope=merged_scope)
|
|
|
|
def _capture_span(self, span: "Optional[StreamedSpan]") -> None:
|
|
if span is None:
|
|
return
|
|
|
|
client = self.get_client()
|
|
if not has_span_streaming_enabled(client.options):
|
|
return
|
|
|
|
merged_scope = self._merge_scopes()
|
|
client._capture_span(span, scope=merged_scope)
|
|
|
|
def capture_message(
|
|
self,
|
|
message: str,
|
|
level: "Optional[LogLevelStr]" = None,
|
|
scope: "Optional[Scope]" = None,
|
|
**scope_kwargs: "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 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._Client.capture_event`).
|
|
"""
|
|
if disable_capture_event.get(False):
|
|
return None
|
|
|
|
if level is None:
|
|
level = "info"
|
|
|
|
event: "Event" = {
|
|
"message": message,
|
|
"level": level,
|
|
}
|
|
|
|
return self.capture_event(event, scope=scope, **scope_kwargs)
|
|
|
|
def capture_exception(
|
|
self,
|
|
error: "Optional[Union[BaseException, ExcInfo]]" = None,
|
|
scope: "Optional[Scope]" = None,
|
|
**scope_kwargs: "Any",
|
|
) -> "Optional[str]":
|
|
"""Captures an 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_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._Client.capture_event`).
|
|
"""
|
|
if disable_capture_event.get(False):
|
|
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=self.get_client().options
|
|
)
|
|
|
|
try:
|
|
return self.capture_event(event, hint=hint, scope=scope, **scope_kwargs)
|
|
except Exception:
|
|
capture_internal_exception(sys.exc_info())
|
|
|
|
return None
|
|
|
|
def start_session(self, *args: "Any", **kwargs: "Any") -> None:
|
|
"""Starts a new session."""
|
|
session_mode = kwargs.pop("session_mode", "application")
|
|
|
|
self.end_session()
|
|
|
|
client = self.get_client()
|
|
self._session = Session(
|
|
release=client.options.get("release"),
|
|
environment=client.options.get("environment"),
|
|
user=self._user,
|
|
session_mode=session_mode,
|
|
)
|
|
|
|
def end_session(self, *args: "Any", **kwargs: "Any") -> None:
|
|
"""Ends the current session if there is one."""
|
|
session = self._session
|
|
self._session = None
|
|
|
|
if session is not None:
|
|
session.close()
|
|
self.get_client().capture_session(session)
|
|
|
|
def stop_auto_session_tracking(self, *args: "Any", **kwargs: "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`.
|
|
"""
|
|
self.end_session()
|
|
self._force_auto_session_tracking = False
|
|
|
|
def resume_auto_session_tracking(self) -> 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: "EventProcessor",
|
|
) -> None:
|
|
"""Register a scope local event processor on the scope.
|
|
|
|
:param func: This function behaves like `before_send.`
|
|
"""
|
|
if len(self._event_processors) > 20:
|
|
logger.warning(
|
|
"Too many event processors on scope! Clearing list to free up some memory: %r",
|
|
self._event_processors,
|
|
)
|
|
del self._event_processors[:]
|
|
|
|
self._event_processors.append(func)
|
|
|
|
def add_error_processor(
|
|
self,
|
|
func: "ErrorProcessor",
|
|
cls: "Optional[Type[BaseException]]" = None,
|
|
) -> None:
|
|
"""Register a scope local error processor on the scope.
|
|
|
|
:param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument.
|
|
|
|
:param cls: Optionally, only process exceptions of this type.
|
|
"""
|
|
if cls is not None:
|
|
cls_ = cls # For mypy.
|
|
real_func = func
|
|
|
|
def func(event: "Event", exc_info: "ExcInfo") -> "Optional[Event]":
|
|
try:
|
|
is_inst = isinstance(exc_info[1], cls_)
|
|
except Exception:
|
|
is_inst = False
|
|
if is_inst:
|
|
return real_func(event, exc_info)
|
|
return event
|
|
|
|
self._error_processors.append(func)
|
|
|
|
def _apply_level_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if self._level is not None:
|
|
event["level"] = self._level
|
|
|
|
def _apply_breadcrumbs_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
event.setdefault("breadcrumbs", {})
|
|
|
|
# This check is just for mypy -
|
|
if not isinstance(event["breadcrumbs"], AnnotatedValue):
|
|
event["breadcrumbs"].setdefault("values", [])
|
|
event["breadcrumbs"]["values"].extend(self._breadcrumbs)
|
|
|
|
# Attempt to sort timestamps
|
|
try:
|
|
if not isinstance(event["breadcrumbs"], AnnotatedValue):
|
|
for crumb in event["breadcrumbs"]["values"]:
|
|
if isinstance(crumb["timestamp"], str):
|
|
crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"])
|
|
|
|
event["breadcrumbs"]["values"].sort(
|
|
key=lambda crumb: crumb["timestamp"]
|
|
)
|
|
except Exception as err:
|
|
logger.debug("Error when sorting breadcrumbs", exc_info=err)
|
|
pass
|
|
|
|
def _apply_user_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if event.get("user") is None and self._user is not None:
|
|
event["user"] = self._user
|
|
|
|
def _apply_transaction_name_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if event.get("transaction") is None and self._transaction is not None:
|
|
event["transaction"] = self._transaction
|
|
|
|
def _apply_transaction_info_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if event.get("transaction_info") is None and self._transaction_info is not None:
|
|
event["transaction_info"] = self._transaction_info
|
|
|
|
def _apply_fingerprint_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if event.get("fingerprint") is None and self._fingerprint is not None:
|
|
event["fingerprint"] = self._fingerprint
|
|
|
|
def _apply_extra_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if self._extras:
|
|
event.setdefault("extra", {}).update(self._extras)
|
|
|
|
def _apply_tags_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if self._tags:
|
|
event.setdefault("tags", {}).update(self._tags)
|
|
|
|
def _apply_contexts_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
if self._contexts:
|
|
event.setdefault("contexts", {}).update(self._contexts)
|
|
|
|
contexts = event.setdefault("contexts", {})
|
|
|
|
# Add "trace" context
|
|
if contexts.get("trace") is None:
|
|
contexts["trace"] = self.get_trace_context()
|
|
|
|
def _apply_flags_to_event(
|
|
self, event: "Event", hint: "Hint", options: "Optional[Dict[str, Any]]"
|
|
) -> None:
|
|
flags = self.flags.get()
|
|
if len(flags) > 0:
|
|
event.setdefault("contexts", {}).setdefault("flags", {}).update(
|
|
{"values": flags}
|
|
)
|
|
|
|
def _apply_scope_attributes_to_telemetry(
|
|
self, telemetry: "Union[Log, Metric, StreamedSpan]"
|
|
) -> None:
|
|
# TODO: turn Logs, Metrics into actual classes
|
|
if isinstance(telemetry, dict):
|
|
attributes = telemetry["attributes"]
|
|
else:
|
|
attributes = telemetry._attributes
|
|
|
|
for attribute, value in self._attributes.items():
|
|
if attribute not in attributes:
|
|
attributes[attribute] = value
|
|
|
|
def _apply_user_attributes_to_telemetry(
|
|
self, telemetry: "Union[Log, Metric, StreamedSpan]"
|
|
) -> None:
|
|
if isinstance(telemetry, dict):
|
|
attributes = telemetry["attributes"]
|
|
else:
|
|
attributes = telemetry._attributes
|
|
|
|
if not should_send_default_pii() or self._user is None:
|
|
return
|
|
|
|
for attribute_name, user_attribute in (
|
|
("user.id", "id"),
|
|
("user.name", "username"),
|
|
("user.email", "email"),
|
|
("user.ip_address", "ip_address"),
|
|
):
|
|
if (
|
|
user_attribute in self._user
|
|
and attribute_name not in attributes
|
|
and self._user[user_attribute] is not None
|
|
):
|
|
attributes[attribute_name] = self._user[user_attribute]
|
|
|
|
def _drop(self, cause: "Any", ty: str) -> "Optional[Any]":
|
|
logger.info("%s (%s) dropped event", ty, cause)
|
|
return None
|
|
|
|
def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]":
|
|
"""
|
|
Runs the error processors on the event and returns the modified event.
|
|
"""
|
|
exc_info = hint.get("exc_info")
|
|
if exc_info is not None:
|
|
error_processors = chain(
|
|
self.get_global_scope()._error_processors,
|
|
self.get_isolation_scope()._error_processors,
|
|
self.get_current_scope()._error_processors,
|
|
)
|
|
|
|
for error_processor in error_processors:
|
|
new_event = error_processor(event, exc_info)
|
|
if new_event is None:
|
|
return self._drop(error_processor, "error processor")
|
|
|
|
event = new_event
|
|
|
|
return event
|
|
|
|
def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]":
|
|
"""
|
|
Runs the event processors on the event and returns the modified event.
|
|
"""
|
|
ty = event.get("type")
|
|
is_check_in = ty == "check_in"
|
|
|
|
if not is_check_in:
|
|
# Get scopes without creating them to prevent infinite recursion
|
|
isolation_scope = _isolation_scope.get()
|
|
current_scope = _current_scope.get()
|
|
|
|
event_processors = chain(
|
|
global_event_processors,
|
|
_global_scope and _global_scope._event_processors or [],
|
|
isolation_scope and isolation_scope._event_processors or [],
|
|
current_scope and current_scope._event_processors or [],
|
|
)
|
|
|
|
for event_processor in event_processors:
|
|
new_event = event
|
|
with capture_internal_exceptions():
|
|
new_event = event_processor(event, hint)
|
|
if new_event is None:
|
|
return self._drop(event_processor, "event processor")
|
|
event = new_event
|
|
|
|
return event
|
|
|
|
@_disable_capture
|
|
def apply_to_event(
|
|
self,
|
|
event: "Event",
|
|
hint: "Hint",
|
|
options: "Optional[Dict[str, Any]]" = None,
|
|
) -> "Optional[Event]":
|
|
"""Applies the information contained on the scope to the given event."""
|
|
ty = event.get("type")
|
|
is_transaction = ty == "transaction"
|
|
is_check_in = ty == "check_in"
|
|
|
|
# put all attachments into the hint. This lets callbacks play around
|
|
# with attachments. We also later pull this out of the hint when we
|
|
# create the envelope.
|
|
attachments_to_send = hint.get("attachments") or []
|
|
for attachment in self._attachments:
|
|
if not is_transaction or attachment.add_to_transactions:
|
|
attachments_to_send.append(attachment)
|
|
hint["attachments"] = attachments_to_send
|
|
|
|
self._apply_contexts_to_event(event, hint, options)
|
|
|
|
if is_check_in:
|
|
# Check-ins only support the trace context, strip all others
|
|
event["contexts"] = {
|
|
"trace": event.setdefault("contexts", {}).get("trace", {})
|
|
}
|
|
|
|
if not is_check_in:
|
|
self._apply_level_to_event(event, hint, options)
|
|
self._apply_fingerprint_to_event(event, hint, options)
|
|
self._apply_user_to_event(event, hint, options)
|
|
self._apply_transaction_name_to_event(event, hint, options)
|
|
self._apply_transaction_info_to_event(event, hint, options)
|
|
self._apply_tags_to_event(event, hint, options)
|
|
self._apply_extra_to_event(event, hint, options)
|
|
|
|
if not is_transaction and not is_check_in:
|
|
self._apply_breadcrumbs_to_event(event, hint, options)
|
|
self._apply_flags_to_event(event, hint, options)
|
|
|
|
event = self.run_error_processors(event, hint)
|
|
if event is None:
|
|
return None
|
|
|
|
event = self.run_event_processors(event, hint)
|
|
if event is None:
|
|
return None
|
|
|
|
return event
|
|
|
|
@_disable_capture
|
|
def apply_to_telemetry(self, telemetry: "Union[Log, Metric, StreamedSpan]") -> None:
|
|
# Attributes-based events and telemetry go through here (logs, metrics,
|
|
# spansV2)
|
|
if not isinstance(telemetry, StreamedSpan):
|
|
trace_context = self.get_trace_context()
|
|
trace_id = trace_context.get("trace_id")
|
|
if telemetry.get("trace_id") is None and trace_id is not None:
|
|
telemetry["trace_id"] = trace_id
|
|
|
|
# span_id should only be populated if there's an active span. We can't
|
|
# use the trace_context here because it synthesizes a span_id if there
|
|
# isn't one
|
|
if telemetry.get("span_id") is None:
|
|
if self._span is not None and not isinstance(
|
|
self._span, (NoOpStreamedSpan, NoOpSpan)
|
|
):
|
|
telemetry["span_id"] = self._span.span_id
|
|
else:
|
|
external_propagation_context = get_external_propagation_context()
|
|
if external_propagation_context:
|
|
_, span_id = external_propagation_context
|
|
if span_id is not None:
|
|
telemetry["span_id"] = span_id
|
|
|
|
self._apply_scope_attributes_to_telemetry(telemetry)
|
|
self._apply_user_attributes_to_telemetry(telemetry)
|
|
|
|
def update_from_scope(self, scope: "Scope") -> None:
|
|
"""Update the scope with another scope's data."""
|
|
if scope._level is not None:
|
|
self._level = scope._level
|
|
if scope._fingerprint is not None:
|
|
self._fingerprint = scope._fingerprint
|
|
if scope._transaction is not None:
|
|
self._transaction = scope._transaction
|
|
if scope._transaction_info is not None:
|
|
self._transaction_info.update(scope._transaction_info)
|
|
if scope._user is not None:
|
|
self._user = scope._user
|
|
if scope._tags:
|
|
self._tags.update(scope._tags)
|
|
if scope._contexts:
|
|
self._contexts.update(scope._contexts)
|
|
if scope._extras:
|
|
self._extras.update(scope._extras)
|
|
if scope._breadcrumbs:
|
|
self._breadcrumbs.extend(scope._breadcrumbs)
|
|
if scope._n_breadcrumbs_truncated:
|
|
self._n_breadcrumbs_truncated = (
|
|
self._n_breadcrumbs_truncated + scope._n_breadcrumbs_truncated
|
|
)
|
|
if scope._gen_ai_original_message_count:
|
|
self._gen_ai_original_message_count.update(
|
|
scope._gen_ai_original_message_count
|
|
)
|
|
if scope._gen_ai_conversation_id:
|
|
self._gen_ai_conversation_id = scope._gen_ai_conversation_id
|
|
if scope._span:
|
|
self._span = scope._span
|
|
if scope._attachments:
|
|
self._attachments.extend(scope._attachments)
|
|
if scope._profile:
|
|
self._profile = scope._profile
|
|
if scope._propagation_context:
|
|
self._propagation_context = scope._propagation_context
|
|
if scope._session:
|
|
self._session = scope._session
|
|
if scope._flags:
|
|
if not self._flags:
|
|
self._flags = deepcopy(scope._flags)
|
|
else:
|
|
for flag in scope._flags.get():
|
|
self._flags.set(flag["flag"], flag["result"])
|
|
if scope._attributes:
|
|
self._attributes.update(scope._attributes)
|
|
|
|
def update_from_kwargs(
|
|
self,
|
|
user: "Optional[Any]" = None,
|
|
level: "Optional[LogLevelStr]" = None,
|
|
extras: "Optional[Dict[str, Any]]" = None,
|
|
contexts: "Optional[Dict[str, Dict[str, Any]]]" = None,
|
|
tags: "Optional[Dict[str, str]]" = None,
|
|
fingerprint: "Optional[List[str]]" = None,
|
|
attributes: "Optional[Attributes]" = None,
|
|
) -> None:
|
|
"""Update the scope's attributes."""
|
|
if level is not None:
|
|
self._level = level
|
|
if user is not None:
|
|
self._user = user
|
|
if extras is not None:
|
|
self._extras.update(extras)
|
|
if contexts is not None:
|
|
self._contexts.update(contexts)
|
|
if tags is not None:
|
|
self._tags.update(tags)
|
|
if fingerprint is not None:
|
|
self._fingerprint = fingerprint
|
|
if attributes is not None:
|
|
self._attributes.update(attributes)
|
|
|
|
def __repr__(self) -> str:
|
|
return "<%s id=%s name=%s type=%s>" % (
|
|
self.__class__.__name__,
|
|
hex(id(self)),
|
|
self._name,
|
|
self._type,
|
|
)
|
|
|
|
@property
|
|
def flags(self) -> "FlagBuffer":
|
|
if self._flags is None:
|
|
max_flags = (
|
|
self.get_client().options["_experiments"].get("max_flags")
|
|
or DEFAULT_FLAG_CAPACITY
|
|
)
|
|
self._flags = FlagBuffer(capacity=max_flags)
|
|
return self._flags
|
|
|
|
def set_attribute(self, attribute: str, value: "AttributeValue") -> None:
|
|
"""
|
|
Set an attribute on the scope.
|
|
|
|
Any attributes-based telemetry (logs, metrics) captured while this scope
|
|
is active will inherit attributes set on the scope.
|
|
"""
|
|
self._attributes[attribute] = format_attribute(value)
|
|
|
|
def remove_attribute(self, attribute: str) -> None:
|
|
"""Remove an attribute if set on the scope. No-op if there is no such attribute."""
|
|
try:
|
|
del self._attributes[attribute]
|
|
except KeyError:
|
|
pass
|
|
|
|
|
|
@contextmanager
|
|
def new_scope() -> "Generator[Scope, None, None]":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Context manager that forks the current scope and runs the wrapped code in it.
|
|
After the wrapped code is executed, the original scope is restored.
|
|
|
|
Example Usage:
|
|
|
|
.. code-block:: python
|
|
|
|
import sentry_sdk
|
|
|
|
with sentry_sdk.new_scope() as scope:
|
|
scope.set_tag("color", "green")
|
|
sentry_sdk.capture_message("hello") # will include `color` tag.
|
|
|
|
sentry_sdk.capture_message("hello, again") # will NOT include `color` tag.
|
|
|
|
"""
|
|
# fork current scope
|
|
current_scope = Scope.get_current_scope()
|
|
new_scope = current_scope.fork()
|
|
token = _current_scope.set(new_scope)
|
|
|
|
try:
|
|
yield new_scope
|
|
|
|
finally:
|
|
try:
|
|
# restore original scope
|
|
_current_scope.reset(token)
|
|
except (LookupError, ValueError):
|
|
capture_internal_exception(sys.exc_info())
|
|
|
|
|
|
@contextmanager
|
|
def use_scope(scope: "Scope") -> "Generator[Scope, None, None]":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Context manager that uses the given `scope` and runs the wrapped code in it.
|
|
After the wrapped code is executed, the original scope is restored.
|
|
|
|
Example Usage:
|
|
Suppose the variable `scope` contains a `Scope` object, which is not currently
|
|
the active scope.
|
|
|
|
.. code-block:: python
|
|
|
|
import sentry_sdk
|
|
|
|
with sentry_sdk.use_scope(scope):
|
|
scope.set_tag("color", "green")
|
|
sentry_sdk.capture_message("hello") # will include `color` tag.
|
|
|
|
sentry_sdk.capture_message("hello, again") # will NOT include `color` tag.
|
|
|
|
"""
|
|
# set given scope as current scope
|
|
token = _current_scope.set(scope)
|
|
|
|
try:
|
|
yield scope
|
|
|
|
finally:
|
|
try:
|
|
# restore original scope
|
|
_current_scope.reset(token)
|
|
except (LookupError, ValueError):
|
|
capture_internal_exception(sys.exc_info())
|
|
|
|
|
|
@contextmanager
|
|
def isolation_scope() -> "Generator[Scope, None, None]":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Context manager that forks the current isolation scope and runs the wrapped code in it.
|
|
The current scope is also forked to not bleed data into the existing current scope.
|
|
After the wrapped code is executed, the original scopes are restored.
|
|
|
|
Example Usage:
|
|
|
|
.. code-block:: python
|
|
|
|
import sentry_sdk
|
|
|
|
with sentry_sdk.isolation_scope() as scope:
|
|
scope.set_tag("color", "green")
|
|
sentry_sdk.capture_message("hello") # will include `color` tag.
|
|
|
|
sentry_sdk.capture_message("hello, again") # will NOT include `color` tag.
|
|
|
|
"""
|
|
# fork current scope
|
|
current_scope = Scope.get_current_scope()
|
|
forked_current_scope = current_scope.fork()
|
|
current_token = _current_scope.set(forked_current_scope)
|
|
|
|
# fork isolation scope
|
|
isolation_scope = Scope.get_isolation_scope()
|
|
new_isolation_scope = isolation_scope.fork()
|
|
isolation_token = _isolation_scope.set(new_isolation_scope)
|
|
|
|
try:
|
|
yield new_isolation_scope
|
|
|
|
finally:
|
|
# restore original scopes
|
|
try:
|
|
_current_scope.reset(current_token)
|
|
except (LookupError, ValueError):
|
|
capture_internal_exception(sys.exc_info())
|
|
|
|
try:
|
|
_isolation_scope.reset(isolation_token)
|
|
except (LookupError, ValueError):
|
|
capture_internal_exception(sys.exc_info())
|
|
|
|
|
|
@contextmanager
|
|
def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, None]":
|
|
"""
|
|
.. versionadded:: 2.0.0
|
|
|
|
Context manager that uses the given `isolation_scope` and runs the wrapped code in it.
|
|
The current scope is also forked to not bleed data into the existing current scope.
|
|
After the wrapped code is executed, the original scopes are restored.
|
|
|
|
Example Usage:
|
|
|
|
.. code-block:: python
|
|
|
|
import sentry_sdk
|
|
|
|
with sentry_sdk.isolation_scope() as scope:
|
|
scope.set_tag("color", "green")
|
|
sentry_sdk.capture_message("hello") # will include `color` tag.
|
|
|
|
sentry_sdk.capture_message("hello, again") # will NOT include `color` tag.
|
|
|
|
"""
|
|
# fork current scope
|
|
current_scope = Scope.get_current_scope()
|
|
forked_current_scope = current_scope.fork()
|
|
current_token = _current_scope.set(forked_current_scope)
|
|
|
|
# set given scope as isolation scope
|
|
isolation_token = _isolation_scope.set(isolation_scope)
|
|
|
|
try:
|
|
yield isolation_scope
|
|
|
|
finally:
|
|
# restore original scopes
|
|
try:
|
|
_current_scope.reset(current_token)
|
|
except (LookupError, ValueError):
|
|
capture_internal_exception(sys.exc_info())
|
|
|
|
try:
|
|
_isolation_scope.reset(isolation_token)
|
|
except (LookupError, ValueError):
|
|
capture_internal_exception(sys.exc_info())
|
|
|
|
|
|
def should_send_default_pii() -> bool:
|
|
"""Shortcut for `Scope.get_client().should_send_default_pii()`."""
|
|
return Scope.get_client().should_send_default_pii()
|
|
|
|
|
|
# Circular imports
|
|
from sentry_sdk.client import NonRecordingClient
|
|
|
|
if TYPE_CHECKING:
|
|
import sentry_sdk.client
|