From 8b234c28b6431eb42c4bd252c438ec04df406057 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Fri, 5 Jul 2019 13:48:01 -0700 Subject: [PATCH 1/8] feat: Use Snuba events in group model Refactor group model to always use events from Snuba instead of Postgres --- src/sentry/models/group.py | 88 ++++++++---------------------- tests/integration/tests.py | 4 +- tests/sentry/tasks/test_unmerge.py | 4 -- 3 files changed, 23 insertions(+), 73 deletions(-) diff --git a/src/sentry/models/group.py b/src/sentry/models/group.py index e3c2dd7bb26506..8679d7032e2c32 100644 --- a/src/sentry/models/group.py +++ b/src/sentry/models/group.py @@ -21,9 +21,7 @@ from django.utils.translation import ugettext_lazy as _ from sentry import eventtypes, tagstore, options -from sentry.constants import ( - DEFAULT_LOGGER_NAME, EVENT_ORDERING_KEY, LOG_LEVELS, MAX_CULPRIT_LENGTH -) +from sentry.constants import DEFAULT_LOGGER_NAME, LOG_LEVELS, MAX_CULPRIT_LENGTH from sentry.db.models import ( BaseManager, BoundedBigIntegerField, BoundedIntegerField, BoundedPositiveIntegerField, FlexibleForeignKey, GzippedDictField, Model, sane_repr @@ -158,24 +156,13 @@ def from_event_id(self, project, event_id): Resolves the 32 character event_id string into a Group for which it is found. """ - from sentry.models import EventMapping, Event + from sentry.models import SnubaEvent group_id = None - # Look up event_id in both Event and EventMapping, - # and bail when it matches one of them, prioritizing - # Event since it contains more history. - for model in Event, EventMapping: - try: - group_id = model.objects.filter( - project_id=project.id, - event_id=event_id, - ).values_list('group_id', flat=True)[0] - - # It's possible that group_id is NULL - if group_id is not None: - break - except IndexError: - pass + event = SnubaEvent.objects.from_event_id(event_id, project.id) + + if event: + group_id = event.group_id if group_id is None: # Raise a Group.DoesNotExist here since it makes @@ -186,18 +173,22 @@ def from_event_id(self, project, event_id): return Group.objects.get(id=group_id) def filter_by_event_id(self, project_ids, event_id): - from sentry.models import EventMapping, Event + from sentry.utils import snuba + group_ids = set() - # see above for explanation as to why we're - # looking at both Event and EventMapping - for model in Event, EventMapping: - group_ids.update( - model.objects.filter( - project_id__in=project_ids, - event_id=event_id, - group_id__isnull=False, - ).values_list('group_id', flat=True) - ) + + group_ids = set([evt['issue'] for evt in snuba.raw_query( + start=datetime.utcfromtimestamp(0), + end=datetime.utcnow(), + selected_columns=['issue'], + conditions=[['issue', 'IS NOT NULL', None]], + filter_keys={ + 'event_id': [event_id], + 'project_id': project_ids, + }, + limit=1, + referrer="Group.filter_by_event_id", + )['data']]) return Group.objects.filter(id__in=group_ids) @@ -325,11 +316,6 @@ def qualified_short_id(self): if self.short_id is not None: return '%s-%s' % (self.project.slug.upper(), base32_encode(self.short_id), ) - @property - def event_set(self): - from sentry.models import Event - return Event.objects.filter(group_id=self.id) - def is_over_resolve_age(self): resolve_age = self.project.get_option('sentry:resolve_age', None) if not resolve_age: @@ -390,21 +376,7 @@ def get_score(self): return type(self).calculate_score(self.times_seen, self.last_seen) def get_latest_event(self): - from sentry.models import Event - - if not hasattr(self, '_latest_event'): - latest_events = sorted( - Event.objects.filter( - group_id=self.id, - ).order_by('-datetime')[0:5], - key=EVENT_ORDERING_KEY, - reverse=True, - ) - try: - self._latest_event = latest_events[0] - except IndexError: - self._latest_event = None - return self._latest_event + return self.get_latest_event_for_environments() def get_latest_event_for_environments(self, environments=()): use_snuba = options.get('snuba.events-queries.enabled') @@ -419,22 +391,6 @@ def get_latest_event_for_environments(self, environments=()): issue_id=self.id, project_id=self.project_id) - def get_oldest_event(self): - from sentry.models import Event - - if not hasattr(self, '_oldest_event'): - oldest_events = sorted( - Event.objects.filter( - group_id=self.id, - ).order_by('datetime')[0:5], - key=EVENT_ORDERING_KEY, - ) - try: - self._oldest_event = oldest_events[0] - except IndexError: - self._oldest_event = None - return self._oldest_event - def get_oldest_event_for_environments(self, environments=()): use_snuba = options.get('snuba.events-queries.enabled') diff --git a/tests/integration/tests.py b/tests/integration/tests.py index 6ac6e1892ed2f3..de4965d3fd3081 100644 --- a/tests/integration/tests.py +++ b/tests/integration/tests.py @@ -150,9 +150,7 @@ def queue_event(method, url, body, headers): assert request.call_count is 1 assert Group.objects.count() == 1 group = Group.objects.get() - assert group.event_set.count() == 1 - instance = group.event_set.get() - assert instance.data['logentry']['formatted'] == 'foo' + assert group.data['title'] == 'foo' class SentryRemoteTest(TestCase): diff --git a/tests/sentry/tasks/test_unmerge.py b/tests/sentry/tasks/test_unmerge.py index 98f8fe5c06a019..b5421fcde32a1d 100644 --- a/tests/sentry/tasks/test_unmerge.py +++ b/tests/sentry/tasks/test_unmerge.py @@ -435,8 +435,6 @@ def create_message_event(template, parameters, environment, release): events.values()[0], ) - assert source.event_set.count() == 10 - assert set( EventMapping.objects.filter( group_id=source.id, @@ -497,8 +495,6 @@ def create_message_event(template, parameters, environment, release): events.values()[1], ) - assert destination.event_set.count() == 7 - assert set( EventMapping.objects.filter( group_id=destination.id, From 338616d565b0978e55b6dfa741977801df6db0cf Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Fri, 5 Jul 2019 16:47:56 -0700 Subject: [PATCH 2/8] fix tests --- .../test_group_integration_details.py | 16 +++++++- .../test_organization_group_index.py | 35 +++++++++-------- .../api/endpoints/test_project_group_index.py | 38 +++++++++---------- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/tests/sentry/api/endpoints/test_group_integration_details.py b/tests/sentry/api/endpoints/test_group_integration_details.py index 751d9fc1e7d33a..ec10189b3dbe8a 100644 --- a/tests/sentry/api/endpoints/test_group_integration_details.py +++ b/tests/sentry/api/endpoints/test_group_integration_details.py @@ -2,6 +2,8 @@ import six import mock +from datetime import timedelta +from django.utils import timezone from sentry.integrations.example.integration import ExampleIntegration from sentry.integrations.exceptions import IntegrationError @@ -11,6 +13,10 @@ class GroupIntegrationDetailsTest(APITestCase): + def setUp(self): + super(GroupIntegrationDetailsTest, self).setUp() + self.min_ago = timezone.now() - timedelta(minutes=1) + def test_simple_get_link(self): self.login_as(user=self.user) org = self.organization @@ -367,8 +373,14 @@ def assert_default_project(path, action, expected_project_field): self.login_as(user=self.user) org = self.organization - group = self.create_group() - self.create_event(group=group) + event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'timestamp': self.min_ago.isoformat()[:19], + }, + project_id=self.project.id + ) + group = event.group integration = Integration.objects.create( provider='example', name='Example', diff --git a/tests/snuba/api/endpoints/test_organization_group_index.py b/tests/snuba/api/endpoints/test_organization_group_index.py index 3e78681e82bbf0..f9e6c0af6172a7 100644 --- a/tests/snuba/api/endpoints/test_organization_group_index.py +++ b/tests/snuba/api/endpoints/test_organization_group_index.py @@ -10,9 +10,9 @@ from mock import patch, Mock from sentry.models import ( - Activity, ApiToken, Event, EventMapping, ExternalIssue, Group, GroupAssignee, - GroupBookmark, GroupHash, GroupLink, GroupSeen, GroupShare, GroupSnooze, - GroupStatus, GroupResolution, GroupSubscription, GroupTombstone, Integration, + Activity, ApiToken, ExternalIssue, Group, GroupAssignee, GroupBookmark, + GroupHash, GroupLink, GroupSeen, GroupShare, GroupSnooze, GroupStatus, + GroupResolution, GroupSubscription, GroupTombstone, Integration, OrganizationIntegration, UserOption, Release ) from sentry.testutils import APITestCase, SnubaTestCase @@ -209,14 +209,13 @@ def test_auto_resolved(self): def test_lookup_by_event_id(self): project = self.project project.update_option('sentry:resolve_age', 1) - group = self.create_group(checksum='a' * 32) - self.create_group(checksum='b' * 32) event_id = 'c' * 32 - Event.objects.create(project_id=self.project.id, event_id=event_id) - EventMapping.objects.create( - event_id=event_id, - project=group.project, - group=group, + event = self.store_event( + data={ + 'event_id': event_id, + 'timestamp': self.min_ago.isoformat()[:19], + }, + project_id=self.project.id ) self.login_as(user=self.user) @@ -224,7 +223,7 @@ def test_lookup_by_event_id(self): response = self.get_valid_response(query='c' * 32) assert response['X-Sentry-Direct-Hit'] == '1' assert len(response.data) == 1 - assert response.data[0]['id'] == six.text_type(group.id) + assert response.data[0]['id'] == six.text_type(event.group.id) assert response.data[0]['matchingEventId'] == event_id def test_lookup_by_event_id_incorrect_project_id(self): @@ -257,20 +256,20 @@ def test_lookup_by_event_id_incorrect_project_id(self): def test_lookup_by_event_id_with_whitespace(self): project = self.project project.update_option('sentry:resolve_age', 1) - group = self.create_group(checksum='a' * 32) event_id = 'c' * 32 - self.create_group(checksum='b' * 32) - EventMapping.objects.create( - event_id=event_id, - project=group.project, - group=group, + event = self.store_event( + data={ + 'event_id': event_id, + 'timestamp': self.min_ago.isoformat()[:19], + }, + project_id=self.project.id ) self.login_as(user=self.user) response = self.get_valid_response(query=' {} '.format('c' * 32)) assert response['X-Sentry-Direct-Hit'] == '1' assert len(response.data) == 1 - assert response.data[0]['id'] == six.text_type(group.id) + assert response.data[0]['id'] == six.text_type(event.group.id) assert response.data[0]['matchingEventId'] == event_id def test_lookup_by_unknown_event_id(self): diff --git a/tests/snuba/api/endpoints/test_project_group_index.py b/tests/snuba/api/endpoints/test_project_group_index.py index 55baece15eb641..76796fdda86ac7 100644 --- a/tests/snuba/api/endpoints/test_project_group_index.py +++ b/tests/snuba/api/endpoints/test_project_group_index.py @@ -10,11 +10,10 @@ from mock import patch, Mock from sentry.models import ( - Activity, ApiToken, EventMapping, Group, GroupAssignee, GroupBookmark, GroupHash, - GroupLink, GroupResolution, GroupSeen, GroupShare, GroupSnooze, GroupStatus, GroupSubscription, + Activity, ApiToken, Group, GroupAssignee, GroupBookmark, GroupHash, GroupLink, + GroupResolution, GroupSeen, GroupShare, GroupSnooze, GroupStatus, GroupSubscription, GroupTombstone, ExternalIssue, Integration, Release, OrganizationIntegration, UserOption ) -from sentry.models.event import Event from sentry.testutils import APITestCase, SnubaTestCase from sentry.testutils.helpers import parse_link_header from six.moves.urllib.parse import quote @@ -192,31 +191,31 @@ def test_auto_resolved(self): def test_lookup_by_event_id(self): project = self.project project.update_option('sentry:resolve_age', 1) - group = self.create_group(checksum='a' * 32) - self.create_group(checksum='b' * 32) event_id = 'c' * 32 - event = Event.objects.create(project_id=self.project.id, event_id=event_id) - EventMapping.objects.create( - event_id=event_id, - project=group.project, - group=group, + event = self.store_event( + data={ + 'event_id': event_id, + 'timestamp': self.min_ago.isoformat()[:19], + }, + project_id=self.project.id ) - self.login_as(user=self.user) response = self.client.get(u'{}?query={}'.format(self.path, 'c' * 32), format='json') assert response.status_code == 200 assert len(response.data) == 1 - assert response.data[0]['id'] == six.text_type(group.id) + assert response.data[0]['id'] == six.text_type(event.group.id) assert response.data[0]['matchingEventId'] == event.id def test_lookup_by_event_with_matching_environment(self): project = self.project project.update_option('sentry:resolve_age', 1) self.create_environment(name="test", project=project) + event = self.store_event( data={ 'environment': 'test', + 'timestamp': self.min_ago.isoformat()[:19], }, project_id=self.project.id, ) @@ -235,21 +234,20 @@ def test_lookup_by_event_with_matching_environment(self): def test_lookup_by_event_id_with_whitespace(self): project = self.project project.update_option('sentry:resolve_age', 1) - group = self.create_group(checksum='a' * 32) - self.create_group(checksum='b' * 32) - EventMapping.objects.create( - event_id='c' * 32, - project=group.project, - group=group, + event = self.store_event( + data={ + 'event_id': 'c' * 32, + 'timestamp': self.min_ago.isoformat()[:19], + }, + project_id=self.project.id ) - self.login_as(user=self.user) response = self.client.get( u'{}?query=%20%20{}%20%20'.format(self.path, 'c' * 32), format='json' ) assert response.status_code == 200 assert len(response.data) == 1 - assert response.data[0]['id'] == six.text_type(group.id) + assert response.data[0]['id'] == six.text_type(event.group.id) def test_lookup_by_unknown_event_id(self): project = self.project From 37463870ce4ada9293ed9e21c0faaf29bc8c662d Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Fri, 5 Jul 2019 17:37:19 -0700 Subject: [PATCH 3/8] fix jira tests --- .../integrations/jira/test_integration.py | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/sentry/integrations/jira/test_integration.py b/tests/sentry/integrations/jira/test_integration.py index d200b054fac6f7..dc10539ce292f6 100644 --- a/tests/sentry/integrations/jira/test_integration.py +++ b/tests/sentry/integrations/jira/test_integration.py @@ -5,6 +5,7 @@ import responses import six import pytest +import copy from django.core.urlresolvers import reverse from django.utils import timezone @@ -17,6 +18,7 @@ ) from sentry.testutils import APITestCase from sentry.utils.http import absolute_uri +from sentry.testutils.factories import DEFAULT_EVENT_DATA SAMPLE_CREATE_META_RESPONSE = """ @@ -438,8 +440,15 @@ def integration(self): def test_get_create_issue_config(self): org = self.organization self.login_as(self.user) - group = self.create_group() - self.create_event(group=group) + event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'message': 'message', + 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), + }, + project_id=self.project.id, + ) + group = event.group installation = self.integration.get_installation(org.id) @@ -462,7 +471,7 @@ def get_client(): 'required': True, }, { 'default': ('Sentry Issue: [%s|%s]\n\n{code}\n' - 'Stacktrace (most recent call last):\n\n ' + 'Stacktrace (most recent call first):\n\n ' 'File "sentry/models/foo.py", line 29, in build_msg\n ' 'string_max_length=self.string_max_length)\n\nmessage\n{code}' ) % ( @@ -510,9 +519,15 @@ def get_client(): def test_get_create_issue_config_with_default_and_param(self): org = self.organization self.login_as(self.user) - group = self.create_group() - self.create_event(group=group) - + event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'message': 'message', + 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), + }, + project_id=self.project.id, + ) + group = event.group installation = self.integration.get_installation(org.id) installation.org_integration.config = { 'project_issue_defaults': { @@ -540,9 +555,15 @@ def get_client(): def test_get_create_issue_config_with_default(self): org = self.organization self.login_as(self.user) - group = self.create_group() - self.create_event(group=group) - + event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'message': 'message', + 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), + }, + project_id=self.project.id, + ) + group = event.group installation = self.integration.get_installation(org.id) installation.org_integration.config = { 'project_issue_defaults': { @@ -570,9 +591,15 @@ def get_client(): def test_get_create_issue_config_with_label_default(self): org = self.organization self.login_as(self.user) - group = self.create_group() - self.create_event(group=group) - + event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'message': 'message', + 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), + }, + project_id=self.project.id, + ) + group = event.group label_default = 'hi' installation = self.integration.get_installation(org.id) From cb20a68bc3810775b18cfaab5bc14ad6b78b07d2 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Fri, 5 Jul 2019 17:47:07 -0700 Subject: [PATCH 4/8] fix more tests --- .../sentry/integrations/github/test_issues.py | 56 +++++++++++++------ .../integrations/jira/test_integration.py | 4 ++ tests/sentry/plugins/bases/test_issue2.py | 14 +++-- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/tests/sentry/integrations/github/test_issues.py b/tests/sentry/integrations/github/test_issues.py index 98ab1f2bf09b57..1daeba602c79a8 100644 --- a/tests/sentry/integrations/github/test_issues.py +++ b/tests/sentry/integrations/github/test_issues.py @@ -155,8 +155,12 @@ def test_link_issue(self, mock_get_jwt): @responses.activate @patch('sentry.integrations.github.client.get_jwt', return_value='jwt_token_1') def test_repo_dropdown_choices(self, mock_get_jwt): - group = self.create_group() - self.create_event(group) + event = self.store_event( + data={ + 'event_id': 'a' * 32, + }, + project_id=self.project.id, + ) responses.add( responses.POST, @@ -176,7 +180,7 @@ def test_repo_dropdown_choices(self, mock_get_jwt): json={'repositories': [{'full_name': 'getsentry/sentry', 'name': 'sentry'}]} ) - resp = self.integration.get_create_issue_config(group=self.group) + resp = self.integration.get_create_issue_config(group=event.group) assert resp[0]['choices'] == [(u'getsentry/sentry', u'sentry')] responses.add( @@ -187,12 +191,12 @@ def test_repo_dropdown_choices(self, mock_get_jwt): # create an issue data = {'params': {'repo': 'getsentry/hello'}} - resp = self.integration.get_create_issue_config(group=self.group, **data) + resp = self.integration.get_create_issue_config(group=event.group, **data) assert resp[0]['choices'] == [(u'getsentry/hello', u'hello'), (u'getsentry/sentry', u'sentry')] # link an issue data = {'params': {'repo': 'getsentry/hello'}} - resp = self.integration.get_link_issue_config(group=self.group, **data) + resp = self.integration.get_link_issue_config(group=event.group, **data) assert resp[0]['choices'] == [(u'getsentry/hello', u'hello'), (u'getsentry/sentry', u'sentry')] @@ -240,8 +244,14 @@ def test_default_repo_link_fields(self, mock_get_jwt): ] }, ) - group = self.create_group() - self.create_event(group=group) + event = self.store_event( + data={ + 'event_id': 'a' * 32, + }, + project_id=self.project.id, + ) + group = event.group + org_integration = self.integration.org_integration org_integration.config = { 'project_issue_defaults': { @@ -278,8 +288,13 @@ def test_default_repo_create_fields(self, mock_get_jwt): 'https://api.github.com/installations/github_external_id/access_tokens', json={'token': 'token_1', 'expires_at': '2018-10-11T22:14:10Z'} ) - group = self.create_group() - self.create_event(group=group) + event = self.store_event( + data={ + 'event_id': 'a' * 32, + }, + project_id=self.project.id, + ) + group = event.group org_integration = self.integration.org_integration org_integration.config = { 'project_issue_defaults': { @@ -304,10 +319,13 @@ def test_default_repo_link_fields_no_repos(self, mock_get_jwt): 'repositories': [] }, ) - group = self.create_group() - self.create_event(group=group) - - fields = self.integration.get_link_issue_config(group) + event = self.store_event( + data={ + 'event_id': 'a' * 32, + }, + project_id=self.project.id, + ) + fields = self.integration.get_link_issue_config(event.group) repo_field = [field for field in fields if field['name'] == 'repo'][0] assert repo_field['default'] is '' assert repo_field['choices'] == [] @@ -327,11 +345,13 @@ def test_default_repo_create_fields_no_repos(self, mock_get_jwt): 'https://api.github.com/installations/github_external_id/access_tokens', json={'token': 'token_1', 'expires_at': '2018-10-11T22:14:10Z'} ) - - group = self.create_group() - self.create_event(group=group) - - fields = self.integration.get_create_issue_config(group) + event = self.store_event( + data={ + 'event_id': 'a' * 32, + }, + project_id=self.project.id, + ) + fields = self.integration.get_create_issue_config(event.group) repo_field = [field for field in fields if field['name'] == 'repo'][0] assignee_field = [field for field in fields if field['name'] == 'assignee'][0] diff --git a/tests/sentry/integrations/jira/test_integration.py b/tests/sentry/integrations/jira/test_integration.py index dc10539ce292f6..045c4d2bbe9208 100644 --- a/tests/sentry/integrations/jira/test_integration.py +++ b/tests/sentry/integrations/jira/test_integration.py @@ -444,6 +444,7 @@ def test_get_create_issue_config(self): data={ 'event_id': 'a' * 32, 'message': 'message', + 'timestamp': timezone.now().isoformat(), 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, @@ -523,6 +524,7 @@ def test_get_create_issue_config_with_default_and_param(self): data={ 'event_id': 'a' * 32, 'message': 'message', + 'timestamp': timezone.now().isoformat(), 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, @@ -559,6 +561,7 @@ def test_get_create_issue_config_with_default(self): data={ 'event_id': 'a' * 32, 'message': 'message', + 'timestamp': timezone.now().isoformat(), 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, @@ -595,6 +598,7 @@ def test_get_create_issue_config_with_label_default(self): data={ 'event_id': 'a' * 32, 'message': 'message', + 'timestamp': timezone.now().isoformat(), 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, diff --git a/tests/sentry/plugins/bases/test_issue2.py b/tests/sentry/plugins/bases/test_issue2.py index 3cda7000d1e6e4..77ed32bc8eed1a 100644 --- a/tests/sentry/plugins/bases/test_issue2.py +++ b/tests/sentry/plugins/bases/test_issue2.py @@ -4,7 +4,9 @@ import json import mock +from datetime import timedelta +from django.utils import timezone from social_auth.models import UserSocialAuth from sentry.models import GroupMeta, User @@ -80,12 +82,16 @@ class IssuePlugin2GroupActionTest(TestCase): def setUp(self): super(IssuePlugin2GroupActionTest, self).setUp() self.project = self.create_project() - self.group = self.create_group(project=self.project) self.plugin_instance = plugins.get(slug='issuetrackingplugin2') - self.event = self.create_event( - event_id='a', - group=self.group, + min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] + self.event = self.store_event( + data={ + 'timestamp': min_ago, + 'fingerprint': ['group-1'] + }, + project_id=self.project.id ) + self.group = self.event.group @mock.patch('sentry.plugins.IssueTrackingPlugin2.is_configured', return_value=True) def test_get_create(self, *args): From 3d3aa8be9807b50b6548c0b1b5a6f5b1027b5f4e Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Fri, 5 Jul 2019 18:04:59 -0700 Subject: [PATCH 5/8] fix moar tests --- src/sentry/models/group.py | 4 +-- .../test_group_integration_details.py | 32 ++++++++++++------- .../sentry/integrations/github/test_issues.py | 8 +++++ .../integrations/jira/test_integration.py | 17 ++++++---- 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/sentry/models/group.py b/src/sentry/models/group.py index 8679d7032e2c32..13f95b7ef51771 100644 --- a/src/sentry/models/group.py +++ b/src/sentry/models/group.py @@ -175,8 +175,6 @@ def from_event_id(self, project, event_id): def filter_by_event_id(self, project_ids, event_id): from sentry.utils import snuba - group_ids = set() - group_ids = set([evt['issue'] for evt in snuba.raw_query( start=datetime.utcfromtimestamp(0), end=datetime.utcnow(), @@ -186,7 +184,7 @@ def filter_by_event_id(self, project_ids, event_id): 'event_id': [event_id], 'project_id': project_ids, }, - limit=1, + limit=1000, referrer="Group.filter_by_event_id", )['data']]) diff --git a/tests/sentry/api/endpoints/test_group_integration_details.py b/tests/sentry/api/endpoints/test_group_integration_details.py index ec10189b3dbe8a..aeb0980c33f942 100644 --- a/tests/sentry/api/endpoints/test_group_integration_details.py +++ b/tests/sentry/api/endpoints/test_group_integration_details.py @@ -4,30 +4,42 @@ import mock from datetime import timedelta from django.utils import timezone +import copy from sentry.integrations.example.integration import ExampleIntegration from sentry.integrations.exceptions import IntegrationError from sentry.models import Activity, ExternalIssue, GroupLink, Integration from sentry.testutils import APITestCase from sentry.utils.http import absolute_uri +from sentry.testutils.factories import DEFAULT_EVENT_DATA class GroupIntegrationDetailsTest(APITestCase): def setUp(self): super(GroupIntegrationDetailsTest, self).setUp() self.min_ago = timezone.now() - timedelta(minutes=1) + self.event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'timestamp': self.min_ago.isoformat()[:19], + 'message': 'message', + 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), + }, + project_id=self.project.id + ) + self.group = self.event.group def test_simple_get_link(self): self.login_as(user=self.user) org = self.organization - group = self.create_group() integration = Integration.objects.create( provider='example', name='Example', ) integration.add_organization(org, self.user) - path = u'/api/0/issues/{}/integrations/{}/?action=link'.format(group.id, integration.id) + path = u'/api/0/issues/{}/integrations/{}/?action=link'.format( + self.group.id, integration.id) with self.feature('organizations:integrations-issue-basic'): response = self.client.get(path) @@ -67,13 +79,12 @@ def test_simple_get_link(self): def test_simple_get_create(self): self.login_as(user=self.user) org = self.organization - group = self.create_group() - self.create_event(group=group) integration = Integration.objects.create( provider='example', name='Example', ) integration.add_organization(org, self.user) + group = self.group path = u'/api/0/issues/{}/integrations/{}/?action=create'.format(group.id, integration.id) @@ -105,7 +116,7 @@ def test_simple_get_create(self): 'required': True, }, { 'default': ('Sentry Issue: [%s](%s)\n\n```\n' - 'Stacktrace (most recent call last):\n\n ' + 'Stacktrace (most recent call first):\n\n ' 'File "sentry/models/foo.py", line 29, in build_msg\n ' 'string_max_length=self.string_max_length)\n\nmessage\n```' ) % (group.qualified_short_id, absolute_uri(group.get_absolute_url(params={'referrer': 'example_integration'}))), @@ -127,15 +138,14 @@ def test_simple_get_create(self): def test_get_create_with_error(self): self.login_as(user=self.user) org = self.organization - group = self.create_group() - self.create_event(group=group) integration = Integration.objects.create( provider='example', name='Example', ) integration.add_organization(org, self.user) - path = u'/api/0/issues/{}/integrations/{}/?action=create'.format(group.id, integration.id) + path = u'/api/0/issues/{}/integrations/{}/?action=create'.format( + self.group.id, integration.id) with self.feature('organizations:integrations-issue-basic'): with mock.patch.object(ExampleIntegration, 'get_create_issue_config', side_effect=IntegrationError('oops')): @@ -147,15 +157,14 @@ def test_get_create_with_error(self): def test_get_feature_disabled(self): self.login_as(user=self.user) org = self.organization - group = self.create_group() - self.create_event(group=group) integration = Integration.objects.create( provider='example', name='Example', ) integration.add_organization(org, self.user) - path = u'/api/0/issues/{}/integrations/{}/?action=create'.format(group.id, integration.id) + path = u'/api/0/issues/{}/integrations/{}/?action=create'.format( + self.group.id, integration.id) with self.feature({'organizations:integrations-issue-basic': False}): response = self.client.get(path) @@ -173,7 +182,6 @@ def test_simple_put(self): integration.add_organization(org, self.user) path = u'/api/0/issues/{}/integrations/{}/'.format(group.id, integration.id) - with self.feature('organizations:integrations-issue-basic'): response = self.client.put(path, data={ 'externalIssue': 'APP-123' diff --git a/tests/sentry/integrations/github/test_issues.py b/tests/sentry/integrations/github/test_issues.py index 1daeba602c79a8..b6ca0fb53052e6 100644 --- a/tests/sentry/integrations/github/test_issues.py +++ b/tests/sentry/integrations/github/test_issues.py @@ -2,10 +2,12 @@ import responses import six +from datetime import timedelta from mock import patch from exam import fixture from django.test import RequestFactory +from django.utils import timezone from sentry.integrations.github.integration import GitHubIntegration from sentry.models import Integration, ExternalIssue @@ -28,6 +30,7 @@ def setUp(self): ) self.model.add_organization(self.organization, self.user) self.integration = GitHubIntegration(self.model, self.organization.id) + self.min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] @responses.activate @patch('sentry.integrations.github.client.get_jwt', return_value='jwt_token_1') @@ -158,6 +161,7 @@ def test_repo_dropdown_choices(self, mock_get_jwt): event = self.store_event( data={ 'event_id': 'a' * 32, + 'timestamp': self.min_ago, }, project_id=self.project.id, ) @@ -247,6 +251,7 @@ def test_default_repo_link_fields(self, mock_get_jwt): event = self.store_event( data={ 'event_id': 'a' * 32, + 'timestamp': self.min_ago, }, project_id=self.project.id, ) @@ -291,6 +296,7 @@ def test_default_repo_create_fields(self, mock_get_jwt): event = self.store_event( data={ 'event_id': 'a' * 32, + 'timestamp': self.min_ago, }, project_id=self.project.id, ) @@ -322,6 +328,7 @@ def test_default_repo_link_fields_no_repos(self, mock_get_jwt): event = self.store_event( data={ 'event_id': 'a' * 32, + 'timestamp': self.min_ago, }, project_id=self.project.id, ) @@ -348,6 +355,7 @@ def test_default_repo_create_fields_no_repos(self, mock_get_jwt): event = self.store_event( data={ 'event_id': 'a' * 32, + 'timestamp': self.min_ago, }, project_id=self.project.id, ) diff --git a/tests/sentry/integrations/jira/test_integration.py b/tests/sentry/integrations/jira/test_integration.py index 045c4d2bbe9208..58b7bf69703b33 100644 --- a/tests/sentry/integrations/jira/test_integration.py +++ b/tests/sentry/integrations/jira/test_integration.py @@ -6,6 +6,7 @@ import six import pytest import copy +from datetime import timedelta from django.core.urlresolvers import reverse from django.utils import timezone @@ -437,6 +438,10 @@ def integration(self): self.user) return integration + def setUp(self): + super(JiraIntegrationTest, self).setUp() + self.min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] + def test_get_create_issue_config(self): org = self.organization self.login_as(self.user) @@ -444,7 +449,7 @@ def test_get_create_issue_config(self): data={ 'event_id': 'a' * 32, 'message': 'message', - 'timestamp': timezone.now().isoformat(), + 'timestamp': self.min_ago, 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, @@ -524,7 +529,7 @@ def test_get_create_issue_config_with_default_and_param(self): data={ 'event_id': 'a' * 32, 'message': 'message', - 'timestamp': timezone.now().isoformat(), + 'timestamp': self.min_ago, 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, @@ -561,7 +566,7 @@ def test_get_create_issue_config_with_default(self): data={ 'event_id': 'a' * 32, 'message': 'message', - 'timestamp': timezone.now().isoformat(), + 'timestamp': self.min_ago, 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, @@ -598,7 +603,7 @@ def test_get_create_issue_config_with_label_default(self): data={ 'event_id': 'a' * 32, 'message': 'message', - 'timestamp': timezone.now().isoformat(), + 'timestamp': self.min_ago, 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), }, project_id=self.project.id, @@ -637,7 +642,7 @@ def test_get_create_issue_config__no_projects(self): event = self.store_event( data={ 'message': 'oh no', - 'timestamp': timezone.now().isoformat() + 'timestamp': self.min_ago, }, project_id=self.project.id ) @@ -663,7 +668,7 @@ def test_get_create_issue_config__no_issue_config(self): event = self.store_event( data={ 'message': 'oh no', - 'timestamp': timezone.now().isoformat() + 'timestamp': self.min_ago, }, project_id=self.project.id ) From d8409e041d50a3a9e08bef685d3b6df33909f011 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Mon, 8 Jul 2019 17:31:11 -0700 Subject: [PATCH 6/8] various updates --- src/sentry/models/group.py | 12 -- .../endpoints/test_shared_group_details.py | 14 ++- .../integrations/bitbucket/test_issues.py | 22 +++- .../sentry/integrations/gitlab/test_issues.py | 26 ++++- tests/sentry/integrations/vsts/test_issues.py | 13 ++- tests/sentry/models/test_group.py | 108 ++++++++++-------- 6 files changed, 124 insertions(+), 71 deletions(-) diff --git a/src/sentry/models/group.py b/src/sentry/models/group.py index 13f95b7ef51771..c5e0d28285439a 100644 --- a/src/sentry/models/group.py +++ b/src/sentry/models/group.py @@ -377,12 +377,6 @@ def get_latest_event(self): return self.get_latest_event_for_environments() def get_latest_event_for_environments(self, environments=()): - use_snuba = options.get('snuba.events-queries.enabled') - - # Fetch without environment if Snuba is not enabled - if not use_snuba: - return self.get_latest_event() - return get_oldest_or_latest_event_for_environments( EventOrdering.LATEST, environments=environments, @@ -390,12 +384,6 @@ def get_latest_event_for_environments(self, environments=()): project_id=self.project_id) def get_oldest_event_for_environments(self, environments=()): - use_snuba = options.get('snuba.events-queries.enabled') - - # Fetch without environment if Snuba is not enabled - if not use_snuba: - return self.get_oldest_event() - return get_oldest_or_latest_event_for_environments( EventOrdering.OLDEST, environments=environments, diff --git a/tests/sentry/api/endpoints/test_shared_group_details.py b/tests/sentry/api/endpoints/test_shared_group_details.py index ba6c8785f10e2e..2bcb539f06051e 100644 --- a/tests/sentry/api/endpoints/test_shared_group_details.py +++ b/tests/sentry/api/endpoints/test_shared_group_details.py @@ -1,6 +1,8 @@ from __future__ import absolute_import, print_function import six +from datetime import timedelta +from django.utils import timezone from sentry.testutils import APITestCase from sentry.models import GroupShare @@ -10,8 +12,14 @@ class SharedGroupDetailsTest(APITestCase): def test_simple(self): self.login_as(user=self.user) - group = self.create_group() - event = self.create_event(group=group) + min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] + event = self.store_event( + data={ + 'timestamp': min_ago, + }, + project_id=self.project.id, + ) + group = event.group share_id = group.get_share_id() assert share_id is None @@ -29,7 +37,7 @@ def test_simple(self): assert response.status_code == 200, response.content assert response.data['id'] == six.text_type(group.id) - assert response.data['latestEvent']['id'] == six.text_type(event.id) + assert response.data['latestEvent']['id'] == six.text_type(event.event_id) assert response.data['project']['slug'] == group.project.slug assert response.data['project']['organization']['slug'] == group.organization.slug diff --git a/tests/sentry/integrations/bitbucket/test_issues.py b/tests/sentry/integrations/bitbucket/test_issues.py index ec8f1c686bdd3f..cf20c9576ba829 100644 --- a/tests/sentry/integrations/bitbucket/test_issues.py +++ b/tests/sentry/integrations/bitbucket/test_issues.py @@ -1,8 +1,14 @@ from __future__ import absolute_import +from datetime import timedelta +import copy + +from django.utils import timezone + from sentry.integrations.bitbucket.issues import ISSUE_TYPES, PRIORITIES from sentry.models import ExternalIssue, Integration from sentry.testutils import APITestCase +from sentry.testutils.factories import DEFAULT_EVENT_DATA import json import responses @@ -24,8 +30,18 @@ def setUp(self): 'subject': self.subject, } ) - self.group = self.create_group() - self.create_event(group=self.group) + min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] + event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'message': 'message', + 'timestamp': min_ago, + 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), + + }, + project_id=self.project.id, + ) + self.group = event.group self.repo_choices = [('myaccount/repo1', 'myaccount/repo1'), ('myaccount/repo2', 'myaccount/repo2')] self.org_integration = self.integration.add_organization(self.organization) @@ -203,7 +219,7 @@ def test_get_create_issue_config(self): }, { 'name': 'description', 'label': 'Description', - 'default': u'Sentry Issue: [BAR-1](http://testserver/organizations/baz/issues/%d/?referrer=bitbucket_integration)\n\n```\nStacktrace (most recent call last):\n\n File "sentry/models/foo.py", line 29, in build_msg\n string_max_length=self.string_max_length)\n\nmessage\n```' % self.group.id, + 'default': u'Sentry Issue: [BAR-1](http://testserver/organizations/baz/issues/%d/?referrer=bitbucket_integration)\n\n```\nStacktrace (most recent call first):\n\n File "sentry/models/foo.py", line 29, in build_msg\n string_max_length=self.string_max_length)\n\nmessage\n```' % self.group.id, 'type': 'textarea', 'autosize': True, 'maxRows': 10, diff --git a/tests/sentry/integrations/gitlab/test_issues.py b/tests/sentry/integrations/gitlab/test_issues.py index 4da18714d76db7..235d8afd6efb3b 100644 --- a/tests/sentry/integrations/gitlab/test_issues.py +++ b/tests/sentry/integrations/gitlab/test_issues.py @@ -2,10 +2,15 @@ import responses import six +from datetime import timedelta +import copy + +from django.utils import timezone from sentry.integrations.exceptions import IntegrationError from sentry.models import ExternalIssue from sentry.utils.http import absolute_uri +from sentry.testutils.factories import DEFAULT_EVENT_DATA from .testutils import GitLabTestCase @@ -13,8 +18,17 @@ class GitlabIssuesTest(GitLabTestCase): def setUp(self): super(GitlabIssuesTest, self).setUp() - self.group = self.create_group() - self.create_event(group=self.group) + min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] + event = self.store_event( + data={ + 'event_id': 'a' * 32, + 'message': 'message', + 'timestamp': min_ago, + 'stacktrace': copy.deepcopy(DEFAULT_EVENT_DATA['stacktrace']), + }, + project_id=self.project.id, + ) + self.group = event.group def test_make_external_key(self): project_name = 'getsentry/sentry' @@ -35,7 +49,7 @@ def test_get_issue_url(self): def test_get_create_issue_config(self): group_description = ( u'Sentry Issue: [%s](%s)\n\n' - '```\nStacktrace (most recent call last):\n\n' + '```\nStacktrace (most recent call first):\n\n' ' File "sentry/models/foo.py", line 29, in build_msg\n' ' string_max_length=self.string_max_length)\n\nmessage\n```' ) % ( @@ -190,7 +204,7 @@ def test_get_issue(self): def test_create_issue_default_project_in_group_api_call(self): group_description = ( u'Sentry Issue: [%s](%s)\n\n' - '```\nStacktrace (most recent call last):\n\n' + '```\nStacktrace (most recent call first):\n\n' ' File "sentry/models/foo.py", line 29, in build_msg\n' ' string_max_length=self.string_max_length)\n\nmessage\n```' ) % ( @@ -254,7 +268,7 @@ def test_create_issue_default_project_in_group_api_call(self): def test_create_issue_default_project_not_in_api_call(self): group_description = ( u'Sentry Issue: [%s](%s)\n\n' - '```\nStacktrace (most recent call last):\n\n' + '```\nStacktrace (most recent call first):\n\n' ' File "sentry/models/foo.py", line 29, in build_msg\n' ' string_max_length=self.string_max_length)\n\nmessage\n```' ) % ( @@ -317,7 +331,7 @@ def test_create_issue_default_project_not_in_api_call(self): def test_create_issue_no_projects(self): group_description = ( u'Sentry Issue: [%s](%s)\n\n' - '```\nStacktrace (most recent call last):\n\n' + '```\nStacktrace (most recent call first):\n\n' ' File "sentry/models/foo.py", line 29, in build_msg\n' ' string_max_length=self.string_max_length)\n\nmessage\n```' ) % ( diff --git a/tests/sentry/integrations/vsts/test_issues.py b/tests/sentry/integrations/vsts/test_issues.py index a9989c67ca2450..b1fadf5c88fdc7 100644 --- a/tests/sentry/integrations/vsts/test_issues.py +++ b/tests/sentry/integrations/vsts/test_issues.py @@ -7,6 +7,8 @@ from exam import fixture from django.test import RequestFactory from time import time +from datetime import timedelta +from django.utils import timezone from sentry.integrations.exceptions import IntegrationError from sentry.integrations.vsts.integration import VstsIntegration @@ -349,8 +351,15 @@ def setUp(self): ] }, ) - self.group = self.create_group() - self.create_event(group=self.group) + min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] + event = self.store_event( + data={ + 'fingerprint': ['group1'], + 'timestamp': min_ago, + }, + project_id=self.project.id, + ) + self.group = event.group def tearDown(self): responses.reset() diff --git a/tests/sentry/models/test_group.py b/tests/sentry/models/test_group.py index 783766d755d864..291b94ffac25f6 100644 --- a/tests/sentry/models/test_group.py +++ b/tests/sentry/models/test_group.py @@ -1,8 +1,6 @@ from __future__ import absolute_import -import six - -from datetime import datetime, timedelta +from datetime import timedelta import pytest from django.db.models import ProtectedError @@ -16,6 +14,14 @@ class GroupTest(TestCase): + + def setUp(self): + super(GroupTest, self).setUp() + self.min_ago = (timezone.now() - timedelta(minutes=1)).isoformat()[:19] + self.two_min_ago = (timezone.now() - timedelta(minutes=2)).isoformat()[:19] + self.sec_ago = (timezone.now() - timedelta(seconds=1)).isoformat()[:19] + self.two_sec_ago = (timezone.now() - timedelta(seconds=1)).isoformat()[:19] + def test_is_resolved(self): group = self.create_group(status=GroupStatus.RESOLVED) assert group.is_resolved() @@ -36,56 +42,68 @@ def test_is_resolved(self): assert group.is_resolved() - def test_get_oldest_latest_event_no_events(self): - group = self.create_group() + def test_get_latest_event_no_events(self): + project = self.create_project() + group = self.create_group(project=project) assert group.get_latest_event() is None - assert group.get_oldest_event() is None - - def test_get_oldest_latest_events(self): - group = self.create_group() - for i in range(0, 3): - self.create_event( - event_id=six.text_type(i), - group=group, - datetime=datetime(2013, 8, 13, 3, 8, i), - ) - assert group.get_latest_event().event_id == '2' - assert group.get_oldest_event().event_id == '0' + def test_get_latest_event(self): + self.store_event( + data={ + 'event_id': 'a' * 32, + 'fingerprint': ['group-1'], + 'timestamp': self.two_min_ago, + }, + project_id=self.project.id, + ) + self.store_event( + data={ + 'event_id': 'b' * 32, + 'fingerprint': ['group-1'], + 'timestamp': self.min_ago, + }, + project_id=self.project.id, + ) - def test_get_oldest_latest_identical_timestamps(self): - group = self.create_group() - for i in range(0, 3): - self.create_event( - event_id=six.text_type(i), - group=group, - datetime=datetime(2013, 8, 13, 3, 8, 50), - ) + group = Group.objects.first() - assert group.get_latest_event().event_id == '2' - assert group.get_oldest_event().event_id == '0' + assert group.get_latest_event().event_id == 'b' * 32 - def test_get_oldest_latest_almost_identical_timestamps(self): - group = self.create_group() - self.create_event( - event_id='0', - group=group, - datetime=datetime(2013, 8, 13, 3, 8, 0), # earliest - ) - for i in range(1, 3): - self.create_event( - event_id=six.text_type(i), - group=group, - datetime=datetime(2013, 8, 13, 3, 8, 30), # all in the middle + def test_get_latest_identical_timestamps(self): + events = [] + for i in 'abc': + event = self.store_event( + data={ + 'event_id': i * 32, + 'fingerprint': ['group-1'], + 'timestamp': self.min_ago, + }, + project_id=self.project.id, ) - self.create_event( - event_id='3', - group=group, - datetime=datetime(2013, 8, 13, 3, 8, 59), # latest + events.append(event) + + assert events[0].group.get_latest_event().event_id == 'c' * 32 + + def test_get_latest_almost_identical_timestamps(self): + self.store_event( + data={ + 'event_id': 'a' * 32, + 'fingerprint': ['group-1'], + 'timestamp': self.two_sec_ago, + }, + project_id=self.project.id, + ) + self.store_event( + data={ + 'event_id': 'b' * 32, + 'fingerprint': ['group-1'], + 'timestamp': self.sec_ago, + }, + project_id=self.project.id, ) + group = Group.objects.first() - assert group.get_latest_event().event_id == '3' - assert group.get_oldest_event().event_id == '0' + assert group.get_latest_event().event_id == 'b' * 32 def test_is_ignored_with_expired_snooze(self): group = self.create_group( From b363bf4c024a3c70358bf3711f5bdebdc41bedcd Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Tue, 9 Jul 2019 10:22:56 -0700 Subject: [PATCH 7/8] fix lint error --- src/sentry/models/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/models/group.py b/src/sentry/models/group.py index c5e0d28285439a..e34e2d828fe4ef 100644 --- a/src/sentry/models/group.py +++ b/src/sentry/models/group.py @@ -20,7 +20,7 @@ from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ -from sentry import eventtypes, tagstore, options +from sentry import eventtypes, tagstore from sentry.constants import DEFAULT_LOGGER_NAME, LOG_LEVELS, MAX_CULPRIT_LENGTH from sentry.db.models import ( BaseManager, BoundedBigIntegerField, BoundedIntegerField, BoundedPositiveIntegerField, From 52f15da9e941f5f508fcf38bcba485cd65e49710 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Tue, 9 Jul 2019 11:02:13 -0700 Subject: [PATCH 8/8] bring back latest event cache --- src/sentry/models/group.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sentry/models/group.py b/src/sentry/models/group.py index e34e2d828fe4ef..97ad02764f0f11 100644 --- a/src/sentry/models/group.py +++ b/src/sentry/models/group.py @@ -374,7 +374,10 @@ def get_score(self): return type(self).calculate_score(self.times_seen, self.last_seen) def get_latest_event(self): - return self.get_latest_event_for_environments() + if not hasattr(self, '_latest_event'): + self._latest_event = self.get_latest_event_for_environments() + + return self._latest_event def get_latest_event_for_environments(self, environments=()): return get_oldest_or_latest_event_for_environments(