123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626 |
- import time
- from datetime import timedelta
- from unittest import mock
- from unittest.mock import patch
- import pytz
- from django.utils import timezone
- from sentry.api.serializers import serialize
- from sentry.api.serializers.models.group import (
- GroupSerializerSnuba,
- StreamGroupSerializerSnuba,
- snuba_tsdb,
- )
- from sentry.models import (
- Environment,
- Group,
- GroupEnvironment,
- GroupLink,
- GroupResolution,
- GroupSnooze,
- GroupStatus,
- GroupSubscription,
- NotificationSetting,
- UserOption,
- )
- from sentry.notifications.types import NotificationSettingOptionValues, NotificationSettingTypes
- from sentry.testutils import APITestCase, SnubaTestCase
- from sentry.testutils.helpers.datetime import before_now, iso_format
- from sentry.types.integrations import ExternalProviders
- from sentry.utils.cache import cache
- from sentry.utils.hashlib import hash_values
- class GroupSerializerSnubaTest(APITestCase, SnubaTestCase):
- def setUp(self):
- super().setUp()
- self.min_ago = before_now(minutes=1)
- self.day_ago = before_now(days=1)
- self.week_ago = before_now(days=7)
- def test_permalink(self):
- group = self.create_group()
- result = serialize(group, self.user, serializer=GroupSerializerSnuba())
- assert "http://" in result["permalink"]
- assert f"{group.organization.slug}/issues/{group.id}" in result["permalink"]
- def test_permalink_outside_org(self):
- outside_user = self.create_user()
- group = self.create_group()
- result = serialize(group, outside_user, serializer=GroupSerializerSnuba())
- assert result["permalink"] is None
- def test_is_ignored_with_expired_snooze(self):
- now = timezone.now()
- user = self.create_user()
- group = self.create_group(status=GroupStatus.IGNORED)
- GroupSnooze.objects.create(group=group, until=now - timedelta(minutes=1))
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "unresolved"
- assert result["statusDetails"] == {}
- def test_is_ignored_with_valid_snooze(self):
- now = timezone.now()
- user = self.create_user()
- group = self.create_group(status=GroupStatus.IGNORED)
- snooze = GroupSnooze.objects.create(group=group, until=now + timedelta(minutes=1))
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "ignored"
- assert result["statusDetails"]["ignoreCount"] == snooze.count
- assert result["statusDetails"]["ignoreWindow"] == snooze.window
- assert result["statusDetails"]["ignoreUserCount"] == snooze.user_count
- assert result["statusDetails"]["ignoreUserWindow"] == snooze.user_window
- assert result["statusDetails"]["ignoreUntil"] == snooze.until
- assert result["statusDetails"]["actor"] is None
- def test_is_ignored_with_valid_snooze_and_actor(self):
- now = timezone.now()
- user = self.create_user()
- group = self.create_group(status=GroupStatus.IGNORED)
- GroupSnooze.objects.create(group=group, until=now + timedelta(minutes=1), actor_id=user.id)
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "ignored"
- assert result["statusDetails"]["actor"]["id"] == str(user.id)
- def test_resolved_in_next_release(self):
- release = self.create_release(project=self.project, version="a")
- user = self.create_user()
- group = self.create_group(status=GroupStatus.RESOLVED)
- GroupResolution.objects.create(
- group=group, release=release, type=GroupResolution.Type.in_next_release
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "resolved"
- assert result["statusDetails"] == {"inNextRelease": True, "actor": None}
- def test_resolved_in_release(self):
- release = self.create_release(project=self.project, version="a")
- user = self.create_user()
- group = self.create_group(status=GroupStatus.RESOLVED)
- GroupResolution.objects.create(
- group=group, release=release, type=GroupResolution.Type.in_release
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "resolved"
- assert result["statusDetails"] == {"inRelease": "a", "actor": None}
- def test_resolved_with_actor(self):
- release = self.create_release(project=self.project, version="a")
- user = self.create_user()
- group = self.create_group(status=GroupStatus.RESOLVED)
- GroupResolution.objects.create(
- group=group, release=release, type=GroupResolution.Type.in_release, actor_id=user.id
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "resolved"
- assert result["statusDetails"]["actor"]["id"] == str(user.id)
- def test_resolved_in_commit(self):
- repo = self.create_repo(project=self.project)
- commit = self.create_commit(repo=repo)
- user = self.create_user()
- group = self.create_group(status=GroupStatus.RESOLVED)
- GroupLink.objects.create(
- group_id=group.id,
- project_id=group.project_id,
- linked_id=commit.id,
- linked_type=GroupLink.LinkedType.commit,
- relationship=GroupLink.Relationship.resolves,
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "resolved"
- assert result["statusDetails"]["inCommit"]["id"] == commit.key
- @patch("sentry.models.Group.is_over_resolve_age")
- def test_auto_resolved(self, mock_is_over_resolve_age):
- mock_is_over_resolve_age.return_value = True
- user = self.create_user()
- group = self.create_group(status=GroupStatus.UNRESOLVED)
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["status"] == "resolved"
- assert result["statusDetails"] == {"autoResolved": True}
- def test_subscribed(self):
- user = self.create_user()
- group = self.create_group()
- GroupSubscription.objects.create(
- user=user, group=group, project=group.project, is_active=True
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert result["isSubscribed"]
- assert result["subscriptionDetails"] == {"reason": "unknown"}
- def test_explicit_unsubscribed(self):
- user = self.create_user()
- group = self.create_group()
- GroupSubscription.objects.create(
- user=user, group=group, project=group.project, is_active=False
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert not result["isSubscribed"]
- assert not result["subscriptionDetails"]
- def test_implicit_subscribed(self):
- user = self.create_user()
- group = self.create_group()
- combinations = (
- # (default, project, subscribed, has_details)
- (
- NotificationSettingOptionValues.ALWAYS,
- NotificationSettingOptionValues.DEFAULT,
- True,
- False,
- ),
- (
- NotificationSettingOptionValues.ALWAYS,
- NotificationSettingOptionValues.ALWAYS,
- True,
- False,
- ),
- (
- NotificationSettingOptionValues.ALWAYS,
- NotificationSettingOptionValues.SUBSCRIBE_ONLY,
- False,
- False,
- ),
- (
- NotificationSettingOptionValues.ALWAYS,
- NotificationSettingOptionValues.NEVER,
- False,
- True,
- ),
- (
- NotificationSettingOptionValues.DEFAULT,
- NotificationSettingOptionValues.DEFAULT,
- False,
- False,
- ),
- (
- NotificationSettingOptionValues.SUBSCRIBE_ONLY,
- NotificationSettingOptionValues.DEFAULT,
- False,
- False,
- ),
- (
- NotificationSettingOptionValues.SUBSCRIBE_ONLY,
- NotificationSettingOptionValues.ALWAYS,
- True,
- False,
- ),
- (
- NotificationSettingOptionValues.SUBSCRIBE_ONLY,
- NotificationSettingOptionValues.SUBSCRIBE_ONLY,
- False,
- False,
- ),
- (
- NotificationSettingOptionValues.SUBSCRIBE_ONLY,
- NotificationSettingOptionValues.NEVER,
- False,
- True,
- ),
- (
- NotificationSettingOptionValues.NEVER,
- NotificationSettingOptionValues.DEFAULT,
- False,
- True,
- ),
- (
- NotificationSettingOptionValues.NEVER,
- NotificationSettingOptionValues.ALWAYS,
- True,
- False,
- ),
- (
- NotificationSettingOptionValues.NEVER,
- NotificationSettingOptionValues.SUBSCRIBE_ONLY,
- False,
- False,
- ),
- (
- NotificationSettingOptionValues.NEVER,
- NotificationSettingOptionValues.NEVER,
- False,
- True,
- ),
- )
- for default_value, project_value, is_subscribed, has_details in combinations:
- UserOption.objects.clear_local_cache()
- NotificationSetting.objects.update_settings(
- ExternalProviders.EMAIL,
- NotificationSettingTypes.WORKFLOW,
- default_value,
- user=user,
- )
- NotificationSetting.objects.update_settings(
- ExternalProviders.EMAIL,
- NotificationSettingTypes.WORKFLOW,
- project_value,
- user=user,
- project=group.project,
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- subscription_details = result.get("subscriptionDetails")
- assert result["isSubscribed"] is is_subscribed
- assert (
- subscription_details == {"disabled": True}
- if has_details
- else subscription_details is None
- )
- def test_global_no_conversations_overrides_group_subscription(self):
- user = self.create_user()
- group = self.create_group()
- GroupSubscription.objects.create(
- user=user, group=group, project=group.project, is_active=True
- )
- NotificationSetting.objects.update_settings(
- ExternalProviders.EMAIL,
- NotificationSettingTypes.WORKFLOW,
- NotificationSettingOptionValues.NEVER,
- user=user,
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert not result["isSubscribed"]
- assert result["subscriptionDetails"] == {"disabled": True}
- def test_project_no_conversations_overrides_group_subscription(self):
- user = self.create_user()
- group = self.create_group()
- GroupSubscription.objects.create(
- user=user, group=group, project=group.project, is_active=True
- )
- NotificationSetting.objects.update_settings(
- ExternalProviders.EMAIL,
- NotificationSettingTypes.WORKFLOW,
- NotificationSettingOptionValues.NEVER,
- user=user,
- project=group.project,
- )
- result = serialize(group, user, serializer=GroupSerializerSnuba())
- assert not result["isSubscribed"]
- assert result["subscriptionDetails"] == {"disabled": True}
- def test_no_user_unsubscribed(self):
- group = self.create_group()
- result = serialize(group, serializer=GroupSerializerSnuba())
- assert not result["isSubscribed"]
- def test_seen_stats(self):
- environment = self.create_environment(project=self.project)
- environment2 = self.create_environment(project=self.project)
- events = []
- for event_id, env, user_id, timestamp in [
- ("a" * 32, environment, 1, iso_format(self.min_ago)),
- ("b" * 32, environment, 2, iso_format(self.min_ago)),
- ("c" * 32, environment2, 3, iso_format(self.week_ago)),
- ]:
- events.append(
- self.store_event(
- data={
- "event_id": event_id,
- "fingerprint": ["put-me-in-group1"],
- "timestamp": timestamp,
- "environment": env.name,
- "user": {"id": user_id},
- },
- project_id=self.project.id,
- )
- )
- # Assert all events are in the same group
- (group_id,) = {e.group.id for e in events}
- group = Group.objects.get(id=group_id)
- group.times_seen = 3
- group.first_seen = self.week_ago - timedelta(days=5)
- group.last_seen = self.week_ago
- group.save()
- # should use group columns when no environments arg passed
- result = serialize(group, serializer=GroupSerializerSnuba(environment_ids=[]))
- assert result["count"] == "3"
- assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
- assert result["firstSeen"] == group.first_seen
- # update this to something different to make sure it's being used
- group_env = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment.id)
- group_env.first_seen = self.day_ago - timedelta(days=3)
- group_env.save()
- group_env2 = GroupEnvironment.objects.get(group_id=group_id, environment_id=environment2.id)
- result = serialize(
- group,
- serializer=GroupSerializerSnuba(environment_ids=[environment.id, environment2.id]),
- )
- assert result["count"] == "3"
- # result is rounded down to nearest second
- assert iso_format(result["lastSeen"]) == iso_format(self.min_ago)
- assert iso_format(result["firstSeen"]) == iso_format(group_env.first_seen)
- assert iso_format(group_env2.first_seen) > iso_format(group_env.first_seen)
- assert result["userCount"] == 3
- result = serialize(
- group,
- serializer=GroupSerializerSnuba(
- environment_ids=[environment.id, environment2.id],
- start=self.week_ago - timedelta(hours=1),
- end=self.week_ago + timedelta(hours=1),
- ),
- )
- assert result["userCount"] == 1
- assert iso_format(result["lastSeen"]) == iso_format(self.week_ago)
- assert iso_format(result["firstSeen"]) == iso_format(self.week_ago)
- assert result["count"] == "1"
- def test_get_start_from_seen_stats(self):
- for days, expected in [(None, 30), (0, 14), (1000, 90)]:
- last_seen = None if days is None else before_now(days=days).replace(tzinfo=pytz.UTC)
- start = GroupSerializerSnuba._get_start_from_seen_stats({"": {"last_seen": last_seen}})
- assert iso_format(start) == iso_format(before_now(days=expected))
- class StreamGroupSerializerTestCase(APITestCase, SnubaTestCase):
- def test_environment(self):
- group = self.group
- environment = Environment.get_or_create(group.project, "production")
- with mock.patch(
- "sentry.api.serializers.models.group.snuba_tsdb.get_range",
- side_effect=snuba_tsdb.get_range,
- ) as get_range:
- serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(
- environment_ids=[environment.id], stats_period="14d"
- ),
- )
- assert get_range.call_count == 1
- for args, kwargs in get_range.call_args_list:
- assert kwargs["environment_ids"] == [environment.id]
- with mock.patch(
- "sentry.api.serializers.models.group.snuba_tsdb.get_range",
- side_effect=snuba_tsdb.get_range,
- ) as get_range:
- serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(environment_ids=None, stats_period="14d"),
- )
- assert get_range.call_count == 1
- for args, kwargs in get_range.call_args_list:
- assert kwargs["environment_ids"] is None
- def test_session_count(self):
- group = self.group
- environment = Environment.get_or_create(group.project, "prod")
- dev_environment = Environment.get_or_create(group.project, "dev")
- no_sessions_environment = Environment.get_or_create(group.project, "no_sessions")
- self.received = time.time()
- self.session_started = time.time() // 60 * 60
- self.session_release = "foo@1.0.0"
- self.session_crashed_release = "foo@2.0.0"
- self.store_session(
- {
- "session_id": "5d52fd05-fcc9-4bf3-9dc9-267783670341",
- "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102667",
- "status": "ok",
- "seq": 0,
- "release": self.session_release,
- "environment": "dev",
- "retention_days": 90,
- "org_id": self.project.organization_id,
- "project_id": self.project.id,
- "duration": 1,
- "errors": 0,
- "started": self.session_started - 120,
- "received": self.received - 120,
- }
- )
- self.store_session(
- {
- "session_id": "5e910c1a-6941-460e-9843-24103fb6a63c",
- "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102668",
- "status": "ok",
- "seq": 0,
- "release": self.session_release,
- "environment": "prod",
- "retention_days": 90,
- "org_id": self.project.organization_id,
- "project_id": self.project.id,
- "duration": 60.0,
- "errors": 0,
- "started": self.session_started - 240,
- "received": self.received - 240,
- }
- )
- self.store_session(
- {
- "session_id": "5e910c1a-6941-460e-9843-24103fb6a63c",
- "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102669",
- "status": "exited",
- "seq": 1,
- "release": self.session_release,
- "environment": "prod",
- "retention_days": 90,
- "org_id": self.project.organization_id,
- "project_id": self.project.id,
- "duration": 30.0,
- "errors": 0,
- "started": self.session_started,
- "received": self.received,
- }
- )
- self.store_session(
- {
- "session_id": "a148c0c5-06a2-423b-8901-6b43b812cf82",
- "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102660",
- "status": "crashed",
- "seq": 0,
- "release": self.session_crashed_release,
- "environment": "prod",
- "retention_days": 90,
- "org_id": self.project.organization_id,
- "project_id": self.project.id,
- "duration": 60.0,
- "errors": 0,
- "started": self.session_started,
- "received": self.received,
- }
- )
- result = serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(stats_period="14d"),
- )
- assert "sessionCount" not in result[0]
- result = serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(
- stats_period="14d",
- expand=["sessions"],
- ),
- )
- assert result[0]["sessionCount"] == 3
- result = serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(
- environment_ids=[environment.id], stats_period="14d", expand=["sessions"]
- ),
- )
- assert result[0]["sessionCount"] == 2
- result = serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(
- environment_ids=[no_sessions_environment.id],
- stats_period="14d",
- expand=["sessions"],
- ),
- )
- assert result[0]["sessionCount"] is None
- result = serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(
- environment_ids=[dev_environment.id], stats_period="14d", expand=["sessions"]
- ),
- )
- assert result[0]["sessionCount"] == 1
- self.store_session(
- {
- "session_id": "a148c0c5-06a2-423b-8901-6b43b812cf83",
- "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102627",
- "status": "ok",
- "seq": 0,
- "release": self.session_release,
- "environment": "dev",
- "retention_days": 90,
- "org_id": self.project.organization_id,
- "project_id": self.project.id,
- "duration": 60.0,
- "errors": 0,
- "started": self.session_started - 1590061, # approximately 18 days
- "received": self.received - 1590061, # approximately 18 days
- }
- )
- result = serialize(
- [group],
- serializer=StreamGroupSerializerSnuba(
- environment_ids=[dev_environment.id],
- stats_period="14d",
- expand=["sessions"],
- start=timezone.now() - timedelta(days=30),
- end=timezone.now() - timedelta(days=15),
- ),
- )
- assert result[0]["sessionCount"] == 1
- # Delete the cache from the query we did above, else this result comes back as 1 instead of 0.5
- key_hash = hash_values([group.project.id, "", "", f"{dev_environment.id}"])
- cache.delete(f"w-s:{key_hash}")
- project2 = self.create_project(
- organization=self.organization, teams=[self.team], name="Another project"
- )
- data = {
- "fingerprint": ["meow"],
- "timestamp": iso_format(timezone.now()),
- "type": "error",
- "exception": [{"type": "Foo"}],
- }
- event = self.store_event(data=data, project_id=project2.id)
- self.store_event(data=data, project_id=project2.id)
- self.store_event(data=data, project_id=project2.id)
- result = serialize(
- [group, event.group],
- serializer=StreamGroupSerializerSnuba(
- environment_ids=[dev_environment.id],
- stats_period="14d",
- expand=["sessions"],
- ),
- )
- assert result[0]["sessionCount"] == 2
- # No sessions in project2
- assert result[1]["sessionCount"] is None
|