Skip to content

Commit 39ba245

Browse files
authored
fix(grouping): Ensure custom titles use the correct frame (#103425)
This fixes a bug caused by #103268, which moved server-side fingerprinting before the call to `normalize_stacktraces_for_grouping`[1]. While it's 99% true that they're unrelated processes (and therefore can happen in any order), the one exception to that is the handling of custom fingerprints which also include title information. In cases where the custom title includes any of the frame variables (`function`, `module`, `package`, or `abs_path`), the frame that gets used is the top in-app frame in the stacktrace. But in-app rules are applied as part of `normalize_stacktraces_for_grouping`, so having it not run until after the custom title is set is obviously a problem. To fix this, the handling of such titles (in other words, the filling-in of the variables and adding of the result to the event) has been moved to live alongside the filling-in-of-the-variables which we do for the fingerprint itself, which is after in-app rules have been applied. A snapshot test illustrating the fix has also been added, showing that it's not the top frame but the top in-app frame which is used for the title. (Why didn't I just switch the order of the two operations back, you ask? Because switching the order was the first step towards absorbing the normalization into variant calculation call which comes immediately after it[2], so server-side fingerprinting needed to get out of its original spot between them.) [1] https://github.com/getsentry/sentry/blob/6812bc023ceb6695bf6b0096852fc184feb82509/src/sentry/grouping/ingest/hashing.py#L58-L66 [2] https://github.com/getsentry/sentry/blob/6812bc023ceb6695bf6b0096852fc184feb82509/src/sentry/grouping/ingest/hashing.py#L68-L69
1 parent 688e214 commit 39ba245

File tree

4 files changed

+123
-9
lines changed

4 files changed

+123
-9
lines changed

src/sentry/grouping/api.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from sentry.models.grouphash import GroupHash
3939
from sentry.utils.cache import cache
4040
from sentry.utils.hashlib import md5_text
41+
from sentry.utils.safe import get_path
4142

4243
if TYPE_CHECKING:
4344
from sentry.grouping.fingerprinting import FingerprintingConfig
@@ -282,16 +283,9 @@ def apply_server_side_fingerprinting(
282283

283284
fingerprint_match = fingerprinting_config.get_fingerprint_values_for_event(event)
284285
if fingerprint_match is not None:
286+
# TODO: We don't need to return attributes as part of the fingerprint match anymore
285287
matched_rule, new_fingerprint, attributes = fingerprint_match
286-
287-
# A custom title attribute is stored in the event to override the
288-
# default title.
289-
if "title" in attributes:
290-
event["title"] = expand_title_template(attributes["title"], event)
291288
event["fingerprint"] = new_fingerprint
292-
293-
# Persist the rule that matched with the fingerprint in the event
294-
# dictionary for later debugging.
295289
fingerprint_info["matched_rule"] = matched_rule.to_json()
296290

297291
if fingerprint_info:
@@ -405,6 +399,9 @@ def get_grouping_variants_for_event(
405399
else resolve_fingerprint_values(raw_fingerprint, event.data)
406400
)
407401

402+
# Check if the fingerprint includes a custom title, and if so, set the event's title accordingly.
403+
_apply_custom_title_if_needed(fingerprint_info, event)
404+
408405
# Run all of the event-data-based grouping strategies. Any which apply will create grouping
409406
# components, which will then be grouped into variants by variant type (system, app, default).
410407
context = GroupingContext(config or _load_default_grouping_config(), event)
@@ -458,6 +455,18 @@ def get_grouping_variants_for_event(
458455
return final_variants
459456

460457

458+
def _apply_custom_title_if_needed(fingerprint_info: FingerprintInfo, event: Event) -> None:
459+
"""
460+
If the given event has a custom fingerprint which includes a title template, apply the custom
461+
title to the event.
462+
"""
463+
custom_title_template = get_path(fingerprint_info, "matched_rule", "attributes", "title")
464+
465+
if custom_title_template:
466+
resolved_title = expand_title_template(custom_title_template, event.data)
467+
event.data["title"] = resolved_title
468+
469+
461470
def get_contributing_variant_and_component(
462471
variants: dict[str, BaseVariant],
463472
) -> tuple[BaseVariant, ContributingComponent | None]:

tests/sentry/grouping/__init__.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
GROUPING_CONFIG_CLASSES,
2727
register_grouping_config,
2828
)
29+
from sentry.grouping.utils import expand_title_template
2930
from sentry.grouping.variants import BaseVariant
3031
from sentry.models.project import Project
3132
from sentry.services import eventstore
@@ -35,6 +36,7 @@
3536
from sentry.testutils.helpers.eventprocessing import save_new_event
3637
from sentry.testutils.pytest.fixtures import InstaSnapshotter, django_db_all
3738
from sentry.utils import json
39+
from sentry.utils.safe import get_path
3840

3941
GROUPING_TESTS_DIR = path.dirname(__file__)
4042
GROUPING_INPUTS_DIR = path.join(GROUPING_TESTS_DIR, "grouping_inputs")
@@ -82,11 +84,22 @@ def _manually_save_event(
8284
mgr.normalize()
8385
data = mgr.get_data()
8486

85-
# Normalize the stacktrace for grouping. This normally happens in `EventManager.save`.
87+
# Before creating the event, manually run the parts of `EventManager.save` which are
88+
# necessary for grouping.
89+
8690
normalize_stacktraces_for_grouping(data, load_grouping_config(grouping_config))
8791

8892
data.setdefault("fingerprint", ["{{ default }}"])
8993
apply_server_side_fingerprinting(data, fingerprinting_config)
94+
fingerprint_info = data.get("_fingerprint_info", {})
95+
custom_title_template = get_path(fingerprint_info, "matched_rule", "attributes", "title")
96+
97+
# Technically handling custom titles happens during grouping, not before it, but we're not
98+
# running grouping until later, and the title needs to be set before we get metadata below.
99+
if custom_title_template:
100+
resolved_title = expand_title_template(custom_title_template, data)
101+
data["title"] = resolved_title
102+
90103
event_type = get_event_type(data)
91104
event_metadata = event_type.get_metadata(data)
92105
data.update(materialize_metadata(data, event_type, event_metadata))
@@ -311,8 +324,18 @@ def create_event(self) -> tuple[FingerprintingConfig, Event]:
311324
mgr.normalize()
312325
data = mgr.get_data()
313326

327+
# Before creating the event, manually run the parts of `EventManager.save` which are
328+
# necessary for fingerprinting.
329+
314330
data.setdefault("fingerprint", ["{{ default }}"])
315331
apply_server_side_fingerprinting(data, config)
332+
fingerprint_info = data.get("_fingerprint_info", {})
333+
custom_title_template = get_path(fingerprint_info, "matched_rule", "attributes", "title")
334+
335+
if custom_title_template:
336+
resolved_title = expand_title_template(custom_title_template, data)
337+
data["title"] = resolved_title
338+
316339
event_type = get_event_type(data)
317340
event_metadata = event_type.get_metadata(data)
318341
data.update(materialize_metadata(data, event_type, event_metadata))
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"_fingerprinting_rules": [
3+
{
4+
"matchers": [["module", "do_dog_stuff"]],
5+
"fingerprint": ["{{ function }}"],
6+
"attributes": {
7+
"title": "Dogs are great ({{ function }})"
8+
}
9+
}
10+
],
11+
"exception": {
12+
"values": [
13+
{
14+
"stacktrace": {
15+
"frames": [
16+
{
17+
"function": "fetch",
18+
"module": "do_dog_stuff",
19+
"in_app": true
20+
},
21+
{
22+
"function": "throw_ball",
23+
"module": "do_dog_stuff",
24+
"in_app": true
25+
},
26+
{
27+
"function": "compute_ball_arc",
28+
"module": "physics",
29+
"in_app": false
30+
}
31+
]
32+
},
33+
"type": "MathError",
34+
"value": "Missing necessary formulas"
35+
}
36+
]
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
source: tests/sentry/grouping/test_fingerprinting.py::test_event_hash_variant
3+
---
4+
config:
5+
rules:
6+
- attributes:
7+
title: Dogs are great ({{ function }})
8+
fingerprint:
9+
- '{{ function }}'
10+
matchers:
11+
- - module
12+
- do_dog_stuff
13+
text: module:"do_dog_stuff" -> "{{ function }}" title="Dogs are great ({{ function
14+
}})"
15+
version: 1
16+
fingerprint:
17+
- '{{ function }}'
18+
title: Dogs are great (throw_ball)
19+
variants:
20+
app:
21+
component:
22+
contributes: false
23+
hint: ignored because custom server fingerprint takes precedence
24+
contributes: false
25+
hint: ignored because custom server fingerprint takes precedence
26+
key: app_exception_stacktrace
27+
type: component
28+
custom_fingerprint:
29+
contributes: true
30+
hint: null
31+
key: custom_fingerprint
32+
matched_rule: module:"do_dog_stuff" -> "{{ function }}" title="Dogs are great
33+
({{ function }})"
34+
type: custom_fingerprint
35+
values:
36+
- throw_ball
37+
system:
38+
component:
39+
contributes: false
40+
hint: ignored because custom server fingerprint takes precedence
41+
contributes: false
42+
hint: ignored because custom server fingerprint takes precedence
43+
key: system_exception_stacktrace
44+
type: component

0 commit comments

Comments
 (0)