From 77596d8242d310fefea946149f7c0504df6f7f54 Mon Sep 17 00:00:00 2001 From: Suejung Shin Date: Wed, 18 Sep 2024 23:56:26 -0700 Subject: [PATCH] add flags, components --- .../coverage_analytics.graphql | 27 ++ .../coverage_analytics/coverage_analytics.py | 254 +++++++++++++++++- .../types/repository/repository.graphql | 8 + 3 files changed, 278 insertions(+), 11 deletions(-) diff --git a/graphql_api/types/coverage_analytics/coverage_analytics.graphql b/graphql_api/types/coverage_analytics/coverage_analytics.graphql index 792bd445d..3325280b8 100644 --- a/graphql_api/types/coverage_analytics/coverage_analytics.graphql +++ b/graphql_api/types/coverage_analytics/coverage_analytics.graphql @@ -20,6 +20,33 @@ type CoverageAnalytics { before: DateTime branch: String ): [Measurement!]! # formerly repository.measurements + + ### FLAGS ### + flagsCount: Int! + flagsMeasurementsActive: Boolean! + flagsMeasurementsBackfilled: Boolean! + flags( + filters: FlagSetFilters + orderingDirection: OrderingDirection + first: Int + after: String + last: Int + before: String + ): FlagConnection! @cost(complexity: 3, multipliers: ["first", "last"]) + + ### COMPONENTS ### + componentsMeasurementsActive: Boolean! + componentsMeasurementsBackfilled: Boolean! + componentsCount: Int! + components( + interval: MeasurementInterval! + before: DateTime! + after: DateTime! + branch: String + filters: ComponentMeasurementsSetFilters + orderingDirection: OrderingDirection + ): [ComponentMeasurements!]! + componentsYaml(termId: String): [ComponentsYaml]! } "CoverageAnalyticsResult is CoverageAnalytics or potential error(s)" diff --git a/graphql_api/types/coverage_analytics/coverage_analytics.py b/graphql_api/types/coverage_analytics/coverage_analytics.py index 1809de1a6..4d80b576e 100644 --- a/graphql_api/types/coverage_analytics/coverage_analytics.py +++ b/graphql_api/types/coverage_analytics/coverage_analytics.py @@ -40,7 +40,9 @@ def resolve_coverage_analytics_result_type(obj, *_): @coverage_analytics_bindable.field("percentCovered") -def resolve_percent_covered(coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo): +def resolve_percent_covered( + coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo +): repository = info.context.get("repository", None) return repository.recent_coverage if repository else None @@ -52,35 +54,43 @@ def resolve_commit_sha(coverage_analytics: CoverageAnalytics, info: GraphQLResol @coverage_analytics_bindable.field("hits") -def resolve_hits(coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo) -> Optional[int]: +def resolve_hits( + coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo +) -> Optional[int]: repository = info.context.get("repository", None) return repository.hits if repository else None @coverage_analytics_bindable.field("misses") -def resolve_misses(coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo) -> Optional[int]: +def resolve_misses( + coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo +) -> Optional[int]: repository = info.context.get("repository", None) return repository.misses if repository else None @coverage_analytics_bindable.field("lines") -def resolve_lines(coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo) -> Optional[int]: +def resolve_lines( + coverage_analytics: CoverageAnalytics, info: GraphQLResolveInfo +) -> Optional[int]: repository = info.context.get("repository", None) return repository.lines if repository else None @coverage_analytics_bindable.field("measurements") async def resolve_measurements( - coverage_analytics: CoverageAnalytics, - info: GraphQLResolveInfo, - interval: Interval, - before: Optional[datetime] = None, - after: Optional[datetime] = None, - branch: Optional[str] = None, + coverage_analytics: CoverageAnalytics, + info: GraphQLResolveInfo, + interval: Interval, + before: Optional[datetime] = None, + after: Optional[datetime] = None, + branch: Optional[str] = None, ) -> Iterable[MeasurementSummary]: repository = info.context.get("repository", None) - coverage_data = await sync_to_async(timeseries_helpers.repository_coverage_measurements_with_fallback)( + coverage_data = await sync_to_async( + timeseries_helpers.repository_coverage_measurements_with_fallback + )( repository, interval, start_date=after, @@ -96,3 +106,225 @@ async def resolve_measurements( ) return measurements + + +@coverage_analytics_bindable.field("flags") +@convert_kwargs_to_snake_case +@sync_to_async +def resolve_flags( + repository: Repository, + info: GraphQLResolveInfo, + filters: Mapping = None, + ordering_direction: OrderingDirection = OrderingDirection.ASC, + **kwargs, +): + queryset = flags_for_repo(repository, filters) + connection = queryset_to_connection_sync( + queryset, + ordering=("flag_name",), + ordering_direction=ordering_direction, + **kwargs, + ) + + # We fetch the measurements in this resolver since there are multiple child + # flag resolvers that depend on this data. Additionally, we're able to fetch + # measurements for all the flags being returned at once. + # Use the lookahead to make sure we don't overfetch measurements that we don't + # need. + node = lookahead(info, ("edges", "node", "measurements")) + if node: + if settings.TIMESERIES_ENABLED: + # TODO: is there a way to have these automatically casted at a + # lower level (i.e. based on the schema)? + interval = node.args["interval"] + if isinstance(interval, str): + interval = Interval[interval] + after = node.args["after"] + if isinstance(after, str): + after = from_current_timezone(datetime.fromisoformat(after)) + before = node.args["before"] + if isinstance(before, str): + before = from_current_timezone(datetime.fromisoformat(before)) + + flag_ids = [edge["node"].pk for edge in connection.edges] + + info.context["flag_measurements"] = flag_measurements( + repository, flag_ids, interval, after, before + ) + else: + info.context["flag_measurements"] = {} + + return connection + + +@coverage_analytics_bindable.field("flagsCount") +@sync_to_async +def resolve_flags_count(repository: Repository, info: GraphQLResolveInfo) -> int: + return repository.flags.filter(deleted__isnot=True).count() + + +@coverage_analytics_bindable.field("flagsMeasurementsActive") +@sync_to_async +def resolve_flags_measurements_active( + repository: Repository, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + return Dataset.objects.filter( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=repository.pk, + ).exists() + + +@coverage_analytics_bindable.field("flagsMeasurementsBackfilled") +@sync_to_async +def resolve_flags_measurements_backfilled( + repository: Repository, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + dataset = Dataset.objects.filter( + name=MeasurementName.FLAG_COVERAGE.value, + repository_id=repository.pk, + ).first() + + if not dataset: + return False + + return dataset.is_backfilled() + + +@coverage_analytics_bindable.field("componentsMeasurementsActive") +@sync_to_async +def resolve_components_measurements_active( + repository: Repository, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + return Dataset.objects.filter( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=repository.pk, + ).exists() + + +@coverage_analytics_bindable.field("componentsMeasurementsBackfilled") +@sync_to_async +def resolve_components_measurements_backfilled( + repository: Repository, info: GraphQLResolveInfo +) -> bool: + if not settings.TIMESERIES_ENABLED: + return False + + dataset = Dataset.objects.filter( + name=MeasurementName.COMPONENT_COVERAGE.value, + repository_id=repository.pk, + ).first() + + if not dataset: + return False + + return dataset.is_backfilled() + + +@coverage_analytics_bindable.field("componentsCount") +@sync_to_async +def resolve_components_count(repository: Repository, info: GraphQLResolveInfo) -> int: + repo_yaml_components = UserYaml.get_final_yaml( + owner_yaml=repository.author.yaml, + repo_yaml=repository.yaml, + ownerid=repository.author.ownerid, + ).get_components() + + return len(repo_yaml_components) + + +@coverage_analytics_bindable.field("components") +@convert_kwargs_to_snake_case +@sync_to_async +def resolve_component_measurements( + repository: Repository, + info: GraphQLResolveInfo, + interval: Interval, + before: datetime, + after: datetime, + branch: Optional[str] = None, + filters: Optional[Mapping] = None, + ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, +): + components = UserYaml.get_final_yaml( + owner_yaml=repository.author.yaml, + repo_yaml=repository.yaml, + ownerid=repository.author.ownerid, + ).get_components() + + if not settings.TIMESERIES_ENABLED or not components: + return [] + + if filters and "components" in filters: + components = [c for c in components if c.component_id in filters["components"]] + + component_ids = [c.component_id for c in components] + all_measurements = component_measurements( + repository, component_ids, interval, after, before, branch + ) + + last_measurements = component_measurements_last_uploaded( + owner_id=repository.author.ownerid, + repo_id=repository.repoid, + measurable_ids=component_ids, + branch=branch, + ) + last_measurements_mapping = { + row["measurable_id"]: row["last_uploaded"] for row in last_measurements + } + + components_mapping = { + component.component_id: component.name for component in components + } + + queried_measurements = [ + ComponentMeasurements( + raw_measurements=all_measurements.get(component_id, []), + component_id=component_id, + interval=interval, + after=after, + before=before, + last_measurement=last_measurements_mapping.get(component_id), + components_mapping=components_mapping, + ) + for component_id in component_ids + ] + + return sorted( + queried_measurements, + key=lambda c: c.name, + reverse=ordering_direction == OrderingDirection.DESC, + ) + + +@coverage_analytics_bindable.field("componentsYaml") +@convert_kwargs_to_snake_case +def resolve_component_yaml( + repository: Repository, info: GraphQLResolveInfo, term_id: Optional[str] +) -> List[str]: + components = UserYaml.get_final_yaml( + owner_yaml=repository.author.yaml, + repo_yaml=repository.yaml, + ownerid=repository.author.ownerid, + ).get_components() + + components = [ + { + "id": c.component_id, + "name": c.name, + } + for c in components + ] + + if term_id: + components = filter(lambda c: term_id in c["id"], components) + + return components diff --git a/graphql_api/types/repository/repository.graphql b/graphql_api/types/repository/repository.graphql index 828dc4ff4..3f3e5df43 100644 --- a/graphql_api/types/repository/repository.graphql +++ b/graphql_api/types/repository/repository.graphql @@ -62,11 +62,17 @@ type Repository { graphToken: String yaml: String bot: Owner + # flagsCount to be removed with #2282 flagsCount: Int! + # flagsMeasurementsActive to be removed with #2282 flagsMeasurementsActive: Boolean! + # flagsMeasurementsBackfilled to be removed with #2282 flagsMeasurementsBackfilled: Boolean! + # componentsMeasurementsActive to be removed with #2282 componentsMeasurementsActive: Boolean! + # componentsMeasurementsBackfilled to be removed with #2282 componentsMeasurementsBackfilled: Boolean! + # componentsCount to be removed with #2282 componentsCount: Int! # (coverage) measurements to be removed with #2282 measurements( @@ -82,6 +88,7 @@ type Repository { languages: [String!] bundleAnalysisEnabled: Boolean coverageEnabled: Boolean + # components to be removed with #2282 components( interval: MeasurementInterval! before: DateTime! @@ -90,6 +97,7 @@ type Repository { filters: ComponentMeasurementsSetFilters orderingDirection: OrderingDirection ): [ComponentMeasurements!]! + # componentsYaml to be removed with #2282 componentsYaml(termId: String): [ComponentsYaml]! testAnalyticsEnabled: Boolean isGithubRateLimited: Boolean