diff --git a/src/sentry/lang/java/plugin.py b/src/sentry/lang/java/plugin.py index 8c795d42b4cbaa..a9acd71f3ab22c 100644 --- a/src/sentry/lang/java/plugin.py +++ b/src/sentry/lang/java/plugin.py @@ -4,7 +4,7 @@ from typing import Any from sentry.lang.java.processing import deobfuscate_exception_value -from sentry.lang.java.utils import deobfuscate_view_hierarchy, has_proguard_file +from sentry.lang.java.utils import has_proguard_file from sentry.plugins.base.v2 import EventPreprocessor, Plugin2 @@ -19,6 +19,6 @@ def get_stacktrace_processors(self, data, stacktrace_infos, platforms, **kwargs) def get_event_preprocessors(self, data: Mapping[str, Any]) -> Sequence[EventPreprocessor]: if has_proguard_file(data): - return [deobfuscate_exception_value, deobfuscate_view_hierarchy] + return [deobfuscate_exception_value] else: return [] diff --git a/src/sentry/lang/java/processing.py b/src/sentry/lang/java/processing.py index c8bf5e6a37302f..136f95064805e3 100644 --- a/src/sentry/lang/java/processing.py +++ b/src/sentry/lang/java/processing.py @@ -3,6 +3,10 @@ from collections.abc import Mapping from typing import Any +import orjson + +from sentry.attachments import CachedAttachment, attachment_cache +from sentry.ingest.consumer.processors import CACHE_TIMEOUT from sentry.lang.java.utils import get_jvm_images, get_proguard_images from sentry.lang.native.error import SymbolicationFailed, write_error from sentry.lang.native.symbolicator import Symbolicator @@ -11,6 +15,7 @@ from sentry.models.release import Release from sentry.stacktraces.processing import find_stacktraces_in_data from sentry.utils import metrics +from sentry.utils.cache import cache_key_for_event from sentry.utils.safe import get_path logger = logging.getLogger(__name__) @@ -139,6 +144,76 @@ def _get_release_package(project: Project, release_name: str | None) -> str | No return release.package if release else None +def _get_window_class_names(attachments: list[CachedAttachment]) -> list[str]: + """Returns the class names of all windows in all view hierarchies + contained in `attachments`.""" + + class_names = [] + windows_to_deobfuscate = [] + + for attachment in attachments: + if attachment.type == "event.view_hierarchy": + view_hierarchy = orjson.loads(attachment_cache.get_data(attachment)) + windows_to_deobfuscate.extend(view_hierarchy.get("windows")) + + while windows_to_deobfuscate: + window = windows_to_deobfuscate.pop() + if window.get("type") is not None: + class_names.append(window["type"]) + if children := window.get("children"): + windows_to_deobfuscate.extend(children) + + return class_names + + +def _deobfuscate_view_hierarchy(view_hierarchy: Any, class_names: dict[str, str]) -> None: + """Deobfuscates a view hierarchy in-place. + + The `class_names` dict is used to resolve obfuscated to deobfuscated names. If + an obfuscated class name isn't present in `class_names`, it is left unchanged.""" + + windows_to_deobfuscate = [*view_hierarchy.get("windows")] + + while windows_to_deobfuscate: + window = windows_to_deobfuscate.pop() + if ( + window.get("type") is not None + and (mapped_type := class_names.get(window["type"])) is not None + ): + window["type"] = mapped_type + if children := window.get("children"): + windows_to_deobfuscate.extend(children) + + +def _deobfuscate_view_hierarchies( + attachments: list[CachedAttachment], class_names: dict[str, str] +) -> list[CachedAttachment]: + """Deobfuscates all view hierarchies contained in `attachments`, returning a new list of attachments. + + Non-view-hierarchy attachments are unchanged. + """ + new_attachments = [] + for attachment in attachments: + if attachment.type == "event.view_hierarchy": + view_hierarchy = orjson.loads(attachment_cache.get_data(attachment)) + _deobfuscate_view_hierarchy(view_hierarchy, class_names) + # Reupload to cache as a unchunked data + new_attachments.append( + CachedAttachment( + type=attachment.type, + id=attachment.id, + name=attachment.name, + content_type=attachment.content_type, + data=orjson.dumps(view_hierarchy), + chunks=None, + ) + ) + else: + new_attachments.append(attachment) + + return new_attachments + + def map_symbolicator_process_jvm_errors( errors: list[dict[str, Any]] | None, ) -> list[dict[str, Any]]: @@ -195,10 +270,17 @@ def process_jvm_stacktraces(symbolicator: Symbolicator, data: Any) -> Any: ] processable_exceptions = _get_exceptions_for_symbolication(data) + cache_key = cache_key_for_event(data) + attachments = [*attachment_cache.get(cache_key)] + window_class_names = _get_window_class_names(attachments) metrics.incr("proguard.symbolicator.events") - if not any(stacktrace["frames"] for stacktrace in stacktraces) and not processable_exceptions: + if ( + not any(stacktrace["frames"] for stacktrace in stacktraces) + and not processable_exceptions + and not window_class_names + ): metrics.incr("proguard.symbolicator.events.skipped") return @@ -211,6 +293,7 @@ def process_jvm_stacktraces(symbolicator: Symbolicator, data: Any) -> Any: stacktraces=stacktraces, modules=modules, release_package=release_package, + classes=window_class_names, ) if not _handle_response_status(data, response): @@ -248,4 +331,8 @@ def process_jvm_stacktraces(symbolicator: Symbolicator, data: Any) -> Any: raw_exc["module"] = exc["module"] raw_exc["type"] = exc["type"] + classes = response.get("classes") + new_attachments = _deobfuscate_view_hierarchies(attachments, classes) + attachment_cache.set(cache_key, attachments=new_attachments, timeout=CACHE_TIMEOUT) + return data diff --git a/src/sentry/lang/java/utils.py b/src/sentry/lang/java/utils.py index c8e23ae0b2a9e5..705ca103b8c7cc 100644 --- a/src/sentry/lang/java/utils.py +++ b/src/sentry/lang/java/utils.py @@ -11,7 +11,6 @@ from sentry.lang.java.proguard import open_proguard_mapper from sentry.models.debugfile import ProjectDebugFile from sentry.models.project import Project -from sentry.stacktraces.processing import StacktraceInfo from sentry.utils.cache import cache_key_for_event from sentry.utils.safe import get_path @@ -70,31 +69,6 @@ def get_proguard_mapper(uuid: str, project: Project): return mapper -def _deobfuscate_view_hierarchy(event_data: dict[str, Any], project: Project, view_hierarchy): - """ - Deobfuscates a view hierarchy in-place. - - If we're unable to fetch a ProGuard uuid or unable to init the mapper, - then the view hierarchy remains unmodified. - """ - proguard_uuids = get_proguard_images(event_data) - if len(proguard_uuids) == 0: - return - - with sentry_sdk.start_span(op="proguard.deobfuscate_view_hierarchy_data"): - for proguard_uuid in proguard_uuids: - mapper = get_proguard_mapper(proguard_uuid, project) - if mapper is None: - return - - windows_to_deobfuscate = [*view_hierarchy.get("windows")] - while windows_to_deobfuscate: - window = windows_to_deobfuscate.pop() - window["type"] = mapper.remap_class(window.get("type")) or window.get("type") - if children := window.get("children"): - windows_to_deobfuscate.extend(children) - - @sentry_sdk.trace def deobfuscation_template(data, map_type, deobfuscation_fn): """ @@ -133,13 +107,8 @@ def deobfuscation_template(data, map_type, deobfuscation_fn): attachment_cache.set(cache_key, attachments=new_attachments, timeout=CACHE_TIMEOUT) -def deobfuscate_view_hierarchy(data): - deobfuscation_template(data, "proguard", _deobfuscate_view_hierarchy) - - -def is_jvm_event(data: Any, stacktraces: list[StacktraceInfo]) -> bool: - """Returns whether `data` is a JVM event, based on its platform, images, and - the supplied stacktraces.""" +def is_jvm_event(data: Any) -> bool: + """Returns whether `data` is a JVM event, based on its images.""" # check if there are any JVM or Proguard images images = get_path( @@ -149,14 +118,4 @@ def is_jvm_event(data: Any, stacktraces: list[StacktraceInfo]) -> bool: filter=lambda x: is_valid_jvm_image(x) or is_valid_proguard_image(x), default=(), ) - if not images: - return False - - if data.get("platform") == "java": - return True - - for stacktrace in stacktraces: - if any(x == "java" for x in stacktrace.platforms): - return True - - return False + return bool(images) diff --git a/src/sentry/lang/native/symbolicator.py b/src/sentry/lang/native/symbolicator.py index bfc3bdc4683ac7..f1f4bced3dd694 100644 --- a/src/sentry/lang/native/symbolicator.py +++ b/src/sentry/lang/native/symbolicator.py @@ -241,6 +241,7 @@ def process_jvm( stacktraces, modules, release_package, + classes, apply_source_context=True, ): """ @@ -262,6 +263,7 @@ def process_jvm( "exceptions": exceptions, "stacktraces": stacktraces, "modules": modules, + "classes": classes, "options": {"apply_source_context": apply_source_context}, } diff --git a/src/sentry/profiles/task.py b/src/sentry/profiles/task.py index 3ab17e66c4e00e..74ac2ccda0a8d4 100644 --- a/src/sentry/profiles/task.py +++ b/src/sentry/profiles/task.py @@ -489,6 +489,7 @@ def symbolicate( modules=modules, release_package=profile.get("transaction_metadata", {}).get("app.identifier"), apply_source_context=False, + classes=[], ) return symbolicator.process_payload( stacktraces=stacktraces, modules=modules, apply_source_context=False diff --git a/src/sentry/tasks/symbolication.py b/src/sentry/tasks/symbolication.py index 15652e7b2d5275..abb8b98aed02d7 100644 --- a/src/sentry/tasks/symbolication.py +++ b/src/sentry/tasks/symbolication.py @@ -109,7 +109,7 @@ def get_symbolication_platforms( platforms = [] - if is_jvm_event(data, stacktraces): + if is_jvm_event(data): platforms.append(SymbolicatorPlatform.jvm) if is_js_event(data, stacktraces): platforms.append(SymbolicatorPlatform.js) diff --git a/tests/relay_integration/lang/java/test_plugin.py b/tests/relay_integration/lang/java/test_plugin.py index 9e8a4ea124a8ac..0b579817ef6c8d 100644 --- a/tests/relay_integration/lang/java/test_plugin.py +++ b/tests/relay_integration/lang/java/test_plugin.py @@ -1556,7 +1556,6 @@ def test_invalid_exception(self): def test_is_jvm_event(self): from sentry.lang.java.utils import is_jvm_event - from sentry.stacktraces.processing import find_stacktraces_in_data event = { "user": {"ip_address": "31.172.207.97"}, @@ -1586,8 +1585,7 @@ def test_is_jvm_event(self): }, "timestamp": iso_format(before_now(seconds=1)), } - stacktraces = find_stacktraces_in_data(event) - assert is_jvm_event(event, stacktraces) + assert is_jvm_event(event) event = { "user": {"ip_address": "31.172.207.97"}, @@ -1616,9 +1614,8 @@ def test_is_jvm_event(self): }, "timestamp": iso_format(before_now(seconds=1)), } - stacktraces = find_stacktraces_in_data(event) # has no platform - assert not is_jvm_event(event, stacktraces) + assert is_jvm_event(event) event = { "user": {"ip_address": "31.172.207.97"}, @@ -1648,6 +1645,5 @@ def test_is_jvm_event(self): }, "timestamp": iso_format(before_now(seconds=1)), } - stacktraces = find_stacktraces_in_data(event) # has no modules - assert not is_jvm_event(event, stacktraces) + assert not is_jvm_event(event) diff --git a/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py b/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py index adcddec8da2b74..2245420c49d049 100644 --- a/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py +++ b/tests/sentry/ingest/ingest_consumer/test_ingest_consumer_processing.py @@ -29,7 +29,7 @@ from sentry.models.userreport import UserReport from sentry.options import set from sentry.testutils.pytest.fixtures import django_db_all -from sentry.testutils.skips import requires_snuba +from sentry.testutils.skips import requires_snuba, requires_symbolicator from sentry.usage_accountant import accountant from sentry.utils.eventuser import EventUser from sentry.utils.json import loads @@ -324,80 +324,83 @@ def test_with_attachments(default_project, task_runner, missing_chunks, monkeypa @django_db_all -def test_deobfuscate_view_hierarchy(default_project, task_runner): - payload = get_normalized_event( - { - "message": "hello world", - "debug_meta": {"images": [{"uuid": PROGUARD_UUID, "type": "proguard"}]}, - }, - default_project, - ) - event_id = payload["event_id"] - attachment_id = "ca90fb45-6dd9-40a0-a18f-8693aa621abb" - project_id = default_project.id - start_time = time.time() - 3600 - - # Create the proguard file - with zipfile.ZipFile(BytesIO(), "w") as f: - f.writestr(f"proguard/{PROGUARD_UUID}.txt", PROGUARD_SOURCE) - create_files_from_dif_zip(f, project=default_project) - - expected_response = b'{"rendering_system":"Test System","windows":[{"identifier":"parent","type":"org.slf4j.helpers.Util$ClassContextSecurityManager","children":[{"identifier":"child","type":"org.slf4j.helpers.Util$ClassContextSecurityManager"}]}]}' - obfuscated_view_hierarchy = { - "rendering_system": "Test System", - "windows": [ +@requires_symbolicator +@pytest.mark.symbolicator +def test_deobfuscate_view_hierarchy(default_project, task_runner, set_sentry_option, live_server): + with set_sentry_option("system.url-prefix", live_server.url): + payload = get_normalized_event( { - "identifier": "parent", - "type": "org.a.b.g$a", - "children": [ - { - "identifier": "child", - "type": "org.a.b.g$a", - } - ], - } - ], - } - - process_attachment_chunk( - { - "payload": orjson.dumps(obfuscated_view_hierarchy), - "event_id": event_id, - "project_id": project_id, - "id": attachment_id, - "chunk_index": 0, + "message": "hello world", + "debug_meta": {"images": [{"uuid": PROGUARD_UUID, "type": "proguard"}]}, + }, + default_project, + ) + event_id = payload["event_id"] + attachment_id = "ca90fb45-6dd9-40a0-a18f-8693aa621abb" + project_id = default_project.id + start_time = time.time() - 3600 + + # Create the proguard file + with zipfile.ZipFile(BytesIO(), "w") as f: + f.writestr(f"proguard/{PROGUARD_UUID}.txt", PROGUARD_SOURCE) + create_files_from_dif_zip(f, project=default_project) + + expected_response = b'{"rendering_system":"Test System","windows":[{"identifier":"parent","type":"org.slf4j.helpers.Util$ClassContextSecurityManager","children":[{"identifier":"child","type":"org.slf4j.helpers.Util$ClassContextSecurityManager"}]}]}' + obfuscated_view_hierarchy = { + "rendering_system": "Test System", + "windows": [ + { + "identifier": "parent", + "type": "org.a.b.g$a", + "children": [ + { + "identifier": "child", + "type": "org.a.b.g$a", + } + ], + } + ], } - ) - with task_runner(): - process_event( + process_attachment_chunk( { - "payload": orjson.dumps(payload).decode(), - "start_time": start_time, + "payload": orjson.dumps(obfuscated_view_hierarchy), "event_id": event_id, "project_id": project_id, - "remote_addr": "127.0.0.1", - "attachments": [ - { - "id": attachment_id, - "name": "view_hierarchy.json", - "content_type": "application/json", - "attachment_type": "event.view_hierarchy", - "chunks": 1, - } - ], - }, - project=default_project, + "id": attachment_id, + "chunk_index": 0, + } ) - persisted_attachments = list( - EventAttachment.objects.filter(project_id=project_id, event_id=event_id) - ) - (attachment,) = persisted_attachments - assert attachment.content_type == "application/json" - assert attachment.name == "view_hierarchy.json" - with attachment.getfile() as file: - assert file.read() == expected_response + with task_runner(): + process_event( + { + "payload": orjson.dumps(payload).decode(), + "start_time": start_time, + "event_id": event_id, + "project_id": project_id, + "remote_addr": "127.0.0.1", + "attachments": [ + { + "id": attachment_id, + "name": "view_hierarchy.json", + "content_type": "application/json", + "attachment_type": "event.view_hierarchy", + "chunks": 1, + } + ], + }, + project=default_project, + ) + + persisted_attachments = list( + EventAttachment.objects.filter(project_id=project_id, event_id=event_id) + ) + (attachment,) = persisted_attachments + assert attachment.content_type == "application/json" + assert attachment.name == "view_hierarchy.json" + with attachment.getfile() as file: + assert file.read() == expected_response @django_db_all