3
3
from collections .abc import Mapping , Sequence
4
4
from typing import Any
5
5
6
- import sentry_sdk
7
-
8
6
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
20
8
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
- )
344
9
345
10
346
11
class JavaPlugin (Plugin2 ):
@@ -350,10 +15,7 @@ def can_configure_for_project(self, project, **kwargs):
350
15
return False
351
16
352
17
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 []
357
19
358
20
def get_event_preprocessors (self , data : Mapping [str , Any ]) -> Sequence [EventPreprocessor ]:
359
21
if has_proguard_file (data ):
0 commit comments