Exemplo n.º 1
0
    def serialize(self, obj, attrs, user, **kwargs):
        def expose_health_data(data):
            if not data:
                return None
            return {
                "durationP50": data["duration_p50"],
                "durationP90": data["duration_p90"],
                "crashFreeUsers": data["crash_free_users"],
                "crashFreeSessions": data["crash_free_sessions"],
                "sessionsCrashed": data["sessions_crashed"],
                "sessionsErrored": data["sessions_errored"],
                "totalUsers": data["total_users"],
                "totalUsers24h": data["total_users_24h"],
                "totalProjectUsers24h": data["total_project_users_24h"],
                "totalSessions": data["total_sessions"],
                "totalSessions24h": data["total_sessions_24h"],
                "totalProjectSessions24h": data["total_project_sessions_24h"],
                "adoption": data["adoption"],
                "sessionsAdoption": data["sessions_adoption"],
                "stats": data.get("stats"),
                # XXX: legacy key, should be removed later.
                "hasHealthData": data["has_health_data"],
            }

        def expose_project(project):
            rv = {
                "id": project["id"],
                "slug": project["slug"],
                "name": project["name"],
                "newGroups": project["new_groups"],
                "platform": project["platform"],
                "platforms": project["platforms"],
                "hasHealthData": project["has_health_data"],
            }
            if "health_data" in project:
                rv["healthData"] = expose_health_data(project["health_data"])
            return rv

        d = {
            "version": obj.version,
            "status": ReleaseStatus.to_string(obj.status),
            "shortVersion": obj.version,
            "versionInfo": expose_version_info(obj.version_info),
            "ref": obj.ref,
            "url": obj.url,
            "dateReleased": obj.date_released,
            "dateCreated": obj.date_added,
            "data": obj.data,
            "newGroups": attrs["new_groups"],
            "owner": attrs["owner"],
            "commitCount": obj.commit_count,
            "lastCommit": attrs.get("last_commit"),
            "deployCount": obj.total_deploys,
            "lastDeploy": attrs.get("last_deploy"),
            "authors": attrs.get("authors", []),
            "projects": [expose_project(p) for p in attrs.get("projects", [])],
            "firstEvent": attrs.get("first_seen"),
            "lastEvent": attrs.get("last_seen"),
        }
        return d
Exemplo n.º 2
0
def add_status_filter_to_queryset(queryset, status_filter):
    """
    Function that adds status filter on a queryset
    """
    try:
        status_int = ReleaseStatus.from_string(status_filter)
    except ValueError:
        raise ParseError(detail="invalid value for status")

    if status_int == ReleaseStatus.OPEN:
        queryset = queryset.filter(Q(status=status_int) | Q(status=None))
    else:
        queryset = queryset.filter(status=status_int)
    return queryset
Exemplo n.º 3
0
    def get(self, request: Request, organization) -> Response:
        """
        List an Organization's Releases
        ```````````````````````````````
        Return a list of releases for a given organization.

        :pparam string organization_slug: the organization short name
        :qparam string query: this parameter can be used to create a
                              "starts with" filter for the version.
        """
        query = request.GET.get("query")
        with_health = request.GET.get("health") == "1"
        with_adoption_stages = request.GET.get("adoptionStages") == "1"
        status_filter = request.GET.get("status", "open")
        flatten = request.GET.get("flatten") == "1"
        sort = request.GET.get("sort") or "date"
        health_stat = request.GET.get("healthStat") or "sessions"
        summary_stats_period = request.GET.get("summaryStatsPeriod") or "14d"
        health_stats_period = request.GET.get("healthStatsPeriod") or (
            "24h" if with_health else "")
        if summary_stats_period not in STATS_PERIODS:
            raise ParseError(detail=get_stats_period_detail(
                "summaryStatsPeriod", STATS_PERIODS))
        if health_stats_period and health_stats_period not in STATS_PERIODS:
            raise ParseError(detail=get_stats_period_detail(
                "healthStatsPeriod", STATS_PERIODS))
        if health_stat not in ("sessions", "users"):
            raise ParseError(detail="invalid healthStat")

        paginator_cls = OffsetPaginator
        paginator_kwargs = {}

        try:
            filter_params = self.get_filter_params(request,
                                                   organization,
                                                   date_filter_optional=True)
        except NoProjects:
            return Response([])

        # This should get us all the projects into postgres that have received
        # health data in the last 24 hours.
        debounce_update_release_health_data(organization,
                                            filter_params["project_id"])

        queryset = Release.objects.filter(organization=organization)

        if status_filter:
            try:
                status_int = ReleaseStatus.from_string(status_filter)
            except ValueError:
                raise ParseError(detail="invalid value for status")

            if status_int == ReleaseStatus.OPEN:
                queryset = queryset.filter(
                    Q(status=status_int) | Q(status=None))
            else:
                queryset = queryset.filter(status=status_int)

        queryset = queryset.select_related("owner").annotate(
            date=F("date_added"))

        queryset = add_environment_to_queryset(queryset, filter_params)
        if query:
            try:
                queryset = _filter_releases_by_query(queryset, organization,
                                                     query, filter_params)
            except InvalidSearchQuery as e:
                return Response(
                    {"detail": str(e)},
                    status=400,
                )

        select_extra = {}

        queryset = queryset.distinct()
        if flatten:
            select_extra[
                "_for_project_id"] = "sentry_release_project.project_id"

        queryset = queryset.filter(
            projects__id__in=filter_params["project_id"])

        if sort == "date":
            queryset = queryset.order_by("-date")
            paginator_kwargs["order_by"] = "-date"
        elif sort == "build":
            queryset = queryset.filter(
                build_number__isnull=False).order_by("-build_number")
            paginator_kwargs["order_by"] = "-build_number"
        elif sort == "semver":
            queryset = queryset.annotate_prerelease_column()

            order_by = [
                F(col).desc(nulls_last=True) for col in Release.SEMVER_COLS
            ]
            # TODO: Adding this extra sort order breaks index usage. Index usage is already broken
            # when we filter by status, so when we fix that we should also consider the best way to
            # make this work as expected.
            order_by.append(F("date_added").desc())
            paginator_kwargs["order_by"] = order_by
        elif sort == "adoption":
            # sort by adoption date (most recently adopted first)
            order_by = F("releaseprojectenvironment__adopted").desc(
                nulls_last=True)
            queryset = queryset.order_by(order_by)
            paginator_kwargs["order_by"] = order_by
        elif sort in self.SESSION_SORTS:
            if not flatten:
                return Response(
                    {
                        "detail":
                        "sorting by crash statistics requires flattening (flatten=1)"
                    },
                    status=400,
                )

            def qs_load_func(queryset, total_offset, qs_offset, limit):
                # We want to fetch at least total_offset + limit releases to check, to make sure
                # we're not fetching only releases that were on previous pages.
                release_versions = list(queryset.order_by_recent().values_list(
                    "version", flat=True)[:total_offset + limit])
                releases_with_session_data = release_health.check_releases_have_health_data(
                    organization.id,
                    filter_params["project_id"],
                    release_versions,
                    filter_params["start"] if filter_params["start"] else
                    datetime.utcnow() - timedelta(days=90),
                    filter_params["end"]
                    if filter_params["end"] else datetime.utcnow(),
                )
                valid_versions = [
                    rv for rv in release_versions
                    if rv not in releases_with_session_data
                ]

                results = list(
                    Release.objects.filter(
                        organization_id=organization.id,
                        version__in=valid_versions,
                    ).order_by_recent()[qs_offset:qs_offset + limit])
                return results

            paginator_cls = MergingOffsetPaginator
            paginator_kwargs.update(
                data_load_func=lambda offset, limit: release_health.
                get_project_releases_by_stability(
                    project_ids=filter_params["project_id"],
                    environments=filter_params.get("environment"),
                    scope=sort,
                    offset=offset,
                    stats_period=summary_stats_period,
                    limit=limit,
                ),
                data_count_func=lambda: release_health.
                get_project_releases_count(
                    organization_id=organization.id,
                    project_ids=filter_params["project_id"],
                    environments=filter_params.get("environment"),
                    scope=sort,
                    stats_period=summary_stats_period,
                ),
                apply_to_queryset=lambda queryset, rows: queryset.filter(
                    version__in=list(x[1] for x in rows)),
                queryset_load_func=qs_load_func,
                key_from_model=lambda x: (x._for_project_id, x.version),
            )
        else:
            return Response({"detail": "invalid sort"}, status=400)

        queryset = queryset.extra(select=select_extra)
        queryset = add_date_filter_to_queryset(queryset, filter_params)

        return self.paginate(
            request=request,
            queryset=queryset,
            paginator_cls=paginator_cls,
            on_results=lambda x: serialize(
                x,
                request.user,
                with_health_data=with_health,
                with_adoption_stages=with_adoption_stages,
                health_stat=health_stat,
                health_stats_period=health_stats_period,
                summary_stats_period=summary_stats_period,
                environments=filter_params.get("environment") or None,
            ),
            **paginator_kwargs,
        )
Exemplo n.º 4
0
    def get(self, request, organization):
        """
        List an Organization's Releases
        ```````````````````````````````
        Return a list of releases for a given organization.

        :pparam string organization_slug: the organization short name
        :qparam string query: this parameter can be used to create a
                              "starts with" filter for the version.
        """
        query = request.GET.get("query")
        with_health = request.GET.get("health") == "1"
        status_filter = request.GET.get("status", "open")
        flatten = request.GET.get("flatten") == "1"
        sort = request.GET.get("sort") or "date"
        health_stat = request.GET.get("healthStat") or "sessions"
        summary_stats_period = request.GET.get("summaryStatsPeriod") or "14d"
        health_stats_period = request.GET.get("healthStatsPeriod") or (
            "24h" if with_health else "")
        if summary_stats_period not in STATS_PERIODS:
            raise ParseError(detail=get_stats_period_detail(
                "summaryStatsPeriod", STATS_PERIODS))
        if health_stats_period and health_stats_period not in STATS_PERIODS:
            raise ParseError(detail=get_stats_period_detail(
                "healthStatsPeriod", STATS_PERIODS))
        if health_stat not in ("sessions", "users"):
            raise ParseError(detail="invalid healthStat")

        paginator_cls = OffsetPaginator
        paginator_kwargs = {}

        try:
            filter_params = self.get_filter_params(request,
                                                   organization,
                                                   date_filter_optional=True)
        except NoProjects:
            return Response([])

        # This should get us all the projects into postgres that have received
        # health data in the last 24 hours.  If health data is not requested
        # we don't upsert releases.
        if with_health:
            debounce_update_release_health_data(organization,
                                                filter_params["project_id"])

        queryset = Release.objects.filter(organization=organization)

        if status_filter:
            try:
                status_int = ReleaseStatus.from_string(status_filter)
            except ValueError:
                raise ParseError(detail="invalid value for status")

            if status_int == ReleaseStatus.OPEN:
                queryset = queryset.filter(
                    Q(status=status_int) | Q(status=None))
            else:
                queryset = queryset.filter(status=status_int)

        queryset = queryset.select_related("owner").annotate(date=Coalesce(
            "date_released", "date_added"), )

        queryset = add_environment_to_queryset(queryset, filter_params)

        if query:
            query_q = Q(version__icontains=query)

            suffix_match = _release_suffix.match(query)
            if suffix_match is not None:
                query_q |= Q(version__icontains="%s+%s" %
                             suffix_match.groups())

            queryset = queryset.filter(query_q)

        select_extra = {}

        queryset = queryset.distinct()
        if flatten:
            select_extra[
                "_for_project_id"] = "sentry_release_project.project_id"

        if sort == "date":
            queryset = queryset.filter(
                projects__id__in=filter_params["project_id"]).order_by("-date")
            paginator_kwargs["order_by"] = "-date"
        elif sort in (
                "crash_free_sessions",
                "crash_free_users",
                "sessions",
                "users",
                "sessions_24h",
                "users_24h",
        ):
            if not flatten:
                return Response(
                    {
                        "detail":
                        "sorting by crash statistics requires flattening (flatten=1)"
                    },
                    status=400,
                )
            paginator_cls = MergingOffsetPaginator
            paginator_kwargs.update(
                data_load_func=lambda offset, limit:
                get_project_releases_by_stability(
                    project_ids=filter_params["project_id"],
                    environments=filter_params.get("environment"),
                    scope=sort,
                    offset=offset,
                    stats_period=summary_stats_period,
                    limit=limit,
                ),
                apply_to_queryset=lambda queryset, rows: queryset.filter(
                    projects__id__in=list(x[0] for x in rows),
                    version__in=list(x[1] for x in rows)),
                key_from_model=lambda x: (x._for_project_id, x.version),
            )
        else:
            return Response({"detail": "invalid sort"}, status=400)

        queryset = queryset.extra(select=select_extra)
        queryset = add_date_filter_to_queryset(queryset, filter_params)

        return self.paginate(
            request=request,
            queryset=queryset,
            paginator_cls=paginator_cls,
            on_results=lambda x: serialize(
                x,
                request.user,
                with_health_data=with_health,
                health_stat=health_stat,
                health_stats_period=health_stats_period,
                summary_stats_period=summary_stats_period,
                environments=filter_params.get("environment") or None,
            ),
            **paginator_kwargs)
Exemplo n.º 5
0
 def validate_status(self, value):
     try:
         return ReleaseStatus.from_string(value)
     except ValueError:
         raise serializers.ValidationError("Invalid status %s" % value)
Exemplo n.º 6
0
    def get(self, request, organization):
        """
        List an Organization's Releases
        ```````````````````````````````
        Return a list of releases for a given organization.

        :pparam string organization_slug: the organization short name
        :qparam string query: this parameter can be used to create a
                              "starts with" filter for the version.
        """
        query = request.GET.get("query")
        with_health = request.GET.get("health") == "1"
        with_adoption_stages = request.GET.get("adoptionStages") == "1"
        status_filter = request.GET.get("status", "open")
        flatten = request.GET.get("flatten") == "1"
        sort = request.GET.get("sort") or "date"
        health_stat = request.GET.get("healthStat") or "sessions"
        summary_stats_period = request.GET.get("summaryStatsPeriod") or "14d"
        health_stats_period = request.GET.get("healthStatsPeriod") or ("24h" if with_health else "")
        if summary_stats_period not in STATS_PERIODS:
            raise ParseError(detail=get_stats_period_detail("summaryStatsPeriod", STATS_PERIODS))
        if health_stats_period and health_stats_period not in STATS_PERIODS:
            raise ParseError(detail=get_stats_period_detail("healthStatsPeriod", STATS_PERIODS))
        if health_stat not in ("sessions", "users"):
            raise ParseError(detail="invalid healthStat")

        paginator_cls = OffsetPaginator
        paginator_kwargs = {}

        try:
            filter_params = self.get_filter_params(request, organization, date_filter_optional=True)
        except NoProjects:
            return Response([])

        # This should get us all the projects into postgres that have received
        # health data in the last 24 hours.
        debounce_update_release_health_data(organization, filter_params["project_id"])

        queryset = Release.objects.filter(organization=organization)

        if status_filter:
            try:
                status_int = ReleaseStatus.from_string(status_filter)
            except ValueError:
                raise ParseError(detail="invalid value for status")

            if status_int == ReleaseStatus.OPEN:
                queryset = queryset.filter(Q(status=status_int) | Q(status=None))
            else:
                queryset = queryset.filter(status=status_int)

        queryset = queryset.select_related("owner").annotate(date=F("date_added"))

        queryset = add_environment_to_queryset(queryset, filter_params)

        if query:
            try:
                queryset = _filter_releases_by_query(queryset, organization, query)
            except InvalidSearchQuery as e:
                return Response(
                    {"detail": str(e)},
                    status=400,
                )

        select_extra = {}

        queryset = queryset.distinct()
        if flatten:
            select_extra["_for_project_id"] = "sentry_release_project.project_id"

        if sort not in self.SESSION_SORTS:
            queryset = queryset.filter(projects__id__in=filter_params["project_id"])

        if sort == "date":
            queryset = queryset.order_by("-date")
            paginator_kwargs["order_by"] = "-date"
        elif sort == "build":
            queryset = queryset.filter(build_number__isnull=False).order_by("-build_number")
            paginator_kwargs["order_by"] = "-build_number"
        elif sort == "semver":
            order_by = [f"-{col}" for col in Release.SEMVER_COLS]
            # TODO: Adding this extra sort order breaks index usage. Index usage is already broken
            # when we filter by status, so when we fix that we should also consider the best way to
            # make this work as expected.
            queryset = (
                queryset.annotate_prerelease_column()
                .filter_to_semver()
                .order_by(*order_by, "-date_added")
            )
            paginator_kwargs["order_by"] = order_by
        elif sort in self.SESSION_SORTS:
            if not flatten:
                return Response(
                    {"detail": "sorting by crash statistics requires flattening (flatten=1)"},
                    status=400,
                )
            paginator_cls = MergingOffsetPaginator
            paginator_kwargs.update(
                data_load_func=lambda offset, limit: get_project_releases_by_stability(
                    project_ids=filter_params["project_id"],
                    environments=filter_params.get("environment"),
                    scope=sort,
                    offset=offset,
                    stats_period=summary_stats_period,
                    limit=limit,
                ),
                apply_to_queryset=lambda queryset, rows: queryset.filter(
                    projects__id__in=list(x[0] for x in rows), version__in=list(x[1] for x in rows)
                ),
                key_from_model=lambda x: (x._for_project_id, x.version),
            )
        else:
            return Response({"detail": "invalid sort"}, status=400)

        queryset = queryset.extra(select=select_extra)
        queryset = add_date_filter_to_queryset(queryset, filter_params)

        with_adoption_stages = with_adoption_stages and features.has(
            "organizations:release-adoption-stage", organization, actor=request.user
        )

        return self.paginate(
            request=request,
            queryset=queryset,
            paginator_cls=paginator_cls,
            on_results=lambda x: serialize(
                x,
                request.user,
                with_health_data=with_health,
                with_adoption_stages=with_adoption_stages,
                health_stat=health_stat,
                health_stats_period=health_stats_period,
                summary_stats_period=summary_stats_period,
                environments=filter_params.get("environment") or None,
            ),
            **paginator_kwargs,
        )