Browse Source

feat(workflow): Add session % to Issue Stream serializer (#25969)

*session % on issue serializer
Chris Fuller 3 years ago
parent
commit
e75e617671
2 changed files with 260 additions and 4 deletions
  1. 82 4
      src/sentry/api/serializers/models/group.py
  2. 178 0
      tests/snuba/api/serializers/test_group.py

+ 82 - 4
src/sentry/api/serializers/models/group.py

@@ -53,12 +53,11 @@ 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.types.integrations import ExternalProviders
-from sentry.utils import snuba
 from sentry.utils.cache import cache
 from sentry.utils.compat import zip
 from sentry.utils.db import attach_foreignkey
 from sentry.utils.safe import safe_execute
-from sentry.utils.snuba import Dataset, raw_query
+from sentry.utils.snuba import Dataset, aliased_query, raw_query
 
 # TODO(jess): remove when snuba is primary backend
 snuba_tsdb = SnubaTSDB(**settings.SENTRY_TSDB_OPTIONS)
@@ -814,8 +813,8 @@ class GroupSerializerSnuba(GroupSerializerBase):
         filters = {"project_id": project_ids, "group_id": group_ids}
         if self.environment_ids:
             filters["environment"] = self.environment_ids
-        result = snuba.aliased_query(
-            dataset=snuba.Dataset.Events,
+        result = aliased_query(
+            dataset=Dataset.Events,
             start=start,
             end=end,
             groupby=["group_id"],
@@ -858,6 +857,7 @@ class GroupSerializerSnuba(GroupSerializerBase):
                 "last_seen": last_seen.get(item.id),
                 "user_count": user_counts.get(item.id, 0),
             }
+
         return attrs
 
     def _get_seen_stats(self, item_list, user):
@@ -948,6 +948,11 @@ class StreamGroupSerializerSnuba(GroupSerializerSnuba, GroupStatsMixin):
             **query_params,
         )
 
+    def _get_session_percent(self, count, sessions):
+        if sessions != 0:
+            return round(int(count) / sessions, 4)
+        return None
+
     def get_attrs(self, item_list, user):
         if not self._collapse("base"):
             attrs = super().get_attrs(item_list, user)
@@ -973,6 +978,57 @@ class StreamGroupSerializerSnuba(GroupSerializerSnuba, GroupStatsMixin):
                     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:
+                        missed_items.append(item)
+                    else:
+                        attrs[item].update(
+                            {
+                                "sessionPercent": self._get_session_percent(
+                                    attrs[item]["times_seen"], num_sessions
+                                )
+                            }
+                        )
+
+                if missed_items:
+                    filters = {"project_id": list({item.project_id for item in missed_items})}
+                    if self.environment_ids:
+                        filters["environment"] = self.environment_ids
+
+                    result_totals = raw_query(
+                        selected_columns=["sessions"],
+                        dataset=Dataset.Sessions,
+                        start=self.start,
+                        end=self.end,
+                        filter_keys=filters,
+                        groupby=["project_id"],
+                        referrer="serializers.GroupSerializerSnuba.session_totals",
+                    )
+                    results = {}
+                    for data in result_totals["data"]:
+                        cache_key = self._build_session_cache_key(data["project_id"])
+                        results[data["project_id"]] = data["sessions"]
+                        cache.set(cache_key, data["sessions"], 3600)
+
+                    for item in missed_items:
+                        if item.project_id in results.keys():
+                            attrs[item].update(
+                                {
+                                    "sessionPercent": self._get_session_percent(
+                                        attrs[item]["times_seen"], data["sessions"]
+                                    )
+                                }
+                            )
+                        else:
+                            attrs[item].update({"sessionPercent": None})
+
         if self._expand("inbox"):
             inbox_stats = get_inbox_details(item_list)
             for item in item_list:
@@ -1019,6 +1075,9 @@ class StreamGroupSerializerSnuba(GroupSerializerSnuba, GroupStatsMixin):
                 else:
                     result["filtered"] = None
 
+            if self._expand("sessions"):
+                result["sessionPercent"] = attrs["sessionPercent"]
+
         if self._expand("inbox"):
             result["inbox"] = attrs["inbox"]
 
@@ -1026,3 +1085,22 @@ class StreamGroupSerializerSnuba(GroupSerializerSnuba, GroupStatsMixin):
             result["owners"] = attrs["owners"]
 
         return result
+
+    def _build_session_cache_key(self, project_id):
+        session_count_key = f"w-s:{project_id}"
+
+        if self.start:
+            session_count_key = f"{session_count_key}-{self.start.replace(minute=0, second=0, microsecond=0, tzinfo=None)}".replace(
+                " ", ""
+            )
+
+        if self.end:
+            session_count_key = f"{session_count_key}-{self.end.replace(minute=0, second=0, microsecond=0, tzinfo=None)}".replace(
+                " ", ""
+            )
+
+        if self.environment_ids:
+            envs = "-".join(str(eid) for eid in self.environment_ids)
+            session_count_key = f"{session_count_key}-{envs}"
+
+        return session_count_key

+ 178 - 0
tests/snuba/api/serializers/test_group.py

@@ -1,3 +1,4 @@
+import time
 from datetime import timedelta
 
 import pytz
@@ -25,6 +26,7 @@ from sentry.notifications.types import NotificationSettingOptionValues, Notifica
 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.compat import mock
 from sentry.utils.compat.mock import patch
 
@@ -441,3 +443,179 @@ class StreamGroupSerializerTestCase(APITestCase, SnubaTestCase):
             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-5d13d2102666",
+                "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,
+                "received": self.received,
+            }
+        )
+
+        self.store_session(
+            {
+                "session_id": "5e910c1a-6941-460e-9843-24103fb6a63c",
+                "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102666",
+                "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": None,
+                "errors": 0,
+                "started": self.session_started,
+                "received": self.received,
+            }
+        )
+
+        self.store_session(
+            {
+                "session_id": "5e910c1a-6941-460e-9843-24103fb6a63c",
+                "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102666",
+                "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-5d13d2102666",
+                "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 "sessionPercent" not in result[0]
+        result = serialize(
+            [group],
+            serializer=StreamGroupSerializerSnuba(stats_period="14d", expand=["sessions"]),
+        )
+        assert result[0]["sessionPercent"] == 0.3333
+        result = serialize(
+            [group],
+            serializer=StreamGroupSerializerSnuba(
+                environment_ids=[environment.id], stats_period="14d", expand=["sessions"]
+            ),
+        )
+        assert result[0]["sessionPercent"] == 0.5
+
+        result = serialize(
+            [group],
+            serializer=StreamGroupSerializerSnuba(
+                environment_ids=[no_sessions_environment.id],
+                stats_period="14d",
+                expand=["sessions"],
+            ),
+        )
+        assert result[0]["sessionPercent"] is None
+
+        result = serialize(
+            [group],
+            serializer=StreamGroupSerializerSnuba(
+                environment_ids=[dev_environment.id], stats_period="14d", expand=["sessions"]
+            ),
+        )
+        assert result[0]["sessionPercent"] == 1
+
+        self.store_session(
+            {
+                "session_id": "a148c0c5-06a2-423b-8901-6b43b812cf83",
+                "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": 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]["sessionPercent"] == 0.0  # No events in that time period
+
+        # Delete the cache from the query we did above, else this result comes back as 1 instead of 0.5
+        cache.delete(f"w-s:{group.project.id}-{dev_environment.id}")
+        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]["sessionPercent"] == 0.5
+        # No sessions in project2
+        assert result[1]["sessionPercent"] is None