Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/sentry/lang/java/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ def process_jvm_stacktraces(symbolicator: Symbolicator, data: Any) -> Any:
stacktrace_infos = find_stacktraces_in_data(data)
stacktraces = [
{
**(
{"exception": {"type": sinfo.exception_type, "module": sinfo.exception_module}}
if sinfo.exception_type and sinfo.exception_module
else {}
),
"frames": [
_normalize_frame(frame, index)
for index, frame in enumerate(sinfo.stacktrace.get("frames") or ())
Expand Down
18 changes: 18 additions & 0 deletions src/sentry/stacktraces/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class StacktraceInfo(NamedTuple):
container: dict[str, Any]
platforms: set[str]
is_exception: bool
exception_type: str | None
exception_module: str | None

def __hash__(self) -> int:
return id(self)
Expand All @@ -50,6 +52,14 @@ def __eq__(self, other: object) -> bool:
def __ne__(self, other: object) -> bool:
return self is not other

def get_exception(self) -> str | None:
"""Returns the fully qualified exception name (module.type) or None if not an exception."""
if self.exception_type is None:
return None
if self.exception_module:
return f"{self.exception_module}.{self.exception_type}"
return self.exception_type

def get_frames(self) -> Sequence[dict[str, Any]]:
return _safe_get_frames(self.stacktrace)

Expand Down Expand Up @@ -225,6 +235,10 @@ def _append_stacktrace(
container: Any = None,
# Whether or not the container is from `exception.values`
is_exception: bool = False,
# The exception type (e.g., "ValueError") if this stacktrace belongs to an exception
exception_type: str | None = None,
# The exception module (e.g., "__builtins__") if this stacktrace belongs to an exception
exception_module: str | None = None,
# Prevent skipping empty/null stacktraces from `exception.values` (other empty/null
# stacktraces are always skipped)
include_empty_exceptions: bool = False,
Expand All @@ -244,6 +258,8 @@ def _append_stacktrace(
container=container,
platforms=platforms,
is_exception=is_exception,
exception_type=exception_type,
exception_module=exception_module,
)
)

Expand All @@ -253,6 +269,8 @@ def _append_stacktrace(
exc.get("stacktrace"),
container=exc,
is_exception=True,
exception_type=exc.get("type"),
exception_module=exc.get("module"),
include_empty_exceptions=include_empty_exceptions,
)

Expand Down
52 changes: 52 additions & 0 deletions tests/sentry/test_stacktraces.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def test_stacktraces_basics(self) -> None:
assert len(infos) == 1
assert len(infos[0].stacktrace["frames"]) == 2
assert infos[0].platforms == {"javascript", "native"}
# Top-level stacktraces are not exceptions
assert infos[0].is_exception is False
assert infos[0].exception_type is None
assert infos[0].exception_module is None
assert infos[0].get_exception() is None

def test_stacktraces_exception(self) -> None:
data: dict[str, Any] = {
Expand Down Expand Up @@ -69,6 +74,42 @@ def test_stacktraces_exception(self) -> None:
infos = find_stacktraces_in_data(data)
assert len(infos) == 1
assert len(infos[0].stacktrace["frames"]) == 2
# Exception stacktraces have type but no module in this case
assert infos[0].is_exception is True
assert infos[0].exception_type == "Error"
assert infos[0].exception_module is None
assert infos[0].get_exception() == "Error"

def test_stacktraces_exception_with_module(self) -> None:
data: dict[str, Any] = {
"message": "hello",
"platform": "java",
"exception": {
"values": [
{
"type": "RuntimeException",
"module": "java.lang",
"stacktrace": {
"frames": [
{
"function": "main",
"module": "com.example.App",
"filename": "App.java",
"lineno": 10,
},
]
},
}
]
},
}

infos = find_stacktraces_in_data(data)
assert len(infos) == 1
assert infos[0].is_exception is True
assert infos[0].exception_type == "RuntimeException"
assert infos[0].exception_module == "java.lang"
assert infos[0].get_exception() == "java.lang.RuntimeException"

def test_stacktraces_threads(self) -> None:
data: dict[str, Any] = {
Expand Down Expand Up @@ -102,6 +143,11 @@ def test_stacktraces_threads(self) -> None:
infos = find_stacktraces_in_data(data)
assert len(infos) == 1
assert len(infos[0].stacktrace["frames"]) == 2
# Thread stacktraces are not exceptions
assert infos[0].is_exception is False
assert infos[0].exception_type is None
assert infos[0].exception_module is None
assert infos[0].get_exception() is None

def test_find_stacktraces_skip_none(self) -> None:
# This tests:
Expand Down Expand Up @@ -147,13 +193,19 @@ def test_find_stacktraces_skip_none(self) -> None:
assert len(infos) == 4
assert sum(1 for x in infos if x.stacktrace) == 3
assert sum(1 for x in infos if x.is_exception) == 4
# All exceptions have type "Error" and no module
assert all(x.exception_type == "Error" for x in infos)
assert all(x.exception_module is None for x in infos)
assert all(x.get_exception() == "Error" for x in infos)
# XXX: The null frame is still part of this stack trace!
assert len(infos[3].stacktrace["frames"]) == 3

infos = find_stacktraces_in_data(data)
assert len(infos) == 1
# XXX: The null frame is still part of this stack trace!
assert len(infos[0].stacktrace["frames"]) == 3
assert infos[0].exception_type == "Error"
assert infos[0].get_exception() == "Error"


@pytest.mark.parametrize(
Expand Down
Loading