2025-12-01
This commit is contained in:
@@ -59,7 +59,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from gevent.hub import Hub
|
||||
|
||||
from sentry_sdk._types import Event, ExcInfo
|
||||
from sentry_sdk._types import Event, ExcInfo, Log, Hint, Metric
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
@@ -77,6 +77,15 @@ BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$")
|
||||
FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0"))
|
||||
TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1"))
|
||||
|
||||
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
|
||||
@@ -380,7 +389,8 @@ class Auth:
|
||||
self.client = client
|
||||
|
||||
def get_api_url(
|
||||
self, type=EndpointType.ENVELOPE # type: EndpointType
|
||||
self,
|
||||
type=EndpointType.ENVELOPE, # type: EndpointType
|
||||
):
|
||||
# type: (...) -> str
|
||||
"""Returns the API url for storing events."""
|
||||
@@ -582,9 +592,14 @@ def serialize_frame(
|
||||
if tb_lineno is None:
|
||||
tb_lineno = frame.f_lineno
|
||||
|
||||
try:
|
||||
os_abs_path = os.path.abspath(abs_path) if abs_path else None
|
||||
except Exception:
|
||||
os_abs_path = None
|
||||
|
||||
rv = {
|
||||
"filename": filename_for_module(module, abs_path) or None,
|
||||
"abs_path": os.path.abspath(abs_path) if abs_path else None,
|
||||
"abs_path": os_abs_path,
|
||||
"function": function or "<unknown>",
|
||||
"module": module,
|
||||
"lineno": tb_lineno,
|
||||
@@ -732,10 +747,23 @@ def single_exception_from_error_tuple(
|
||||
max_value_length=max_value_length,
|
||||
custom_repr=custom_repr,
|
||||
)
|
||||
for tb in iter_stacks(tb)
|
||||
# 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 frames:
|
||||
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
|
||||
)
|
||||
|
||||
elif frames:
|
||||
if not full_stack:
|
||||
new_frames = frames
|
||||
else:
|
||||
@@ -823,7 +851,9 @@ def exceptions_from_error(
|
||||
parent_id = exception_id
|
||||
exception_id += 1
|
||||
|
||||
should_supress_context = hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore
|
||||
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).
|
||||
@@ -941,7 +971,7 @@ def to_string(value):
|
||||
|
||||
|
||||
def iter_event_stacktraces(event):
|
||||
# type: (Event) -> Iterator[Dict[str, Any]]
|
||||
# type: (Event) -> Iterator[Annotated[Dict[str, Any]]]
|
||||
if "stacktrace" in event:
|
||||
yield event["stacktrace"]
|
||||
if "threads" in event:
|
||||
@@ -950,13 +980,16 @@ def iter_event_stacktraces(event):
|
||||
yield thread["stacktrace"]
|
||||
if "exception" in event:
|
||||
for exception in event["exception"].get("values") or ():
|
||||
if "stacktrace" in exception:
|
||||
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
|
||||
|
||||
@@ -964,6 +997,9 @@ def iter_event_frames(event):
|
||||
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 {}
|
||||
|
||||
set_in_app_in_frames(
|
||||
stacktrace.get("frames"),
|
||||
in_app_exclude=in_app_exclude,
|
||||
@@ -1448,17 +1484,37 @@ class TimeoutThread(threading.Thread):
|
||||
waiting_time and raises a custom ServerlessTimeout exception.
|
||||
"""
|
||||
|
||||
def __init__(self, waiting_time, configured_timeout):
|
||||
# type: (float, int) -> None
|
||||
def __init__(
|
||||
self, waiting_time, configured_timeout, isolation_scope=None, current_scope=None
|
||||
):
|
||||
# type: (float, int, Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]) -> None
|
||||
threading.Thread.__init__(self)
|
||||
self.waiting_time = waiting_time
|
||||
self.configured_timeout = configured_timeout
|
||||
|
||||
self.isolation_scope = isolation_scope
|
||||
self.current_scope = current_scope
|
||||
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
# type: () -> None
|
||||
self._stop_event.set()
|
||||
|
||||
def _capture_exception(self):
|
||||
# type: () -> ExcInfo
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
client = sentry_sdk.get_client()
|
||||
event, hint = event_from_exception(
|
||||
exc_info,
|
||||
client_options=client.options,
|
||||
mechanism={"type": "threading", "handled": False},
|
||||
)
|
||||
sentry_sdk.capture_event(event, hint=hint)
|
||||
|
||||
return exc_info
|
||||
|
||||
def run(self):
|
||||
# type: () -> None
|
||||
|
||||
@@ -1474,6 +1530,18 @@ class TimeoutThread(threading.Thread):
|
||||
integer_configured_timeout = integer_configured_timeout + 1
|
||||
|
||||
# Raising Exception after timeout duration is reached
|
||||
if self.isolation_scope is not None and self.current_scope is not None:
|
||||
with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
|
||||
with sentry_sdk.scope.use_scope(self.current_scope):
|
||||
try:
|
||||
raise ServerlessTimeoutWarning(
|
||||
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
|
||||
integer_configured_timeout
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
reraise(*self._capture_exception())
|
||||
|
||||
raise ServerlessTimeoutWarning(
|
||||
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
|
||||
integer_configured_timeout
|
||||
@@ -1812,7 +1880,6 @@ 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]
|
||||
@@ -1888,3 +1955,109 @@ def should_be_treated_as_error(ty, value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
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. Return None if the conversion fails, i.e. if the function
|
||||
raises an exception.
|
||||
"""
|
||||
try:
|
||||
if isinstance(value, convert_func): # type: ignore
|
||||
return value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return convert_func(value)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def safe_serialize(data):
|
||||
# type: (Any) -> str
|
||||
"""Safely serialize to a readable string."""
|
||||
|
||||
def serialize_item(item):
|
||||
# type: (Any) -> Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]
|
||||
if callable(item):
|
||||
try:
|
||||
module = getattr(item, "__module__", None)
|
||||
qualname = getattr(item, "__qualname__", None)
|
||||
name = getattr(item, "__name__", "anonymous")
|
||||
|
||||
if module and qualname:
|
||||
full_path = f"{module}.{qualname}"
|
||||
elif module and name:
|
||||
full_path = f"{module}.{name}"
|
||||
else:
|
||||
full_path = name
|
||||
|
||||
return f"<function {full_path}>"
|
||||
except Exception:
|
||||
return f"<callable {type(item).__name__}>"
|
||||
elif isinstance(item, dict):
|
||||
return {k: serialize_item(v) for k, v in item.items()}
|
||||
elif isinstance(item, (list, tuple)):
|
||||
return [serialize_item(x) for x in item]
|
||||
elif hasattr(item, "__dict__"):
|
||||
try:
|
||||
attrs = {
|
||||
k: serialize_item(v)
|
||||
for k, v in vars(item).items()
|
||||
if not k.startswith("_")
|
||||
}
|
||||
return f"<{type(item).__name__} {attrs}>"
|
||||
except Exception:
|
||||
return repr(item)
|
||||
else:
|
||||
return item
|
||||
|
||||
try:
|
||||
serialized = serialize_item(data)
|
||||
return json.dumps(serialized, default=str)
|
||||
except Exception:
|
||||
return str(data)
|
||||
|
||||
|
||||
def has_logs_enabled(options):
|
||||
# type: (Optional[dict[str, Any]]) -> bool
|
||||
if options is None:
|
||||
return False
|
||||
|
||||
return bool(
|
||||
options.get("enable_logs", False)
|
||||
or options["_experiments"].get("enable_logs", False)
|
||||
)
|
||||
|
||||
|
||||
def get_before_send_log(options):
|
||||
# type: (Optional[dict[str, Any]]) -> Optional[Callable[[Log, Hint], Optional[Log]]]
|
||||
if options is None:
|
||||
return None
|
||||
|
||||
return options.get("before_send_log") or options["_experiments"].get(
|
||||
"before_send_log"
|
||||
)
|
||||
|
||||
|
||||
def has_metrics_enabled(options):
|
||||
# type: (Optional[dict[str, Any]]) -> bool
|
||||
if options is None:
|
||||
return False
|
||||
|
||||
return bool(options["_experiments"].get("enable_metrics", False))
|
||||
|
||||
|
||||
def get_before_send_metric(options):
|
||||
# type: (Optional[dict[str, Any]]) -> Optional[Callable[[Metric, Hint], Optional[Metric]]]
|
||||
if options is None:
|
||||
return None
|
||||
|
||||
return options["_experiments"].get("before_send_metric")
|
||||
|
||||
Reference in New Issue
Block a user