Browse Source

[api] Expand EventUser usage (#5282)

Add EventUser.name
Add UserInterface.name
Add UserReport.event_user_id
Add unique users stat endpoint
Add project user details endpoint
Add organization user issues endpoint
David Cramer 7 years ago
parent
commit
78809d731a

+ 2 - 0
CHANGES

@@ -19,6 +19,8 @@ Schema Changes
 - Added index on ``EventTag.date_added``
 - Added unique index on ``Environment(organization_id, name)``
 - Added unique index on ``ReleaseEnvironment(organization_id, release_id, environment_id)``
+- Added ``EventUser.name`` column
+- Added ``UserReport.event_user_id`` column
 
 Version 8.16.1
 --------------

+ 63 - 0
src/sentry/api/endpoints/organization_user_issues.py

@@ -0,0 +1,63 @@
+from __future__ import absolute_import
+
+from django.db.models import Q
+from operator import or_
+from rest_framework.response import Response
+from six.moves import reduce
+
+from sentry.api.bases.organization import OrganizationEndpoint
+from sentry.api.serializers import serialize
+from sentry.api.serializers.models.group import TagBasedStreamGroupSerializer
+from sentry.models import (
+    EventUser, Group, GroupTagValue
+)
+
+
+class OrganizationUserIssuesEndpoint(OrganizationEndpoint):
+    def get(self, request, organization, user_id):
+        limit = request.GET.get('limit', 100)
+
+        euser = EventUser.objects.select_related('project__team').get(
+            project__organization=organization,
+            id=user_id,
+        )
+        # they have organization access but not to this project, thus
+        # they shouldn't be able to see this user
+        if not request.access.has_team_access(euser.project.team):
+            return Response([])
+
+        other_eusers = euser.find_similar_users(request.user)
+        event_users = [euser] + list(other_eusers)
+
+        if event_users:
+            tag_filters = [
+                Q(value=eu.tag_value, project_id=eu.project_id)
+                for eu in event_users
+            ]
+            tags = GroupTagValue.objects.filter(
+                reduce(or_, tag_filters),
+                key='sentry:user',
+            ).order_by('-last_seen')[:limit]
+        else:
+            tags = GroupTagValue.objects.none()
+
+        tags = {t.group_id: t for t in tags}
+        if tags:
+            groups = sorted(
+                Group.objects.filter(
+                    id__in=tags.keys(),
+                ).order_by('-last_seen')[:limit],
+                key=lambda x: tags[x.id].last_seen,
+                reverse=True,
+            )
+        else:
+            groups = []
+
+        context = serialize(
+            groups, request.user, TagBasedStreamGroupSerializer(
+                stats_period=None,
+                tags=tags,
+            )
+        )
+
+        return Response(context)

+ 22 - 28
src/sentry/api/endpoints/organization_user_issues_search.py

@@ -6,13 +6,12 @@ from sentry.api.bases.organization import OrganizationEndpoint
 from sentry.api.serializers import serialize
 from sentry.api.serializers.models.group import StreamGroupSerializer
 from sentry.models import (
-    EventUser, Group, GroupTagValue, OrganizationMember,
-    OrganizationMemberTeam, Project, Team
+    EventUser, Group, GroupTagValue,
+    OrganizationMemberTeam, Project
 )
 
 
 class OrganizationUserIssuesSearchEndpoint(OrganizationEndpoint):
-
     def get(self, request, organization):
         email = request.GET.get('email')
 
@@ -22,33 +21,30 @@ class OrganizationUserIssuesSearchEndpoint(OrganizationEndpoint):
         limit = request.GET.get('limit', 100)
 
         # limit to only teams user has opted into
-        member = OrganizationMember.objects.get(user=request.user,
-                                                organization=organization)
-        teams = Team.objects.filter(
-            id__in=OrganizationMemberTeam.objects.filter(
-                organizationmember=member,
+        project_ids = list(Project.objects.filter(
+            team__in=OrganizationMemberTeam.objects.filter(
+                organizationmember__user=request.user,
+                organizationmember__organization=organization,
                 is_active=True,
-            ).values('team')
-        )
-
-        projects = Project.objects.filter(
-            team__in=list(teams),
-        )
-
-        event_users = EventUser.objects.filter(email=email,
-                                               project_id__in=[p.id for p in projects])[:1000]
+            ).values('team'),
+        ).values_list('id', flat=True)[:1000])
 
-        projects = list(set([e.project_id for e in event_users]))
+        event_users = EventUser.objects.filter(
+            email__iexact=email,
+            project_id__in=project_ids,
+        )[:1000]
 
-        tag_values = [eu.tag_value for eu in event_users]
-        tags = GroupTagValue.objects.filter(key='sentry:user',
-                                            value__in=tag_values,
-                                            project_id__in=projects)
+        project_ids = list(set([e.project_id for e in event_users]))
 
-        group_ids = set(tags.values_list('group_id', flat=True))
+        group_ids = list(GroupTagValue.objects.filter(
+            key='sentry:user',
+            value__in=[eu.tag_value for eu in event_users],
+            project_id__in=project_ids,
+        ).order_by('-last_seen').values_list('group_id', flat=True)[:limit])
 
-        groups = Group.objects.filter(id__in=group_ids,
-                                      project_id__in=projects).order_by('-last_seen')[:limit]
+        groups = Group.objects.filter(
+            id__in=group_ids,
+        ).order_by('-last_seen')[:limit]
 
         context = serialize(
             list(groups), request.user, StreamGroupSerializer(
@@ -56,6 +52,4 @@ class OrganizationUserIssuesSearchEndpoint(OrganizationEndpoint):
             )
         )
 
-        response = Response(context)
-
-        return response
+        return Response(context)

+ 19 - 0
src/sentry/api/endpoints/project_user_details.py

@@ -0,0 +1,19 @@
+from __future__ import absolute_import
+
+from rest_framework.response import Response
+
+from sentry.api.base import DocSection
+from sentry.api.bases.project import ProjectEndpoint
+from sentry.api.serializers import serialize
+from sentry.models import EventUser
+
+
+class ProjectUserDetailsEndpoint(ProjectEndpoint):
+    doc_section = DocSection.PROJECTS
+
+    def get(self, request, project, user_hash):
+        euser = EventUser.objects.get(
+            project=project,
+            hash=user_hash,
+        )
+        return Response(serialize(euser, request.user))

+ 40 - 3
src/sentry/api/endpoints/project_user_reports.py

@@ -10,7 +10,9 @@ from sentry.api.base import DocSection
 from sentry.api.bases.project import ProjectEndpoint
 from sentry.api.serializers import serialize, ProjectUserReportSerializer
 from sentry.api.paginator import DateTimePaginator
-from sentry.models import EventMapping, Group, GroupStatus, UserReport
+from sentry.models import (
+    Event, EventMapping, EventUser, Group, GroupStatus, UserReport
+)
 from sentry.utils.apidocs import scenario, attach_scenarios
 
 
@@ -113,15 +115,50 @@ class ProjectUserReportsEndpoint(ProjectEndpoint):
             # something wrong with the SDK, but this behavior is
             # more reasonable than just hard erroring and is more
             # expected.
-            report = UserReport.objects.get(
+            existing_report = UserReport.objects.get(
                 project=report.project,
                 event_id=report.event_id,
             )
-            report.update(
+            euser = self.find_event_user(existing_report)
+            if euser and not euser.uname and report.name:
+                euser.update(report.name)
+
+            existing_report.update(
                 name=report.name,
                 email=report.email,
                 comments=report.comments,
                 date_added=timezone.now(),
+                event_user_id=euser.id if euser else None,
             )
+            report = existing_report
 
         return Response(serialize(report, request.user, ProjectUserReportSerializer()))
+
+    def find_event_user(self, report):
+        try:
+            event = Event.objects.get(
+                group_id=report.group_id,
+                event_id=report.event_id,
+            )
+        except Event.DoesNotExist:
+            if not report.email:
+                return None
+            try:
+                return EventUser.objects.filter(
+                    project=report.project_id,
+                    email__iexact=report.email,
+                )[0]
+            except IndexError:
+                return None
+
+        tag = event.get_tag('sentry:user')
+        if not tag:
+            return None
+
+        try:
+            return EventUser.for_tags(
+                project_id=report.project_id,
+                values=[tag],
+            )[tag]
+        except KeyError:
+            pass

+ 23 - 0
src/sentry/api/endpoints/project_user_stats.py

@@ -0,0 +1,23 @@
+from __future__ import absolute_import
+
+from datetime import timedelta
+from django.utils import timezone
+from rest_framework.response import Response
+
+from sentry.app import tsdb
+from sentry.api.bases.project import ProjectEndpoint
+
+
+class ProjectUserStatsEndpoint(ProjectEndpoint):
+    def get(self, request, project):
+        now = timezone.now()
+        then = now - timedelta(days=30)
+
+        results = tsdb.rollup(tsdb.get_distinct_counts_series(
+            tsdb.models.users_affected_by_project,
+            (project.id,),
+            then,
+            now,
+        ), 3600 * 24)[project.id]
+
+        return Response(results)

+ 3 - 3
src/sentry/api/endpoints/project_users.py

@@ -4,7 +4,7 @@ from rest_framework.response import Response
 
 from sentry.api.base import DocSection
 from sentry.api.bases.project import ProjectEndpoint
-from sentry.api.paginator import OffsetPaginator
+from sentry.api.paginator import DateTimePaginator
 from sentry.api.serializers import serialize
 from sentry.models import EventUser
 
@@ -45,7 +45,7 @@ class ProjectUsersEndpoint(ProjectEndpoint):
         return self.paginate(
             request=request,
             queryset=queryset,
-            order_by='hash',
-            paginator_cls=OffsetPaginator,
+            order_by='-date_added',
+            paginator_cls=DateTimePaginator,
             on_results=lambda x: serialize(x, request.user),
         )

+ 3 - 0
src/sentry/api/serializers/models/eventuser.py

@@ -12,10 +12,13 @@ class EventUserSerializer(Serializer):
     def serialize(self, obj, attrs, user):
         return {
             'id': six.text_type(obj.id),
+            'hash': obj.hash,
             'tagValue': obj.tag_value,
             'identifier': obj.ident,
             'username': obj.username,
             'email': obj.email,
+            'name': obj.get_display_name(),
             'ipAddress': obj.ip_address,
+            'dateCreated': obj.date_added,
             'avatarUrl': get_gravatar_url(obj.email, size=32),
         }

+ 12 - 0
src/sentry/api/serializers/models/group.py

@@ -290,6 +290,18 @@ class StreamGroupSerializer(GroupSerializer):
         return result
 
 
+class TagBasedStreamGroupSerializer(StreamGroupSerializer):
+    def __init__(self, tags, **kwargs):
+        super(TagBasedStreamGroupSerializer, self).__init__(**kwargs)
+        self.tags = tags
+
+    def serialize(self, obj, attrs, user):
+        result = super(TagBasedStreamGroupSerializer, self).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(SharedGroupSerializer, self).serialize(obj, attrs, user)

+ 34 - 6
src/sentry/api/serializers/models/userreport.py

@@ -3,21 +3,49 @@ from __future__ import absolute_import
 import six
 
 from sentry.api.serializers import register, serialize, Serializer
-from sentry.models import UserReport
+from sentry.models import EventUser, UserReport
 
 
 @register(UserReport)
 class UserReportSerializer(Serializer):
+    def get_attrs(self, item_list, user):
+        queryset = list(EventUser.objects.filter(
+            id__in=[i.event_user_id for i in item_list],
+        ))
+        event_users = {
+            e.id: d for e, d in zip(queryset, serialize(queryset, user))
+        }
+
+        attrs = {}
+        for item in item_list:
+            attrs[item] = {
+                'event_user': event_users.get(item.event_user_id),
+            }
+        return attrs
+
     def serialize(self, obj, attrs, user):
         # TODO(dcramer): add in various context from the event
         # context == user / http / extra interfaces
         return {
             'id': six.text_type(obj.id),
             'eventID': obj.event_id,
-            'name': obj.name,
-            'email': obj.email,
+            'name': (
+                obj.name or
+                obj.email or (
+                    attrs['event_user'].get_display_name()
+                    if attrs['event_user'] else
+                    None
+                )
+            ),
+            'email': (
+                obj.email or
+                attrs['event_user'].email
+                if attrs['event_user'] else
+                None
+            ),
             'comments': obj.comments,
             'dateCreated': obj.date_added,
+            'user': attrs['event_user'],
         }
 
 
@@ -29,11 +57,11 @@ class ProjectUserReportSerializer(UserReportSerializer):
             for d in serialize(set(i.group for i in item_list if i.group_id), user)
         }
 
-        attrs = {}
+        attrs = super(ProjectUserReportSerializer, self).get_attrs(item_list, user)
         for item in item_list:
-            attrs[item] = {
+            attrs[item].update({
                 'group': groups[six.text_type(item.group_id)] if item.group_id else None,
-            }
+            })
         return attrs
 
     def serialize(self, obj, attrs, user):

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