11from __future__ import annotations
22
33from collections .abc import Generator , Iterator , Sequence
4- from typing import Any
4+ from typing import Any , Self
55
66from sentry .grouping .utils import hash_from_values
77
2323}
2424
2525
26- def _calculate_contributes (values : Sequence [str | int | BaseGroupingComponent ]) -> bool :
26+ def _calculate_contributes [ ValuesType ] (values : Sequence [ValuesType ]) -> bool :
2727 for value in values or ():
2828 if not isinstance (value , BaseGroupingComponent ) or value .contributes :
2929 return True
3030 return False
3131
3232
33- class BaseGroupingComponent :
33+ class BaseGroupingComponent [ ValuesType : str | int | BaseGroupingComponent [ Any ]] :
3434 """A grouping component is a recursive structure that is flattened
3535 into components to make a hash for grouping purposes.
3636 """
3737
3838 id : str = "default"
3939 hint : str | None = None
4040 contributes : bool = False
41- values : Sequence [str | int | BaseGroupingComponent ]
41+ values : Sequence [ValuesType ]
4242
4343 def __init__ (
4444 self ,
45- id : str ,
45+ id : str | None = None ,
4646 hint : str | None = None ,
4747 contributes : bool | None = None ,
48- values : Sequence [str | int | BaseGroupingComponent ] | None = None ,
48+ values : Sequence [ValuesType ] | None = None ,
4949 variant_provider : bool = False ,
5050 ):
51- self .id = id
51+ self .id = id or self . id
5252 self .variant_provider = variant_provider
5353
5454 self .update (
@@ -73,7 +73,7 @@ def description(self) -> str:
7373 paths = []
7474
7575 def _walk_components (
76- component : BaseGroupingComponent , current_path : list [str | None ]
76+ component : BaseGroupingComponent [ Any ] , current_path : list [str | None ]
7777 ) -> None :
7878 # Keep track of the names of the nodes from the root of the component tree to here
7979 current_path .append (component .name )
@@ -101,13 +101,13 @@ def _walk_components(
101101
102102 def get_subcomponent (
103103 self , id : str , only_contributing : bool = False
104- ) -> str | int | BaseGroupingComponent | None :
104+ ) -> str | int | BaseGroupingComponent [ Any ] | None :
105105 """Looks up a subcomponent by the id and returns the first or `None`."""
106106 return next (self .iter_subcomponents (id = id , only_contributing = only_contributing ), None )
107107
108108 def iter_subcomponents (
109109 self , id : str , recursive : bool = False , only_contributing : bool = False
110- ) -> Iterator [str | int | BaseGroupingComponent | None ]:
110+ ) -> Iterator [str | int | BaseGroupingComponent [ Any ] | None ]:
111111 """Finds all subcomponents matching an id, optionally recursively."""
112112 for value in self .values :
113113 if isinstance (value , BaseGroupingComponent ):
@@ -125,7 +125,7 @@ def update(
125125 self ,
126126 hint : str | None = None ,
127127 contributes : bool | None = None ,
128- values : Sequence [str | int | BaseGroupingComponent ] | None = None ,
128+ values : Sequence [ValuesType ] | None = None ,
129129 ) -> None :
130130 """Updates an already existing component with new values."""
131131 if hint is not None :
@@ -137,14 +137,14 @@ def update(
137137 if contributes is not None :
138138 self .contributes = contributes
139139
140- def shallow_copy (self ) -> BaseGroupingComponent :
140+ def shallow_copy (self ) -> Self :
141141 """Creates a shallow copy."""
142142 rv = object .__new__ (self .__class__ )
143143 rv .__dict__ .update (self .__dict__ )
144144 rv .values = list (self .values )
145145 return rv
146146
147- def iter_values (self ) -> Generator [str | int | BaseGroupingComponent ]:
147+ def iter_values (self ) -> Generator [str | int | BaseGroupingComponent [ Any ] ]:
148148 """Recursively walks the component and flattens it into a list of
149149 values.
150150 """
@@ -183,3 +183,139 @@ def as_dict(self) -> dict[str, Any]:
183183
184184 def __repr__ (self ) -> str :
185185 return f"{ self .__class__ .__name__ } ({ self .id !r} , hint={ self .hint !r} , contributes={ self .contributes !r} , values={ self .values !r} )"
186+
187+
188+ # NOTE: In all of the classes below, the type(s) passed to `BaseGroupingComponent` represent
189+ # the type(s) which can appear in the `values` attribute
190+
191+
192+ # Error-related inner components
193+
194+
195+ class ContextLineGroupingComponent (BaseGroupingComponent [str ]):
196+ id : str = "context-line"
197+
198+
199+ class ErrorTypeGroupingComponent (BaseGroupingComponent [str ]):
200+ id : str = "type"
201+
202+
203+ class ErrorValueGroupingComponent (BaseGroupingComponent [str ]):
204+ id : str = "value"
205+
206+
207+ class FilenameGroupingComponent (BaseGroupingComponent [str ]):
208+ id : str = "filename"
209+
210+
211+ class FunctionGroupingComponent (BaseGroupingComponent [str ]):
212+ id : str = "function"
213+
214+
215+ class LineNumberGroupingComponent (BaseGroupingComponent [str ]):
216+ id : str = "lineno"
217+
218+
219+ class ModuleGroupingComponent (BaseGroupingComponent [str ]):
220+ id : str = "module"
221+
222+
223+ class NSErrorGroupingComponent (BaseGroupingComponent [str | int ]):
224+ id : str = "ns-error"
225+
226+
227+ class SymbolGroupingComponent (BaseGroupingComponent [str ]):
228+ id : str = "symbol"
229+
230+
231+ class FrameGroupingComponent (
232+ BaseGroupingComponent [
233+ ContextLineGroupingComponent
234+ | FilenameGroupingComponent
235+ | FunctionGroupingComponent
236+ | LineNumberGroupingComponent # only in legacy config
237+ | ModuleGroupingComponent
238+ | SymbolGroupingComponent # only in legacy config
239+ ]
240+ ):
241+ id : str = "frame"
242+
243+
244+ # Security-related inner components
245+
246+
247+ class HostnameGroupingComponent (BaseGroupingComponent [str ]):
248+ id : str = "hostname"
249+
250+
251+ class SaltGroupingComponent (BaseGroupingComponent [str ]):
252+ id : str = "salt"
253+ hint : str = "a static salt"
254+
255+
256+ class ViolationGroupingComponent (BaseGroupingComponent [str ]):
257+ id : str = "violation"
258+
259+
260+ class URIGroupingComponent (BaseGroupingComponent [str ]):
261+ id : str = "uri"
262+
263+
264+ # Top-level components
265+
266+
267+ class MessageGroupingComponent (BaseGroupingComponent [str ]):
268+ id : str = "message"
269+
270+
271+ class StacktraceGroupingComponent (BaseGroupingComponent [FrameGroupingComponent ]):
272+ id : str = "stacktrace"
273+
274+
275+ class ExceptionGroupingComponent (
276+ BaseGroupingComponent [
277+ ErrorTypeGroupingComponent
278+ | ErrorValueGroupingComponent
279+ | NSErrorGroupingComponent
280+ | StacktraceGroupingComponent
281+ ]
282+ ):
283+ id : str = "exception"
284+
285+
286+ class ChainedExceptionGroupingComponent (BaseGroupingComponent [ExceptionGroupingComponent ]):
287+ id : str = "chained-exception"
288+
289+
290+ class ThreadsGroupingComponent (BaseGroupingComponent [StacktraceGroupingComponent ]):
291+ id : str = "threads"
292+
293+
294+ class CSPGroupingComponent (
295+ BaseGroupingComponent [SaltGroupingComponent | ViolationGroupingComponent | URIGroupingComponent ]
296+ ):
297+ id : str = "csp"
298+
299+
300+ class ExpectCTGroupingComponent (
301+ BaseGroupingComponent [HostnameGroupingComponent | SaltGroupingComponent ]
302+ ):
303+ id : str = "expect-ct"
304+
305+
306+ class ExpectStapleGroupingComponent (
307+ BaseGroupingComponent [HostnameGroupingComponent | SaltGroupingComponent ]
308+ ):
309+ id : str = "expect-staple"
310+
311+
312+ class HPKPGroupingComponent (
313+ BaseGroupingComponent [HostnameGroupingComponent | SaltGroupingComponent ]
314+ ):
315+ id : str = "hpkp"
316+
317+
318+ class TemplateGroupingComponent (
319+ BaseGroupingComponent [ContextLineGroupingComponent | FilenameGroupingComponent ]
320+ ):
321+ id : str = "template"
0 commit comments