diff --git a/src/sentry/grouping/api.py b/src/sentry/grouping/api.py index 4e3ae71f6c5e1a..2815a8db8d125a 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 @@ -282,16 +283,9 @@ 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 - - # 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: @@ -405,6 +399,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) @@ -458,6 +455,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]: 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)) 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