Skip to content

Commit 7814082

Browse files
authored
ref(proguard): Unconditionally use Symbolicator and remove StacktraceProcessor (#73905)
We're at a point where we can definitely switch to Symbolicator for proguard processing. This removes all code related to gradual rollout and A/B testing (except the options), as well as the previous StacktraceProcessors.
1 parent 9ae4ff8 commit 7814082

File tree

6 files changed

+43
-458
lines changed

6 files changed

+43
-458
lines changed

src/sentry/lang/java/plugin.py

Lines changed: 2 additions & 340 deletions
Original file line numberDiff line numberDiff line change
@@ -3,344 +3,9 @@
33
from collections.abc import Mapping, Sequence
44
from typing import Any
55

6-
import sentry_sdk
7-
86
from sentry.lang.java.processing import deobfuscate_exception_value
9-
from sentry.lang.java.proguard import open_proguard_mapper
10-
from sentry.lang.java.utils import (
11-
deobfuscate_view_hierarchy,
12-
get_jvm_images,
13-
get_proguard_images,
14-
has_proguard_file,
15-
)
16-
from sentry.lang.javascript.utils import get_source_context, trim_line
17-
from sentry.models.artifactbundle import ArtifactBundleArchive
18-
from sentry.models.debugfile import ProjectDebugFile
19-
from sentry.models.eventerror import EventError
7+
from sentry.lang.java.utils import deobfuscate_view_hierarchy, has_proguard_file
208
from sentry.plugins.base.v2 import EventPreprocessor, Plugin2
21-
from sentry.stacktraces.processing import StacktraceProcessor
22-
from sentry.utils.safe import get_path
23-
24-
25-
class JavaStacktraceProcessor(StacktraceProcessor):
26-
def __init__(self, *args, **kwargs):
27-
StacktraceProcessor.__init__(self, *args, **kwargs)
28-
29-
self.images = get_proguard_images(self.data)
30-
self.available = len(self.images) > 0
31-
self.mapping_views = []
32-
33-
def handles_frame(self, frame, stacktrace_info):
34-
platform = frame.get("platform") or self.data.get("platform")
35-
return platform == "java" and self.available and "function" in frame and "module" in frame
36-
37-
def preprocess_step(self, processing_task):
38-
if not self.available:
39-
return False
40-
41-
with sentry_sdk.start_span(op="proguard.fetch_debug_files"):
42-
dif_paths = ProjectDebugFile.difcache.fetch_difs(
43-
self.project, self.images, features=["mapping"]
44-
)
45-
46-
for debug_id in self.images:
47-
error_type = None
48-
49-
dif_path = dif_paths.get(debug_id)
50-
if dif_path is None:
51-
error_type = EventError.PROGUARD_MISSING_MAPPING
52-
else:
53-
view = open_proguard_mapper(dif_path)
54-
if not view.has_line_info:
55-
error_type = EventError.PROGUARD_MISSING_LINENO
56-
else:
57-
self.mapping_views.append(view)
58-
59-
if error_type is None:
60-
continue
61-
62-
self.data.setdefault("_metrics", {})["flag.processing.error"] = True
63-
64-
self.data.setdefault("errors", []).append(
65-
{"type": error_type, "mapping_uuid": debug_id}
66-
)
67-
68-
return True
69-
70-
def process_exception(self, exception):
71-
if not self.available:
72-
return False
73-
74-
ty = exception.get("type")
75-
mod = exception.get("module")
76-
if not ty or not mod:
77-
return False
78-
79-
key = f"{mod}.{ty}"
80-
81-
for view in self.mapping_views:
82-
mapped = view.remap_class(key)
83-
if mapped:
84-
new_module, new_cls = mapped.rsplit(".", 1)
85-
exception["module"] = new_module
86-
exception["type"] = new_cls
87-
return True
88-
89-
return False
90-
91-
def process_frame(self, processable_frame, processing_task):
92-
frame = processable_frame.frame
93-
raw_frame = dict(frame)
94-
release = super().get_release()
95-
96-
# first, try to remap complete frames
97-
for view in self.mapping_views:
98-
mapped = view.remap_frame(frame["module"], frame["function"], frame.get("lineno") or 0)
99-
100-
if len(mapped) > 0:
101-
new_frames = []
102-
bottom_class = mapped[-1].class_name
103-
104-
# sentry expects stack traces in reverse order
105-
for new_frame in reversed(mapped):
106-
frame = dict(raw_frame)
107-
frame["module"] = new_frame.class_name
108-
frame["function"] = new_frame.method
109-
frame["lineno"] = new_frame.line
110-
111-
# clear the filename for all *foreign* classes
112-
if frame["module"] != bottom_class:
113-
frame.pop("filename", None)
114-
frame.pop("abs_path", None)
115-
116-
# mark the frame as in_app after deobfuscation based on the release package name
117-
# only if it's not present
118-
if release and release.package and frame.get("in_app") is None:
119-
if frame["module"].startswith(release.package):
120-
frame["in_app"] = True
121-
122-
new_frames.append(frame)
123-
124-
return new_frames, [raw_frame], []
125-
126-
# second, if that is not possible, try to re-map only the class-name
127-
for view in self.mapping_views:
128-
mapped_class = view.remap_class(frame["module"])
129-
130-
if mapped_class:
131-
frame = dict(raw_frame)
132-
frame["module"] = mapped_class
133-
134-
# mark the frame as in_app after deobfuscation based on the release package name
135-
# only if it's not present
136-
if release and release.package and frame.get("in_app") is None:
137-
if frame["module"].startswith(release.package):
138-
frame["in_app"] = True
139-
140-
return [frame], [raw_frame], []
141-
142-
return
143-
144-
145-
# A processor that delegates to JavaStacktraceProcessor for restoring code
146-
# obfuscated by ProGuard or similar. It then tries to look up source context
147-
# for either the de-obfuscated stack frame or the stack frame that was passed in.
148-
class JavaSourceLookupStacktraceProcessor(StacktraceProcessor):
149-
def __init__(self, *args, **kwargs):
150-
StacktraceProcessor.__init__(self, *args, **kwargs)
151-
self.proguard_processor = JavaStacktraceProcessor(*args, **kwargs)
152-
self._proguard_processor_handles_frame = {}
153-
self._handles_frame = {}
154-
self.images = get_jvm_images(self.data)
155-
self._archives = []
156-
self.available = len(self.images) > 0
157-
158-
def _deep_freeze(self, d):
159-
if isinstance(d, dict):
160-
return frozenset((key, self._deep_freeze(value)) for key, value in d.items())
161-
elif isinstance(d, list):
162-
return tuple(self._deep_freeze(value) for value in d)
163-
return d
164-
165-
def close(self):
166-
for archive in self._archives:
167-
archive.close()
168-
169-
# Symbolicator/Python A/B testing
170-
try:
171-
self.perform_ab_test()
172-
except Exception as e:
173-
sentry_sdk.capture_exception(e)
174-
175-
def handles_frame(self, frame, stacktrace_info):
176-
key = self._deep_freeze(frame)
177-
self._proguard_processor_handles_frame[key] = self.proguard_processor.handles_frame(
178-
frame, stacktrace_info
179-
)
180-
181-
platform = frame.get("platform") or self.data.get("platform")
182-
self._handles_frame[key] = platform == "java" and self.available and "module" in frame
183-
return self._proguard_processor_handles_frame[key] or self._handles_frame[key]
184-
185-
def preprocess_step(self, processing_task):
186-
proguard_processor_preprocess_rv = self.proguard_processor.preprocess_step(processing_task)
187-
188-
if not self.available:
189-
return proguard_processor_preprocess_rv
190-
191-
difs = ProjectDebugFile.objects.find_by_debug_ids(self.project, self.images)
192-
for key, dif in difs.items():
193-
try:
194-
file = dif.file.getfile(prefetch=True)
195-
self._archives.append(ArtifactBundleArchive(file))
196-
except Exception:
197-
pass
198-
199-
return proguard_processor_preprocess_rv or self.available
200-
201-
def process_exception(self, exception):
202-
return self.proguard_processor.process_exception(exception)
203-
204-
# if path contains a '$' sign or doesn't contain a '.' it has most likely been obfuscated
205-
def _is_valid_path(self, abs_path):
206-
if abs_path is None:
207-
return False
208-
abs_path_dollar_index = abs_path.find("$")
209-
abs_path_dot_index = abs_path.find(".")
210-
return abs_path_dollar_index < 0 and abs_path_dot_index > 0
211-
212-
def _build_source_file_name(self, frame):
213-
abs_path = frame.get("abs_path")
214-
module = frame["module"]
215-
216-
if self._is_valid_path(abs_path):
217-
# extract package from module (io.sentry.Sentry -> io.sentry) and append abs_path
218-
module_dot_index = module.rfind(".")
219-
if module_dot_index >= 0:
220-
source_file_name = module[:module_dot_index].replace(".", "/") + "/"
221-
else:
222-
source_file_name = ""
223-
224-
abs_path_dot_index = abs_path.rfind(".")
225-
source_file_name += abs_path[:abs_path_dot_index]
226-
else:
227-
# use module as filename (excluding inner classes, marked by $) and append .java
228-
module_dollar_index = module.find("$")
229-
if module_dollar_index >= 0:
230-
source_file_name = module[:module_dollar_index].replace(".", "/")
231-
else:
232-
source_file_name = module.replace(".", "/")
233-
234-
source_file_name += (
235-
".jvm" # fake extension because we don't know whether it's .java, .kt or something else
236-
)
237-
238-
return "~/" + source_file_name
239-
240-
def process_frame(self, processable_frame, processing_task):
241-
new_frames = None
242-
raw_frames = None
243-
processing_errors = None
244-
bare_frame = processable_frame.frame
245-
key = self._deep_freeze(bare_frame)
246-
247-
if self._proguard_processor_handles_frame[key]:
248-
proguard_result = self.proguard_processor.process_frame(
249-
processable_frame, processing_task
250-
)
251-
252-
if proguard_result:
253-
new_frames, raw_frames, processing_errors = proguard_result
254-
255-
if not self._handles_frame[key]:
256-
return new_frames, raw_frames, processing_errors
257-
258-
if not new_frames:
259-
new_frames = [dict(bare_frame)]
260-
261-
for new_frame in new_frames:
262-
lineno = new_frame.get("lineno")
263-
if not lineno:
264-
continue
265-
266-
source_file_name = self._build_source_file_name(new_frame)
267-
268-
for archive in self._archives:
269-
try:
270-
result, _ = archive.get_file_by_url(source_file_name)
271-
source_view = list(result.readlines())
272-
source_context = get_source_context(source_view, lineno)
273-
274-
(pre_context, context_line, post_context) = source_context
275-
276-
if pre_context is not None and len(pre_context) > 0:
277-
new_frame["pre_context"] = [trim_line(x.decode()) for x in pre_context]
278-
if context_line is not None:
279-
new_frame["context_line"] = trim_line(context_line.decode())
280-
if post_context is not None and len(post_context) > 0:
281-
new_frame["post_context"] = [trim_line(x.decode()) for x in post_context]
282-
except KeyError:
283-
# file not available in source bundle, proceed
284-
pass
285-
286-
return new_frames, raw_frames, processing_errors
287-
288-
def perform_ab_test(self):
289-
symbolicator_stacktraces = self.data.pop("symbolicator_stacktraces", None)
290-
291-
if symbolicator_stacktraces is None:
292-
return
293-
294-
# This should always be set if symbolicator_stacktraces is, but better safe than sorry
295-
symbolicator_exceptions = self.data.pop("symbolicator_exceptions", ())
296-
297-
def frames_differ(a, b):
298-
return (
299-
a.get("lineno", 0) != b.get("lineno", 0)
300-
or a.get("abs_path") != b.get("abs_path")
301-
or a.get("function") != b.get("function")
302-
or a.get("filename") != b.get("filename")
303-
or a.get("module") != b.get("module")
304-
or a.get("in_app") != b.get("in_app")
305-
or a.get("context_line") != b.get("context_line")
306-
)
307-
308-
def exceptions_differ(a, b):
309-
return a.get("type") != b.get("type") or a.get("module") != b.get("module")
310-
311-
# During symbolication, empty stacktrace_infos are disregarded. We need to do that here as well or results
312-
# won't line up.
313-
python_stacktraces = [
314-
sinfo.stacktrace for sinfo in self.stacktrace_infos if sinfo.stacktrace is not None
315-
]
316-
317-
different_frames = []
318-
for symbolicator_stacktrace, python_stacktrace in zip(
319-
symbolicator_stacktraces, python_stacktraces
320-
):
321-
for symbolicator_frame, python_frame in zip(
322-
symbolicator_stacktrace, python_stacktrace["frames"]
323-
):
324-
if frames_differ(symbolicator_frame, python_frame):
325-
different_frames.append((symbolicator_frame, python_frame))
326-
327-
different_exceptions = []
328-
for symbolicator_exception, python_exception in zip(
329-
symbolicator_exceptions,
330-
get_path(self.data, "exception", "values", filter=True, default=()),
331-
):
332-
if exceptions_differ(symbolicator_exception, python_exception):
333-
different_exceptions.append((symbolicator_exception, python_exception))
334-
335-
if different_frames or different_exceptions:
336-
with sentry_sdk.isolation_scope() as scope:
337-
scope.set_extra("different_frames", different_frames)
338-
scope.set_extra("different_exceptions", different_exceptions)
339-
scope.set_extra("event_id", self.data.get("event_id"))
340-
scope.set_tag("project_id", self.project.id)
341-
sentry_sdk.capture_message(
342-
"JVM symbolication differences between symbolicator and python."
343-
)
3449

34510

34611
class JavaPlugin(Plugin2):
@@ -350,10 +15,7 @@ def can_configure_for_project(self, project, **kwargs):
35015
return False
35116

35217
def get_stacktrace_processors(self, data, stacktrace_infos, platforms, **kwargs):
353-
if data.pop("processed_by_symbolicator", False):
354-
return []
355-
if "java" in platforms:
356-
return [JavaSourceLookupStacktraceProcessor]
18+
return []
35719

35820
def get_event_preprocessors(self, data: Mapping[str, Any]) -> Sequence[EventPreprocessor]:
35921
if has_proguard_file(data):

0 commit comments

Comments
 (0)