diff --git a/src/sentry/api/event_search.py b/src/sentry/api/event_search.py index fcc9a1e4709291..1deb66ad9c5d84 100644 --- a/src/sentry/api/event_search.py +++ b/src/sentry/api/event_search.py @@ -1129,6 +1129,7 @@ def generic_visit(self, node, children): boolean_keys={ "error.handled", "error.unhandled", + "error.main_thread", "stack.in_app", TEAM_KEY_TRANSACTION_ALIAS, }, diff --git a/src/sentry/data/samples/android-ndk.json b/src/sentry/data/samples/android-ndk.json index ba1596503fc9ba..3b78c09a362c4d 100644 --- a/src/sentry/data/samples/android-ndk.json +++ b/src/sentry/data/samples/android-ndk.json @@ -6,6 +6,7 @@ { "type": "SIGSEGV", "value": "Segfault", + "thread_id": 1, "mechanism": { "type": "signalhandler", "synthetic": true, @@ -132,6 +133,14 @@ } ] }, + "threads": { + "values": [ + { + "id": 1, + "main": true + } + ] + }, "platform": "native", "debug_meta": { "images": [ diff --git a/src/sentry/issues/search.py b/src/sentry/issues/search.py index 63b24e2956cc96..12c6721a2d36ea 100644 --- a/src/sentry/issues/search.py +++ b/src/sentry/issues/search.py @@ -267,7 +267,7 @@ def _update_profiling_search_filters( for sf in search_filters: # XXX: we replace queries on these keys to something that should return nothing since # profiling issues doesn't support stacktraces - if sf.key.name in ("error.unhandled", "error.handled"): + if sf.key.name in ("error.unhandled", "error.handled", "error.main_thread"): raise UnsupportedSearchQuery( f"{sf.key.name} filter isn't supported for {GroupCategory.PROFILE.name}" ) diff --git a/src/sentry/rules/conditions/event_attribute.py b/src/sentry/rules/conditions/event_attribute.py index 5588f563efb047..730f10f5f9e6d4 100644 --- a/src/sentry/rules/conditions/event_attribute.py +++ b/src/sentry/rules/conditions/event_attribute.py @@ -18,6 +18,7 @@ "type": Columns.TYPE, "error.handled": Columns.ERROR_HANDLED, "error.unhandled": Columns.ERROR_HANDLED, + "error.main_thread": Columns.ERROR_MAIN_THREAD, "exception.type": Columns.ERROR_TYPE, "exception.value": Columns.ERROR_VALUE, "user.id": Columns.USER_ID, @@ -118,6 +119,8 @@ def _get_attribute_values(self, event: GroupEvent, attr: str) -> Sequence[str]: return [getattr(e, path[1]) for e in event.interfaces["exception"].values] elif path[0] == "error": + # TODO: add support for error.main_thread + if path[1] not in ("handled", "unhandled"): return [] diff --git a/src/sentry/snuba/events.py b/src/sentry/snuba/events.py index 1fdd56a1870e18..a709dbc3f7e919 100644 --- a/src/sentry/snuba/events.py +++ b/src/sentry/snuba/events.py @@ -480,6 +480,14 @@ class Columns(Enum): discover_name="exception_stacks.mechanism_handled", alias="error.handled", ) + ERROR_MAIN_THREAD = Column( + group_name="events.exception_main_thread", + event_name="exception_main_thread", + transaction_name=None, + discover_name="exception_main_thread", + issue_platform_name=None, + alias="error.main_thread", + ) ERROR_RECEIVED = Column( group_name=None, event_name="received", diff --git a/tests/sentry/rules/conditions/test_event_attribute.py b/tests/sentry/rules/conditions/test_event_attribute.py index e0d6a431438638..ebd595216b43c6 100644 --- a/tests/sentry/rules/conditions/test_event_attribute.py +++ b/tests/sentry/rules/conditions/test_event_attribute.py @@ -32,6 +32,7 @@ def get_event(self, **kwargs): } ] }, + "thread_id": 1, } ] }, @@ -57,6 +58,14 @@ def get_event(self, **kwargs): "crash_type": "crash", }, }, + "threads": { + "values": [ + { + "id": 1, + "main": True, + }, + ], + }, } data.update(kwargs) event = self.store_event(data, project_id=self.project.id) diff --git a/tests/snuba/api/endpoints/test_organization_events.py b/tests/snuba/api/endpoints/test_organization_events.py index 6bab12ca2dc62a..64ae80ec2761ed 100644 --- a/tests/snuba/api/endpoints/test_organization_events.py +++ b/tests/snuba/api/endpoints/test_organization_events.py @@ -1165,6 +1165,32 @@ def test_groupby_error_handled_and_unhandled(self): assert 1 == response.data["data"][1]["error.unhandled"] assert 1 == response.data["data"][1]["count()"] + def test_error_main_thread_condition(self): + prototype = self.load_data(platform="android-ndk") + + prototype["timestamp"] = self.ten_mins_ago_iso + self.store_event(data=prototype, project_id=self.project.id) + + with self.feature("organizations:discover-basic"): + query = { + "field": ["id", "project.id"], + "query": "error.main_thread:true", + "project": [self.project.id], + } + response = self.do_request(query) + assert response.status_code == 200, response.data + assert 1 == len(response.data["data"]) + + with self.feature("organizations:discover-basic"): + query = { + "field": ["id", "project.id"], + "query": "error.main_thread:false", + "project": [self.project.id], + } + response = self.do_request(query) + assert response.status_code == 200, response.data + assert 0 == len(response.data["data"]) + def test_implicit_groupby(self): self.store_event( data={ diff --git a/tests/snuba/search/test_backend.py b/tests/snuba/search/test_backend.py index 8106336824e271..d1cf76461e4e94 100644 --- a/tests/snuba/search/test_backend.py +++ b/tests/snuba/search/test_backend.py @@ -1974,6 +1974,126 @@ def test_message_negation(self): assert list(results) == list(results2) + def test_error_main_thread_true(self): + myProject = self.create_project( + name="Foo", slug="foo", teams=[self.team], fire_project_created=True + ) + + event = self.store_event( + data={ + "event_id": "1" * 32, + "message": "something", + "timestamp": iso_format(self.base_datetime), + "exception": { + "values": [ + { + "type": "SyntaxError", + "value": "hello world", + "thread_id": 1, + }, + ], + }, + "threads": { + "values": [ + { + "id": 1, + "main": True, + }, + ], + }, + }, + project_id=myProject.id, + ) + + myGroup = event.groups[0] + + results = self.make_query( + projects=[myProject], + search_filter_query="error.main_thread:1", + sort_by="date", + ) + + assert list(results) == [myGroup] + + def test_error_main_thread_false(self): + myProject = self.create_project( + name="Foo2", slug="foo2", teams=[self.team], fire_project_created=True + ) + + event = self.store_event( + data={ + "event_id": "2" * 32, + "message": "something", + "timestamp": iso_format(self.base_datetime), + "exception": { + "values": [ + { + "type": "SyntaxError", + "value": "hello world", + "thread_id": 1, + }, + ], + }, + "threads": { + "values": [ + { + "id": 1, + "main": False, + }, + ], + }, + }, + project_id=myProject.id, + ) + + myGroup = event.groups[0] + + results = self.make_query( + projects=[myProject], + search_filter_query="error.main_thread:0", + sort_by="date", + ) + + assert list(results) == [myGroup] + + def test_error_main_thread_no_results(self): + myProject = self.create_project( + name="Foo3", slug="foo3", teams=[self.team], fire_project_created=True + ) + + self.store_event( + data={ + "event_id": "3" * 32, + "message": "something", + "timestamp": iso_format(self.base_datetime), + "exception": { + "values": [ + { + "type": "SyntaxError", + "value": "hello world", + "thread_id": 1, + }, + ], + }, + "threads": { + "values": [ + { + "id": 1, + }, + ], + }, + }, + project_id=myProject.id, + ) + + results = self.make_query( + projects=[myProject], + search_filter_query="error.main_thread:1", + sort_by="date", + ) + + assert len(results) == 0 + class EventsTransactionsSnubaSearchTest(SharedSnubaTest): @property @@ -2507,7 +2627,31 @@ def test_rejected_filters(self): count_hits=True, ) - assert list(results) == list(results2) == list(result3) == list(results4) == [] + results5 = self.make_query( + projects=[self.project], + search_filter_query="issue.category:profile error.main_thread:0", + sort_by="date", + limit=1, + count_hits=True, + ) + + results6 = self.make_query( + projects=[self.project], + search_filter_query="issue.category:profile error.main_thread:1", + sort_by="date", + limit=1, + count_hits=True, + ) + + assert ( + list(results) + == list(results2) + == list(result3) + == list(results4) + == list(results5) + == list(results6) + == [] + ) class CdcEventsSnubaSearchTest(SharedSnubaTest):