From 63caab2f2e5aab6c0ab0f3ae6f01f2ab9ad4000c Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Sat, 15 Nov 2025 15:42:09 -0800 Subject: [PATCH 1/5] add helper for applying custom title from fingerprint --- src/sentry/grouping/api.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/sentry/grouping/api.py b/src/sentry/grouping/api.py index 4e3ae71f6c5e1a..c4a86d8b0ea810 100644 --- a/src/sentry/grouping/api.py +++ b/src/sentry/grouping/api.py @@ -38,6 +38,7 @@ from sentry.models.grouphash import GroupHash from sentry.utils.cache import cache from sentry.utils.hashlib import md5_text +from sentry.utils.safe import get_path if TYPE_CHECKING: from sentry.grouping.fingerprinting import FingerprintingConfig @@ -458,6 +459,18 @@ def get_grouping_variants_for_event( return final_variants +def _apply_custom_title_if_needed(fingerprint_info: FingerprintInfo, event: Event) -> None: + """ + If the given event has a custom fingerprint which includes a title template, apply the custom + title to the event. + """ + custom_title_template = get_path(fingerprint_info, "matched_rule", "attributes", "title") + + if custom_title_template: + resolved_title = expand_title_template(custom_title_template, event.data) + event.data["title"] = resolved_title + + def get_contributing_variant_and_component( variants: dict[str, BaseVariant], ) -> tuple[BaseVariant, ContributingComponent | None]: From 081ca37a01554c81443e5e03f3398abec0c081fa Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Sat, 15 Nov 2025 15:42:10 -0800 Subject: [PATCH 2/5] move custom titling logic later --- src/sentry/grouping/api.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/sentry/grouping/api.py b/src/sentry/grouping/api.py index c4a86d8b0ea810..aefc34e9b9e263 100644 --- a/src/sentry/grouping/api.py +++ b/src/sentry/grouping/api.py @@ -284,15 +284,7 @@ def apply_server_side_fingerprinting( fingerprint_match = fingerprinting_config.get_fingerprint_values_for_event(event) if fingerprint_match is not None: matched_rule, new_fingerprint, attributes = fingerprint_match - - # A custom title attribute is stored in the event to override the - # default title. - if "title" in attributes: - event["title"] = expand_title_template(attributes["title"], event) event["fingerprint"] = new_fingerprint - - # Persist the rule that matched with the fingerprint in the event - # dictionary for later debugging. fingerprint_info["matched_rule"] = matched_rule.to_json() if fingerprint_info: @@ -406,6 +398,9 @@ def get_grouping_variants_for_event( else resolve_fingerprint_values(raw_fingerprint, event.data) ) + # Check if the fingerprint includes a custom title, and if so, set the event's title accordingly. + _apply_custom_title_if_needed(fingerprint_info, event) + # Run all of the event-data-based grouping strategies. Any which apply will create grouping # components, which will then be grouped into variants by variant type (system, app, default). context = GroupingContext(config or _load_default_grouping_config(), event) From 34b98df5fb106ac5ae884da75e6631d42e338654 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Sat, 15 Nov 2025 15:42:10 -0800 Subject: [PATCH 3/5] add TODO --- src/sentry/grouping/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/grouping/api.py b/src/sentry/grouping/api.py index aefc34e9b9e263..2815a8db8d125a 100644 --- a/src/sentry/grouping/api.py +++ b/src/sentry/grouping/api.py @@ -283,6 +283,7 @@ def apply_server_side_fingerprinting( fingerprint_match = fingerprinting_config.get_fingerprint_values_for_event(event) if fingerprint_match is not None: + # TODO: We don't need to return attributes as part of the fingerprint match anymore matched_rule, new_fingerprint, attributes = fingerprint_match event["fingerprint"] = new_fingerprint fingerprint_info["matched_rule"] = matched_rule.to_json() From d1e21203d371ff6f25fa34ce14cb722b4b2f0695 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Sat, 15 Nov 2025 15:42:10 -0800 Subject: [PATCH 4/5] add title logic to manual save in snapshot tests --- tests/sentry/grouping/__init__.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/sentry/grouping/__init__.py b/tests/sentry/grouping/__init__.py index 54c7483da639af..aa4724ab8457dc 100644 --- a/tests/sentry/grouping/__init__.py +++ b/tests/sentry/grouping/__init__.py @@ -26,6 +26,7 @@ GROUPING_CONFIG_CLASSES, register_grouping_config, ) +from sentry.grouping.utils import expand_title_template from sentry.grouping.variants import BaseVariant from sentry.models.project import Project from sentry.services import eventstore @@ -35,6 +36,7 @@ from sentry.testutils.helpers.eventprocessing import save_new_event from sentry.testutils.pytest.fixtures import InstaSnapshotter, django_db_all from sentry.utils import json +from sentry.utils.safe import get_path GROUPING_TESTS_DIR = path.dirname(__file__) GROUPING_INPUTS_DIR = path.join(GROUPING_TESTS_DIR, "grouping_inputs") @@ -82,11 +84,22 @@ def _manually_save_event( mgr.normalize() data = mgr.get_data() - # Normalize the stacktrace for grouping. This normally happens in `EventManager.save`. + # Before creating the event, manually run the parts of `EventManager.save` which are + # necessary for grouping. + normalize_stacktraces_for_grouping(data, load_grouping_config(grouping_config)) data.setdefault("fingerprint", ["{{ default }}"]) apply_server_side_fingerprinting(data, fingerprinting_config) + fingerprint_info = data.get("_fingerprint_info", {}) + custom_title_template = get_path(fingerprint_info, "matched_rule", "attributes", "title") + + # Technically handling custom titles happens during grouping, not before it, but we're not + # running grouping until later, and the title needs to be set before we get metadata below. + if custom_title_template: + resolved_title = expand_title_template(custom_title_template, data) + data["title"] = resolved_title + event_type = get_event_type(data) event_metadata = event_type.get_metadata(data) data.update(materialize_metadata(data, event_type, event_metadata)) @@ -311,8 +324,18 @@ def create_event(self) -> tuple[FingerprintingConfig, Event]: mgr.normalize() data = mgr.get_data() + # Before creating the event, manually run the parts of `EventManager.save` which are + # necessary for fingerprinting. + data.setdefault("fingerprint", ["{{ default }}"]) apply_server_side_fingerprinting(data, config) + fingerprint_info = data.get("_fingerprint_info", {}) + custom_title_template = get_path(fingerprint_info, "matched_rule", "attributes", "title") + + if custom_title_template: + resolved_title = expand_title_template(custom_title_template, data) + data["title"] = resolved_title + event_type = get_event_type(data) event_metadata = event_type.get_metadata(data) data.update(materialize_metadata(data, event_type, event_metadata)) From a426487fba880fc92fe1fe3e8e7dfc1b383ca4c9 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Sat, 15 Nov 2025 15:42:10 -0800 Subject: [PATCH 5/5] add fingerprint snapshot input --- ...ngerprint-title-uses-top-in-app-frame.json | 38 ++++++++++++++++ ...erprint_title_uses_top_in_app_frame.pysnap | 44 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/sentry/grouping/fingerprint_inputs/fingerprint-title-uses-top-in-app-frame.json create mode 100644 tests/sentry/grouping/snapshots/test_fingerprinting/test_event_hash_variant/fingerprint_title_uses_top_in_app_frame.pysnap diff --git a/tests/sentry/grouping/fingerprint_inputs/fingerprint-title-uses-top-in-app-frame.json b/tests/sentry/grouping/fingerprint_inputs/fingerprint-title-uses-top-in-app-frame.json new file mode 100644 index 00000000000000..1a890658a5a751 --- /dev/null +++ b/tests/sentry/grouping/fingerprint_inputs/fingerprint-title-uses-top-in-app-frame.json @@ -0,0 +1,38 @@ +{ + "_fingerprinting_rules": [ + { + "matchers": [["module", "do_dog_stuff"]], + "fingerprint": ["{{ function }}"], + "attributes": { + "title": "Dogs are great ({{ function }})" + } + } + ], + "exception": { + "values": [ + { + "stacktrace": { + "frames": [ + { + "function": "fetch", + "module": "do_dog_stuff", + "in_app": true + }, + { + "function": "throw_ball", + "module": "do_dog_stuff", + "in_app": true + }, + { + "function": "compute_ball_arc", + "module": "physics", + "in_app": false + } + ] + }, + "type": "MathError", + "value": "Missing necessary formulas" + } + ] + } +} diff --git a/tests/sentry/grouping/snapshots/test_fingerprinting/test_event_hash_variant/fingerprint_title_uses_top_in_app_frame.pysnap b/tests/sentry/grouping/snapshots/test_fingerprinting/test_event_hash_variant/fingerprint_title_uses_top_in_app_frame.pysnap new file mode 100644 index 00000000000000..27d18754ae6208 --- /dev/null +++ b/tests/sentry/grouping/snapshots/test_fingerprinting/test_event_hash_variant/fingerprint_title_uses_top_in_app_frame.pysnap @@ -0,0 +1,44 @@ +--- +source: tests/sentry/grouping/test_fingerprinting.py::test_event_hash_variant +--- +config: + rules: + - attributes: + title: Dogs are great ({{ function }}) + fingerprint: + - '{{ function }}' + matchers: + - - module + - do_dog_stuff + text: module:"do_dog_stuff" -> "{{ function }}" title="Dogs are great ({{ function + }})" + version: 1 +fingerprint: +- '{{ function }}' +title: Dogs are great (throw_ball) +variants: + app: + component: + contributes: false + hint: ignored because custom server fingerprint takes precedence + contributes: false + hint: ignored because custom server fingerprint takes precedence + key: app_exception_stacktrace + type: component + custom_fingerprint: + contributes: true + hint: null + key: custom_fingerprint + matched_rule: module:"do_dog_stuff" -> "{{ function }}" title="Dogs are great + ({{ function }})" + type: custom_fingerprint + values: + - throw_ball + system: + component: + contributes: false + hint: ignored because custom server fingerprint takes precedence + contributes: false + hint: ignored because custom server fingerprint takes precedence + key: system_exception_stacktrace + type: component