def test_get_project_releases_by_stability(self): # Add an extra session with a different `distinct_id` so that sorting by users # is stable self.store_session({ "session_id": "5e910c1a-6941-460e-9843-24103fb6a63c", "distinct_id": "39887d89-13b2-4c84-8c23-5d13d2102665", "status": "ok", "seq": 0, "release": self.session_release, "environment": "prod", "retention_days": 90, "org_id": self.project.organization_id, "project_id": self.project.id, "duration": None, "errors": 0, "started": self.session_started, "received": self.received, }) for scope in "sessions", "users": data = get_project_releases_by_stability([self.project.id], offset=0, limit=100, scope=scope, stats_period="24h") assert data == [ (self.project.id, self.session_release), (self.project.id, self.session_crashed_release), ]
def test_get_project_releases_by_stability(self): for scope in "sessions", "users": data = get_project_releases_by_stability( [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h", ) assert data == [ (self.project.id, self.session_release), (self.project.id, self.session_crashed_release), ]
def test_get_project_releases_by_stability_for_crash_free_sort(self): """ Test that ensures that using crash free rate sort options, returns a list of ASC releases according to the chosen crash_free sort option """ for scope in "crash_free_sessions", "crash_free_users": data = get_project_releases_by_stability( [self.project.id], offset=0, limit=100, scope=scope, stats_period="24h" ) assert data == [ (self.project.id, self.session_crashed_release), (self.project.id, self.session_release), ]
def test_get_project_releases_by_stability_for_releases_with_users_data( self): """ Test that ensures if releases contain no users data, then those releases should not be returned on `users` and `crash_free_users` sorts """ self.store_session({ "session_id": "bd1521fc-d27c-11eb-b8bc-0242ac130003", "status": "ok", "seq": 0, "release": "release-with-no-users", "environment": "prod", "retention_days": 90, "org_id": self.project.organization_id, "project_id": self.project.id, "duration": None, "errors": 0, "started": self.session_started, "received": self.received, }) data = get_project_releases_by_stability([self.project.id], offset=0, limit=100, scope="users", stats_period="24h") assert set(data) == { (self.project.id, self.session_release), (self.project.id, self.session_crashed_release), } data = get_project_releases_by_stability([self.project.id], offset=0, limit=100, scope="crash_free_users", stats_period="24h") assert set(data) == { (self.project.id, self.session_crashed_release), (self.project.id, self.session_release), }
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" 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).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 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" flatten = request.GET.get("flatten") == "1" sort = request.GET.get("sort") or "date" 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)) paginator_cls = OffsetPaginator paginator_kwargs = {} try: filter_params = self.get_filter_params(request, organization, date_filter_optional=True) except NoProjects: return Response([]) except OrganizationEventsError as e: return Response({"detail": six.text_type(e)}, status=400) # 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).select_related("owner") if "environment" in filter_params: queryset = queryset.filter( releaseprojectenvironment__environment__name__in=filter_params[ "environment"], releaseprojectenvironment__project_id__in=filter_params[ "project_id"], ) if query: queryset = queryset.filter(version__istartswith=query) select_extra = {} sort_query = None if flatten: select_extra[ "_for_project_id"] = "sentry_release_project.project_id" else: queryset = queryset.distinct() if sort == "date": sort_query = "COALESCE(sentry_release.date_released, sentry_release.date_added)" elif sort in ("crash_free_sessions", "crash_free_users", "sessions", "users"): 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) if sort_query is not None: queryset = queryset.filter( projects__id__in=filter_params["project_id"]) select_extra["sort"] = sort_query paginator_kwargs["order_by"] = "-sort" queryset = queryset.extra(select=select_extra) if filter_params["start"] and filter_params["end"]: queryset = queryset.extra( where=[ "COALESCE(sentry_release.date_released, sentry_release.date_added) BETWEEN %s and %s" ], params=[filter_params["start"], filter_params["end"]], ) 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_stats_period=health_stats_period, summary_stats_period=summary_stats_period, ), **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" 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 = 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: 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: 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) 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, )