33from collections .abc import Mapping , Sequence
44from typing import Any
55
6- import sentry_sdk
7-
86from 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
208from 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
34611class 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