Skip to content

Commit a2827ab

Browse files
committed
create grouping component taxonomy
1 parent 6448eb9 commit a2827ab

File tree

7 files changed

+252
-108
lines changed

7 files changed

+252
-108
lines changed

src/sentry/grouping/component.py

+139-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Generator, Iterator, Sequence
4-
from typing import Any, TypeVar
4+
from typing import Any, Generic, TypeVar
55

66
from sentry.grouping.utils import hash_from_values
77

@@ -174,3 +174,141 @@ def __repr__(self) -> str:
174174

175175

176176
GroupingComponent = TypeVar("GroupingComponent", bound=BaseGroupingComponent)
177+
ValueType = TypeVar("ValueType", bound=(str | int))
178+
179+
180+
class ValueGroupingComponent(BaseGroupingComponent, Generic[ValueType]):
181+
"""
182+
A component whose `values` is a list of a single actual value rather than a lit of other
183+
grouping components.
184+
"""
185+
186+
values: list[ValueType]
187+
188+
189+
# Top-level components
190+
191+
192+
class MessageGroupingComponent(ValueGroupingComponent[str]):
193+
id: str = "message"
194+
195+
196+
class ExceptionGroupingComponent(BaseGroupingComponent):
197+
id: str = "exception"
198+
values: list[
199+
ErrorTypeGroupingComponent
200+
| ErrorValueGroupingComponent
201+
| NSErrorGroupingComponent
202+
| StacktraceGroupingComponent
203+
]
204+
205+
206+
class ChainedExceptionGroupingComponent(BaseGroupingComponent):
207+
id: str = "chained-exception"
208+
values: list[ExceptionGroupingComponent]
209+
210+
211+
class StacktraceGroupingComponent(BaseGroupingComponent):
212+
id: str = "stacktrace"
213+
values: list[FrameGroupingComponent]
214+
215+
216+
class ThreadsGroupingComponent(BaseGroupingComponent):
217+
id: str = "threads"
218+
values: list[StacktraceGroupingComponent]
219+
220+
221+
class CSPGroupingComponent(BaseGroupingComponent):
222+
id: str = "csp"
223+
values: list[SaltGroupingComponent | ViolationGroupingComponent | URIGroupingComponent]
224+
225+
226+
class ExpectCTGroupingComponent(BaseGroupingComponent):
227+
id: str = "expect-ct"
228+
values: list[HostnameGroupingComponent | SaltGroupingComponent]
229+
230+
231+
class ExpectStapleGroupingComponent(BaseGroupingComponent):
232+
id: str = "expect-staple"
233+
values: list[HostnameGroupingComponent | SaltGroupingComponent]
234+
235+
236+
class HPKPGroupingComponent(BaseGroupingComponent):
237+
id: str = "hpkp"
238+
values: list[HostnameGroupingComponent | SaltGroupingComponent]
239+
240+
241+
class TemplateGroupingComponent(BaseGroupingComponent):
242+
id: str = "template"
243+
values: list[ContextLineGroupingComponent | FilenameGroupingComponent]
244+
245+
246+
# Error-related inner components
247+
248+
249+
class FrameGroupingComponent(BaseGroupingComponent):
250+
id: str = "frame"
251+
values: list[
252+
ContextLineGroupingComponent
253+
| FilenameGroupingComponent
254+
| FunctionGroupingComponent
255+
| LineNumberGroupingComponent # only in legacy config
256+
| ModuleGroupingComponent
257+
| SymbolGroupingComponent # only in legacy config
258+
]
259+
260+
261+
class ContextLineGroupingComponent(ValueGroupingComponent[str]):
262+
id: str = "context-line"
263+
264+
265+
class ErrorTypeGroupingComponent(ValueGroupingComponent[str]):
266+
id: str = "type"
267+
268+
269+
class ErrorValueGroupingComponent(ValueGroupingComponent[str]):
270+
id: str = "value"
271+
272+
273+
class FilenameGroupingComponent(ValueGroupingComponent[str]):
274+
id: str = "filename"
275+
276+
277+
class FunctionGroupingComponent(ValueGroupingComponent[str]):
278+
id: str = "function"
279+
280+
281+
class LineNumberGroupingComponent(ValueGroupingComponent[str]):
282+
id: str = "lineno"
283+
284+
285+
class ModuleGroupingComponent(ValueGroupingComponent[str]):
286+
id: str = "module"
287+
288+
289+
class NSErrorGroupingComponent(ValueGroupingComponent[str | int]):
290+
id: str = "ns-error"
291+
292+
293+
class SymbolGroupingComponent(ValueGroupingComponent[str]):
294+
id: str = "symbol"
295+
296+
297+
# Security-related inner components
298+
299+
300+
class HostnameGroupingComponent(ValueGroupingComponent[str]):
301+
id: str = "hostname"
302+
303+
304+
class SaltGroupingComponent(ValueGroupingComponent[str]):
305+
id: str = "salt"
306+
hint: str = "a static salt"
307+
308+
309+
class ViolationGroupingComponent(ValueGroupingComponent[str]):
310+
id: str = "violation"
311+
312+
313+
class URIGroupingComponent(ValueGroupingComponent[str]):
314+
id: str = "uri"

src/sentry/grouping/enhancer/__init__.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from sentry_ophio.enhancers import Enhancements as RustEnhancements
1616

1717
from sentry import projectoptions
18-
from sentry.grouping.component import BaseGroupingComponent
18+
from sentry.grouping.component import StacktraceGroupingComponent
1919
from sentry.stacktraces.functions import set_in_app
2020
from sentry.utils.safe import get_path, set_path
2121

@@ -179,8 +179,7 @@ def assemble_stacktrace_component(self, components, frames, platform, exception_
179179
for py_component, rust_component in zip(components, rust_components):
180180
py_component.update(contributes=rust_component.contributes, hint=rust_component.hint)
181181

182-
component = BaseGroupingComponent(
183-
id="stacktrace",
182+
component = StacktraceGroupingComponent(
184183
values=components,
185184
hint=rust_results.hint,
186185
contributes=rust_results.contributes,

src/sentry/grouping/strategies/legacy.py

+37-27
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,22 @@
33
from typing import Any
44

55
from sentry.eventstore.models import Event
6-
from sentry.grouping.component import BaseGroupingComponent
6+
from sentry.grouping.component import (
7+
ChainedExceptionGroupingComponent,
8+
ContextLineGroupingComponent,
9+
ErrorTypeGroupingComponent,
10+
ErrorValueGroupingComponent,
11+
ExceptionGroupingComponent,
12+
FilenameGroupingComponent,
13+
FrameGroupingComponent,
14+
FunctionGroupingComponent,
15+
LineNumberGroupingComponent,
16+
ModuleGroupingComponent,
17+
SaltGroupingComponent,
18+
StacktraceGroupingComponent,
19+
SymbolGroupingComponent,
20+
ThreadsGroupingComponent,
21+
)
722
from sentry.grouping.strategies.base import (
823
GroupingContext,
924
ReturnedVariants,
@@ -164,17 +179,15 @@ def single_exception_legacy(
164179
interface: SingleException, event: Event, context: GroupingContext, **meta: Any
165180
) -> ReturnedVariants:
166181

167-
type_component = BaseGroupingComponent(
168-
id="type",
182+
type_component = ErrorTypeGroupingComponent(
169183
values=[interface.type] if interface.type else [],
170184
contributes=False,
171185
)
172-
value_component = BaseGroupingComponent(
173-
id="value",
186+
value_component = ErrorValueGroupingComponent(
174187
values=[interface.value] if interface.value else [],
175188
contributes=False,
176189
)
177-
stacktrace_component = BaseGroupingComponent(id="stacktrace")
190+
stacktrace_component = StacktraceGroupingComponent()
178191

179192
if interface.stacktrace is not None:
180193
stacktrace_component = context.get_single_grouping_component(
@@ -195,8 +208,8 @@ def single_exception_legacy(
195208
value_component.update(contributes=True)
196209

197210
return {
198-
context["variant"]: BaseGroupingComponent(
199-
id="exception", values=[stacktrace_component, type_component, value_component]
211+
context["variant"]: ExceptionGroupingComponent(
212+
values=[stacktrace_component, type_component, value_component]
200213
)
201214
}
202215

@@ -231,7 +244,7 @@ def chained_exception_legacy(
231244
if stacktrace_component is None or not stacktrace_component.contributes:
232245
value.update(contributes=False, hint="exception has no stacktrace")
233246

234-
return {context["variant"]: BaseGroupingComponent(id="chained-exception", values=values)}
247+
return {context["variant"]: ChainedExceptionGroupingComponent(values=values)}
235248

236249

237250
@chained_exception_legacy.variant_processor
@@ -262,7 +275,7 @@ def frame_legacy(
262275
# Safari throws [native code] frames in for calls like ``forEach``
263276
# whereas Chrome ignores these. Let's remove it from the hashing algo
264277
# so that they're more likely to group together
265-
filename_component = BaseGroupingComponent(id="filename")
278+
filename_component = FilenameGroupingComponent()
266279
if interface.filename == "<anonymous>":
267280
filename_component.update(
268281
contributes=False, values=[interface.filename], hint="anonymous filename discarded"
@@ -293,13 +306,13 @@ def frame_legacy(
293306
# if we have a module we use that for grouping. This will always
294307
# take precedence over the filename, even if the module is
295308
# considered unhashable.
296-
module_component = BaseGroupingComponent(id="module")
309+
module_component = ModuleGroupingComponent()
297310
if interface.module:
298311
if is_unhashable_module_legacy(interface, platform):
299312
module_component.update(
300313
values=[
301-
BaseGroupingComponent(
302-
id="salt", values=["<module>"], hint="normalized generated module name"
314+
SaltGroupingComponent(
315+
values=["<module>"], hint="normalized generated module name"
303316
)
304317
],
305318
hint="ignored module",
@@ -313,7 +326,7 @@ def frame_legacy(
313326
)
314327

315328
# Context line when available is the primary contributor
316-
context_line_component = BaseGroupingComponent(id="context-line")
329+
context_line_component = ContextLineGroupingComponent()
317330
if interface.context_line is not None:
318331
if len(interface.context_line) > 120:
319332
context_line_component.update(hint="discarded because line too long")
@@ -322,9 +335,9 @@ def frame_legacy(
322335
else:
323336
context_line_component.update(values=[interface.context_line])
324337

325-
symbol_component = BaseGroupingComponent(id="symbol")
326-
function_component = BaseGroupingComponent(id="function")
327-
lineno_component = BaseGroupingComponent(id="lineno")
338+
symbol_component = SymbolGroupingComponent()
339+
function_component = FunctionGroupingComponent()
340+
lineno_component = LineNumberGroupingComponent()
328341

329342
# The context line grouping information is the most reliable one.
330343
# If we did not manage to find some information there, we want to
@@ -347,8 +360,8 @@ def frame_legacy(
347360
if is_unhashable_function_legacy(func):
348361
function_component.update(
349362
values=[
350-
BaseGroupingComponent(
351-
id="salt", values=["<function>"], hint="normalized lambda function name"
363+
SaltGroupingComponent(
364+
values=["<function>"], hint="normalized lambda function name"
352365
)
353366
]
354367
)
@@ -380,8 +393,7 @@ def frame_legacy(
380393
)
381394

382395
return {
383-
context["variant"]: BaseGroupingComponent(
384-
id="frame",
396+
context["variant"]: FrameGroupingComponent(
385397
values=[
386398
module_component,
387399
filename_component,
@@ -461,8 +473,7 @@ def threads_legacy(
461473
thread_count = len(interface.values)
462474
if thread_count != 1:
463475
return {
464-
context["variant"]: BaseGroupingComponent(
465-
id="threads",
476+
context["variant"]: ThreadsGroupingComponent(
466477
contributes=False,
467478
hint="ignored because contains %d threads" % thread_count,
468479
)
@@ -471,14 +482,13 @@ def threads_legacy(
471482
stacktrace = interface.values[0].get("stacktrace")
472483
if not stacktrace:
473484
return {
474-
context["variant"]: BaseGroupingComponent(
475-
id="threads", contributes=False, hint="thread has no stacktrace"
485+
context["variant"]: ThreadsGroupingComponent(
486+
contributes=False, hint="thread has no stacktrace"
476487
)
477488
}
478489

479490
return {
480-
context["variant"]: BaseGroupingComponent(
481-
id="threads",
491+
context["variant"]: ThreadsGroupingComponent(
482492
values=[context.get_single_grouping_component(stacktrace, event=event, **meta)],
483493
)
484494
}

src/sentry/grouping/strategies/message.py

+3-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from sentry import analytics
55
from sentry.eventstore.models import Event
66
from sentry.features.rollout import in_rollout_group
7-
from sentry.grouping.component import BaseGroupingComponent
7+
from sentry.grouping.component import MessageGroupingComponent
88
from sentry.grouping.parameterization import Parameterizer, UniqueIdExperiment
99
from sentry.grouping.strategies.base import (
1010
GroupingContext,
@@ -108,17 +108,10 @@ def message_v1(
108108
raw = interface.message or interface.formatted or ""
109109
normalized = normalize_message_for_grouping(raw, event)
110110
hint = "stripped event-specific values" if raw != normalized else None
111-
return {
112-
context["variant"]: BaseGroupingComponent(
113-
id="message",
114-
values=[normalized],
115-
hint=hint,
116-
)
117-
}
111+
return {context["variant"]: MessageGroupingComponent(values=[normalized], hint=hint)}
118112
else:
119113
return {
120-
context["variant"]: BaseGroupingComponent(
121-
id="message",
114+
context["variant"]: MessageGroupingComponent(
122115
values=[interface.message or interface.formatted or ""],
123116
)
124117
}

0 commit comments

Comments
 (0)