import base64 import json import linecache import logging import math import os import random import re import subprocess import sys import threading import time from collections import namedtuple from datetime import datetime, timezone from decimal import Decimal from functools import partial, partialmethod, wraps from numbers import Real from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
try: # Python 3.11 from builtins import BaseExceptionGroup except ImportError: # Python 3.10 and below
BaseExceptionGroup = None# type: ignore
import sentry_sdk from sentry_sdk._compat import PY37 from sentry_sdk.consts import (
DEFAULT_ADD_FULL_STACK,
DEFAULT_MAX_STACK_FRAMES,
DEFAULT_MAX_VALUE_LENGTH,
EndpointType,
) from sentry_sdk._types import Annotated, AnnotatedValue, SENSITIVE_DATA_SUBSTITUTE
from typing import TYPE_CHECKING
if TYPE_CHECKING: from types import FrameType, TracebackType from typing import (
Any,
Callable,
cast,
ContextManager,
Dict,
Iterator,
List,
NoReturn,
Optional,
overload,
ParamSpec,
Set,
Tuple,
Type,
TypeVar,
Union,
)
from gevent.hub import Hub
from sentry_sdk._types import Event, ExcInfo
P = ParamSpec("P")
R = TypeVar("R")
epoch = datetime(1970, 1, 1)
# The logger is created here but initialized in the debug support module
logger = logging.getLogger("sentry_sdk.errors")
MAX_STACK_FRAMES = 2000 """Maximum number of stack frames to send to Sentry.
If we have more than this number of stack frames, we will stop processing
the stacktrace to avoid getting stuck in a long-lasting loop. This value
exceeds the default sys.getrecursionlimit() of 1000, so users will only
be affected by this limit if they have a custom recursion limit. """
def env_to_bool(value, *, strict=False): # type: (Any, Optional[bool]) -> bool | None """Casts an ENV variable value to boolean using the constants defined above. In strict mode, it may returnNoneif the value doesn't match any of the predefined values. """
normalized = str(value).lower() if value isnotNoneelseNone
if normalized in FALSY_ENV_VALUES: returnFalse
if normalized in TRUTHY_ENV_VALUES: returnTrue
returnNoneif strict else bool(value)
def json_dumps(data): # type: (Any) -> bytes """Serialize data into a compact JSON representation encoded as UTF-8.""" return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8")
def get_git_revision(): # type: () -> Optional[str] try: with open(os.path.devnull, "w+") as null: # prevent command prompt windows from popping up on windows
startupinfo = None if sys.platform == "win32"or sys.platform == "cygwin":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
def get_default_release(): # type: () -> Optional[str] """Try to guess a default release."""
release = os.environ.get("SENTRY_RELEASE") if release: return release
release = get_git_revision() if release: return release
for var in ( "HEROKU_SLUG_COMMIT", "SOURCE_VERSION", "CODEBUILD_RESOLVED_SOURCE_VERSION", "CIRCLE_SHA1", "GAE_DEPLOYMENT_ID",
):
release = os.environ.get(var) if release: return release returnNone
def get_sdk_name(installed_integrations): # type: (List[str]) -> str """Return the SDK name including the name of the used web framework."""
# Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier # here because if django is not installed the integration is not accessible.
framework_integrations = [ "django", "flask", "fastapi", "bottle", "falcon", "quart", "sanic", "starlette", "litestar", "starlite", "chalice", "serverless", "pyramid", "tornado", "aiohttp", "aws_lambda", "gcp", "beam", "asgi", "wsgi",
]
for integration in framework_integrations: if integration in installed_integrations: return"sentry.python.{}".format(integration)
def capture_internal_exception(exc_info): # type: (ExcInfo) -> None """
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. """ if sentry_sdk.get_client().is_active():
logger.error("Internal error in sentry_sdk", exc_info=exc_info)
def format_timestamp(value): # type: (datetime) -> str """Formats a timestamp in RFC 3339 format.
Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC. """
utctime = value.astimezone(timezone.utc)
# We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for # several years now), and isoformat is slightly different. return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
ISO_TZ_SEPARATORS = frozenset(("+", "-"))
def datetime_from_isoformat(value): # type: (str) -> datetime try:
result = datetime.fromisoformat(value) except (AttributeError, ValueError): # py 3.6
timestamp_format = ( "%Y-%m-%dT%H:%M:%S.%f"if"."in value else"%Y-%m-%dT%H:%M:%S"
) if value.endswith("Z"):
value = value[:-1] + "+0000"
if value[-6] in ISO_TZ_SEPARATORS:
timestamp_format += "%z"
value = value[:-3] + value[-2:] elif value[-5] in ISO_TZ_SEPARATORS:
timestamp_format += "%z"
result = datetime.strptime(value, timestamp_format) return result.astimezone(timezone.utc)
def event_hint_with_exc_info(exc_info=None): # type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]] """Creates a hint with the exc info filled in.""" if exc_info isNone:
exc_info = sys.exc_info() else:
exc_info = exc_info_from_error(exc_info) if exc_info[0] isNone:
exc_info = None return {"exc_info": exc_info}
class BadDsn(ValueError): """Raised on invalid DSNs."""
class Dsn: """Represents a DSN."""
def __init__(self, value): # type: (Union[Dsn, str]) -> None if isinstance(value, Dsn):
self.__dict__ = dict(value.__dict__) return
parts = urlsplit(str(value))
try:
pre_context = [
strip_string(line.strip("\r\n"), max_length=max_length) for line in source[lower_bound:lineno]
]
context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length)
post_context = [
strip_string(line.strip("\r\n"), max_length=max_length) for line in source[(lineno + 1) : upper_bound]
] return pre_context, context_line, post_context except IndexError: # the file may have changed since it was loaded into memory return [], None, []
# __notes__ should be a list of strings when notes are added # via add_note, but can be anything else if __notes__ is set # directly. We only support strings in __notes__, since that # is the correct use.
notes = getattr(exc_value, "__notes__", None) # type: object if isinstance(notes, list) and len(notes) > 0:
message += "\n" + "\n".join(note for note in notes if isinstance(note, str))
return message
def single_exception_from_error_tuple(
exc_type, # type: Optional[type]
exc_value, # type: Optional[BaseException]
tb, # type: Optional[TracebackType]
client_options=None, # type: Optional[Dict[str, Any]]
mechanism=None, # type: Optional[Dict[str, Any]]
exception_id=None, # type: Optional[int]
parent_id=None, # type: Optional[int]
source=None, # type: Optional[str]
full_stack=None, # type: Optional[list[dict[str, Any]]]
): # type: (...) -> Dict[str, Any] """
Creates a dict that goes into the events `exception.values` list andis ingestible by Sentry.
See the Exception Interface documentation for more details: https://develop.sentry.dev/sdk/event-payloads/exception/ """
exception_value = {} # type: Dict[str, Any]
exception_value["mechanism"] = (
mechanism.copy() if mechanism else {"type": "generic", "handled": True}
) if exception_id isnotNone:
exception_value["mechanism"]["exception_id"] = exception_id
if exc_value isnotNone:
errno = get_errno(exc_value) else:
errno = None
if errno isnotNone:
exception_value["mechanism"].setdefault("meta", {}).setdefault( "errno", {}
).setdefault("number", errno)
if source isnotNone:
exception_value["mechanism"]["source"] = source
frames = [
serialize_frame(
tb.tb_frame,
tb_lineno=tb.tb_lineno,
include_local_variables=include_local_variables,
include_source_context=include_source_context,
max_value_length=max_value_length,
custom_repr=custom_repr,
) # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on # processing a super-long stacktrace. for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1))
] # type: List[Dict[str, Any]]
if len(frames) > MAX_STACK_FRAMES: # If we have more frames than the limit, we remove the stacktrace completely. # We don't trim the stacktrace here because we have not processed the whole # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would # intelligently trim by removing frames in the middle of the stacktrace, but # since we don't have the whole stacktrace, we can't do that. Instead, we # drop the entire stacktrace.
exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit(
value=None
)
while (
exc_type isnotNone and exc_value isnotNone and id(exc_value) notin seen_exception_ids
): yield exc_type, exc_value, tb
# Avoid hashing random types we don't know anything # about. Use the list to keep a ref so that the `id` is # not used for another object.
seen_exceptions.append(exc_value)
seen_exception_ids.add(id(exc_value))
if exc_value.__suppress_context__:
cause = exc_value.__cause__ else:
cause = exc_value.__context__ if cause isNone: break
exc_type = type(cause)
exc_value = cause
tb = getattr(cause, "__traceback__", None)
should_supress_context = hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore if should_supress_context: # Add direct cause. # The field `__cause__` is set when raised with the exception (using the `from` keyword).
exception_has_cause = (
exc_value and hasattr(exc_value, "__cause__") and exc_value.__cause__ isnotNone
) if exception_has_cause:
cause = exc_value.__cause__ # type: ignore
(exception_id, child_exceptions) = exceptions_from_error(
exc_type=type(cause),
exc_value=cause,
tb=getattr(cause, "__traceback__", None),
client_options=client_options,
mechanism=mechanism,
exception_id=exception_id,
source="__cause__",
full_stack=full_stack,
)
exceptions.extend(child_exceptions)
else: # Add indirect cause. # The field `__context__` is assigned if another exception occurs while handling the exception.
exception_has_content = (
exc_value and hasattr(exc_value, "__context__") and exc_value.__context__ isnotNone
) if exception_has_content:
context = exc_value.__context__ # type: ignore
(exception_id, child_exceptions) = exceptions_from_error(
exc_type=type(context),
exc_value=context,
tb=getattr(context, "__traceback__", None),
client_options=client_options,
mechanism=mechanism,
exception_id=exception_id,
source="__context__",
full_stack=full_stack,
)
exceptions.extend(child_exceptions)
# Add exceptions from an ExceptionGroup.
is_exception_group = exc_value and hasattr(exc_value, "exceptions") if is_exception_group: for idx, e in enumerate(exc_value.exceptions): # type: ignore
(exception_id, child_exceptions) = exceptions_from_error(
exc_type=type(e),
exc_value=e,
tb=getattr(e, "__traceback__", None),
client_options=client_options,
mechanism=mechanism,
exception_id=exception_id,
parent_id=parent_id,
source="exceptions[%s]" % idx,
full_stack=full_stack,
)
exceptions.extend(child_exceptions)
def iter_event_stacktraces(event): # type: (Event) -> Iterator[Annotated[Dict[str, Any]]] if"stacktrace"in event: yield event["stacktrace"] if"threads"in event: for thread in event["threads"].get("values") or (): if"stacktrace"in thread: yield thread["stacktrace"] if"exception"in event: for exception in event["exception"].get("values") or (): if isinstance(exception, dict) and"stacktrace"in exception: yield exception["stacktrace"]
def iter_event_frames(event): # type: (Event) -> Iterator[Dict[str, Any]] for stacktrace in iter_event_stacktraces(event): if isinstance(stacktrace, AnnotatedValue):
stacktrace = stacktrace.value or {}
for frame in stacktrace.get("frames") or (): yield frame
def handle_in_app(event, in_app_exclude=None, in_app_include=None, project_root=None): # type: (Event, Optional[List[str]], Optional[List[str]], Optional[str]) -> Event for stacktrace in iter_event_stacktraces(event): if isinstance(stacktrace, AnnotatedValue):
stacktrace = stacktrace.value or {}
if TYPE_CHECKING: # This cast is safe because exc_type and exc_value are either both # None or both not None.
exc_info = cast(ExcInfo, exc_info)
return exc_info
def merge_stack_frames(frames, full_stack, client_options): # type: (List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]]) -> List[Dict[str, Any]] """
Add the missing frames from full_stack to frames andreturn the merged list. """
frame_ids = {
(
frame["abs_path"],
frame["context_line"],
frame["lineno"],
frame["function"],
) for frame in frames
}
new_frames = [
stackframe for stackframe in full_stack if (
stackframe["abs_path"],
stackframe["context_line"],
stackframe["lineno"],
stackframe["function"],
) notin frame_ids
]
new_frames.extend(frames)
# Limit the number of frames
max_stack_frames = (
client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES) if client_options elseNone
) if max_stack_frames isnotNone:
new_frames = new_frames[len(new_frames) - max_stack_frames :]
def _module_in_list(name, items): # type: (Optional[str], Optional[List[str]]) -> bool if name isNone: returnFalse
ifnot items: returnFalse
for item in items: if item == name or name.startswith(item + "."): returnTrue
returnFalse
def _is_external_source(abs_path): # type: (Optional[str]) -> bool # check if frame is in 'site-packages' or 'dist-packages' if abs_path isNone: returnFalse
# check if path is in the project root if abs_path.startswith(project_root): returnTrue
returnFalse
def _truncate_by_bytes(string, max_bytes): # type: (str, int) -> str """
Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes. """
truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore")
def _is_contextvars_broken(): # type: () -> bool """
Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars. """ try: import gevent from gevent.monkey import is_object_patched
# Get the MAJOR and MINOR version numbers of Gevent
version_tuple = tuple(
[int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
) if is_object_patched("threading", "local"): # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine. # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609 # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support # for contextvars, is able to patch both thread locals and contextvars, in # that case, check if contextvars are effectively patched. if ( # Gevent 20.9.0+
(sys.version_info >= (3, 7) and version_tuple >= (20, 9)) # Gevent 20.5.0+ or Python < 3.7 or (is_object_patched("contextvars", "ContextVar"))
): returnFalse
returnTrue except ImportError: pass
try: import greenlet from eventlet.patcher import is_monkey_patched # type: ignore
def reset(self, token): # type: (Any) -> None
self._local.value = getattr(self._original_local, token) # delete the original value (this way it works in Python 3.6+) del self._original_local.__dict__[token]
return ContextVar
def _get_contextvars(): # type: () -> Tuple[bool, type] """
Figure out the "right" contextvars installation to use. Returns a
`contextvars.ContextVar`-like classwith a limited API.
See https://docs.sentry.io/platforms/python/contextvars/for more information. """ ifnot _is_contextvars_broken(): # aiocontextvars is a PyPI package that ensures that the contextvars # backport (also a PyPI package) works with asyncio under Python 3.6 # # Import it if available. if sys.version_info < (3, 7): # `aiocontextvars` is absolutely required for functional # contextvars on Python 3.6. try: from aiocontextvars import ContextVar
returnTrue, ContextVar except ImportError: pass else: # On Python 3.7 contextvars are functional. try: from contextvars import ContextVar
With asyncio/ASGI applications, the Sentry SDK requires a functional
installation of `contextvars` to avoid leaking scope/context data across
requests.
def qualname_from_function(func): # type: (Callable[..., Any]) -> Optional[str] """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
func_qualname = None# type: Optional[str]
class ServerlessTimeoutWarning(Exception): # noqa: N818 """Raised when a serverless method is about to reach its timeout."""
pass
class TimeoutThread(threading.Thread): """Creates a Thread which runs (sleeps) for a time duration equal to
waiting_time and raises a custom ServerlessTimeout exception. """
# Setting up the exact integer value of configured time(in seconds) if integer_configured_timeout < self.configured_timeout:
integer_configured_timeout = integer_configured_timeout + 1
# Raising Exception after timeout duration is reached raise ServerlessTimeoutWarning( "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
integer_configured_timeout
)
)
def to_base64(original): # type: (str) -> Optional[str] """
Convert a string to base64, via UTF-8. Returns None on invalid input. """
base64_string = None
try:
utf8_bytes = original.encode("UTF-8")
base64_bytes = base64.b64encode(utf8_bytes)
base64_string = base64_bytes.decode("UTF-8") except Exception as err:
logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
return base64_string
def from_base64(base64_string): # type: (str) -> Optional[str] """
Convert a string from base64, via UTF-8. Returns None on invalid input. """
utf8_string = None
def parse_url(url, sanitize=True): # type: (str, bool) -> ParsedUrl """
Splits a URL into a url (including path), query and fragment. If sanitize isTrue, the query
parameters will be sanitized to remove sensitive data. The autority (username and password) in the URL will always be removed. """
parsed_url = sanitize_url(
url, remove_authority=True, remove_query_values=sanitize, split=True
)
def is_valid_sample_rate(rate, source): # type: (Any, str) -> bool """
Checks the given sample rate to make sure it is valid type and value (a
boolean or a number between 0and1, inclusive). """
# both booleans and NaN are instances of Real, so a) checking for Real # checks for the possibility of a boolean also, and b) we have to check # separately for NaN and Decimal does not derive from Real so need to check that too ifnot isinstance(rate, (Real, Decimal)) or math.isnan(rate):
logger.warning( "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
source=source, rate=rate, type=type(rate)
)
) returnFalse
# in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
rate = float(rate) if rate < 0or rate > 1:
logger.warning( "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
source=source, rate=rate
)
) returnFalse
for item_matcher in regex_list: ifnot substring_matching and item_matcher[-1] != "$":
item_matcher += "$"
matched = re.search(item_matcher, item) if matched: returnTrue
returnFalse
def is_sentry_url(client, url): # type: (sentry_sdk.client.BaseClient, str) -> bool """
Determines whether the given URL matches the Sentry DSN. """ return (
client isnotNone and client.transport isnotNone and client.transport.parsed_dsn isnotNone and client.transport.parsed_dsn.netloc in url
)
yielded = set() for dist in metadata.distributions():
name = dist.metadata.get("Name", None) # type: ignore[attr-defined] # `metadata` values may be `None`, see: # https://github.com/python/cpython/issues/91216 # and # https://github.com/python/importlib_metadata/issues/371 if name isnotNone:
normalized_name = _normalize_module_name(name) if dist.version isnotNoneand normalized_name notin yielded: yield normalized_name, dist.version
yielded.add(normalized_name)
def ensure_integration_enabled(
integration, # type: type[sentry_sdk.integrations.Integration]
original_function=_no_op, # type: Union[Callable[P, R], Callable[P, None]]
): # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]] """
Ensures a given integration is enabled prior to calling a Sentry-patched function.
The function takes as its parameters the integration that must be enabled and the original
function that the SDK is patching. The function returns a function that takes the
decorated (Sentry-patched) function as its parameter, and returns a function that, when
called, checks whether the given integration is enabled. If the integration is enabled, the
function calls the decorated, Sentry-patched function. If the integration isnot enabled,
the original function is called.
The function also takes care of preserving the original function's signature and docstring.
Example usage:
```python
@ensure_integration_enabled(MyIntegration, my_function) def patch_my_function(): with sentry_sdk.start_transaction(...): return my_function()
``` """ if TYPE_CHECKING: # Type hint to ensure the default function has the right typing. The overloads # ensure the default _no_op function is only used when R is None.
original_function = cast(Callable[P, R], original_function)
try: from gevent import get_hub as get_gevent_hub from gevent.monkey import is_module_patched except ImportError:
# it's not great that the signatures are different, get_hub can't return None # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub] def get_gevent_hub(): # type: ignore[misc] # type: () -> Optional[Hub] returnNone
def is_module_patched(mod_name): # type: (str) -> bool # unable to import from gevent means no modules have been patched returnFalse
def get_current_thread_meta(thread=None): # type: (Optional[threading.Thread]) -> Tuple[Optional[int], Optional[str]] """ Try to get the id of the current thread, with various fall backs. """
# if a thread is specified, that takes priority if thread isnotNone: try:
thread_id = thread.ident
thread_name = thread.name if thread_id isnotNone: return thread_id, thread_name except AttributeError: pass
# if the app is using gevent, we should look at the gevent hub first # as the id there differs from what the threading module reports if is_gevent():
gevent_hub = get_gevent_hub() if gevent_hub isnotNone: try: # this is undocumented, so wrap it in try except to be safe return gevent_hub.thread_ident, None except AttributeError: pass
# use the current thread's id if possible try:
thread = threading.current_thread()
thread_id = thread.ident
thread_name = thread.name if thread_id isnotNone: return thread_id, thread_name except AttributeError: pass
# if we can't get the current thread id, fall back to the main thread id try:
thread = threading.main_thread()
thread_id = thread.ident
thread_name = thread.name if thread_id isnotNone: return thread_id, thread_name except AttributeError: pass
# we've tried everything, time to give up returnNone, None
def try_convert(convert_func, value): # type: (Callable[[Any], T], Any) -> Optional[T] """
Attempt to convert from an unknown type to a specific type, using the
given function. ReturnNoneif the conversion fails, i.e. if the function
raises an exception. """ try: return convert_func(value) except Exception: returnNone
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.