Repository: getsentry/sentry

Issue:
Possible to sort by User?
Hi,

in Sentry i can sort by First Seen, Frequency, etc. but i cannot sort by user. 
I know, detecting unique users can be hard, but the numbers are there already, when i can sort by user it would give me at least some hint how many people are affected. 
A bug that happened for 10k users once is in my view more important than a bug that a happened 10k times for two user :)

PS: keep up the great work, sentry is great!



Discussion:
Upvoted https://twitter.com/drewml/status/732688410662473728

What if we just implemented it like this:

![image](https://cloud.githubusercontent.com/assets/2153/15720653/180834a2-27ea-11e6-9902-9eb351379c44.png)

++

And what is the status of? 
The user count that's displayed in the stream view [is derived from the `GroupTagKey`](https://github.com/getsentry/sentry/blob/02472b6fc4d8fba2cd0ab50a2a109adad6b88234/src/sentry/api/serializers/models/group.py#L123-L128) (`sentry_grouptagkey` table) record. As far as I can tell, the biggest blocker here is that there is no index that would satisfy this query. Most (maybe all?) of the other sort clauses are directly off indexed[1] columns on the `sentry_groupedmessage` table.

There are a couple of options:

1. If we wanted to implement arbitrary threshold filters (as well as sorting choices) on the number of distinct times any tag value — not just user — has been seen, we'd probably want to create an index similar to `(project_id, key, values_seen DESC)` on the `sentry_grouptagkey` table.
2. If we only wanted to index the user tag for a sort clause, [we could do that with a partial index](https://www.postgresql.org/docs/9.3/static/indexes-partial.html), at least for PostgreSQL.

Regardless, we'd have to create some additional index on this table in either case which could potentially be expensive. I do think this is a potentially useful feature, but not sure offhand what the cost of creating this index would be on write operations or on disk utilization.

[1]: or at least *should* be indexed…
Interested to hear if there has been any progress with this. Sorting by users still is one prominent wish for us and would greatly improve our work with Sentry.
Indeed. My organization judges issue severity by number of users impacted, rather than total occurrences.
I don't really understand why is this complicated? Cleary, when I do an API call to issue the response, does have userCount field. Can you just include this to you Sentry CLI then?
Anyways, I just made a small script for me to use. Let me know if anyone want this happy to share it. I hope the sentry team can add this as their feature.

![Screenshot 2019-12-13 at 16 32 31](https://user-images.githubusercontent.com/291424/70786013-98a67f80-1dc6-11ea-8e37-97ea619582d7.png)

Here is the script that I've been using to get the details out that I need, sorted by users affected:

```
curl -H 'Authorization: Bearer $SENTRY_TOKEN' $SENTRY_DOMAIN/api/0/projects/$ORGANIZATION/$PROJECT/issues/ | \
jq -r '[.[] | {id: .id, title: .title, count: .count | tonumber, users: .userCount, firstSeen: .firstSeen, lastSeen: .lastSeen, culprit: .culprit, filename: .metadata.filename, link: .permalink}] | sort_by(-.users)' | \
in2csv -f json > /tmp/errors.csv
```

Relies on [jq](https://stedolan.github.io/jq/) and [csvkit](https://csvkit.readthedocs.io/en/latest/). If you want the output similar to @chocnut, instead of redirecting to a file, pipe to `csvlook -y 0`
Can't believe this was raised in 2016 and not yet addressed.

When ordered by Frequency there are cases when the error rate per user is higher than some other issues. For example on the screenshot below the 3rd highest had 15k events from 326 users where as the 5th most frequent issue had 19k events from 16k users. I would think that most people would prioritize the 19k events from 16k users issue rather then the 15k events from 326 users issue.

![image](https://user-images.githubusercontent.com/9847041/76304169-dea7ac80-62fd-11ea-8042-0af966551d25.png)

Anyone who still interested in or looking for a tool. Please check my repo [here](https://github.com/chocnut/sentry-monitor) 

Resolving patch:
diff --git a/src/sentry/search/snuba/executors.py b/src/sentry/search/snuba/executors.py
--- a/src/sentry/search/snuba/executors.py
+++ b/src/sentry/search/snuba/executors.py
@@ -235,6 +235,7 @@ class PostgresSnubaQueryExecutor(AbstractQueryExecutor):
         "freq": "times_seen",
         "new": "first_seen",
         "priority": "priority",
+        "user": "user_count",
     }
 
     aggregation_defs = {
@@ -245,6 +246,7 @@ class PostgresSnubaQueryExecutor(AbstractQueryExecutor):
         "priority": ["toUInt64(plus(multiply(log(times_seen), 600), last_seen))", ""],
         # Only makes sense with WITH TOTALS, returns 1 for an individual group.
         "total": ["uniq", ISSUE_FIELD_NAME],
+        "user_count": ["uniq", "tags[sentry:user]"],
     }
 
     @property


Test patch:
diff --git a/tests/snuba/search/test_backend.py b/tests/snuba/search/test_backend.py
--- a/tests/snuba/search/test_backend.py
+++ b/tests/snuba/search/test_backend.py
@@ -43,7 +43,7 @@ def setUp(self):
                 "event_id": "a" * 32,
                 "message": "foo. Also, this message is intended to be greater than 256 characters so that we can put some unique string identifier after that point in the string. The purpose of this is in order to verify we are using snuba to search messages instead of Postgres (postgres truncates at 256 characters and clickhouse does not). santryrox.",
                 "environment": "production",
-                "tags": {"server": "example.com"},
+                "tags": {"server": "example.com", "sentry:user": "event1@example.com"},
                 "timestamp": event1_timestamp,
                 "stacktrace": {"frames": [{"module": "group1"}]},
             },
@@ -55,7 +55,7 @@ def setUp(self):
                 "event_id": "c" * 32,
                 "message": "group1",
                 "environment": "production",
-                "tags": {"server": "example.com"},
+                "tags": {"server": "example.com", "sentry:user": "event3@example.com"},
                 "timestamp": iso_format(self.base_datetime),
                 "stacktrace": {"frames": [{"module": "group1"}]},
             },
@@ -82,7 +82,11 @@ def setUp(self):
                 "message": "bar",
                 "stacktrace": {"frames": [{"module": "group2"}]},
                 "environment": "staging",
-                "tags": {"server": "example.com", "url": "http://example.com"},
+                "tags": {
+                    "server": "example.com",
+                    "url": "http://example.com",
+                    "sentry:user": "event2@example.com",
+                },
             },
             project_id=self.project.id,
         )
@@ -257,6 +261,9 @@ def test_sort(self):
         results = self.make_query(sort_by="priority")
         assert list(results) == [self.group1, self.group2]
 
+        results = self.make_query(sort_by="user")
+        assert list(results) == [self.group1, self.group2]
+
     def test_sort_with_environment(self):
         for dt in [
             self.group1.first_seen + timedelta(days=1),
@@ -287,6 +294,9 @@ def test_sort_with_environment(self):
         )
         assert list(results) == [self.group2, self.group1]
 
+        results = self.make_query(environments=[self.environments["production"]], sort_by="user")
+        assert list(results) == [self.group1, self.group2]
+
     def test_status(self):
         results = self.make_query(search_filter_query="is:unresolved")
         assert set(results) == set([self.group1])
@@ -947,6 +957,17 @@ def __eq__(self, other):
             **common_args
         )
 
+        self.make_query(search_filter_query="foo", sort_by="user")
+        assert query_mock.call_args == mock.call(
+            orderby=["-user_count", "group_id"],
+            aggregations=[
+                ["uniq", "group_id", "total"],
+                ["uniq", "tags[sentry:user]", "user_count"],
+            ],
+            having=[],
+            **common_args
+        )
+
     def test_pre_and_post_filtering(self):
         prev_max_pre = options.get("snuba.search.max-pre-snuba-candidates")
         options.set("snuba.search.max-pre-snuba-candidates", 1)
@@ -1246,6 +1267,9 @@ def test_sort_multi_project(self):
         results = self.make_query([self.project, self.project2], sort_by="priority")
         assert list(results) == [self.group1, self.group2, self.group_p2]
 
+        results = self.make_query([self.project, self.project2], sort_by="user")
+        assert list(results) == [self.group1, self.group2, self.group_p2]
+
     def test_first_release_any_or_no_environments(self):
         # test scenarios for tickets:
         # SEN-571