Browse Source

ref(groups): move StreamGroupSerializer* related classes to a separate file (#38075)

* split up StreamGroupSerializer* classes to new file. do the same for the tests

* fix merged in change that wasn't moved over

* fix incorrect import from module

* fix more imports

* fix mock.patch reference
Gilbert Szeto 2 years ago
parent
commit
5c8eaa2c68

+ 1 - 1
src/sentry/api/endpoints/organization_group_index.py

@@ -21,7 +21,7 @@ from sentry.api.helpers.group_index import (
 )
 from sentry.api.paginator import DateTimePaginator, Paginator
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.group import StreamGroupSerializerSnuba
+from sentry.api.serializers.models.group_stream import StreamGroupSerializerSnuba
 from sentry.api.utils import InvalidParams, get_date_range_from_stats_period
 from sentry.constants import ALLOWED_FUTURE_DELTA
 from sentry.exceptions import InvalidSearchQuery

+ 1 - 1
src/sentry/api/endpoints/organization_group_index_stats.py

@@ -6,7 +6,7 @@ from sentry.api.bases import OrganizationEventPermission, OrganizationEventsEndp
 from sentry.api.endpoints.organization_group_index import ERR_INVALID_STATS_PERIOD
 from sentry.api.helpers.group_index import build_query_params_from_request, calculate_stats_period
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.group import StreamGroupSerializerSnuba
+from sentry.api.serializers.models.group_stream import StreamGroupSerializerSnuba
 from sentry.api.utils import InvalidParams, get_date_range_from_stats_period
 from sentry.models import Group
 from sentry.types.ratelimit import RateLimit, RateLimitCategory

+ 1 - 1
src/sentry/api/endpoints/organization_user_issues.py

@@ -5,7 +5,7 @@ from sentry import tagstore
 from sentry.api.base import EnvironmentMixin
 from sentry.api.bases.organization import OrganizationEndpoint
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.group import TagBasedStreamGroupSerializer
+from sentry.api.serializers.models.group_stream import TagBasedStreamGroupSerializer
 from sentry.models import EventUser, Group, ProjectTeam, Team
 
 

+ 1 - 1
src/sentry/api/endpoints/project_group_index.py

@@ -15,7 +15,7 @@ from sentry.api.helpers.group_index import (
     update_groups,
 )
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.group import StreamGroupSerializer
+from sentry.api.serializers.models.group_stream import StreamGroupSerializer
 from sentry.models import QUERY_STATUS_LOOKUP, Environment, Group, GroupStatus
 from sentry.search.events.constants import EQUALITY_OPERATORS
 from sentry.signals import advanced_search

+ 1 - 0
src/sentry/api/serializers/models/__init__.py

@@ -27,6 +27,7 @@ from .exporteddata import *  # noqa: F401,F403
 from .external_actor import *  # noqa: F401,F403
 from .filechange import *  # noqa: F401,F403
 from .group import *  # noqa: F401,F403
+from .group_stream import *  # noqa: F401,F403
 from .grouprelease import *  # noqa: F401,F403
 from .groupseen import *  # noqa: F401,F403
 from .grouptombstone import *  # noqa: F401,F403

+ 2 - 375
src/sentry/api/serializers/models/group.py

@@ -1,6 +1,5 @@
 from __future__ import annotations
 
-import functools
 import itertools
 import logging
 from collections import defaultdict
@@ -21,16 +20,15 @@ import pytz
 import sentry_sdk
 from django.conf import settings
 from django.db.models import Min, prefetch_related_objects
-from django.utils import timezone
 
-from sentry import release_health, tagstore, tsdb
+from sentry import tagstore
 from sentry.api.serializers import Serializer, register, serialize
 from sentry.api.serializers.models.actor import ActorSerializer
 from sentry.api.serializers.models.plugin import is_plugin_deprecated
 from sentry.api.serializers.models.user import UserSerializerResponse
 from sentry.app import env
 from sentry.auth.superuser import is_active_superuser
-from sentry.constants import LOG_LEVELS, StatsPeriod
+from sentry.constants import LOG_LEVELS
 from sentry.models import (
     ActorTuple,
     ApiToken,
@@ -53,8 +51,6 @@ from sentry.models import (
     SentryAppInstallationToken,
     User,
 )
-from sentry.models.groupinbox import get_inbox_details
-from sentry.models.groupowner import get_owner_details
 from sentry.notifications.helpers import (
     collect_groups_by_project,
     get_groups_for_query,
@@ -68,9 +64,7 @@ from sentry.search.events.constants import RELEASE_STAGE_ALIAS
 from sentry.search.events.filter import convert_search_filter_to_snuba_query
 from sentry.tagstore.snuba.backend import fix_tag_value_data
 from sentry.tsdb.snuba import SnubaTSDB
-from sentry.utils import metrics
 from sentry.utils.cache import cache
-from sentry.utils.hashlib import hash_values
 from sentry.utils.json import JSONData
 from sentry.utils.safe import safe_execute
 from sentry.utils.snuba import Dataset, aliased_query, raw_query
@@ -693,145 +687,6 @@ class GroupSerializer(GroupSerializerBase):
         return attrs
 
 
-class GroupStatsMixin:
-    STATS_PERIOD_CHOICES = {
-        "14d": StatsPeriod(14, timedelta(hours=24)),
-        "24h": StatsPeriod(24, timedelta(hours=1)),
-    }
-
-    CUSTOM_ROLLUP_CHOICES = {
-        "1h": timedelta(hours=1).total_seconds(),
-        "2h": timedelta(hours=2).total_seconds(),
-        "3h": timedelta(hours=3).total_seconds(),
-        "6h": timedelta(hours=6).total_seconds(),
-        "12h": timedelta(hours=12).total_seconds(),
-        "24h": timedelta(hours=24).total_seconds(),
-    }
-
-    CUSTOM_SEGMENTS = 29  # for 30 segments use 1/29th intervals
-    CUSTOM_SEGMENTS_12H = 35  # for 12h 36 segments, otherwise 15-16-17 bars is too few
-    CUSTOM_ROLLUP_6H = timedelta(hours=6).total_seconds()  # rollups should be increments of 6hs
-
-    def query_tsdb(self, group_ids, query_params):
-        raise NotImplementedError
-
-    def get_stats(self, item_list, user, **kwargs):
-        if self.stats_period:
-            # we need to compute stats at 1d (1h resolution), and 14d or a custom given period
-            group_ids = [g.id for g in item_list]
-
-            if self.stats_period == "auto":
-                total_period = (self.stats_period_end - self.stats_period_start).total_seconds()
-                if total_period < timedelta(hours=24).total_seconds():
-                    rollup = total_period / self.CUSTOM_SEGMENTS
-                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["1h"]:
-                    rollup = self.CUSTOM_ROLLUP_CHOICES["1h"]
-                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["2h"]:
-                    rollup = self.CUSTOM_ROLLUP_CHOICES["2h"]
-                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["3h"]:
-                    rollup = self.CUSTOM_ROLLUP_CHOICES["3h"]
-                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["6h"]:
-                    rollup = self.CUSTOM_ROLLUP_CHOICES["6h"]
-                elif (
-                    total_period < self.CUSTOM_SEGMENTS_12H * self.CUSTOM_ROLLUP_CHOICES["12h"]
-                ):  # 36 segments is ok
-                    rollup = self.CUSTOM_ROLLUP_CHOICES["12h"]
-                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["24h"]:
-                    rollup = self.CUSTOM_ROLLUP_CHOICES["24h"]
-                else:
-                    delta_day = self.CUSTOM_ROLLUP_CHOICES["24h"]
-                    rollup = round(total_period / (self.CUSTOM_SEGMENTS * delta_day)) * delta_day
-
-                query_params = {
-                    "start": self.stats_period_start,
-                    "end": self.stats_period_end,
-                    "rollup": int(rollup),
-                }
-            else:
-                segments, interval = self.STATS_PERIOD_CHOICES[self.stats_period]
-                now = timezone.now()
-                query_params = {
-                    "start": now - ((segments - 1) * interval),
-                    "end": now,
-                    "rollup": int(interval.total_seconds()),
-                }
-
-            return self.query_tsdb(group_ids, query_params, **kwargs)
-
-
-class StreamGroupSerializer(GroupSerializer, GroupStatsMixin):
-    def __init__(
-        self,
-        environment_func=None,
-        stats_period=None,
-        stats_period_start=None,
-        stats_period_end=None,
-        matching_event_id=None,
-        matching_event_environment=None,
-    ):
-        super().__init__(environment_func)
-
-        if stats_period is not None:
-            assert stats_period in self.STATS_PERIOD_CHOICES or stats_period == "auto"
-
-        self.stats_period = stats_period
-        self.stats_period_start = stats_period_start
-        self.stats_period_end = stats_period_end
-        self.matching_event_id = matching_event_id
-        self.matching_event_environment = matching_event_environment
-
-    def query_tsdb(self, group_ids, query_params, **kwargs):
-        try:
-            environment = self.environment_func()
-        except Environment.DoesNotExist:
-            stats = {key: tsdb.make_series(0, **query_params) for key in group_ids}
-        else:
-            stats = tsdb.get_range(
-                model=tsdb.models.group,
-                keys=group_ids,
-                environment_ids=environment and [environment.id],
-                **query_params,
-            )
-
-        return stats
-
-    def get_attrs(self, item_list, user):
-        attrs = super().get_attrs(item_list, user)
-
-        if self.stats_period:
-            stats = self.get_stats(item_list, user)
-            for item in item_list:
-                attrs[item].update({"stats": stats[item.id]})
-
-        return attrs
-
-    def serialize(self, obj, attrs, user):
-        result = super().serialize(obj, attrs, user)
-
-        if self.stats_period:
-            result["stats"] = {self.stats_period: attrs["stats"]}
-
-        if self.matching_event_id:
-            result["matchingEventId"] = self.matching_event_id
-
-        if self.matching_event_environment:
-            result["matchingEventEnvironment"] = self.matching_event_environment
-
-        return result
-
-
-class TagBasedStreamGroupSerializer(StreamGroupSerializer):
-    def __init__(self, tags, **kwargs):
-        super().__init__(**kwargs)
-        self.tags = tags
-
-    def serialize(self, obj, attrs, user):
-        result = super().serialize(obj, attrs, user)
-        result["tagLastSeen"] = self.tags[obj.id].last_seen
-        result["tagFirstSeen"] = self.tags[obj.id].first_seen
-        return result
-
-
 class SharedGroupSerializer(GroupSerializer):
     def serialize(self, obj, attrs, user):
         result = super().serialize(obj, attrs, user)
@@ -976,231 +831,3 @@ class GroupSerializerSnuba(GroupSerializerBase):
             conditions=self.conditions,
             environment_ids=self.environment_ids,
         )
-
-
-class StreamGroupSerializerSnuba(GroupSerializerSnuba, GroupStatsMixin):
-    def __init__(
-        self,
-        environment_ids=None,
-        stats_period=None,
-        stats_period_start=None,
-        stats_period_end=None,
-        matching_event_id=None,
-        start=None,
-        end=None,
-        search_filters=None,
-        collapse=None,
-        expand=None,
-        organization_id=None,
-    ):
-        super().__init__(
-            environment_ids,
-            start,
-            end,
-            search_filters,
-            collapse=collapse,
-            expand=expand,
-            organization_id=organization_id,
-        )
-
-        if stats_period is not None:
-            assert stats_period in self.STATS_PERIOD_CHOICES or (
-                stats_period == "auto" and stats_period_start and stats_period_end
-            )
-
-        self.stats_period = stats_period
-        self.stats_period_start = stats_period_start
-        self.stats_period_end = stats_period_end
-        self.matching_event_id = matching_event_id
-
-    def _get_seen_stats(self, item_list, user):
-        if not self._collapse("stats"):
-            partial_execute_seen_stats_query = functools.partial(
-                self._execute_seen_stats_query,
-                item_list=item_list,
-                environment_ids=self.environment_ids,
-                start=self.start,
-                end=self.end,
-            )
-            time_range_result = partial_execute_seen_stats_query()
-            filtered_result = (
-                partial_execute_seen_stats_query(conditions=self.conditions)
-                if self.conditions and not self._collapse("filtered")
-                else None
-            )
-            if not self._collapse("lifetime"):
-                lifetime_result = (
-                    partial_execute_seen_stats_query(start=None, end=None)
-                    if self.start or self.end
-                    else time_range_result
-                )
-            else:
-                lifetime_result = None
-
-            for item in item_list:
-                time_range_result[item].update(
-                    {
-                        "filtered": filtered_result.get(item) if filtered_result else None,
-                        "lifetime": lifetime_result.get(item) if lifetime_result else None,
-                    }
-                )
-            return time_range_result
-        return None
-
-    def query_tsdb(self, group_ids, query_params, conditions=None, environment_ids=None, **kwargs):
-        return snuba_tsdb.get_range(
-            model=snuba_tsdb.models.group,
-            keys=group_ids,
-            environment_ids=environment_ids,
-            conditions=conditions,
-            **query_params,
-        )
-
-    def get_attrs(self, item_list, user):
-        if not self._collapse("base"):
-            attrs = super().get_attrs(item_list, user)
-        else:
-            seen_stats = self._get_seen_stats(item_list, user)
-            if seen_stats:
-                attrs = {item: seen_stats.get(item, {}) for item in item_list}
-            else:
-                attrs = {item: {} for item in item_list}
-
-        if self.stats_period and not self._collapse("stats"):
-            partial_get_stats = functools.partial(
-                self.get_stats, item_list=item_list, user=user, environment_ids=self.environment_ids
-            )
-            stats = partial_get_stats()
-            filtered_stats = (
-                partial_get_stats(conditions=self.conditions)
-                if self.conditions and not self._collapse("filtered")
-                else None
-            )
-            for item in item_list:
-                if filtered_stats:
-                    attrs[item].update({"filtered_stats": filtered_stats[item.id]})
-                attrs[item].update({"stats": stats[item.id]})
-
-            if self._expand("sessions"):
-                uniq_project_ids = list({item.project_id for item in item_list})
-                cache_keys = {pid: self._build_session_cache_key(pid) for pid in uniq_project_ids}
-                cache_data = cache.get_many(cache_keys.values())
-                missed_items = []
-                for item in item_list:
-                    num_sessions = cache_data.get(cache_keys[item.project_id])
-                    if num_sessions is None:
-                        found = "miss"
-                        missed_items.append(item)
-                    else:
-                        found = "hit"
-                        attrs[item].update(
-                            {
-                                "sessionCount": num_sessions,
-                            }
-                        )
-                    metrics.incr(f"group.get_session_counts.{found}")
-
-                if missed_items:
-                    project_ids = list({item.project_id for item in missed_items})
-                    project_sessions = release_health.get_num_sessions_per_project(
-                        project_ids,
-                        self.start,
-                        self.end,
-                        self.environment_ids,
-                    )
-
-                    results = {}
-                    for project_id, count in project_sessions:
-                        cache_key = self._build_session_cache_key(project_id)
-                        results[project_id] = count
-                        cache.set(cache_key, count, 3600)
-
-                    for item in missed_items:
-                        if item.project_id in results.keys():
-                            attrs[item].update(
-                                {
-                                    "sessionCount": results[item.project_id],
-                                }
-                            )
-                        else:
-                            attrs[item].update({"sessionCount": None})
-
-        if self._expand("inbox"):
-            inbox_stats = get_inbox_details(item_list)
-            for item in item_list:
-                attrs[item].update({"inbox": inbox_stats.get(item.id)})
-
-        if self._expand("owners"):
-            owner_details = get_owner_details(item_list, user)
-            for item in item_list:
-                attrs[item].update({"owners": owner_details.get(item.id)})
-
-        return attrs
-
-    def serialize(self, obj, attrs, user):
-        if not self._collapse("base"):
-            result = super().serialize(obj, attrs, user)
-        else:
-            result = {
-                "id": str(obj.id),
-            }
-            if "times_seen" in attrs:
-                result.update(self._convert_seen_stats(attrs))
-
-        if self.matching_event_id:
-            result["matchingEventId"] = self.matching_event_id
-
-        if not self._collapse("stats"):
-            if self.stats_period:
-                result["stats"] = {self.stats_period: attrs["stats"]}
-
-            if not self._collapse("lifetime"):
-                result["lifetime"] = self._convert_seen_stats(attrs["lifetime"])
-                if self.stats_period:
-                    result["lifetime"].update(
-                        {"stats": None}
-                    )  # Not needed in current implementation
-
-            if not self._collapse("filtered"):
-                if self.conditions:
-                    result["filtered"] = self._convert_seen_stats(attrs["filtered"])
-                    if self.stats_period:
-                        result["filtered"].update(
-                            {"stats": {self.stats_period: attrs["filtered_stats"]}}
-                        )
-                else:
-                    result["filtered"] = None
-
-            if self._expand("sessions"):
-                result["sessionCount"] = attrs["sessionCount"]
-
-        if self._expand("inbox"):
-            result["inbox"] = attrs["inbox"]
-
-        if self._expand("owners"):
-            result["owners"] = attrs["owners"]
-
-        return result
-
-    def _build_session_cache_key(self, project_id):
-        start_key = end_key = env_key = ""
-        if self.start:
-            start_key = self.start.replace(second=0, microsecond=0, tzinfo=None)
-
-        if self.end:
-            end_key = self.end.replace(second=0, microsecond=0, tzinfo=None)
-
-        if self.end and self.start and self.end - self.start >= timedelta(minutes=60):
-            # Cache to the hour for longer time range queries, and to the minute if the query if for a time period under 1 hour
-            end_key = end_key.replace(minute=0)
-            start_key = start_key.replace(minute=0)
-
-        if self.environment_ids:
-            self.environment_ids.sort()
-            env_key = "-".join(str(eid) for eid in self.environment_ids)
-
-        start_key = start_key.strftime("%m/%d/%Y, %H:%M:%S") if start_key != "" else ""
-        end_key = end_key.strftime("%m/%d/%Y, %H:%M:%S") if end_key != "" else ""
-        key_hash = hash_values([project_id, start_key, end_key, env_key])
-        session_cache_key = f"w-s:{key_hash}"
-        return session_cache_key

+ 383 - 0
src/sentry/api/serializers/models/group_stream.py

@@ -0,0 +1,383 @@
+from __future__ import annotations
+
+import functools
+from datetime import timedelta
+
+from django.utils import timezone
+
+from sentry import release_health, tsdb
+from sentry.api.serializers.models.group import GroupSerializer, GroupSerializerSnuba, snuba_tsdb
+from sentry.constants import StatsPeriod
+from sentry.models import Environment
+from sentry.models.groupinbox import get_inbox_details
+from sentry.models.groupowner import get_owner_details
+from sentry.utils import metrics
+from sentry.utils.cache import cache
+from sentry.utils.hashlib import hash_values
+
+
+class GroupStatsMixin:
+    STATS_PERIOD_CHOICES = {
+        "14d": StatsPeriod(14, timedelta(hours=24)),
+        "24h": StatsPeriod(24, timedelta(hours=1)),
+    }
+
+    CUSTOM_ROLLUP_CHOICES = {
+        "1h": timedelta(hours=1).total_seconds(),
+        "2h": timedelta(hours=2).total_seconds(),
+        "3h": timedelta(hours=3).total_seconds(),
+        "6h": timedelta(hours=6).total_seconds(),
+        "12h": timedelta(hours=12).total_seconds(),
+        "24h": timedelta(hours=24).total_seconds(),
+    }
+
+    CUSTOM_SEGMENTS = 29  # for 30 segments use 1/29th intervals
+    CUSTOM_SEGMENTS_12H = 35  # for 12h 36 segments, otherwise 15-16-17 bars is too few
+    CUSTOM_ROLLUP_6H = timedelta(hours=6).total_seconds()  # rollups should be increments of 6hs
+
+    def query_tsdb(self, group_ids, query_params):
+        raise NotImplementedError
+
+    def get_stats(self, item_list, user, **kwargs):
+        if self.stats_period:
+            # we need to compute stats at 1d (1h resolution), and 14d or a custom given period
+            group_ids = [g.id for g in item_list]
+
+            if self.stats_period == "auto":
+                total_period = (self.stats_period_end - self.stats_period_start).total_seconds()
+                if total_period < timedelta(hours=24).total_seconds():
+                    rollup = total_period / self.CUSTOM_SEGMENTS
+                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["1h"]:
+                    rollup = self.CUSTOM_ROLLUP_CHOICES["1h"]
+                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["2h"]:
+                    rollup = self.CUSTOM_ROLLUP_CHOICES["2h"]
+                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["3h"]:
+                    rollup = self.CUSTOM_ROLLUP_CHOICES["3h"]
+                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["6h"]:
+                    rollup = self.CUSTOM_ROLLUP_CHOICES["6h"]
+                elif (
+                    total_period < self.CUSTOM_SEGMENTS_12H * self.CUSTOM_ROLLUP_CHOICES["12h"]
+                ):  # 36 segments is ok
+                    rollup = self.CUSTOM_ROLLUP_CHOICES["12h"]
+                elif total_period < self.CUSTOM_SEGMENTS * self.CUSTOM_ROLLUP_CHOICES["24h"]:
+                    rollup = self.CUSTOM_ROLLUP_CHOICES["24h"]
+                else:
+                    delta_day = self.CUSTOM_ROLLUP_CHOICES["24h"]
+                    rollup = round(total_period / (self.CUSTOM_SEGMENTS * delta_day)) * delta_day
+
+                query_params = {
+                    "start": self.stats_period_start,
+                    "end": self.stats_period_end,
+                    "rollup": int(rollup),
+                }
+            else:
+                segments, interval = self.STATS_PERIOD_CHOICES[self.stats_period]
+                now = timezone.now()
+                query_params = {
+                    "start": now - ((segments - 1) * interval),
+                    "end": now,
+                    "rollup": int(interval.total_seconds()),
+                }
+
+            return self.query_tsdb(group_ids, query_params, **kwargs)
+
+
+class StreamGroupSerializer(GroupSerializer, GroupStatsMixin):
+    def __init__(
+        self,
+        environment_func=None,
+        stats_period=None,
+        stats_period_start=None,
+        stats_period_end=None,
+        matching_event_id=None,
+        matching_event_environment=None,
+    ):
+        super().__init__(environment_func)
+
+        if stats_period is not None:
+            assert stats_period in self.STATS_PERIOD_CHOICES or stats_period == "auto"
+
+        self.stats_period = stats_period
+        self.stats_period_start = stats_period_start
+        self.stats_period_end = stats_period_end
+        self.matching_event_id = matching_event_id
+        self.matching_event_environment = matching_event_environment
+
+    def query_tsdb(self, group_ids, query_params, **kwargs):
+        try:
+            environment = self.environment_func()
+        except Environment.DoesNotExist:
+            stats = {key: tsdb.make_series(0, **query_params) for key in group_ids}
+        else:
+            stats = tsdb.get_range(
+                model=tsdb.models.group,
+                keys=group_ids,
+                environment_ids=environment and [environment.id],
+                **query_params,
+            )
+
+        return stats
+
+    def get_attrs(self, item_list, user):
+        attrs = super().get_attrs(item_list, user)
+
+        if self.stats_period:
+            stats = self.get_stats(item_list, user)
+            for item in item_list:
+                attrs[item].update({"stats": stats[item.id]})
+
+        return attrs
+
+    def serialize(self, obj, attrs, user):
+        result = super().serialize(obj, attrs, user)
+
+        if self.stats_period:
+            result["stats"] = {self.stats_period: attrs["stats"]}
+
+        if self.matching_event_id:
+            result["matchingEventId"] = self.matching_event_id
+
+        if self.matching_event_environment:
+            result["matchingEventEnvironment"] = self.matching_event_environment
+
+        return result
+
+
+class TagBasedStreamGroupSerializer(StreamGroupSerializer):
+    def __init__(self, tags, **kwargs):
+        super().__init__(**kwargs)
+        self.tags = tags
+
+    def serialize(self, obj, attrs, user):
+        result = super().serialize(obj, attrs, user)
+        result["tagLastSeen"] = self.tags[obj.id].last_seen
+        result["tagFirstSeen"] = self.tags[obj.id].first_seen
+        return result
+
+
+class StreamGroupSerializerSnuba(GroupSerializerSnuba, GroupStatsMixin):
+    def __init__(
+        self,
+        environment_ids=None,
+        stats_period=None,
+        stats_period_start=None,
+        stats_period_end=None,
+        matching_event_id=None,
+        start=None,
+        end=None,
+        search_filters=None,
+        collapse=None,
+        expand=None,
+        organization_id=None,
+    ):
+        super().__init__(
+            environment_ids,
+            start,
+            end,
+            search_filters,
+            collapse=collapse,
+            expand=expand,
+            organization_id=organization_id,
+        )
+
+        if stats_period is not None:
+            assert stats_period in self.STATS_PERIOD_CHOICES or (
+                stats_period == "auto" and stats_period_start and stats_period_end
+            )
+
+        self.stats_period = stats_period
+        self.stats_period_start = stats_period_start
+        self.stats_period_end = stats_period_end
+        self.matching_event_id = matching_event_id
+
+    def _get_seen_stats(self, item_list, user):
+        if not self._collapse("stats"):
+            partial_execute_seen_stats_query = functools.partial(
+                self._execute_seen_stats_query,
+                item_list=item_list,
+                environment_ids=self.environment_ids,
+                start=self.start,
+                end=self.end,
+            )
+            time_range_result = partial_execute_seen_stats_query()
+            filtered_result = (
+                partial_execute_seen_stats_query(conditions=self.conditions)
+                if self.conditions and not self._collapse("filtered")
+                else None
+            )
+            if not self._collapse("lifetime"):
+                lifetime_result = (
+                    partial_execute_seen_stats_query(start=None, end=None)
+                    if self.start or self.end
+                    else time_range_result
+                )
+            else:
+                lifetime_result = None
+
+            for item in item_list:
+                time_range_result[item].update(
+                    {
+                        "filtered": filtered_result.get(item) if filtered_result else None,
+                        "lifetime": lifetime_result.get(item) if lifetime_result else None,
+                    }
+                )
+            return time_range_result
+        return None
+
+    def query_tsdb(self, group_ids, query_params, conditions=None, environment_ids=None, **kwargs):
+        return snuba_tsdb.get_range(
+            model=snuba_tsdb.models.group,
+            keys=group_ids,
+            environment_ids=environment_ids,
+            conditions=conditions,
+            **query_params,
+        )
+
+    def get_attrs(self, item_list, user):
+        if not self._collapse("base"):
+            attrs = super().get_attrs(item_list, user)
+        else:
+            seen_stats = self._get_seen_stats(item_list, user)
+            if seen_stats:
+                attrs = {item: seen_stats.get(item, {}) for item in item_list}
+            else:
+                attrs = {item: {} for item in item_list}
+
+        if self.stats_period and not self._collapse("stats"):
+            partial_get_stats = functools.partial(
+                self.get_stats, item_list=item_list, user=user, environment_ids=self.environment_ids
+            )
+            stats = partial_get_stats()
+            filtered_stats = (
+                partial_get_stats(conditions=self.conditions)
+                if self.conditions and not self._collapse("filtered")
+                else None
+            )
+            for item in item_list:
+                if filtered_stats:
+                    attrs[item].update({"filtered_stats": filtered_stats[item.id]})
+                attrs[item].update({"stats": stats[item.id]})
+
+            if self._expand("sessions"):
+                uniq_project_ids = list({item.project_id for item in item_list})
+                cache_keys = {pid: self._build_session_cache_key(pid) for pid in uniq_project_ids}
+                cache_data = cache.get_many(cache_keys.values())
+                missed_items = []
+                for item in item_list:
+                    num_sessions = cache_data.get(cache_keys[item.project_id])
+                    if num_sessions is None:
+                        found = "miss"
+                        missed_items.append(item)
+                    else:
+                        found = "hit"
+                        attrs[item].update(
+                            {
+                                "sessionCount": num_sessions,
+                            }
+                        )
+                    metrics.incr(f"group.get_session_counts.{found}")
+
+                if missed_items:
+                    project_ids = list({item.project_id for item in missed_items})
+                    project_sessions = release_health.get_num_sessions_per_project(
+                        project_ids,
+                        self.start,
+                        self.end,
+                        self.environment_ids,
+                    )
+
+                    results = {}
+                    for project_id, count in project_sessions:
+                        cache_key = self._build_session_cache_key(project_id)
+                        results[project_id] = count
+                        cache.set(cache_key, count, 3600)
+
+                    for item in missed_items:
+                        if item.project_id in results.keys():
+                            attrs[item].update(
+                                {
+                                    "sessionCount": results[item.project_id],
+                                }
+                            )
+                        else:
+                            attrs[item].update({"sessionCount": None})
+
+        if self._expand("inbox"):
+            inbox_stats = get_inbox_details(item_list)
+            for item in item_list:
+                attrs[item].update({"inbox": inbox_stats.get(item.id)})
+
+        if self._expand("owners"):
+            owner_details = get_owner_details(item_list, user)
+            for item in item_list:
+                attrs[item].update({"owners": owner_details.get(item.id)})
+
+        return attrs
+
+    def serialize(self, obj, attrs, user):
+        if not self._collapse("base"):
+            result = super().serialize(obj, attrs, user)
+        else:
+            result = {
+                "id": str(obj.id),
+            }
+            if "times_seen" in attrs:
+                result.update(self._convert_seen_stats(attrs))
+
+        if self.matching_event_id:
+            result["matchingEventId"] = self.matching_event_id
+
+        if not self._collapse("stats"):
+            if self.stats_period:
+                result["stats"] = {self.stats_period: attrs["stats"]}
+
+            if not self._collapse("lifetime"):
+                result["lifetime"] = self._convert_seen_stats(attrs["lifetime"])
+                if self.stats_period:
+                    result["lifetime"].update(
+                        {"stats": None}
+                    )  # Not needed in current implementation
+
+            if not self._collapse("filtered"):
+                if self.conditions:
+                    result["filtered"] = self._convert_seen_stats(attrs["filtered"])
+                    if self.stats_period:
+                        result["filtered"].update(
+                            {"stats": {self.stats_period: attrs["filtered_stats"]}}
+                        )
+                else:
+                    result["filtered"] = None
+
+            if self._expand("sessions"):
+                result["sessionCount"] = attrs["sessionCount"]
+
+        if self._expand("inbox"):
+            result["inbox"] = attrs["inbox"]
+
+        if self._expand("owners"):
+            result["owners"] = attrs["owners"]
+
+        return result
+
+    def _build_session_cache_key(self, project_id):
+        start_key = end_key = env_key = ""
+        if self.start:
+            start_key = self.start.replace(second=0, microsecond=0, tzinfo=None)
+
+        if self.end:
+            end_key = self.end.replace(second=0, microsecond=0, tzinfo=None)
+
+        if self.end and self.start and self.end - self.start >= timedelta(minutes=60):
+            # Cache to the hour for longer time range queries, and to the minute if the query if for a time period under 1 hour
+            end_key = end_key.replace(minute=0)
+            start_key = start_key.replace(minute=0)
+
+        if self.environment_ids:
+            self.environment_ids.sort()
+            env_key = "-".join(str(eid) for eid in self.environment_ids)
+
+        start_key = start_key.strftime("%m/%d/%Y, %H:%M:%S") if start_key != "" else ""
+        end_key = end_key.strftime("%m/%d/%Y, %H:%M:%S") if end_key != "" else ""
+        key_hash = hash_values([project_id, start_key, end_key, env_key])
+        session_cache_key = f"w-s:{key_hash}"
+        return session_cache_key

+ 1 - 1
src/sentry/integrations/jira/views/issue_hook.py

@@ -10,7 +10,7 @@ from rest_framework.request import Request
 from rest_framework.response import Response
 
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.group import StreamGroupSerializer
+from sentry.api.serializers.models.group_stream import StreamGroupSerializer
 from sentry.integrations.utils import AtlassianConnectValidationError, get_integration_from_request
 from sentry.models import ExternalIssue, Group
 from sentry.shared_integrations.exceptions import ApiError

+ 0 - 39
tests/sentry/api/serializers/test_group.py

@@ -1,13 +1,10 @@
 from datetime import timedelta
-from unittest import mock
 from unittest.mock import patch
 
 from django.utils import timezone
 
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.group import StreamGroupSerializer
 from sentry.models import (
-    Environment,
     Group,
     GroupLink,
     GroupResolution,
@@ -353,39 +350,3 @@ class GroupSerializerTest(TestCase):
                 "dateCreated": result["statusDetails"]["info"]["dateCreated"],
             },
         }
-
-
-class StreamGroupSerializerTestCase(TestCase):
-    def test_environment(self):
-        group = self.group
-
-        environment = Environment.get_or_create(group.project, "production")
-
-        from sentry.api.serializers.models.group import tsdb
-
-        with mock.patch(
-            "sentry.api.serializers.models.group.tsdb.get_range", side_effect=tsdb.get_range
-        ) as get_range:
-            serialize(
-                [group],
-                serializer=StreamGroupSerializer(
-                    environment_func=lambda: environment, stats_period="14d"
-                ),
-            )
-            assert get_range.call_count == 1
-            for args, kwargs in get_range.call_args_list:
-                assert kwargs["environment_ids"] == [environment.id]
-
-        def get_invalid_environment():
-            raise Environment.DoesNotExist()
-
-        with mock.patch(
-            "sentry.api.serializers.models.group.tsdb.make_series", side_effect=tsdb.make_series
-        ) as make_series:
-            serialize(
-                [group],
-                serializer=StreamGroupSerializer(
-                    environment_func=get_invalid_environment, stats_period="14d"
-                ),
-            )
-            assert make_series.call_count == 1

+ 43 - 0
tests/sentry/api/serializers/test_group_stream.py

@@ -0,0 +1,43 @@
+from unittest import mock
+
+from sentry.api.serializers import serialize
+from sentry.api.serializers.models.group_stream import StreamGroupSerializer
+from sentry.models import Environment
+from sentry.testutils import TestCase
+
+
+class StreamGroupSerializerTestCase(TestCase):
+    def test_environment(self):
+        group = self.group
+
+        environment = Environment.get_or_create(group.project, "production")
+
+        from sentry.api.serializers.models.group_stream import tsdb
+
+        with mock.patch(
+            "sentry.api.serializers.models.group_stream.tsdb.get_range", side_effect=tsdb.get_range
+        ) as get_range:
+            serialize(
+                [group],
+                serializer=StreamGroupSerializer(
+                    environment_func=lambda: environment, stats_period="14d"
+                ),
+            )
+            assert get_range.call_count == 1
+            for args, kwargs in get_range.call_args_list:
+                assert kwargs["environment_ids"] == [environment.id]
+
+        def get_invalid_environment():
+            raise Environment.DoesNotExist()
+
+        with mock.patch(
+            "sentry.api.serializers.models.group_stream.tsdb.make_series",
+            side_effect=tsdb.make_series,
+        ) as make_series:
+            serialize(
+                [group],
+                serializer=StreamGroupSerializer(
+                    environment_func=get_invalid_environment, stats_period="14d"
+                ),
+            )
+            assert make_series.call_count == 1

Some files were not shown because too many files changed in this diff