Repository: getsentry/sentry

Issue:
Investigate Deobfustication for the file io issue
https://sentry.slack.com/archives/CP4UUUF1S/p1669634712272829


Here's an obfusticated event: 
https://gist.github.com/wmak/5bfd55d24d5b8794304ec312d2075a29

Debug Meta:
- Yes it should be on the server already with the UUID from debug_meta
- there's a proguard section and you can download it from there


Discussion:

Resolving patch:
diff --git a/src/sentry/utils/performance_issues/performance_detection.py b/src/sentry/utils/performance_issues/performance_detection.py
--- a/src/sentry/utils/performance_issues/performance_detection.py
+++ b/src/sentry/utils/performance_issues/performance_detection.py
@@ -13,10 +13,11 @@
 from urllib.parse import urlparse
 
 import sentry_sdk
+from symbolic import ProguardMapper  # type: ignore
 
 from sentry import features, nodestore, options, projectoptions
 from sentry.eventstore.models import Event
-from sentry.models import Organization, Project, ProjectOption
+from sentry.models import Organization, Project, ProjectDebugFile, ProjectOption
 from sentry.types.issues import GROUP_TYPE_TO_TEXT, GroupType
 from sentry.utils import metrics
 from sentry.utils.event_frames import get_sdk_name
@@ -1415,6 +1416,51 @@ def init(self):
         self.most_recent_start_time = {}
         self.most_recent_hash = {}
         self.stored_problems = {}
+        self.mapper = None
+        self._prepare_deobfuscation()
+
+    def _prepare_deobfuscation(self):
+        event = self._event
+        if "debug_meta" in event:
+            images = event["debug_meta"].get("images", [])
+            project_id = event.get("project")
+            if not isinstance(images, list):
+                return
+            if project_id is not None:
+                project = Project.objects.get_from_cache(id=project_id)
+            else:
+                return
+
+            for image in images:
+                if image.get("type") == "proguard":
+                    uuid = image.get("uuid")
+                    dif_paths = ProjectDebugFile.difcache.fetch_difs(
+                        project, [uuid], features=["mapping"]
+                    )
+                    debug_file_path = dif_paths.get(uuid)
+                    if debug_file_path is None:
+                        return
+
+                    mapper = ProguardMapper.open(debug_file_path)
+                    if not mapper.has_line_info:
+                        return
+                    self.mapper = mapper
+                    return
+
+    def _deobfuscate_module(self, module: str) -> str:
+        if self.mapper is not None:
+            return self.mapper.remap_class(module)
+        else:
+            return module
+
+    def _deobfuscate_function(self, frame):
+        if self.mapper is not None and "module" in frame and "function" in frame:
+            functions = self.mapper.remap_frame(
+                frame["module"], frame["function"], frame.get("lineno") or 0
+            )
+            return ".".join([func.method for func in functions])
+        else:
+            return frame.get("function", "")
 
     def visit_span(self, span: Span):
         if self._is_file_io_on_main_thread(span):
@@ -1436,12 +1482,12 @@ def visit_span(self, span: Span):
                 )
 
     def _fingerprint(self, span) -> str:
-        call_stack = ".".join(
-            [
-                f"{item.get('module', '')}.{item.get('function', '')}"
-                for item in span.get("data", {}).get("call_stack", [])
-            ]
-        ).encode("utf8")
+        call_stack_strings = []
+        for item in span.get("data", {}).get("call_stack", []):
+            module = self._deobfuscate_module(item.get("module", ""))
+            function = self._deobfuscate_function(item)
+            call_stack_strings.append(f"{module}.{function}")
+        call_stack = ".".join(call_stack_strings).encode("utf8")
         hashed_stack = hashlib.sha1(call_stack).hexdigest()
         return f"1-{GroupType.PERFORMANCE_FILE_IO_MAIN_THREAD}-{hashed_stack}"
 
@@ -1450,7 +1496,7 @@ def _is_file_io_on_main_thread(self, span: Span) -> bool:
         if data is None:
             return False
         # doing is True since the value can be any type
-        return data.get("blocked_ui_thread", False) is True
+        return data.get("blocked_main_thread", False) is True
 
 
 # Reports metrics and creates spans for detection


Test patch:
diff --git a/tests/sentry/utils/performance_issues/test_file_io_on_main_thread_detector.py b/tests/sentry/utils/performance_issues/test_file_io_on_main_thread_detector.py
--- a/tests/sentry/utils/performance_issues/test_file_io_on_main_thread_detector.py
+++ b/tests/sentry/utils/performance_issues/test_file_io_on_main_thread_detector.py
@@ -1,23 +1,47 @@
-import unittest
+import hashlib
+from io import BytesIO
+from zipfile import ZipFile
 
 import pytest
 
+from sentry.models import create_files_from_dif_zip
+from sentry.testutils import TestCase
 from sentry.testutils.performance_issues.event_generators import EVENTS
 from sentry.testutils.silo import region_silo_test
+from sentry.types.issues import GroupType
 from sentry.utils.performance_issues.performance_detection import (
     FileIOMainThreadDetector,
     get_detection_settings,
     run_detector_on_data,
 )
 
+PROGUARD_SOURCE = b"""\
+# compiler: R8
+# compiler_version: 2.0.74
+# min_api: 16
+# pg_map_id: 5b46fdc
+# common_typos_disable
+# {"id":"com.android.tools.r8.mapping","version":"1.0"}
+org.slf4j.helpers.Util$ClassContextSecurityManager -> org.a.b.g$a:
+    65:65:void <init>() -> <init>
+    67:67:java.lang.Class[] getClassContext() -> a
+    69:69:java.lang.Class[] getExtraClassContext() -> a
+    65:65:void <init>(org.slf4j.helpers.Util$1) -> <init>
+"""
+
 
 @region_silo_test
 @pytest.mark.django_db
-class NPlusOneAPICallsDetectorTest(unittest.TestCase):
+class NPlusOneAPICallsDetectorTest(TestCase):
     def setUp(self):
         super().setUp()
         self.settings = get_detection_settings()
 
+    def create_proguard(self, uuid):
+        with ZipFile(BytesIO(), "w") as f:
+            f.writestr(f"proguard/{uuid}.txt", PROGUARD_SOURCE)
+            create_files_from_dif_zip(f, project=self.project)
+
     def test_gives_problem_correct_title(self):
         event = EVENTS["file-io-on-main-thread"]
 
@@ -25,3 +49,20 @@ def test_gives_problem_correct_title(self):
         run_detector_on_data(detector, event)
         problem = list(detector.stored_problems.values())[0]
         assert problem.title == "File IO on Main Thread"
+
+    def test_file_io_with_proguard(self):
+        event = EVENTS["file-io-on-main-thread-with-obfuscation"]
+        event["project"] = self.project.id
+
+        uuid = event["debug_meta"]["images"][0]["uuid"]
+        self.create_proguard(uuid)
+
+        detector = FileIOMainThreadDetector(self.settings, event)
+        run_detector_on_data(detector, event)
+        problem = list(detector.stored_problems.values())[0]
+        call_stack = b"org.slf4j.helpers.Util$ClassContextSecurityManager.getExtraClassContext"
+        hashed_stack = hashlib.sha1(call_stack).hexdigest()
+        assert (
+            problem.fingerprint == f"1-{GroupType.PERFORMANCE_FILE_IO_MAIN_THREAD}-{hashed_stack}"
+        )
+        assert problem.title == "File IO on Main Thread"
diff --git a/tests/sentry/utils/performance_issues/test_performance_detection.py b/tests/sentry/utils/performance_issues/test_performance_detection.py
--- a/tests/sentry/utils/performance_issues/test_performance_detection.py
+++ b/tests/sentry/utils/performance_issues/test_performance_detection.py
@@ -1050,7 +1050,7 @@ def test_detects_file_io_main_thread(self):
 
     def test_does_not_detect_file_io_main_thread(self):
         file_io_event = EVENTS["file-io-on-main-thread"]
-        file_io_event["spans"][0]["data"]["blocked_ui_thread"] = False
+        file_io_event["spans"][0]["data"]["blocked_main_thread"] = False
         sdk_span_mock = Mock()
 
         _detect_performance_problems(file_io_event, sdk_span_mock)