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
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
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, )
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)
def validate_status(self, value): try: return ReleaseStatus.from_string(value) except ValueError: raise serializers.ValidationError("Invalid status %s" % value)
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, )