1616from parsimonious .grammar import Grammar
1717from parsimonious .nodes import NodeVisitor
1818from sentry_ophio .enhancers import Cache as RustCache
19+ from sentry_ophio .enhancers import Component as RustComponent
1920from sentry_ophio .enhancers import Enhancements as RustEnhancements
2021
2122from sentry import projectoptions
23+ from sentry .features .rollout import in_random_rollout
2224from sentry .grouping .component import GroupingComponent
2325from sentry .stacktraces .functions import set_in_app
2426from sentry .utils import metrics
9698
9799class StacktraceState :
98100 def __init__ (self ):
99- self .vars = {"max-frames" : 0 , "min-frames" : 0 , "invert-stacktrace" : 0 }
101+ self .vars = {"max-frames" : 0 , "min-frames" : 0 , "invert-stacktrace" : False }
100102 self .setters = {}
101103
102104 def set (self , var , value , rule = None ):
@@ -171,7 +173,25 @@ def parse_rust_enhancements(
171173 return rust_enhancements
172174
173175
176+ RustAssembleResult = tuple [bool | None , str | None , bool , list [RustComponent ]]
174177RustEnhancedFrames = list [tuple [str | None , bool | None ]]
178+ RustExceptionData = dict [str , bytes | None ]
179+
180+
181+ def make_rust_exception_data (
182+ exception_data : dict [str , Any ],
183+ ) -> RustExceptionData :
184+ e = exception_data or {}
185+ e = {
186+ "ty" : e .get ("type" ),
187+ "value" : e .get ("value" ),
188+ "mechanism" : get_path (e , "mechanism" , "type" ),
189+ }
190+ for key in e .keys ():
191+ value = e [key ]
192+ if isinstance (value , str ):
193+ e [key ] = value .encode ("utf-8" )
194+ return e
175195
176196
177197def apply_rust_enhancements (
@@ -188,19 +208,8 @@ def apply_rust_enhancements(
188208 return None
189209
190210 try :
191- e = exception_data or {}
192- e = {
193- "ty" : e .get ("type" ),
194- "value" : e .get ("value" ),
195- "mechanism" : get_path (e , "mechanism" , "type" ),
196- }
197- for key in e .keys ():
198- value = e [key ]
199- if isinstance (value , str ):
200- e [key ] = value .encode ("utf-8" )
201-
202211 rust_enhanced_frames = rust_enhancements .apply_modifications_to_frames (
203- iter ( match_frames ), e
212+ match_frames , make_rust_exception_data ( exception_data )
204213 )
205214 metrics .incr ("rust_enhancements.modifications_run" )
206215 return rust_enhanced_frames
@@ -228,6 +237,85 @@ def compare_rust_enhancers(
228237 sentry_sdk .capture_message ("Rust Enhancements mismatch" )
229238
230239
240+ def assemble_rust_components (
241+ rust_enhancements : RustEnhancements | None ,
242+ match_frames : list [dict [str , bytes ]],
243+ exception_data : dict [str , Any ],
244+ components : list [GroupingComponent ],
245+ ) -> RustAssembleResult | None :
246+ """
247+ If `RustEnhancements` were successfully parsed and usage is enabled,
248+ this will update all the frame `components` contributions.
249+
250+ This primarily means updating the `contributes`, `hint` as well as other attributes
251+ of each frames `GroupingComponent`.
252+ Instead of modifying the input `components` directly, the results are returned
253+ as a list of `RustComponent`.
254+ """
255+ if not rust_enhancements :
256+ return None
257+
258+ try :
259+ rust_components = [
260+ RustComponent (
261+ is_prefix_frame = c .is_prefix_frame or False ,
262+ is_sentinel_frame = c .is_sentinel_frame or False ,
263+ contributes = c .contributes ,
264+ )
265+ for c in components
266+ ]
267+
268+ rust_results = rust_enhancements .assemble_stacktrace_component (
269+ match_frames , make_rust_exception_data (exception_data ), rust_components
270+ )
271+
272+ return (
273+ rust_results .contributes ,
274+ rust_results .hint ,
275+ rust_results .invert_stacktrace ,
276+ rust_components ,
277+ )
278+ except Exception :
279+ logger .exception ("failed running Rust Enhancements component contributions" )
280+ return None
281+
282+
283+ def compare_rust_components (
284+ component : GroupingComponent ,
285+ invert_stacktrace : bool ,
286+ rust_results : RustAssembleResult | None ,
287+ frames : Sequence [dict [str , Any ]],
288+ ):
289+ """
290+ Compares the results of `rust_results` with the component modifications
291+ applied by Python code directly to `components`.
292+
293+ This will log an internal error on every mismatch.
294+ """
295+ if not rust_results :
296+ return
297+
298+ contributes , hint , invert , rust_components_ = rust_results
299+
300+ python_components = [
301+ (c .contributes , c .is_prefix_frame , c .is_sentinel_frame ) for c in component .values
302+ ]
303+ rust_components = [
304+ (c .contributes , c .is_prefix_frame , c .is_sentinel_frame ) for c in rust_components_
305+ ]
306+
307+ python_res = (component .contributes , component .hint , invert_stacktrace , python_components )
308+ rust_res = (contributes , hint , invert , rust_components )
309+
310+ if python_res != rust_res :
311+ with sentry_sdk .push_scope () as scope :
312+ scope .set_extra ("python_res" , python_res )
313+ scope .set_extra ("rust_res" , rust_res )
314+ scope .set_extra ("frames" , frames )
315+
316+ sentry_sdk .capture_message ("Rust Enhancements mismatch" )
317+
318+
231319class Enhancements :
232320 # NOTE: You must add a version to ``VERSIONS`` any time attributes are added
233321 # to this class, s.t. no enhancements lacking these attributes are loaded
@@ -314,12 +402,11 @@ def apply_modifications_to_frame(
314402
315403 compare_rust_enhancers (frames , rust_enhanced_frames )
316404
317- def update_frame_components_contributions (self , components , frames , platform , exception_data ):
318- in_memory_cache : dict [str , str ] = {}
319-
320- match_frames = [create_match_frame (frame , platform ) for frame in frames ]
321-
405+ def update_frame_components_contributions (
406+ self , components , frames , match_frames , platform , exception_data
407+ ):
322408 stacktrace_state = StacktraceState ()
409+ in_memory_cache : dict [str , str ] = {}
323410 # Apply direct frame actions and update the stack state alongside
324411 for rule in self ._updater_rules :
325412 for idx , action in rule .get_matching_frame_actions (
@@ -360,10 +447,18 @@ def assemble_stacktrace_component(
360447 Internally this invokes the `update_frame_components_contributions` method
361448 but also handles cases where the entire stacktrace should be discarded.
362449 """
450+ match_frames = [create_match_frame (frame , platform ) for frame in frames ]
451+
452+ rust_results = None
453+ if in_random_rollout ("grouping.rust_enhancers.compare_components" ):
454+ rust_results = assemble_rust_components (
455+ self .rust_enhancements , match_frames , exception_data , components
456+ )
457+
363458 hint = None
364459 contributes = None
365460 stacktrace_state = self .update_frame_components_contributions (
366- components , frames , platform , exception_data
461+ components , frames , match_frames , platform , exception_data
367462 )
368463
369464 min_frames = stacktrace_state .get ("min-frames" )
@@ -378,12 +473,14 @@ def assemble_stacktrace_component(
378473 hint = stacktrace_state .add_to_hint (hint , var = "min-frames" )
379474 contributes = False
380475
381- inverted_hierarchy = stacktrace_state .get ("invert-stacktrace" )
476+ invert_stacktrace = stacktrace_state .get ("invert-stacktrace" )
382477 component = GroupingComponent (
383478 id = "stacktrace" , values = components , hint = hint , contributes = contributes , ** kw
384479 )
385480
386- return component , inverted_hierarchy
481+ compare_rust_components (component , invert_stacktrace , rust_results , frames )
482+
483+ return component , invert_stacktrace
387484
388485 def as_dict (self , with_rules = False ):
389486 rv = {
0 commit comments