def get(self, request, organization): """ List an Organization's Issues ````````````````````````````` Return a list of issues (groups) bound to an organization. All parameters are supplied as query string parameters. A default query of ``is:unresolved`` is applied. To return results with other statuses send an new query value (i.e. ``?query=`` for all results). The ``groupStatsPeriod`` parameter can be used to select the timeline stats which should be present. Possible values are: '' (disable), '24h', '14d' The ``statsPeriod`` parameter can be used to select a date window starting from now. Ex. ``14d``. The ``start`` and ``end`` parameters can be used to select an absolute date period to fetch issues from. :qparam string statsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam string groupStatsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam string start: Beginning date. You must also provide ``end``. :qparam string end: End date. You must also provide ``start``. :qparam bool shortIdLookup: if this is set to true then short IDs are looked up by this function as well. This can cause the return value of the function to return an event issue of a different project which is why this is an opt-in. Set to `1` to enable. :qparam querystring query: an optional Sentry structured search query. If not provided an implied ``"is:unresolved"`` is assumed.) :pparam string organization_slug: the slug of the organization the issues belong to. :auth: required TODO(Chris F.): Add details on expand/collapse. """ stats_period = request.GET.get("groupStatsPeriod") try: start, end = get_date_range_from_params(request.GET) except InvalidParams as e: raise ParseError(detail=six.text_type(e)) expand = request.GET.getlist("expand", []) collapse = request.GET.getlist("collapse", []) has_inbox = features.has("organizations:inbox", organization, actor=request.user) if stats_period not in (None, "", "24h", "14d", "auto"): return Response({"detail": ERR_INVALID_STATS_PERIOD}, status=400) elif stats_period is None: # default stats_period = "24h" elif stats_period == "": # disable stats stats_period = None if stats_period == "auto": stats_period_start = start stats_period_end = end else: stats_period_start = None stats_period_end = None environments = self.get_environments(request, organization) serializer = functools.partial( StreamGroupSerializerSnuba, environment_ids=[env.id for env in environments], stats_period=stats_period, stats_period_start=stats_period_start, stats_period_end=stats_period_end, expand=expand, collapse=collapse, has_inbox=has_inbox, ) projects = self.get_projects(request, organization) project_ids = [p.id for p in projects] if not projects: return Response([]) if len(projects) > 1 and not features.has( "organizations:global-views", organization, actor=request.user ): return Response( {"detail": "You do not have the multi project stream feature enabled"}, status=400 ) # we ignore date range for both short id and event ids query = request.GET.get("query", "").strip() if query: # check to see if we've got an event ID event_id = normalize_event_id(query) if event_id: # For a direct hit lookup we want to use any passed project ids # (we've already checked permissions on these) plus any other # projects that the user is a member of. This gives us a better # chance of returning the correct result, even if the wrong # project is selected. direct_hit_projects = set(project_ids) | set( [project.id for project in request.access.projects] ) groups = list(Group.objects.filter_by_event_id(direct_hit_projects, event_id)) if len(groups) == 1: response = Response( serialize(groups, request.user, serializer(matching_event_id=event_id)) ) response["X-Sentry-Direct-Hit"] = "1" return response if groups: return Response(serialize(groups, request.user, serializer())) group = get_by_short_id(organization.id, request.GET.get("shortIdLookup"), query) if group is not None: # check all projects user has access to if request.access.has_project_access(group.project): response = Response(serialize([group], request.user, serializer())) response["X-Sentry-Direct-Hit"] = "1" return response # If group ids specified, just ignore any query components try: group_ids = set(map(int, request.GET.getlist("group"))) except ValueError: return Response({"detail": "Group ids must be integers"}, status=400) if group_ids: groups = list(Group.objects.filter(id__in=group_ids, project_id__in=project_ids)) if any(g for g in groups if not request.access.has_project_access(g.project)): raise PermissionDenied return Response(serialize(groups, request.user, serializer())) try: cursor_result, query_kwargs = self._search( request, organization, projects, environments, {"count_hits": True, "date_to": end, "date_from": start}, ) except (ValidationError, discover.InvalidSearchQuery) as exc: return Response({"detail": six.text_type(exc)}, status=400) results = list(cursor_result) context = serialize( results, request.user, serializer( start=start, end=end, search_filters=query_kwargs["search_filters"] if "search_filters" in query_kwargs else None, ), ) # HACK: remove auto resolved entries # TODO: We should try to integrate this into the search backend, since # this can cause us to arbitrarily return fewer results than requested. status = [ search_filter for search_filter in query_kwargs.get("search_filters", []) if search_filter.key.name == "status" ] if status and status[0].value.raw_value == GroupStatus.UNRESOLVED: context = [r for r in context if r["status"] == "unresolved"] response = Response(context) self.add_cursor_headers(request, response, cursor_result) # TODO(jess): add metrics that are similar to project endpoint here return response
def get(self, request, organization): """ List an Organization's Issues ````````````````````````````` Return a list of issues (groups) bound to an organization. All parameters are supplied as query string parameters. A default query of ``is:unresolved`` is applied. To return results with other statuses send an new query value (i.e. ``?query=`` for all results). The ``groupStatsPeriod`` parameter can be used to select the timeline stats which should be present. Possible values are: '' (disable), '24h', '14d' The ``statsPeriod`` parameter can be used to select a date window starting from now. Ex. ``14d``. The ``start`` and ``end`` parameters can be used to select an absolute date period to fetch issues from. :qparam string statsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam string groupStatsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam string start: Beginning date. You must also provide ``end``. :qparam string end: End date. You must also provide ``start``. :qparam bool shortIdLookup: if this is set to true then short IDs are looked up by this function as well. This can cause the return value of the function to return an event issue of a different project which is why this is an opt-in. Set to `1` to enable. :qparam querystring query: an optional Sentry structured search query. If not provided an implied ``"is:unresolved"`` is assumed.) :pparam string organization_slug: the slug of the organization the issues belong to. :auth: required """ stats_period = request.GET.get('groupStatsPeriod') if stats_period not in (None, '', '24h', '14d'): return Response({"detail": ERR_INVALID_STATS_PERIOD}, status=400) elif stats_period is None: # default stats_period = '24h' elif stats_period == '': # disable stats stats_period = None environments = self.get_environments(request, organization) serializer = functools.partial( StreamGroupSerializerSnuba, environment_ids=[env.id for env in environments], stats_period=stats_period, ) projects = self.get_projects(request, organization) project_ids = [p.id for p in projects] if not projects: return Response([]) # we ignore date range for both short id and event ids query = request.GET.get('query', '').strip() if query: # check to see if we've got an event ID if len(query) == 32: groups = list( Group.objects.filter_by_event_id(project_ids, query) ) if len(groups) == 1: response = Response( serialize( groups, request.user, serializer( matching_event_id=query ) ) ) response['X-Sentry-Direct-Hit'] = '1' return response if groups: return Response(serialize(groups, request.user, serializer())) group = get_by_short_id(organization.id, request.GET.get('shortIdLookup'), query) if group is not None: # check to make sure user has access to project if group.project_id in project_ids: response = Response( serialize( [group], request.user, serializer() ) ) response['X-Sentry-Direct-Hit'] = '1' return response try: start, end = get_date_range_from_params(request.GET) except InvalidParams as exc: return Response({'detail': exc.message}, status=400) try: cursor_result, query_kwargs = self._search( request, organization, projects, environments, { 'count_hits': True, 'date_to': end, 'date_from': start, }) except ValidationError as exc: return Response({'detail': six.text_type(exc)}, status=400) results = list(cursor_result) context = serialize(results, request.user, serializer()) # HACK: remove auto resolved entries if query_kwargs.get('status') == GroupStatus.UNRESOLVED: context = [r for r in context if r['status'] == 'unresolved'] response = Response(context) self.add_cursor_headers(request, response, cursor_result) # TODO(jess): add metrics that are similar to project endpoint here return response
def get(self, request, project): """ List a Project's Issues ``````````````````````` Return a list of issues (groups) bound to a project. All parameters are supplied as query string parameters. A default query of ``is:unresolved`` is applied. To return results with other statuses send an new query value (i.e. ``?query=`` for all results). The ``statsPeriod`` parameter can be used to select the timeline stats which should be present. Possible values are: ``""`` (disable), ``"24h"``, ``"14d"`` :qparam string statsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam bool shortIdLookup: if this is set to true then short IDs are looked up by this function as well. This can cause the return value of the function to return an event issue of a different project which is why this is an opt-in. Set to `1` to enable. :qparam querystring query: an optional Sentry structured search query. If not provided an implied ``"is:unresolved"`` is assumed.) :qparam string environment: this restricts the issues to ones containing events from this environment :pparam string organization_slug: the slug of the organization the issues belong to. :pparam string project_slug: the slug of the project the issues belong to. :auth: required """ stats_period = request.GET.get("statsPeriod") if stats_period not in (None, "", "24h", "14d"): return Response({"detail": ERR_INVALID_STATS_PERIOD}, status=400) elif stats_period is None: # default stats_period = "24h" elif stats_period == "": # disable stats stats_period = None serializer = functools.partial( StreamGroupSerializer, environment_func=self._get_environment_func( request, project.organization_id), stats_period=stats_period, ) query = request.GET.get("query", "").strip() if query: matching_group = None matching_event = None event_id = normalize_event_id(query) if event_id: # check to see if we've got an event ID try: matching_group = Group.objects.from_event_id( project, event_id) except Group.DoesNotExist: pass else: matching_event = eventstore.get_event_by_id( project.id, event_id) elif matching_group is None: matching_group = get_by_short_id( project.organization_id, request.GET.get("shortIdLookup"), query) if matching_group is not None and matching_group.project_id != project.id: matching_group = None if matching_group is not None: matching_event_environment = None try: matching_event_environment = ( matching_event.get_environment().name if matching_event else None) except Environment.DoesNotExist: pass response = Response( serialize( [matching_group], request.user, serializer( matching_event_id=getattr(matching_event, "event_id", None), matching_event_environment= matching_event_environment, ), )) response["X-Sentry-Direct-Hit"] = "1" return response try: cursor_result, query_kwargs = prep_search(self, request, project, {"count_hits": True}) except ValidationError as exc: return Response({"detail": str(exc)}, status=400) results = list(cursor_result) context = serialize(results, request.user, serializer()) # HACK: remove auto resolved entries # TODO: We should try to integrate this into the search backend, since # this can cause us to arbitrarily return fewer results than requested. status = [ search_filter for search_filter in query_kwargs.get("search_filters", []) if search_filter.key.name == "status" ] if status and (GroupStatus.UNRESOLVED in status[0].value.raw_value): status_labels = { QUERY_STATUS_LOOKUP[s] for s in status[0].value.raw_value } context = [ r for r in context if "status" not in r or r["status"] in status_labels ] response = Response(context) self.add_cursor_headers(request, response, cursor_result) if results and query: advanced_search.send(project=project, sender=request.user) analytics.record( "project_issue.searched", user_id=request.user.id, organization_id=project.organization_id, project_id=project.id, query=query, ) return response
def get(self, request, organization): """ List an Organization's Issues ````````````````````````````` Return a list of issues (groups) bound to an organization. All parameters are supplied as query string parameters. A default query of ``is:unresolved`` is applied. To return results with other statuses send an new query value (i.e. ``?query=`` for all results). The ``groupStatsPeriod`` parameter can be used to select the timeline stats which should be present. Possible values are: '' (disable), '24h', '14d' The ``statsPeriod`` parameter can be used to select a date window starting from now. Ex. ``14d``. The ``start`` and ``end`` parameters can be used to select an absolute date period to fetch issues from. :qparam string statsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam string groupStatsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam string start: Beginning date. You must also provide ``end``. :qparam string end: End date. You must also provide ``start``. :qparam bool shortIdLookup: if this is set to true then short IDs are looked up by this function as well. This can cause the return value of the function to return an event issue of a different project which is why this is an opt-in. Set to `1` to enable. :qparam querystring query: an optional Sentry structured search query. If not provided an implied ``"is:unresolved"`` is assumed.) :pparam string organization_slug: the slug of the organization the issues belong to. :auth: required """ stats_period = request.GET.get('groupStatsPeriod') if stats_period not in (None, '', '24h', '14d'): return Response({"detail": ERR_INVALID_STATS_PERIOD}, status=400) elif stats_period is None: # default stats_period = '24h' elif stats_period == '': # disable stats stats_period = None environments = self.get_environments(request, organization) serializer = functools.partial( StreamGroupSerializerSnuba, environment_ids=[env.id for env in environments], stats_period=stats_period, ) projects = self.get_projects(request, organization) project_ids = [p.id for p in projects] if not projects: return Response([]) if len(projects) > 1 and not features.has( 'organizations:global-views', organization, actor=request.user): return Response({ 'detail': 'You do not have the multi project stream feature enabled' }, status=400) # we ignore date range for both short id and event ids query = request.GET.get('query', '').strip() if query: # check to see if we've got an event ID if is_event_id(query): groups = list( Group.objects.filter_by_event_id(project_ids, query) ) if len(groups) == 1: response = Response( serialize( groups, request.user, serializer( matching_event_id=query ) ) ) response['X-Sentry-Direct-Hit'] = '1' return response if groups: return Response(serialize(groups, request.user, serializer())) group = get_by_short_id(organization.id, request.GET.get('shortIdLookup'), query) if group is not None: # check all projects user has access to if request.access.has_project_access(group.project): response = Response( serialize( [group], request.user, serializer() ) ) response['X-Sentry-Direct-Hit'] = '1' return response try: start, end = get_date_range_from_params(request.GET) except InvalidParams as exc: return Response({'detail': exc.message}, status=400) try: cursor_result, query_kwargs = self._search( request, organization, projects, environments, { 'count_hits': True, 'date_to': end, 'date_from': start, }) except ValidationError as exc: return Response({'detail': six.text_type(exc)}, status=400) results = list(cursor_result) context = serialize(results, request.user, serializer()) # HACK: remove auto resolved entries # TODO: We should try to integrate this into the search backend, since # this can cause us to arbitrarily return fewer results than requested. status = [ search_filter for search_filter in query_kwargs.get('search_filters', []) if search_filter.key.name == 'status' ] if status and status[0].value.raw_value == GroupStatus.UNRESOLVED: context = [r for r in context if r['status'] == 'unresolved'] response = Response(context) self.add_cursor_headers(request, response, cursor_result) # TODO(jess): add metrics that are similar to project endpoint here return response
def get(self, request, project): """ List a Project's Issues ``````````````````````` Return a list of issues (groups) bound to a project. All parameters are supplied as query string parameters. A default query of ``is:unresolved`` is applied. To return results with other statuses send an new query value (i.e. ``?query=`` for all results). The ``statsPeriod`` parameter can be used to select the timeline stats which should be present. Possible values are: '' (disable), '24h', '14d' :qparam string statsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam bool shortIdLookup: if this is set to true then short IDs are looked up by this function as well. This can cause the return value of the function to return an event issue of a different project which is why this is an opt-in. Set to `1` to enable. :qparam querystring query: an optional Sentry structured search query. If not provided an implied ``"is:unresolved"`` is assumed.) :pparam string organization_slug: the slug of the organization the issues belong to. :pparam string project_slug: the slug of the project the issues belong to. :auth: required """ stats_period = request.GET.get('statsPeriod') if stats_period not in (None, '', '24h', '14d'): return Response({"detail": ERR_INVALID_STATS_PERIOD}, status=400) elif stats_period is None: # default stats_period = '24h' elif stats_period == '': # disable stats stats_period = None serializer = functools.partial( StreamGroupSerializer, environment_func=self._get_environment_func(request, project.organization_id), stats_period=stats_period, ) query = request.GET.get('query', '').strip() if query: matching_group = None matching_event = None if len(query) == 32: # check to see if we've got an event ID try: matching_group = Group.objects.from_event_id(project, query) except Group.DoesNotExist: pass else: matching_event = Event.objects.from_event_id(query, project.id) if matching_event is not None: Event.objects.bind_nodes([matching_event], 'data') elif matching_group is None: matching_group = get_by_short_id( project.organization_id, request.GET.get('shortIdLookup'), query, ) if matching_group is not None and matching_group.project_id != project.id: matching_group = None if matching_group is not None: matching_event_environment = None try: matching_event_environment = matching_event.get_environment().name if matching_event else None except Environment.DoesNotExist: pass response = Response( serialize( [matching_group], request.user, serializer( matching_event_id=getattr(matching_event, 'id', None), matching_event_environment=matching_event_environment, ) ) ) response['X-Sentry-Direct-Hit'] = '1' return response try: cursor_result, query_kwargs = self._search(request, project, {'count_hits': True}) except ValidationError as exc: return Response({'detail': six.text_type(exc)}, status=400) results = list(cursor_result) context = serialize(results, request.user, serializer()) # HACK: remove auto resolved entries if query_kwargs.get('status') == GroupStatus.UNRESOLVED: context = [r for r in context if r['status'] == 'unresolved'] response = Response(context) self.add_cursor_headers(request, response, cursor_result) if results and query not in DEFAULT_SAVED_SEARCH_QUERIES: advanced_search.send(project=project, sender=request.user) analytics.record('project_issue.searched', user_id=request.user.id, organization_id=project.organization_id, project_id=project.id, query=query) return response
def get(self, request, project): """ List a Project's Issues ``````````````````````` Return a list of issues (groups) bound to a project. All parameters are supplied as query string parameters. A default query of ``is:unresolved`` is applied. To return results with other statuses send an new query value (i.e. ``?query=`` for all results). The ``statsPeriod`` parameter can be used to select the timeline stats which should be present. Possible values are: '' (disable), '24h', '14d' :qparam string statsPeriod: an optional stat period (can be one of ``"24h"``, ``"14d"``, and ``""``). :qparam bool shortIdLookup: if this is set to true then short IDs are looked up by this function as well. This can cause the return value of the function to return an event issue of a different project which is why this is an opt-in. Set to `1` to enable. :qparam querystring query: an optional Sentry structured search query. If not provided an implied ``"is:unresolved"`` is assumed.) :pparam string organization_slug: the slug of the organization the issues belong to. :pparam string project_slug: the slug of the project the issues belong to. :auth: required """ stats_period = request.GET.get('statsPeriod') if stats_period not in (None, '', '24h', '14d'): return Response({"detail": ERR_INVALID_STATS_PERIOD}, status=400) elif stats_period is None: # default stats_period = '24h' elif stats_period == '': # disable stats stats_period = None serializer = functools.partial( StreamGroupSerializer, environment_func=self._get_environment_func(request, project.organization_id), stats_period=stats_period, ) query = request.GET.get('query', '').strip() if query: matching_group = None matching_event = None if is_event_id(query): # check to see if we've got an event ID try: matching_group = Group.objects.from_event_id(project, query) except Group.DoesNotExist: pass else: matching_event = Event.objects.from_event_id(query, project.id) if matching_event is not None: Event.objects.bind_nodes([matching_event], 'data') elif matching_group is None: matching_group = get_by_short_id( project.organization_id, request.GET.get('shortIdLookup'), query, ) if matching_group is not None and matching_group.project_id != project.id: matching_group = None if matching_group is not None: matching_event_environment = None try: matching_event_environment = matching_event.get_environment().name if matching_event else None except Environment.DoesNotExist: pass response = Response( serialize( [matching_group], request.user, serializer( matching_event_id=getattr(matching_event, 'id', None), matching_event_environment=matching_event_environment, ) ) ) response['X-Sentry-Direct-Hit'] = '1' return response try: cursor_result, query_kwargs = self._search(request, project, {'count_hits': True}) except ValidationError as exc: return Response({'detail': six.text_type(exc)}, status=400) results = list(cursor_result) context = serialize(results, request.user, serializer()) # HACK: remove auto resolved entries # TODO: We should try to integrate this into the search backend, since # this can cause us to arbitrarily return fewer results than requested. status = [ search_filter for search_filter in query_kwargs.get('search_filters', []) if search_filter.key.name == 'status' ] if status and status[0].value.raw_value == GroupStatus.UNRESOLVED: context = [r for r in context if r['status'] == 'unresolved'] response = Response(context) self.add_cursor_headers(request, response, cursor_result) if results and query not in DEFAULT_SAVED_SEARCH_QUERIES: advanced_search.send(project=project, sender=request.user) analytics.record('project_issue.searched', user_id=request.user.id, organization_id=project.organization_id, project_id=project.id, query=query) return response