From 7378dbd539a1cbbab6ba1581a460ff972a835e97 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 12 Mar 2026 13:59:21 -0700 Subject: [PATCH 1/2] ref(preprod): Remove graduated preprod-frontend-routes feature flag The preprod-frontend-routes flag has been at 100% rollout and is fully graduated. Remove all gating code across backend endpoints, frontend components, and tests since all code paths are already executing in production. --- .../organization_trace_item_attributes.py | 1 - src/sentry/features/temporary.py | 2 - src/sentry/preprod/api/endpoints/builds.py | 11 - .../organization_preprod_artifact_assemble.py | 8 +- .../project_preprod_artifact_delete.py | 7 +- .../project_preprod_build_details.py | 7 +- ...zation_preprod_artifact_install_details.py | 6 - .../organization_preprod_size_analysis.py | 6 - .../project_preprod_size_analysis_compare.py | 12 +- ..._preprod_size_analysis_compare_download.py | 7 +- .../project_preprod_size_analysis_download.py | 8 +- .../header/buildDetailsHeaderContent.tsx | 35 +- static/app/views/preprod/index.tsx | 25 +- .../releases/detail/header/releaseHeader.tsx | 5 +- static/app/views/releases/list/index.spec.tsx | 2 +- static/app/views/releases/list/index.tsx | 376 ++++++++++-------- .../project/navigationConfiguration.tsx | 1 - .../views/settings/project/preprod/index.tsx | 4 +- .../bases/test_preprod_artifact_endpoint.py | 2 - ...zation_preprod_artifact_install_details.py | 13 - ...test_organization_preprod_size_analysis.py | 13 - ...t_project_preprod_size_analysis_compare.py | 45 --- ..._preprod_size_analysis_compare_download.py | 2 - ..._project_preprod_size_analysis_download.py | 6 - .../preprod/api/endpoints/test_builds.py | 68 ---- ..._organization_preprod_artifact_assemble.py | 29 -- .../test_project_preprod_artifact_delete.py | 21 - .../test_project_preprod_artifact_download.py | 3 +- .../test_project_preprod_build_details.py | 18 - .../test_project_preprod_check_for_updates.py | 9 - .../test_organization_events_preprod_size.py | 2 +- ..._organization_events_stats_preprod_size.py | 2 +- 32 files changed, 234 insertions(+), 522 deletions(-) diff --git a/src/sentry/api/endpoints/organization_trace_item_attributes.py b/src/sentry/api/endpoints/organization_trace_item_attributes.py index c46894bd2de607..0839655d21d084 100644 --- a/src/sentry/api/endpoints/organization_trace_item_attributes.py +++ b/src/sentry/api/endpoints/organization_trace_item_attributes.py @@ -141,7 +141,6 @@ class OrganizationTraceItemAttributesEndpointBase(OrganizationEventsEndpointBase "organizations:ourlogs-enabled", "organizations:visibility-explore-view", "organizations:tracemetrics-enabled", - "organizations:preprod-frontend-routes", ] def has_feature(self, organization: Organization, request: Request) -> bool: diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index fe2aac3767b0b8..e9c2abc4afa3e1 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -243,8 +243,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:performance-web-vitals-seer-suggestions", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the warning banner to inform users of pending deprecation of the transactions dataset manager.add("organizations:performance-transaction-deprecation-banner", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable preprod frontend routes - manager.add("organizations:preprod-frontend-routes", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable preprod_artifact webhook subscription UI in Sentry App settings manager.add("organizations:preprod-artifact-webhooks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable preprod issue reporting diff --git a/src/sentry/preprod/api/endpoints/builds.py b/src/sentry/preprod/api/endpoints/builds.py index 8bb20e598ed62f..f6a919d39ce8b1 100644 --- a/src/sentry/preprod/api/endpoints/builds.py +++ b/src/sentry/preprod/api/endpoints/builds.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -21,8 +20,6 @@ logger = logging.getLogger(__name__) -ERR_FEATURE_REQUIRED = "Feature {} is not enabled for the organization." - @cell_silo_endpoint class BuildsEndpoint(OrganizationEndpoint): @@ -32,14 +29,6 @@ class BuildsEndpoint(OrganizationEndpoint): } def get(self, request: Request, organization: Organization) -> Response: - if not features.has( - "organizations:preprod-frontend-routes", organization, actor=request.user - ): - return Response( - {"detail": ERR_FEATURE_REQUIRED.format("organizations:preprod-frontend-routes")}, - status=403, - ) - def on_results(artifacts: list[PreprodArtifact]) -> list[dict[str, Any]]: results = [] for artifact in artifacts: diff --git a/src/sentry/preprod/api/endpoints/organization_preprod_artifact_assemble.py b/src/sentry/preprod/api/endpoints/organization_preprod_artifact_assemble.py index 13305efab8b9b1..821e77f9fd6f12 100644 --- a/src/sentry/preprod/api/endpoints/organization_preprod_artifact_assemble.py +++ b/src/sentry/preprod/api/endpoints/organization_preprod_artifact_assemble.py @@ -5,11 +5,10 @@ import jsonschema import orjson import sentry_sdk -from django.conf import settings from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -145,11 +144,6 @@ def post(self, request: Request, project: Project) -> Response: ) ) - if not settings.IS_DEV and not features.has( - "organizations:preprod-frontend-routes", project.organization, actor=request.user - ): - return Response({"error": "Feature not enabled"}, status=403) - with sentry_sdk.start_span(op="preprod_artifact.assemble"): data, error_message = validate_preprod_artifact_schema(request.body) if error_message: diff --git a/src/sentry/preprod/api/endpoints/project_preprod_artifact_delete.py b/src/sentry/preprod/api/endpoints/project_preprod_artifact_delete.py index add3d2d99e4bd9..3f070712ca495f 100644 --- a/src/sentry/preprod/api/endpoints/project_preprod_artifact_delete.py +++ b/src/sentry/preprod/api/endpoints/project_preprod_artifact_delete.py @@ -5,7 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -34,11 +34,6 @@ def delete( ) -> Response: """Delete a preprod artifact and all associated data""" - if not features.has( - "organizations:preprod-frontend-routes", project.organization, actor=request.user - ): - return Response({"error": "Feature not enabled"}, status=403) - analytics.record( PreprodArtifactApiDeleteEvent( organization_id=project.organization_id, diff --git a/src/sentry/preprod/api/endpoints/project_preprod_build_details.py b/src/sentry/preprod/api/endpoints/project_preprod_build_details.py index 8a9e3d5e83de91..5bc3e0261afea2 100644 --- a/src/sentry/preprod/api/endpoints/project_preprod_build_details.py +++ b/src/sentry/preprod/api/endpoints/project_preprod_build_details.py @@ -5,7 +5,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -58,11 +58,6 @@ def get( ) ) - if not features.has( - "organizations:preprod-frontend-routes", project.organization, actor=request.user - ): - return Response({"error": "Feature not enabled"}, status=403) - cutoff = get_size_retention_cutoff(project.organization) if head_artifact.date_added < cutoff: return Response({"detail": "This build's size data has expired."}, status=404) diff --git a/src/sentry/preprod/api/endpoints/public/organization_preprod_artifact_install_details.py b/src/sentry/preprod/api/endpoints/public/organization_preprod_artifact_install_details.py index a385f0ac8e43a5..dd7381f3146943 100644 --- a/src/sentry/preprod/api/endpoints/public/organization_preprod_artifact_install_details.py +++ b/src/sentry/preprod/api/endpoints/public/organization_preprod_artifact_install_details.py @@ -4,7 +4,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -72,11 +71,6 @@ def get( and iOS-specific code signing information. """ - if not features.has( - "organizations:preprod-frontend-routes", organization, actor=request.user - ): - return Response({"detail": "Feature not enabled"}, status=403) - try: artifact = PreprodArtifact.objects.select_related( "mobile_app_info", diff --git a/src/sentry/preprod/api/endpoints/public/organization_preprod_size_analysis.py b/src/sentry/preprod/api/endpoints/public/organization_preprod_size_analysis.py index 3d4f8028aff1f8..40b25c88af2c44 100644 --- a/src/sentry/preprod/api/endpoints/public/organization_preprod_size_analysis.py +++ b/src/sentry/preprod/api/endpoints/public/organization_preprod_size_analysis.py @@ -8,7 +8,6 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -119,11 +118,6 @@ def get( - `COMPLETED`: Analysis finished successfully with full size data. """ - if not features.has( - "organizations:preprod-frontend-routes", organization, actor=request.user - ): - return Response({"detail": "Feature not enabled"}, status=403) - try: head_artifact = PreprodArtifact.objects.select_related( "mobile_app_info", "build_configuration", "commit_comparison" diff --git a/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare.py b/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare.py index b845451f8bd380..09e9b56a88c3da 100644 --- a/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare.py +++ b/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare.py @@ -7,7 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -113,11 +113,6 @@ def get( ) ) - if not features.has( - "organizations:preprod-frontend-routes", project.organization, actor=request.user - ): - return Response({"detail": "Feature not enabled"}, status=403) - cutoff = get_size_retention_cutoff(project.organization) if head_artifact.date_added < cutoff or base_artifact.date_added < cutoff: return Response({"detail": "This build's size data has expired."}, status=404) @@ -279,11 +274,6 @@ def post( ) ) - if not features.has( - "organizations:preprod-frontend-routes", project.organization, actor=request.user - ): - return Response({"detail": "Feature not enabled"}, status=403) - cutoff = get_size_retention_cutoff(project.organization) if head_artifact.date_added < cutoff or base_artifact.date_added < cutoff: return Response({"detail": "This build's size data has expired."}, status=404) diff --git a/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare_download.py b/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare_download.py index 1ffd7c7b9f47cc..0bfae994a7719d 100644 --- a/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare_download.py +++ b/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_compare_download.py @@ -6,7 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -55,11 +55,6 @@ def get( ) ) - if not features.has( - "organizations:preprod-frontend-routes", project.organization, actor=request.user - ): - return Response({"detail": "Feature not enabled"}, status=403) - logger.info( "preprod.size_analysis.compare.api.download", extra={ diff --git a/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_download.py b/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_download.py index c3fab44caf1076..b8fa4acc721772 100644 --- a/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_download.py +++ b/src/sentry/preprod/api/endpoints/size_analysis/project_preprod_size_analysis_download.py @@ -1,11 +1,10 @@ from __future__ import annotations -from django.conf import settings from django.http.response import HttpResponseBase from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, features +from sentry import analytics from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import cell_silo_endpoint @@ -58,11 +57,6 @@ def get( ) ) - if not settings.IS_DEV and not features.has( - "organizations:preprod-frontend-routes", project.organization, actor=request.user - ): - return Response({"detail": "Feature not enabled"}, status=403) - cutoff = get_size_retention_cutoff(project.organization) if head_artifact.date_added < cutoff: return Response({"detail": "This build's size data has expired."}, status=404) diff --git a/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx b/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx index c5cf3793e548aa..017f03f976e419 100644 --- a/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx +++ b/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx @@ -5,7 +5,6 @@ import {Button, LinkButton} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; -import Feature from 'sentry/components/acl/feature'; import {Breadcrumbs, type Crumb} from 'sentry/components/breadcrumbs'; import {ConfirmDelete} from 'sentry/components/confirmDelete'; import {DropdownButton} from 'sentry/components/dropdownButton'; @@ -198,15 +197,13 @@ export function BuildDetailsHeaderContent(props: BuildDetailsHeaderContentProps) > {t('Compare Build')} - - {project && ( - } - aria-label={t('Settings')} - to={`/settings/${organization.slug}/projects/${project.slug}/mobile-builds/`} - /> - )} - + {project && ( + } + aria-label={t('Settings')} + to={`/settings/${organization.slug}/projects/${project.slug}/mobile-builds/`} + /> + )} {t('Compare Build')} - - {project && ( - } - aria-label={t('Settings')} - to={`/settings/${organization.slug}/projects/${project.slug}/mobile-builds/`} - /> - )} - + {project && ( + } + aria-label={t('Settings')} + to={`/settings/${organization.slug}/projects/${project.slug}/mobile-builds/`} + /> + )} ( - - - - {t("You don't have access to this feature")} - - - - )} - > - - - - + + + ); } diff --git a/static/app/views/releases/detail/header/releaseHeader.tsx b/static/app/views/releases/detail/header/releaseHeader.tsx index c1ab3a6b647be9..4852dbf8158b68 100644 --- a/static/app/views/releases/detail/header/releaseHeader.tsx +++ b/static/app/views/releases/detail/header/releaseHeader.tsx @@ -103,10 +103,7 @@ export function ReleaseHeader({ to: 'builds/', }; - if ( - organization.features?.includes('preprod-frontend-routes') && - (numberOfMobileBuilds || isMobileRelease(project.platform, false)) - ) { + if (numberOfMobileBuilds || isMobileRelease(project.platform, false)) { tabs.push(buildsTab); } diff --git a/static/app/views/releases/list/index.spec.tsx b/static/app/views/releases/list/index.spec.tsx index 4f22a46eb4f3df..37874f430225ec 100644 --- a/static/app/views/releases/list/index.spec.tsx +++ b/static/app/views/releases/list/index.spec.tsx @@ -21,7 +21,7 @@ import {ReleasesStatusOption} from 'sentry/views/releases/list/releasesStatusOpt describe('ReleasesList', () => { const organization = OrganizationFixture({ - features: ['preprod-frontend-routes'], + features: [], }); const projects = [ProjectFixture({features: ['releases']})]; const semverVersionInfo = { diff --git a/static/app/views/releases/list/index.tsx b/static/app/views/releases/list/index.tsx index 5978ad7ff89478..fa652f0a7f76cd 100644 --- a/static/app/views/releases/list/index.tsx +++ b/static/app/views/releases/list/index.tsx @@ -1,69 +1,75 @@ -import {Fragment, useCallback, useEffect, useMemo} from 'react'; -import {forceCheck} from 'react-lazyload'; -import styled from '@emotion/styled'; -import {keepPreviousData, useQuery} from '@tanstack/react-query'; - -import {FeatureBadge} from '@sentry/scraps/badge'; -import {Flex, Stack} from '@sentry/scraps/layout'; -import {TabList} from '@sentry/scraps/tabs'; - -import {fetchTagValues} from 'sentry/actionCreators/tags'; -import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; -import * as Layout from 'sentry/components/layouts/thirds'; -import {LoadingError} from 'sentry/components/loadingError'; -import {NoProjectMessage} from 'sentry/components/noProjectMessage'; -import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants'; -import {PageFiltersContainer} from 'sentry/components/pageFilters/container'; -import {DatePageFilter} from 'sentry/components/pageFilters/date/datePageFilter'; -import {EnvironmentPageFilter} from 'sentry/components/pageFilters/environment/environmentPageFilter'; -import {PageFilterBar} from 'sentry/components/pageFilters/pageFilterBar'; -import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; -import {ProjectPageFilter} from 'sentry/components/pageFilters/project/projectPageFilter'; -import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; -import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip'; -import {PreprodBuildsDisplay} from 'sentry/components/preprod/preprodBuildsDisplay'; -import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; -import type {GetTagValues} from 'sentry/components/searchQueryBuilder'; -import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; -import {ReleasesSortOption} from 'sentry/constants/releases'; -import {t} from 'sentry/locale'; -import {ProjectsStore} from 'sentry/stores/projectsStore'; -import type {TagCollection} from 'sentry/types/group'; -import type {Release} from 'sentry/types/release'; -import {ReleaseStatus} from 'sentry/types/release'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; -import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours'; -import {SEMVER_TAGS} from 'sentry/utils/discover/fields'; -import {FieldKey} from 'sentry/utils/fields'; -import {decodeScalar} from 'sentry/utils/queryString'; -import {RequestError} from 'sentry/utils/requestError/requestError'; -import {useApi} from 'sentry/utils/useApi'; -import {useLocation} from 'sentry/utils/useLocation'; -import {useNavigate} from 'sentry/utils/useNavigate'; -import {useOrganization} from 'sentry/utils/useOrganization'; -import {useProjects} from 'sentry/utils/useProjects'; -import {TopBar} from 'sentry/views/navigation/topBar'; -import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature'; -import {buildDetailsApiOptions} from 'sentry/views/preprod/utils/buildDetailsApiOptions'; -import {ReleaseArchivedNotice} from 'sentry/views/releases/detail/overview/releaseArchivedNotice'; -import {MobileBuilds} from 'sentry/views/releases/list/mobileBuilds'; -import {ReleaseHealthCTA} from 'sentry/views/releases/list/releaseHealthCTA'; -import {ReleaseListInner} from 'sentry/views/releases/list/releaseListInner'; -import {isMobileRelease} from 'sentry/views/releases/utils'; - -import {ReleasesDisplayOption, ReleasesDisplayOptions} from './releasesDisplayOptions'; -import {ReleasesSortOptions} from './releasesSortOptions'; -import {ReleasesStatusOption, ReleasesStatusOptions} from './releasesStatusOptions'; -import {validateSummaryStatsPeriod} from './utils'; - -type ReleaseTab = 'releases' | 'mobile-builds' | 'snapshots'; +import { Fragment, useCallback, useEffect, useMemo } from "react"; +import { forceCheck } from "react-lazyload"; +import styled from "@emotion/styled"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; + +import { FeatureBadge } from "@sentry/scraps/badge"; +import { Flex, Stack } from "@sentry/scraps/layout"; +import { TabList } from "@sentry/scraps/tabs"; + +import { fetchTagValues } from "sentry/actionCreators/tags"; +import { FeedbackButton } from "sentry/components/feedbackButton/feedbackButton"; +import * as Layout from "sentry/components/layouts/thirds"; +import { LoadingError } from "sentry/components/loadingError"; +import { NoProjectMessage } from "sentry/components/noProjectMessage"; +import { ALL_ACCESS_PROJECTS } from "sentry/components/pageFilters/constants"; +import { PageFiltersContainer } from "sentry/components/pageFilters/container"; +import { DatePageFilter } from "sentry/components/pageFilters/date/datePageFilter"; +import { EnvironmentPageFilter } from "sentry/components/pageFilters/environment/environmentPageFilter"; +import { PageFilterBar } from "sentry/components/pageFilters/pageFilterBar"; +import { normalizeDateTimeParams } from "sentry/components/pageFilters/parse"; +import { ProjectPageFilter } from "sentry/components/pageFilters/project/projectPageFilter"; +import { usePageFilters } from "sentry/components/pageFilters/usePageFilters"; +import { PageHeadingQuestionTooltip } from "sentry/components/pageHeadingQuestionTooltip"; +import { PreprodBuildsDisplay } from "sentry/components/preprod/preprodBuildsDisplay"; +import { SearchQueryBuilder } from "sentry/components/searchQueryBuilder"; +import type { GetTagValues } from "sentry/components/searchQueryBuilder"; +import { SentryDocumentTitle } from "sentry/components/sentryDocumentTitle"; +import { ReleasesSortOption } from "sentry/constants/releases"; +import { t } from "sentry/locale"; +import { ProjectsStore } from "sentry/stores/projectsStore"; +import type { TagCollection } from "sentry/types/group"; +import type { Release } from "sentry/types/release"; +import { ReleaseStatus } from "sentry/types/release"; +import { trackAnalytics } from "sentry/utils/analytics"; +import { apiOptions, selectJsonWithHeaders } from "sentry/utils/api/apiOptions"; +import { DemoTourElement, DemoTourStep } from "sentry/utils/demoMode/demoTours"; +import { SEMVER_TAGS } from "sentry/utils/discover/fields"; +import { FieldKey } from "sentry/utils/fields"; +import { decodeScalar } from "sentry/utils/queryString"; +import { RequestError } from "sentry/utils/requestError/requestError"; +import { useApi } from "sentry/utils/useApi"; +import { useLocation } from "sentry/utils/useLocation"; +import { useNavigate } from "sentry/utils/useNavigate"; +import { useOrganization } from "sentry/utils/useOrganization"; +import { useProjects } from "sentry/utils/useProjects"; +import { TopBar } from "sentry/views/navigation/topBar"; +import { useHasPageFrameFeature } from "sentry/views/navigation/useHasPageFrameFeature"; +import { buildDetailsApiOptions } from "sentry/views/preprod/utils/buildDetailsApiOptions"; +import { ReleaseArchivedNotice } from "sentry/views/releases/detail/overview/releaseArchivedNotice"; +import { MobileBuilds } from "sentry/views/releases/list/mobileBuilds"; +import { ReleaseHealthCTA } from "sentry/views/releases/list/releaseHealthCTA"; +import { ReleaseListInner } from "sentry/views/releases/list/releaseListInner"; +import { isMobileRelease } from "sentry/views/releases/utils"; + +import { + ReleasesDisplayOption, + ReleasesDisplayOptions, +} from "./releasesDisplayOptions"; +import { ReleasesSortOptions } from "./releasesSortOptions"; +import { + ReleasesStatusOption, + ReleasesStatusOptions, +} from "./releasesStatusOptions"; +import { validateSummaryStatsPeriod } from "./utils"; + +type ReleaseTab = "releases" | "mobile-builds" | "snapshots"; const RELEASE_FILTER_KEYS = [ ...Object.values(SEMVER_TAGS), { - key: 'release', - name: 'release', + key: "release", + name: "release", }, { key: FieldKey.RELEASE_CREATED, @@ -85,42 +91,45 @@ function makeReleaseListApiOptions({ activeSort?: ReleasesSortOption; activeStatus?: ReleasesStatusOption; }) { - return apiOptions.as()('/organizations/$organizationIdOrSlug/releases/', { - path: {organizationIdOrSlug: organizationSlug}, - query: { - project: location.query.project, - environment: location.query.environment, - cursor: location.query.cursor, - query: location.query.query, - sort: location.query.sort, - summaryStatsPeriod: validateSummaryStatsPeriod( - decodeScalar(location.query.statsPeriod) - ), - per_page: 20, - flatten: activeSort === ReleasesSortOption.DATE ? 0 : 1, - adoptionStages: 1, - status: - activeStatus === ReleasesStatusOption.ARCHIVED - ? ReleaseStatus.ARCHIVED - : ReleaseStatus.ACTIVE, + return apiOptions.as()( + "/organizations/$organizationIdOrSlug/releases/", + { + path: { organizationIdOrSlug: organizationSlug }, + query: { + project: location.query.project, + environment: location.query.environment, + cursor: location.query.cursor, + query: location.query.query, + sort: location.query.sort, + summaryStatsPeriod: validateSummaryStatsPeriod( + decodeScalar(location.query.statsPeriod), + ), + per_page: 20, + flatten: activeSort === ReleasesSortOption.DATE ? 0 : 1, + adoptionStages: 1, + status: + activeStatus === ReleasesStatusOption.ARCHIVED + ? ReleaseStatus.ARCHIVED + : ReleaseStatus.ACTIVE, + }, + staleTime: Infinity, }, - staleTime: Infinity, - }); + ); } const releasesFeedbackOptions = { - messagePlaceholder: t('How can we improve the Releases experience?'), + messagePlaceholder: t("How can we improve the Releases experience?"), tags: { - ['feedback.source']: 'releases-list-header', + ["feedback.source"]: "releases-list-header", }, }; export default function ReleasesList() { - const api = useApi({persistInFlight: true}); + const api = useApi({ persistInFlight: true }); const organization = useOrganization(); const hasPageFrameFeature = useHasPageFrameFeature(); - const {projects} = useProjects(); - const {selection} = usePageFilters(); + const { projects } = useProjects(); + const { selection } = usePageFilters(); const location = useLocation(); const navigate = useNavigate(); @@ -139,18 +148,18 @@ export default function ReleasesList() { statsPeriod: validatedStatsPeriod, }, }, - {replace: true} + { replace: true }, ); } }, [navigate, location.pathname, location.query]); const activeQuery = useMemo(() => { - const {query: locationQuery} = location.query; - return typeof locationQuery === 'string' ? locationQuery : ''; + const { query: locationQuery } = location.query; + return typeof locationQuery === "string" ? locationQuery : ""; }, [location.query]); const activeSort = useMemo(() => { - const {sort: locationSort} = location.query; + const { sort: locationSort } = location.query; // Require 1 environment for date adopted if ( @@ -161,7 +170,7 @@ export default function ReleasesList() { } const sortExists = Object.values(ReleasesSortOption).includes( - locationSort as ReleasesSortOption + locationSort as ReleasesSortOption, ); if (sortExists) { return locationSort as ReleasesSortOption; @@ -171,7 +180,7 @@ export default function ReleasesList() { }, [selection.environments, location.query]); const activeDisplay = useMemo(() => { - const {display: locationDisplay} = location.query; + const { display: locationDisplay } = location.query; switch (locationDisplay) { case ReleasesDisplayOption.USERS: @@ -182,7 +191,7 @@ export default function ReleasesList() { }, [location.query]); const activeStatus = useMemo(() => { - const {status} = location.query; + const { status } = location.query; switch (status) { case ReleasesStatusOption.ARCHIVED: @@ -229,79 +238,84 @@ export default function ReleasesList() { return projects[0]; } - const selectedProjectId = selection.projects?.length === 1 && selection.projects[0]; - return projects?.find(p => p.id === `${selectedProjectId}`); + const selectedProjectId = + selection.projects?.length === 1 && selection.projects[0]; + return projects?.find((p) => p.id === `${selectedProjectId}`); }, [selection.projects, projects]); // Get selected project IDs, handling "All Projects" case const selectedProjectIds = useMemo(() => { - const selectedIds = selection.projects.filter(id => id !== ALL_ACCESS_PROJECTS); + const selectedIds = selection.projects.filter( + (id) => id !== ALL_ACCESS_PROJECTS, + ); // If no specific projects selected, pass [-1] to represent "all projects" // This avoids expanding to hundreds of project IDs which causes URL length issues return selectedIds.length === 0 ? [`${ALL_ACCESS_PROJECTS}`] - : selectedIds.map(id => `${id}`); + : selectedIds.map((id) => `${id}`); }, [selection.projects]); - const hasPreprodFeature = organization.features?.includes('preprod-frontend-routes'); - const hasSnapshotsFeature = organization.features?.includes('preprod-snapshots'); + const hasSnapshotsFeature = + organization.features?.includes("preprod-snapshots"); - const {statsPeriod, start, end, utc} = normalizeDateTimeParams(location.query); + const { statsPeriod, start, end, utc } = normalizeDateTimeParams( + location.query, + ); const buildsProbeQuery = useQuery({ ...buildDetailsApiOptions({ organization, queryParams: { per_page: 1, project: selectedProjectIds, - ...(statsPeriod && {statsPeriod}), - ...(start && {start}), - ...(end && {end}), - ...(utc && {utc}), + ...(statsPeriod && { statsPeriod }), + ...(start && { start }), + ...(end && { end }), + ...(utc && { utc }), }, }), staleTime: 60_000, - enabled: !!hasPreprodFeature, placeholderData: keepPreviousData, }); - // When "All Projects" is selected (represented by [-1]), check all accessible projects - // When specific projects are selected, check only those projects const hasAnyStrictlyMobileProject = useMemo(() => { const isAllProjects = selectedProjectIds.length === 1 && selectedProjectIds[0] === `${ALL_ACCESS_PROJECTS}`; const projectIdsToCheck = isAllProjects - ? projects.map(p => p.id) + ? projects.map((p) => p.id) : selectedProjectIds; - // Check if at least one project has a mobile platform return projectIdsToCheck - .map(id => ProjectsStore.getById(id)) + .map((id) => ProjectsStore.getById(id)) .filter(Boolean) - .some(project => project?.platform && isMobileRelease(project.platform, false)); + .some( + (project) => + project?.platform && isMobileRelease(project.platform, false), + ); }, [selectedProjectIds, projects]); const hasBuildsData = !buildsProbeQuery.isPending && (buildsProbeQuery.data?.length ?? 0) > 0; const shouldShowMobileBuildsTab = - hasPreprodFeature && (hasBuildsData || hasAnyStrictlyMobileProject); + hasBuildsData || hasAnyStrictlyMobileProject; const shouldShowSnapshotsTab = !!hasSnapshotsFeature; - const shouldShowPreprodTabs = shouldShowMobileBuildsTab || shouldShowSnapshotsTab; + const shouldShowPreprodTabs = + shouldShowMobileBuildsTab || shouldShowSnapshotsTab; const selectedTab = useMemo(() => { if (!shouldShowPreprodTabs) { - return 'releases'; + return "releases"; } const tab = decodeScalar(location.query.tab) as ReleaseTab | undefined; - if (tab === 'snapshots' && !shouldShowSnapshotsTab) { - return 'releases'; + if (tab === "snapshots" && !shouldShowSnapshotsTab) { + return "releases"; } - if (tab === 'mobile-builds' && !shouldShowMobileBuildsTab) { - return 'releases'; + if (tab === "mobile-builds" && !shouldShowMobileBuildsTab) { + return "releases"; } - return tab || 'releases'; + return tab || "releases"; }, [ shouldShowPreprodTabs, shouldShowMobileBuildsTab, @@ -313,20 +327,20 @@ export default function ReleasesList() { (query: string) => { navigate({ ...location, - query: {...location.query, cursor: undefined, query}, + query: { ...location.query, cursor: undefined, query }, }); }, - [location, navigate] + [location, navigate], ); const handleSortBy = useCallback( (sort: string) => { navigate({ ...location, - query: {...location.query, cursor: undefined, sort}, + query: { ...location.query, cursor: undefined, sort }, }); }, - [location, navigate] + [location, navigate], ); const handleDisplay = useCallback( @@ -356,41 +370,41 @@ export default function ReleasesList() { navigate({ ...location, - query: {...location.query, cursor: undefined, display, sort}, + query: { ...location.query, cursor: undefined, display, sort }, }); }, - [location, navigate] + [location, navigate], ); const handleStatus = useCallback( (status: string) => { navigate({ ...location, - query: {...location.query, cursor: undefined, status}, + query: { ...location.query, cursor: undefined, status }, }); }, - [location, navigate] + [location, navigate], ); const handleTabChange = useCallback( (newTab: string) => { - if (newTab === 'mobile-builds') { - trackAnalytics('preprod.releases.mobile-builds.tab-clicked', { + if (newTab === "mobile-builds") { + trackAnalytics("preprod.releases.mobile-builds.tab-clicked", { organization, }); } }, - [organization] + [organization], ); const tagValueLoader = useCallback( (key: string, search: string) => { - const {project} = location.query; + const { project } = location.query; // Coerce the url param into an array const projectIds = Array.isArray(project) ? project - : typeof project === 'string' + : typeof project === "string" ? [project] : []; @@ -403,15 +417,15 @@ export default function ReleasesList() { endpointParams: normalizeDateTimeParams(location.query), }); }, - [api, location, organization] + [api, location, organization], ); const getTagValues = useCallback( async (tag, currentQuery) => { const values = await tagValueLoader(tag.key, currentQuery); - return values.map(({value}) => value); + return values.map(({ value }) => value); }, - [tagValueLoader] + [tagValueLoader], ); const hasAnyMobileProject = useMemo(() => { @@ -421,13 +435,15 @@ export default function ReleasesList() { selectedProjectIds.length === 1 && selectedProjectIds[0] === `${ALL_ACCESS_PROJECTS}`; const projectIdsToCheck = isAllProjects - ? projects.map(p => p.id) + ? projects.map((p) => p.id) : selectedProjectIds; return projectIdsToCheck - .map(id => ProjectsStore.getById(id)) + .map((id) => ProjectsStore.getById(id)) .filter(Boolean) - .some(project => project?.platform && isMobileRelease(project.platform)); + .some( + (project) => project?.platform && isMobileRelease(project.platform), + ); }, [selectedProjectIds, projects]); const showReleaseAdoptionStages = @@ -435,9 +451,9 @@ export default function ReleasesList() { const shouldShowQuickstart = Boolean( selectedProject && // Has not set up releases - !selectedProject?.features.includes('releases') && + !selectedProject?.features.includes("releases") && // Has no releases - !releases?.length + !releases?.length, ); const releasesPageLinks = data?.headers.Link; @@ -449,12 +465,12 @@ export default function ReleasesList() { // eslint-disable-next-line @typescript-eslint/no-base-to-string return String(releasesError.responseJSON?.detail); } - return t('There was an error loading releases'); + return t("There was an error loading releases"); }, [releasesError]); return ( - + @@ -462,11 +478,11 @@ export default function ReleasesList() { - {t('Releases')} + {t("Releases")} @@ -491,29 +507,37 @@ export default function ReleasesList() { {shouldShowPreprodTabs && ( - - + + - {t('Releases')} + {t("Releases")} - {t('Mobile Builds')} + {t("Mobile Builds")} @@ -541,13 +565,13 @@ export default function ReleasesList() { query: { ...location.query, query: undefined, - tab: 'snapshots', + tab: "snapshots", }, }} - textValue={t('Snapshots')} + textValue={t("Snapshots")} > - {t('Snapshots')} + {t("Snapshots")} @@ -559,14 +583,14 @@ export default function ReleasesList() { - {selectedTab === 'mobile-builds' && ( + {selectedTab === "mobile-builds" && ( )} - {selectedTab === 'snapshots' && shouldShowSnapshotsTab && ( + {selectedTab === "snapshots" && shouldShowSnapshotsTab && ( )} - {selectedTab === 'releases' && ( + {selectedTab === "releases" && ( - {props => ( + {(props) => (
)} @@ -654,28 +682,30 @@ export default function ReleasesList() { ); } -const ReleasesPageFilterBar = styled(PageFilterBar)<{shouldShowPreprodTabs: boolean}>` - ${p => !p.shouldShowPreprodTabs && `margin-bottom: ${p.theme.space.xl};`} +const ReleasesPageFilterBar = styled(PageFilterBar)<{ + shouldShowPreprodTabs: boolean; +}>` + ${(p) => !p.shouldShowPreprodTabs && `margin-bottom: ${p.theme.space.xl};`} `; -const SortAndFilterWrapper = styled('div')` +const SortAndFilterWrapper = styled("div")` display: grid; grid-template-columns: 1fr repeat(3, max-content); - gap: ${p => p.theme.space.xl}; + gap: ${(p) => p.theme.space.xl}; - @media (max-width: ${p => p.theme.breakpoints.md}) { + @media (max-width: ${(p) => p.theme.breakpoints.md}) { grid-template-columns: repeat(3, 1fr); & > div { width: auto; } } - @media (max-width: ${p => p.theme.breakpoints.sm}) { + @media (max-width: ${(p) => p.theme.breakpoints.sm}) { grid-template-columns: minmax(0, 1fr); } `; const StyledSearchQueryBuilder = styled(SearchQueryBuilder)` - @media (max-width: ${p => p.theme.breakpoints.md}) { + @media (max-width: ${(p) => p.theme.breakpoints.md}) { grid-column: 1 / -1; } `; diff --git a/static/app/views/settings/project/navigationConfiguration.tsx b/static/app/views/settings/project/navigationConfiguration.tsx index 9d0ea6de594e0a..5e38c7baf81cd1 100644 --- a/static/app/views/settings/project/navigationConfiguration.tsx +++ b/static/app/views/settings/project/navigationConfiguration.tsx @@ -131,7 +131,6 @@ export function getNavigationConfiguration({ { path: `${pathPrefix}/mobile-builds/`, title: t('Mobile Builds'), - show: () => !!organization?.features?.includes('preprod-frontend-routes'), badge: () => 'new', description: t('Size analysis and build distribution configuration.'), }, diff --git a/static/app/views/settings/project/preprod/index.tsx b/static/app/views/settings/project/preprod/index.tsx index 2345e1c8a9fd5a..6b4b3c3a931a87 100644 --- a/static/app/views/settings/project/preprod/index.tsx +++ b/static/app/views/settings/project/preprod/index.tsx @@ -58,7 +58,7 @@ export default function PreprodSettings() { }; return ( - + )}
- + ); } diff --git a/tests/sentry/preprod/api/bases/test_preprod_artifact_endpoint.py b/tests/sentry/preprod/api/bases/test_preprod_artifact_endpoint.py index 4b6007ea017b65..d0971c2c67ab73 100644 --- a/tests/sentry/preprod/api/bases/test_preprod_artifact_endpoint.py +++ b/tests/sentry/preprod/api/bases/test_preprod_artifact_endpoint.py @@ -1,6 +1,5 @@ from sentry.constants import ObjectStatus from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.features import with_feature class PreprodArtifactEndpointTest(APITestCase): @@ -12,7 +11,6 @@ def setUp(self) -> None: def _get_url(self, org_slug, artifact_id): return f"/api/0/organizations/{org_slug}/preprodartifacts/{artifact_id}/build-details/" - @with_feature("organizations:preprod-frontend-routes") def test_extracts_project_from_artifact(self) -> None: url = self._get_url(self.organization.slug, self.artifact.id) response = self.client.get(url) diff --git a/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_artifact_install_details.py b/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_artifact_install_details.py index 633a9c63a1bc97..19f63c8f0b3231 100644 --- a/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_artifact_install_details.py +++ b/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_artifact_install_details.py @@ -27,13 +27,6 @@ def setUp(self) -> None: build_number=42, ) - self.feature_context = self.feature({"organizations:preprod-frontend-routes": True}) - self.feature_context.__enter__() - - def tearDown(self): - self.feature_context.__exit__(None, None, None) - super().tearDown() - def _get_url(self, artifact_id=None): artifact_id = artifact_id or self.preprod_artifact.id return reverse( @@ -41,12 +34,6 @@ def _get_url(self, artifact_id=None): args=[self.organization.slug, artifact_id], ) - def test_feature_flag_disabled(self) -> None: - with self.feature({"organizations:preprod-frontend-routes": False}): - response = self.client.get(self._get_url()) - assert response.status_code == 403 - assert response.json()["detail"] == "Feature not enabled" - def test_artifact_not_found(self) -> None: response = self.client.get(self._get_url(artifact_id=999999)) assert response.status_code == 404 diff --git a/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_size_analysis.py b/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_size_analysis.py index 2c3dba12951162..7169c17d1c2168 100644 --- a/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_size_analysis.py +++ b/tests/sentry/preprod/api/endpoints/public/test_organization_preprod_size_analysis.py @@ -34,13 +34,6 @@ def setUp(self) -> None: build_number=42, ) - self.feature_context = self.feature({"organizations:preprod-frontend-routes": True}) - self.feature_context.__enter__() - - def tearDown(self): - self.feature_context.__exit__(None, None, None) - super().tearDown() - def _get_url(self, artifact_id=None): artifact_id = artifact_id or self.preprod_artifact.id return reverse( @@ -67,12 +60,6 @@ def _make_analysis_data(self, **overrides): defaults.update(overrides) return defaults - def test_feature_flag_disabled(self) -> None: - with self.feature({"organizations:preprod-frontend-routes": False}): - response = self.client.get(self._get_url()) - assert response.status_code == 403 - assert response.json()["detail"] == "Feature not enabled" - def test_artifact_not_found(self) -> None: response = self.client.get(self._get_url(artifact_id=999999)) assert response.status_code == 404 diff --git a/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare.py b/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare.py index 08e84a5419ec70..df05202d76185e 100644 --- a/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare.py +++ b/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare.py @@ -1,7 +1,6 @@ from datetime import timedelta from unittest.mock import patch -from django.test import override_settings from django.urls import reverse from django.utils import timezone @@ -85,7 +84,6 @@ def _get_url(self, head_artifact_id=None, base_artifact_id=None): args=[self.organization.slug, head_artifact_id, base_artifact_id], ) - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_success_with_completed_comparison(self) -> None: """Test GET endpoint returns successful comparison when comparison exists and is completed""" # Create a successful comparison @@ -121,7 +119,6 @@ def test_get_comparison_success_with_completed_comparison(self) -> None: assert comparison_data["error_code"] is None assert comparison_data["error_message"] is None - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_success_with_failed_comparison(self) -> None: """Test GET endpoint returns failed comparison when comparison exists and failed""" # Create a failed comparison @@ -146,7 +143,6 @@ def test_get_comparison_success_with_failed_comparison(self) -> None: assert comparison_data["error_code"] == str(PreprodArtifactSizeComparison.ErrorCode.UNKNOWN) assert comparison_data["error_message"] == "Comparison failed due to processing error" - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_success_with_pending_comparison(self) -> None: """Test GET endpoint returns processing state for pending comparison""" # Create a pending comparison (which should be shown as PROCESSING to frontend) @@ -170,7 +166,6 @@ def test_get_comparison_success_with_pending_comparison(self) -> None: assert comparison_data["error_code"] is None assert comparison_data["error_message"] is None - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_success_with_processing_comparison(self) -> None: """Test GET endpoint returns processing comparison when comparison is in progress""" # Create a processing comparison @@ -193,7 +188,6 @@ def test_get_comparison_success_with_processing_comparison(self) -> None: assert comparison_data["error_code"] is None assert comparison_data["error_message"] is None - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_success_with_no_comparison(self) -> None: """Test GET endpoint returns no comparison when no comparison exists yet""" response = self.get_success_response( @@ -204,7 +198,6 @@ def test_get_comparison_success_with_no_comparison(self) -> None: data = response.data assert len(data["comparisons"]) == 0 - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_success_with_no_matching_base_metric(self) -> None: """Test GET endpoint handles case where no matching base metric exists""" self.create_preprod_artifact_size_comparison( @@ -243,7 +236,6 @@ def test_get_comparison_success_with_no_matching_base_metric(self) -> None: assert watch_comparison["error_code"] == "NO_BASE_METRIC" assert watch_comparison["error_message"] == "No matching base artifact size metric found." - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_head_artifact_not_found(self) -> None: """Test GET endpoint returns 404 when head artifact doesn't exist""" response = self.get_error_response( @@ -254,7 +246,6 @@ def test_get_comparison_head_artifact_not_found(self) -> None: ) assert "The requested head preprod artifact does not exist" in response.data["detail"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_base_artifact_not_found(self) -> None: """Test GET endpoint returns 404 when base artifact doesn't exist""" response = self.get_error_response( @@ -265,7 +256,6 @@ def test_get_comparison_base_artifact_not_found(self) -> None: ) assert "The requested base preprod artifact does not exist" in response.data["detail"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_artifacts_different_projects(self) -> None: """Test GET endpoint returns 404 when head and base artifacts belong to different projects""" other_project = self.create_project(organization=self.organization) @@ -285,7 +275,6 @@ def test_get_comparison_artifacts_different_projects(self) -> None: ) assert response.data["detail"] == "The requested base preprod artifact does not exist" - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_base_artifact_wrong_project(self) -> None: """Test GET endpoint returns 404 when base artifact belongs to different project""" other_project = self.create_project(organization=self.organization) @@ -305,7 +294,6 @@ def test_get_comparison_base_artifact_wrong_project(self) -> None: ) assert response.data["detail"] == "The requested base preprod artifact does not exist" - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_head_artifact_no_size_metrics(self) -> None: """Test GET endpoint returns 404 when head artifact has no size metrics""" # Create artifact without size metrics @@ -328,7 +316,6 @@ def test_get_comparison_head_artifact_no_size_metrics(self) -> None: in response.data["detail"] ) - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_base_artifact_no_size_metrics(self) -> None: """Test GET endpoint returns 404 when base artifact has no size metrics""" # Create artifact without size metrics @@ -351,18 +338,6 @@ def test_get_comparison_base_artifact_no_size_metrics(self) -> None: in response.data["detail"] ) - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": False}) - def test_get_comparison_feature_disabled(self) -> None: - """Test GET endpoint returns 403 when feature flag is disabled""" - response = self.get_error_response( - self.organization.slug, - self.head_artifact.id, - self.base_artifact.id, - status_code=403, - ) - assert response.data["detail"] == "Feature not enabled" - - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_get_comparison_multiple_metrics(self) -> None: """Test GET endpoint handles multiple size metrics correctly""" self.create_preprod_artifact_size_comparison( @@ -425,7 +400,6 @@ def test_get_comparison_multiple_metrics(self) -> None: assert watch_comparison_data["base_size_metric_id"] == base_watch_metric.id assert watch_comparison_data["comparison_id"] == watch_comparison.id - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) @patch("sentry.preprod.size_analysis.tasks.manual_size_analysis_comparison.apply_async") def test_post_comparison_success(self, mock_apply_async): """Test POST endpoint successfully triggers comparison and creates PENDING records""" @@ -467,7 +441,6 @@ def test_post_comparison_success(self, mock_apply_async): assert comparison.state == PreprodArtifactSizeComparison.State.PENDING assert comparison.organization_id == self.organization.id - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_head_artifact_not_found(self) -> None: """Test POST endpoint returns 404 when head artifact doesn't exist""" response = self.get_error_response( @@ -479,7 +452,6 @@ def test_post_comparison_head_artifact_not_found(self) -> None: ) assert "The requested head preprod artifact does not exist" in response.data["detail"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_base_artifact_not_found(self) -> None: """Test POST endpoint returns 404 when base artifact doesn't exist""" response = self.get_error_response( @@ -491,7 +463,6 @@ def test_post_comparison_base_artifact_not_found(self) -> None: ) assert "The requested base preprod artifact does not exist" in response.data["detail"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_head_artifact_no_size_metrics(self) -> None: """Test POST endpoint returns 404 when head artifact has no size metrics""" artifact_no_metrics = self.create_preprod_artifact( @@ -510,7 +481,6 @@ def test_post_comparison_head_artifact_no_size_metrics(self) -> None: in response.json()["detail"] ) - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_base_artifact_no_size_metrics(self) -> None: """Test POST endpoint returns 404 when base artifact has no size metrics""" artifact_no_metrics = self.create_preprod_artifact( @@ -529,7 +499,6 @@ def test_post_comparison_base_artifact_no_size_metrics(self) -> None: in response.json()["detail"] ) - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_head_artifact_processing(self) -> None: self.head_size_metric.state = PreprodArtifactSizeMetrics.SizeAnalysisState.PROCESSING self.head_size_metric.save() @@ -541,7 +510,6 @@ def test_post_comparison_head_artifact_processing(self) -> None: assert data["status"] == "processing" assert "not completed size analysis yet" in data["message"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_base_artifact_processing(self) -> None: self.base_size_metric.state = PreprodArtifactSizeMetrics.SizeAnalysisState.PROCESSING self.base_size_metric.save() @@ -553,7 +521,6 @@ def test_post_comparison_base_artifact_processing(self) -> None: assert data["status"] == "processing" assert "not completed size analysis yet" in data["message"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_mixed_completed_and_pending_returns_202(self) -> None: self.create_preprod_artifact_size_metrics( self.head_artifact, @@ -578,7 +545,6 @@ def test_post_comparison_mixed_completed_and_pending_returns_202(self) -> None: assert data["status"] == "processing" assert "not completed size analysis yet" in data["message"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_existing_comparison(self) -> None: """Test POST endpoint returns existing comparison when comparison already exists""" # Create an existing comparison @@ -599,7 +565,6 @@ def test_post_comparison_existing_comparison(self) -> None: assert len(data["comparisons"]) == 1 assert data["comparisons"][0]["comparison_id"] == comparison.id - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) @patch("sentry.preprod.size_analysis.tasks.manual_size_analysis_comparison.apply_async") def test_post_comparison_existing_failed_comparison_auto_retries( self, mock_apply_async @@ -626,7 +591,6 @@ def test_post_comparison_existing_failed_comparison_auto_retries( mock_apply_async.assert_called_once() - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_cannot_compare_size_metrics(self) -> None: """Test POST endpoint returns 400 when size metrics cannot be compared""" # Create additional head metric to make the lists different lengths @@ -647,7 +611,6 @@ def test_post_comparison_cannot_compare_size_metrics(self) -> None: assert "Head has 2 metric(s)" in detail assert "base has 1 metric(s)" in detail - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) @patch("sentry.preprod.size_analysis.tasks.manual_size_analysis_comparison.apply_async") def test_post_comparison_multiple_metrics(self, mock_apply_async): """Test POST endpoint handles multiple size metrics correctly and creates PENDING records""" @@ -704,7 +667,6 @@ def test_post_comparison_multiple_metrics(self, mock_apply_async): } ) - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_no_matching_base_metric(self) -> None: """Test POST endpoint returns 400 when head has more metrics than base""" # Create head metric with different identifier that won't match base @@ -725,7 +687,6 @@ def test_post_comparison_no_matching_base_metric(self) -> None: assert "Head has 2 metric(s)" in detail assert "base has 1 metric(s)" in detail - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_mismatched_metric_types(self) -> None: """Test POST endpoint returns detailed error when comparing mismatched metric types/identifiers""" # Replace the default head metric with one that has a different identifier @@ -754,7 +715,6 @@ def test_post_comparison_mismatched_metric_types(self) -> None: # Should mention the identifiers involved assert "release" in detail.lower() or "main" in detail.lower() - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_comparison_different_build_configurations(self) -> None: """Test POST endpoint returns 400 when artifacts have different build configurations""" # Create a build configuration for the base artifact @@ -774,7 +734,6 @@ def test_post_comparison_different_build_configurations(self) -> None: ) assert response.data["detail"] == "Head and base build configurations must be the same." - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) @patch( "sentry.preprod.api.endpoints.size_analysis.project_preprod_size_analysis_compare.get_size_retention_cutoff" ) @@ -791,7 +750,6 @@ def test_get_returns_404_for_expired_head_artifact(self, mock_cutoff): assert response.status_code == 404 assert response.data["detail"] == "This build's size data has expired." - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) @patch( "sentry.preprod.api.endpoints.size_analysis.project_preprod_size_analysis_compare.get_size_retention_cutoff" ) @@ -808,7 +766,6 @@ def test_get_returns_404_for_expired_base_artifact(self, mock_cutoff): assert response.status_code == 404 assert response.data["detail"] == "This build's size data has expired." - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) @patch("sentry.preprod.size_analysis.tasks.manual_size_analysis_comparison.apply_async") def test_post_rerun_as_staff_deletes_and_recreates(self, mock_apply_async): """Test POST with rerun=true as active staff deletes existing and creates new comparisons""" @@ -837,7 +794,6 @@ def test_post_rerun_as_staff_deletes_and_recreates(self, mock_apply_async): assert data["comparisons"][0]["state"] == PreprodArtifactSizeComparison.State.PENDING mock_apply_async.assert_called_once() - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_rerun_as_non_staff_returns_403(self): """Test POST with rerun=true as non-staff returns 403""" self.create_preprod_artifact_size_comparison( @@ -853,7 +809,6 @@ def test_post_rerun_as_non_staff_returns_403(self): assert response.status_code == 403 assert response.json()["detail"] == "Only staff can rerun comparisons." - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_post_rerun_as_inactive_staff_raises_staff_required(self): """Test POST with rerun=true as is_staff user without active staff session raises StaffRequired""" self.create_preprod_artifact_size_comparison( diff --git a/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare_download.py b/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare_download.py index a4296bc18d5312..597e3924fef94b 100644 --- a/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare_download.py +++ b/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_compare_download.py @@ -1,7 +1,6 @@ from datetime import timedelta from unittest.mock import patch -from django.test import override_settings from django.urls import reverse from django.utils import timezone @@ -13,7 +12,6 @@ from sentry.testutils.cases import TestCase -@override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) class ProjectPreprodArtifactSizeAnalysisCompareDownloadEndpointTest(TestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_download.py b/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_download.py index 98b26c036321fe..814e9a435558db 100644 --- a/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_download.py +++ b/tests/sentry/preprod/api/endpoints/size_analysis/test_project_preprod_size_analysis_download.py @@ -2,18 +2,12 @@ from io import BytesIO from unittest.mock import patch -from django.test import override_settings from django.utils import timezone from sentry.preprod.models import PreprodArtifact, PreprodArtifactSizeMetrics from sentry.testutils.cases import APITestCase -@override_settings( - SENTRY_FEATURES={ - "organizations:preprod-frontend-routes": True, - } -) class ProjectPreprodArtifactSizeAnalysisDownloadEndpointTest(APITestCase): endpoint = "sentry-api-0-organization-preprod-artifact-size-analysis-download" diff --git a/tests/sentry/preprod/api/endpoints/test_builds.py b/tests/sentry/preprod/api/endpoints/test_builds.py index 5c2e466cc9df07..db784c43a0027c 100644 --- a/tests/sentry/preprod/api/endpoints/test_builds.py +++ b/tests/sentry/preprod/api/endpoints/test_builds.py @@ -7,7 +7,6 @@ from sentry.preprod.models import PreprodArtifact, PreprodArtifactSizeMetrics from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.datetime import before_now -from sentry.testutils.helpers.features import with_feature class BuildsEndpointTest(APITestCase): @@ -30,13 +29,6 @@ def _request(self, query, token=None): def _assert_is_successful(self, response): assert response.status_code == 200, f"status {response.status_code} body {response.json()}" - def test_needs_feature(self) -> None: - response = self._request({}) - assert response.status_code == 403 - assert response.json() == { - "detail": "Feature organizations:preprod-frontend-routes is not enabled for the organization." - } - def test_invalid_token(self) -> None: response = self._request({}, token="Invalid") assert response.status_code == 401 @@ -59,13 +51,11 @@ def test_missing_scopes(self) -> None: assert response.status_code == 403 assert response.json() == {"detail": "You do not have permission to perform this action."} - @with_feature("organizations:preprod-frontend-routes") def test_no_builds(self) -> None: response = self._request({}) self._assert_is_successful(response) assert response.json() == [] - @with_feature("organizations:preprod-frontend-routes") def test_one_build(self) -> None: self.create_preprod_artifact() response = self._request({}) @@ -117,21 +107,18 @@ def test_one_build(self) -> None: } ] - @with_feature("organizations:preprod-frontend-routes") def test_bad_project(self) -> None: self.create_preprod_artifact() response = self._request({"project": [1]}) assert response.status_code == 403 assert response.json() == {"detail": "You do not have permission to perform this action."} - @with_feature("organizations:preprod-frontend-routes") def test_bad_project_slug(self) -> None: self.create_preprod_artifact() response = self._request({"projectSlug": ["invalid"]}) assert response.status_code == 403 assert response.json() == {"detail": "You do not have permission to perform this action."} - @with_feature("organizations:preprod-frontend-routes") def test_build_in_another_project(self) -> None: another_project = self.create_project(name="Baz", slug="baz") self.create_preprod_artifact(project=another_project) @@ -139,7 +126,6 @@ def test_build_in_another_project(self) -> None: self._assert_is_successful(response) assert response.json() == [] - @with_feature("organizations:preprod-frontend-routes") def test_build_in_another_project_slug(self) -> None: another_project = self.create_project(name="Baz", slug="baz") self.create_preprod_artifact(project=another_project) @@ -147,21 +133,18 @@ def test_build_in_another_project_slug(self) -> None: self._assert_is_successful(response) assert response.json() == [] - @with_feature("organizations:preprod-frontend-routes") def test_build_in_this_project(self) -> None: self.create_preprod_artifact() response = self._request({"project": [self.project.id]}) self._assert_is_successful(response) assert len(response.json()) == 1 - @with_feature("organizations:preprod-frontend-routes") def test_build_in_this_project_slug(self) -> None: self.create_preprod_artifact() response = self._request({"projectSlug": [self.project.slug]}) self._assert_is_successful(response) assert len(response.json()) == 1 - @with_feature("organizations:preprod-frontend-routes") def test_multiple_projects(self) -> None: project_a = self.create_project(name="AAA", slug="aaa") self.create_preprod_artifact(project=project_a) @@ -171,7 +154,6 @@ def test_multiple_projects(self) -> None: self._assert_is_successful(response) assert len(response.json()) == 2 - @with_feature("organizations:preprod-frontend-routes") def test_multiple_project_slugs(self) -> None: project_a = self.create_project(name="AAA", slug="aaa") self.create_preprod_artifact(project=project_a) @@ -181,7 +163,6 @@ def test_multiple_project_slugs(self) -> None: self._assert_is_successful(response) assert len(response.json()) == 2 - @with_feature("organizations:preprod-frontend-routes") def test_per_page_respected(self) -> None: self.create_preprod_artifact() self.create_preprod_artifact() @@ -189,7 +170,6 @@ def test_per_page_respected(self) -> None: self._assert_is_successful(response) assert len(response.json()) == 1 - @with_feature("organizations:preprod-frontend-routes") def test_start_end_respected(self) -> None: self.create_preprod_artifact(date_added=before_now(days=5)) middle = self.create_preprod_artifact(date_added=before_now(days=3)) @@ -200,14 +180,12 @@ def test_start_end_respected(self) -> None: assert len(response.json()) == 1 assert response.json()[0]["id"] == str(middle.id) - @with_feature("organizations:preprod-frontend-routes") def test_query_invalid(self) -> None: self.create_preprod_artifact(app_id="foo") response = self._request({"query": "no_such_key:foo"}) assert response.status_code == 400 assert response.json() == {"detail": "Invalid key for this search: no_such_key"} - @with_feature("organizations:preprod-frontend-routes") def test_query_app_id_equals(self) -> None: self.create_preprod_artifact(app_id="foo") self.create_preprod_artifact(app_id="bar") @@ -216,7 +194,6 @@ def test_query_app_id_equals(self) -> None: assert len(response.json()) == 1 assert response.json()[0]["app_info"]["app_id"] == "foo" - @with_feature("organizations:preprod-frontend-routes") def test_query_app_id_not_equals(self) -> None: self.create_preprod_artifact(app_id="foo") self.create_preprod_artifact(app_id="bar") @@ -225,7 +202,6 @@ def test_query_app_id_not_equals(self) -> None: assert len(response.json()) == 1 assert response.json()[0]["app_info"]["app_id"] == "bar" - @with_feature("organizations:preprod-frontend-routes") def test_query_app_id_in(self) -> None: self.create_preprod_artifact(app_id="foo") self.create_preprod_artifact(app_id="bar") @@ -234,7 +210,6 @@ def test_query_app_id_in(self) -> None: assert len(response.json()) == 1 assert response.json()[0]["app_info"]["app_id"] == "foo" - @with_feature("organizations:preprod-frontend-routes") def test_query_app_id_in_is_list(self) -> None: self.create_preprod_artifact(app_id="foo") self.create_preprod_artifact(app_id="bar") @@ -242,7 +217,6 @@ def test_query_app_id_in_is_list(self) -> None: self._assert_is_successful(response) assert len(response.json()) == 0 - @with_feature("organizations:preprod-frontend-routes") def test_query_app_id_not_in(self) -> None: self.create_preprod_artifact(app_id="foo") self.create_preprod_artifact(app_id="bar") @@ -252,7 +226,6 @@ def test_query_app_id_not_in(self) -> None: assert len(response.json()) == 1 assert response.json()[0]["app_info"]["app_id"] == "baz" - @with_feature("organizations:preprod-frontend-routes") def test_download_count_for_installable_artifact(self) -> None: # Create an installable artifact (has both installable_app_file_id and build_number) artifact = self.create_preprod_artifact( @@ -275,7 +248,6 @@ def test_download_count_for_installable_artifact(self) -> None: assert data[0]["distribution_info"]["download_count"] == 15 assert data[0]["distribution_info"]["is_installable"] is True - @with_feature("organizations:preprod-frontend-routes") def test_is_installable(self) -> None: self.create_preprod_artifact(app_id="not_installable") artifact = self.create_preprod_artifact( @@ -291,7 +263,6 @@ def test_is_installable(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "installable" - @with_feature("organizations:preprod-frontend-routes") def test_is_not_installable(self) -> None: self.create_preprod_artifact(app_id="not_installable") artifact = self.create_preprod_artifact( @@ -307,7 +278,6 @@ def test_is_not_installable(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "not_installable" - @with_feature("organizations:preprod-frontend-routes") def test_download_count_zero_for_non_installable_artifact(self) -> None: # Create a non-installable artifact (no installable_app_file_id) self.create_preprod_artifact() @@ -319,7 +289,6 @@ def test_download_count_zero_for_non_installable_artifact(self) -> None: assert data[0]["distribution_info"]["download_count"] == 0 assert data[0]["distribution_info"]["is_installable"] is False - @with_feature("organizations:preprod-frontend-routes") def test_download_count_multiple_artifacts(self) -> None: # Create multiple installable artifacts with different download counts artifact1 = self.create_preprod_artifact( @@ -357,7 +326,6 @@ def test_download_count_multiple_artifacts(self) -> None: assert app_one["distribution_info"]["download_count"] == 100 assert app_two["distribution_info"]["download_count"] == 75 - @with_feature("organizations:preprod-frontend-routes") def test_query_install_size(self) -> None: # Create artifacts with different install sizes via size metrics small_artifact = self.create_preprod_artifact(app_id="small.app") @@ -380,7 +348,6 @@ def test_query_install_size(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "small.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_build_configuration_name(self) -> None: debug_config = self.create_preprod_build_configuration(name="Debug") release_config = self.create_preprod_build_configuration(name="Release") @@ -394,7 +361,6 @@ def test_query_build_configuration_name(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "debug.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_git_head_ref(self) -> None: main_cc = self.create_commit_comparison(organization=self.organization, head_ref="main") feature_cc = self.create_commit_comparison( @@ -410,7 +376,6 @@ def test_query_git_head_ref(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "main.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_has_git_head_ref(self) -> None: cc_with_branch = self.create_commit_comparison( organization=self.organization, head_ref="main" @@ -431,7 +396,6 @@ def test_query_has_git_head_ref(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "with_branch.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_not_has_git_head_ref(self) -> None: cc_with_branch = self.create_commit_comparison( organization=self.organization, head_ref="main" @@ -453,7 +417,6 @@ def test_query_not_has_git_head_ref(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"without_branch.app", "no_cc.app"} - @with_feature("organizations:preprod-frontend-routes") def test_query_git_head_sha(self) -> None: cc1 = self.create_commit_comparison( organization=self.organization, head_sha="abc123" + "0" * 34 @@ -471,7 +434,6 @@ def test_query_git_head_sha(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "sha1.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_git_base_sha(self) -> None: cc1 = self.create_commit_comparison( organization=self.organization, base_sha="base111" + "1" * 33 @@ -489,7 +451,6 @@ def test_query_git_base_sha(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "base1.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_git_pr_number(self) -> None: cc1 = self.create_commit_comparison(organization=self.organization, pr_number=123) cc2 = self.create_commit_comparison(organization=self.organization, pr_number=456) @@ -503,7 +464,6 @@ def test_query_git_pr_number(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "pr123.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_platform_name_apple(self) -> None: self.create_preprod_artifact( app_id="ios.app", artifact_type=PreprodArtifact.ArtifactType.XCARCHIVE @@ -518,7 +478,6 @@ def test_query_platform_name_apple(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "ios.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_platform_name_android(self) -> None: self.create_preprod_artifact( app_id="ios.app", artifact_type=PreprodArtifact.ArtifactType.XCARCHIVE @@ -537,7 +496,6 @@ def test_query_platform_name_android(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"android.apk", "android.aab"} - @with_feature("organizations:preprod-frontend-routes") def test_query_platform_name_in(self) -> None: self.create_preprod_artifact( app_id="ios.app", artifact_type=PreprodArtifact.ArtifactType.XCARCHIVE @@ -556,7 +514,6 @@ def test_query_platform_name_in(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"ios.app", "android.apk", "android.aab"} - @with_feature("organizations:preprod-frontend-routes") def test_query_platform_name_not_in(self) -> None: self.create_preprod_artifact( app_id="ios.app", artifact_type=PreprodArtifact.ArtifactType.XCARCHIVE @@ -575,7 +532,6 @@ def test_query_platform_name_not_in(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"android.apk", "android.aab"} - @with_feature("organizations:preprod-frontend-routes") def test_query_operator_greater_than(self) -> None: self.create_preprod_artifact(app_id="build100.app", build_number=100) self.create_preprod_artifact(app_id="build200.app", build_number=200) @@ -587,7 +543,6 @@ def test_query_operator_greater_than(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "build300.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_operator_less_than(self) -> None: self.create_preprod_artifact(app_id="build100.app", build_number=100) self.create_preprod_artifact(app_id="build200.app", build_number=200) @@ -599,7 +554,6 @@ def test_query_operator_less_than(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "build100.app" - @with_feature("organizations:preprod-frontend-routes") def test_query_operator_greater_than_or_equal(self) -> None: self.create_preprod_artifact(app_id="build100.app", build_number=100) self.create_preprod_artifact(app_id="build200.app", build_number=200) @@ -612,7 +566,6 @@ def test_query_operator_greater_than_or_equal(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"build200.app", "build300.app"} - @with_feature("organizations:preprod-frontend-routes") def test_query_operator_less_than_or_equal(self) -> None: self.create_preprod_artifact(app_id="build100.app", build_number=100) self.create_preprod_artifact(app_id="build200.app", build_number=200) @@ -625,7 +578,6 @@ def test_query_operator_less_than_or_equal(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"build100.app", "build200.app"} - @with_feature("organizations:preprod-frontend-routes") def test_query_operator_contains(self) -> None: cc1 = self.create_commit_comparison( organization=self.organization, head_ref="feature/add-login" @@ -648,7 +600,6 @@ def test_query_operator_contains(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"login.app", "signup.app"} - @with_feature("organizations:preprod-frontend-routes") def test_query_operator_not_contains(self) -> None: cc1 = self.create_commit_comparison( organization=self.organization, head_ref="feature/add-login" @@ -669,7 +620,6 @@ def test_query_operator_not_contains(self) -> None: data = response.json() assert data[0]["app_info"]["app_id"] == "crash.app" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_app_id(self) -> None: self.create_preprod_artifact(app_id="com.example.myapp") self.create_preprod_artifact(app_id="com.other.app") @@ -681,7 +631,6 @@ def test_free_text_search_app_id(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "com.example.myapp" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_app_name(self) -> None: self.create_preprod_artifact(app_id="com.example.one", app_name="MyAwesomeApp") self.create_preprod_artifact(app_id="com.example.two", app_name="OtherApp") @@ -692,7 +641,6 @@ def test_free_text_search_app_name(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "com.example.one" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_build_version(self) -> None: self.create_preprod_artifact(app_id="app1", build_version="1.2.3-beta") self.create_preprod_artifact(app_id="app2", build_version="2.0.0-release") @@ -703,7 +651,6 @@ def test_free_text_search_build_version(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "app1" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_commit_sha(self) -> None: cc1 = self.create_commit_comparison( organization=self.organization, head_sha="abc123def456" + "0" * 28 @@ -721,7 +668,6 @@ def test_free_text_search_commit_sha(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "app1" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_branch(self) -> None: cc1 = self.create_commit_comparison( organization=self.organization, head_ref="feature/new-login" @@ -739,7 +685,6 @@ def test_free_text_search_branch(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "app1" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_pr_number(self) -> None: cc1 = self.create_commit_comparison(organization=self.organization, pr_number=12345) cc2 = self.create_commit_comparison(organization=self.organization, pr_number=67890) @@ -753,7 +698,6 @@ def test_free_text_search_pr_number(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "app1" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_by_build_id(self) -> None: artifact1 = self.create_preprod_artifact(app_id="app1") self.create_preprod_artifact(app_id="app2") @@ -764,7 +708,6 @@ def test_free_text_search_by_build_id(self) -> None: assert len(data) == 1 assert data[0]["id"] == str(artifact1.id) - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_no_matches(self) -> None: self.create_preprod_artifact(app_id="com.example.app") self.create_preprod_artifact(app_id="com.other.app") @@ -774,7 +717,6 @@ def test_free_text_search_no_matches(self) -> None: data = response.json() assert len(data) == 0 - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_empty_query(self) -> None: self.create_preprod_artifact(app_id="app1") self.create_preprod_artifact(app_id="app2") @@ -784,7 +726,6 @@ def test_free_text_search_empty_query(self) -> None: data = response.json() assert len(data) == 2 - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_whitespace_only(self) -> None: self.create_preprod_artifact(app_id="app1") self.create_preprod_artifact(app_id="app2") @@ -794,7 +735,6 @@ def test_free_text_search_whitespace_only(self) -> None: data = response.json() assert len(data) == 2 - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_case_insensitive(self) -> None: self.create_preprod_artifact(app_id="com.Example.MyApp") @@ -804,7 +744,6 @@ def test_free_text_search_case_insensitive(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "com.Example.MyApp" - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_multiple_matches(self) -> None: self.create_preprod_artifact(app_id="com.test.one", app_name="TestApp") self.create_preprod_artifact(app_id="com.test.two", build_version="1.0-test") @@ -816,7 +755,6 @@ def test_free_text_search_multiple_matches(self) -> None: app_ids = {d["app_info"]["app_id"] for d in data} assert app_ids == {"com.test.one", "com.test.two"} - @with_feature("organizations:preprod-frontend-routes") def test_free_text_search_with_structured_filter(self) -> None: cc = self.create_commit_comparison( organization=self.organization, head_ref="feature/awesome" @@ -838,7 +776,6 @@ def test_free_text_search_with_structured_filter(self) -> None: assert len(data) == 1 assert data[0]["app_info"]["app_id"] == "com.example.android" - @with_feature("organizations:preprod-frontend-routes") def test_size_state_filter(self) -> None: artifact_not_ran = self.create_preprod_artifact(app_id="not_ran.app") self.create_preprod_artifact_size_metrics( @@ -866,7 +803,6 @@ def test_size_state_filter(self) -> None: self._assert_is_successful(response) assert len(response.json()) == 2 - @with_feature("organizations:preprod-frontend-routes") def test_size_state_filter_mixed_metrics(self) -> None: artifact = self.create_preprod_artifact(app_id="mixed.app") self.create_preprod_artifact_size_metrics( @@ -883,13 +819,11 @@ def test_size_state_filter_mixed_metrics(self) -> None: self._assert_is_successful(response) assert len(response.json()) == 0 - @with_feature("organizations:preprod-frontend-routes") def test_size_state_invalid_values(self) -> None: self.create_preprod_artifact(app_id="test.app") assert self._request({"query": "size_state:bogus"}).status_code == 400 assert self._request({"query": "size_state:[bogus, completed]"}).status_code == 400 - @with_feature("organizations:preprod-frontend-routes") def test_distribution_error_code_filter(self) -> None: self.create_preprod_artifact( app_id="quota.app", @@ -920,12 +854,10 @@ def test_distribution_error_code_filter(self) -> None: self._assert_is_successful(response) assert len(response.json()) == 2 - @with_feature("organizations:preprod-frontend-routes") def test_distribution_error_code_invalid_values(self) -> None: self.create_preprod_artifact(app_id="test.app") assert self._request({"query": "distribution_error_code:bogus"}).status_code == 400 - @with_feature("organizations:preprod-frontend-routes") @patch("sentry.preprod.api.endpoints.builds.get_size_retention_cutoff") def test_excludes_expired_artifacts(self, mock_cutoff) -> None: mock_cutoff.return_value = before_now(days=30) diff --git a/tests/sentry/preprod/api/endpoints/test_organization_preprod_artifact_assemble.py b/tests/sentry/preprod/api/endpoints/test_organization_preprod_artifact_assemble.py index e4db4f7a16e2f8..8f49dd2e737e94 100644 --- a/tests/sentry/preprod/api/endpoints/test_organization_preprod_artifact_assemble.py +++ b/tests/sentry/preprod/api/endpoints/test_organization_preprod_artifact_assemble.py @@ -19,7 +19,6 @@ from sentry.silo.base import SiloMode from sentry.tasks.assemble import AssembleTask, ChunkFileState, set_assemble_status from sentry.testutils.cases import APITestCase, TestCase -from sentry.testutils.helpers.features import Feature from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode from sentry.utils.security.orgauthtoken_token import generate_token, hash_token @@ -353,34 +352,6 @@ def setUp(self) -> None: args=[self.organization.slug, self.project.slug], ) - self.feature_context = Feature("organizations:preprod-frontend-routes") - self.feature_context.__enter__() - - def tearDown(self) -> None: - self.feature_context.__exit__(None, None, None) - super().tearDown() - - def test_feature_flag_disabled_returns_403(self) -> None: - """Test that endpoint returns 404 when feature flag is disabled.""" - self.feature_context.__exit__(None, None, None) - - try: - content = b"test content" - total_checksum = sha1(content).hexdigest() - - response = self.client.post( - self.url, - data={ - "checksum": total_checksum, - "chunks": [], - }, - HTTP_AUTHORIZATION=f"Bearer {self.token.token}", - ) - assert response.status_code == 403 - finally: - self.feature_context = Feature("organizations:preprod-frontend-routes") - self.feature_context.__enter__() - def test_assemble_json_schema_integration(self) -> None: """Integration test for schema validation through the endpoint.""" response = self.client.post( diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_delete.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_delete.py index 1f5dd3910165e7..be851a7feb9dee 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_delete.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_delete.py @@ -1,5 +1,3 @@ -from django.test import override_settings - from sentry.models.files.file import File from sentry.preprod.models import ( InstallablePreprodArtifact, @@ -19,7 +17,6 @@ def setUp(self) -> None: self.project = self.create_project(organization=self.organization) self.login_as(user=self.user) - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_delete_artifact_success(self) -> None: main_file = self.create_file(name="test_artifact.zip", type="application/zip") installable_file = self.create_file(name="test_app.ipa", type="application/octet-stream") @@ -70,7 +67,6 @@ def test_delete_artifact_success(self) -> None: assert not PreprodArtifactSizeMetrics.objects.filter(id=size_metric.id).exists() assert not InstallablePreprodArtifact.objects.filter(id=installable.id).exists() - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_delete_artifact_not_found(self) -> None: response = self.get_error_response( self.organization.slug, @@ -80,23 +76,6 @@ def test_delete_artifact_not_found(self) -> None: assert "The requested head preprod artifact does not exist" in response.data["detail"] - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": False}) - def test_delete_artifact_feature_disabled(self) -> None: - artifact = self.create_preprod_artifact( - app_name="test_artifact", - app_id="com.test.app", - build_version="1.0.0", - build_number=1, - ) - response = self.get_error_response( - self.organization.slug, - artifact.id, - status_code=403, - ) - - assert response.data["error"] == "Feature not enabled" - - @override_settings(SENTRY_FEATURES={"organizations:preprod-frontend-routes": True}) def test_delete_artifact_minimal(self) -> None: """Test deleting an artifact with only the minimum required fields""" # Create the preprod artifact without optional files diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_download.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_download.py index c38fc224de424b..83ee3fc94646db 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_download.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_artifact_download.py @@ -72,8 +72,7 @@ def test_download_preprod_artifact_no_file(self) -> None: headers = self._get_authenticated_request_headers(url) - with self.feature("organizations:preprod-frontend-routes"): - response = self.client.get(url, **headers) + response = self.client.get(url, **headers) assert response.status_code == 404 assert response.json()["detail"] == "The requested resource does not exist" diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_build_details.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_build_details.py index 77f15ee2fe65d9..771537615161af 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_build_details.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_build_details.py @@ -49,15 +49,6 @@ def setUp(self) -> None: build_number=42, ) - # Enable the feature flag for all tests by default - self.feature_context = self.feature({"organizations:preprod-frontend-routes": True}) - self.feature_context.__enter__() - - def tearDown(self) -> None: - # Exit the feature flag context manager - self.feature_context.__exit__(None, None, None) - super().tearDown() - def _get_url(self, artifact_id=None): artifact_id = artifact_id or self.preprod_artifact.id return reverse( @@ -127,15 +118,6 @@ def test_get_build_details_not_found(self) -> None: assert response.status_code == 404 assert "The requested head preprod artifact does not exist" in response.json()["detail"] - def test_get_build_details_feature_flag_disabled(self) -> None: - with self.feature({"organizations:preprod-frontend-routes": False}): - url = self._get_url() - response = self.client.get( - url, format="json", HTTP_AUTHORIZATION=f"Bearer {self.api_token.token}" - ) - assert response.status_code == 403 - assert response.json()["error"] == "Feature not enabled" - def test_get_build_details_dates_and_types(self) -> None: url = self._get_url() response = self.client.get( diff --git a/tests/sentry/preprod/api/endpoints/test_project_preprod_check_for_updates.py b/tests/sentry/preprod/api/endpoints/test_project_preprod_check_for_updates.py index 32092e37b4d1ad..40a9204b076092 100644 --- a/tests/sentry/preprod/api/endpoints/test_project_preprod_check_for_updates.py +++ b/tests/sentry/preprod/api/endpoints/test_project_preprod_check_for_updates.py @@ -28,15 +28,6 @@ def setUp(self) -> None: self.file = self.create_file(name="test_artifact.apk", type="application/octet-stream") - # Enable the feature flag for all tests by default - self.feature_context = self.feature({"organizations:preprod-frontend-routes": True}) - self.feature_context.__enter__() - - def tearDown(self) -> None: - # Exit the feature flag context manager - self.feature_context.__exit__(None, None, None) - super().tearDown() - def _get_url(self): return reverse( "sentry-api-0-project-preprod-check-for-updates", diff --git a/tests/snuba/api/endpoints/test_organization_events_preprod_size.py b/tests/snuba/api/endpoints/test_organization_events_preprod_size.py index 4ba9c5d5659488..e584d2a044a562 100644 --- a/tests/snuba/api/endpoints/test_organization_events_preprod_size.py +++ b/tests/snuba/api/endpoints/test_organization_events_preprod_size.py @@ -20,7 +20,7 @@ def setUp(self) -> None: def _do_request(self, data, features=None): if features is None: - features = {"organizations:preprod-frontend-routes": True} + features = {} features.update(self.features) url = reverse( "sentry-api-0-organization-events", diff --git a/tests/snuba/api/endpoints/test_organization_events_stats_preprod_size.py b/tests/snuba/api/endpoints/test_organization_events_stats_preprod_size.py index 587440d9ef16f1..5b42b558aa9a37 100644 --- a/tests/snuba/api/endpoints/test_organization_events_stats_preprod_size.py +++ b/tests/snuba/api/endpoints/test_organization_events_stats_preprod_size.py @@ -27,7 +27,7 @@ def setUp(self) -> None: def _do_request(self, data, url=None, features=None): if features is None: - features = {"organizations:preprod-frontend-routes": True} + features = {} features.update(self.features) with self.feature(features): return self.client.get(self.url if url is None else url, data=data, format="json") From d31548857dc2195fb39ac99237317a739269ed92 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:25:09 +0000 Subject: [PATCH 2/2] :hammer_and_wrench: apply pre-commit fixes --- static/app/views/releases/list/index.tsx | 362 +++++++++++------------ 1 file changed, 166 insertions(+), 196 deletions(-) diff --git a/static/app/views/releases/list/index.tsx b/static/app/views/releases/list/index.tsx index fa652f0a7f76cd..fd89ee0626c112 100644 --- a/static/app/views/releases/list/index.tsx +++ b/static/app/views/releases/list/index.tsx @@ -1,75 +1,69 @@ -import { Fragment, useCallback, useEffect, useMemo } from "react"; -import { forceCheck } from "react-lazyload"; -import styled from "@emotion/styled"; -import { keepPreviousData, useQuery } from "@tanstack/react-query"; - -import { FeatureBadge } from "@sentry/scraps/badge"; -import { Flex, Stack } from "@sentry/scraps/layout"; -import { TabList } from "@sentry/scraps/tabs"; - -import { fetchTagValues } from "sentry/actionCreators/tags"; -import { FeedbackButton } from "sentry/components/feedbackButton/feedbackButton"; -import * as Layout from "sentry/components/layouts/thirds"; -import { LoadingError } from "sentry/components/loadingError"; -import { NoProjectMessage } from "sentry/components/noProjectMessage"; -import { ALL_ACCESS_PROJECTS } from "sentry/components/pageFilters/constants"; -import { PageFiltersContainer } from "sentry/components/pageFilters/container"; -import { DatePageFilter } from "sentry/components/pageFilters/date/datePageFilter"; -import { EnvironmentPageFilter } from "sentry/components/pageFilters/environment/environmentPageFilter"; -import { PageFilterBar } from "sentry/components/pageFilters/pageFilterBar"; -import { normalizeDateTimeParams } from "sentry/components/pageFilters/parse"; -import { ProjectPageFilter } from "sentry/components/pageFilters/project/projectPageFilter"; -import { usePageFilters } from "sentry/components/pageFilters/usePageFilters"; -import { PageHeadingQuestionTooltip } from "sentry/components/pageHeadingQuestionTooltip"; -import { PreprodBuildsDisplay } from "sentry/components/preprod/preprodBuildsDisplay"; -import { SearchQueryBuilder } from "sentry/components/searchQueryBuilder"; -import type { GetTagValues } from "sentry/components/searchQueryBuilder"; -import { SentryDocumentTitle } from "sentry/components/sentryDocumentTitle"; -import { ReleasesSortOption } from "sentry/constants/releases"; -import { t } from "sentry/locale"; -import { ProjectsStore } from "sentry/stores/projectsStore"; -import type { TagCollection } from "sentry/types/group"; -import type { Release } from "sentry/types/release"; -import { ReleaseStatus } from "sentry/types/release"; -import { trackAnalytics } from "sentry/utils/analytics"; -import { apiOptions, selectJsonWithHeaders } from "sentry/utils/api/apiOptions"; -import { DemoTourElement, DemoTourStep } from "sentry/utils/demoMode/demoTours"; -import { SEMVER_TAGS } from "sentry/utils/discover/fields"; -import { FieldKey } from "sentry/utils/fields"; -import { decodeScalar } from "sentry/utils/queryString"; -import { RequestError } from "sentry/utils/requestError/requestError"; -import { useApi } from "sentry/utils/useApi"; -import { useLocation } from "sentry/utils/useLocation"; -import { useNavigate } from "sentry/utils/useNavigate"; -import { useOrganization } from "sentry/utils/useOrganization"; -import { useProjects } from "sentry/utils/useProjects"; -import { TopBar } from "sentry/views/navigation/topBar"; -import { useHasPageFrameFeature } from "sentry/views/navigation/useHasPageFrameFeature"; -import { buildDetailsApiOptions } from "sentry/views/preprod/utils/buildDetailsApiOptions"; -import { ReleaseArchivedNotice } from "sentry/views/releases/detail/overview/releaseArchivedNotice"; -import { MobileBuilds } from "sentry/views/releases/list/mobileBuilds"; -import { ReleaseHealthCTA } from "sentry/views/releases/list/releaseHealthCTA"; -import { ReleaseListInner } from "sentry/views/releases/list/releaseListInner"; -import { isMobileRelease } from "sentry/views/releases/utils"; - -import { - ReleasesDisplayOption, - ReleasesDisplayOptions, -} from "./releasesDisplayOptions"; -import { ReleasesSortOptions } from "./releasesSortOptions"; -import { - ReleasesStatusOption, - ReleasesStatusOptions, -} from "./releasesStatusOptions"; -import { validateSummaryStatsPeriod } from "./utils"; - -type ReleaseTab = "releases" | "mobile-builds" | "snapshots"; +import {Fragment, useCallback, useEffect, useMemo} from 'react'; +import {forceCheck} from 'react-lazyload'; +import styled from '@emotion/styled'; +import {keepPreviousData, useQuery} from '@tanstack/react-query'; + +import {FeatureBadge} from '@sentry/scraps/badge'; +import {Flex, Stack} from '@sentry/scraps/layout'; +import {TabList} from '@sentry/scraps/tabs'; + +import {fetchTagValues} from 'sentry/actionCreators/tags'; +import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; +import * as Layout from 'sentry/components/layouts/thirds'; +import {LoadingError} from 'sentry/components/loadingError'; +import {NoProjectMessage} from 'sentry/components/noProjectMessage'; +import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants'; +import {PageFiltersContainer} from 'sentry/components/pageFilters/container'; +import {DatePageFilter} from 'sentry/components/pageFilters/date/datePageFilter'; +import {EnvironmentPageFilter} from 'sentry/components/pageFilters/environment/environmentPageFilter'; +import {PageFilterBar} from 'sentry/components/pageFilters/pageFilterBar'; +import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; +import {ProjectPageFilter} from 'sentry/components/pageFilters/project/projectPageFilter'; +import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; +import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip'; +import {PreprodBuildsDisplay} from 'sentry/components/preprod/preprodBuildsDisplay'; +import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; +import type {GetTagValues} from 'sentry/components/searchQueryBuilder'; +import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; +import {ReleasesSortOption} from 'sentry/constants/releases'; +import {t} from 'sentry/locale'; +import {ProjectsStore} from 'sentry/stores/projectsStore'; +import type {TagCollection} from 'sentry/types/group'; +import type {Release} from 'sentry/types/release'; +import {ReleaseStatus} from 'sentry/types/release'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; +import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours'; +import {SEMVER_TAGS} from 'sentry/utils/discover/fields'; +import {FieldKey} from 'sentry/utils/fields'; +import {decodeScalar} from 'sentry/utils/queryString'; +import {RequestError} from 'sentry/utils/requestError/requestError'; +import {useApi} from 'sentry/utils/useApi'; +import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import {useOrganization} from 'sentry/utils/useOrganization'; +import {useProjects} from 'sentry/utils/useProjects'; +import {TopBar} from 'sentry/views/navigation/topBar'; +import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature'; +import {buildDetailsApiOptions} from 'sentry/views/preprod/utils/buildDetailsApiOptions'; +import {ReleaseArchivedNotice} from 'sentry/views/releases/detail/overview/releaseArchivedNotice'; +import {MobileBuilds} from 'sentry/views/releases/list/mobileBuilds'; +import {ReleaseHealthCTA} from 'sentry/views/releases/list/releaseHealthCTA'; +import {ReleaseListInner} from 'sentry/views/releases/list/releaseListInner'; +import {isMobileRelease} from 'sentry/views/releases/utils'; + +import {ReleasesDisplayOption, ReleasesDisplayOptions} from './releasesDisplayOptions'; +import {ReleasesSortOptions} from './releasesSortOptions'; +import {ReleasesStatusOption, ReleasesStatusOptions} from './releasesStatusOptions'; +import {validateSummaryStatsPeriod} from './utils'; + +type ReleaseTab = 'releases' | 'mobile-builds' | 'snapshots'; const RELEASE_FILTER_KEYS = [ ...Object.values(SEMVER_TAGS), { - key: "release", - name: "release", + key: 'release', + name: 'release', }, { key: FieldKey.RELEASE_CREATED, @@ -91,45 +85,42 @@ function makeReleaseListApiOptions({ activeSort?: ReleasesSortOption; activeStatus?: ReleasesStatusOption; }) { - return apiOptions.as()( - "/organizations/$organizationIdOrSlug/releases/", - { - path: { organizationIdOrSlug: organizationSlug }, - query: { - project: location.query.project, - environment: location.query.environment, - cursor: location.query.cursor, - query: location.query.query, - sort: location.query.sort, - summaryStatsPeriod: validateSummaryStatsPeriod( - decodeScalar(location.query.statsPeriod), - ), - per_page: 20, - flatten: activeSort === ReleasesSortOption.DATE ? 0 : 1, - adoptionStages: 1, - status: - activeStatus === ReleasesStatusOption.ARCHIVED - ? ReleaseStatus.ARCHIVED - : ReleaseStatus.ACTIVE, - }, - staleTime: Infinity, + return apiOptions.as()('/organizations/$organizationIdOrSlug/releases/', { + path: {organizationIdOrSlug: organizationSlug}, + query: { + project: location.query.project, + environment: location.query.environment, + cursor: location.query.cursor, + query: location.query.query, + sort: location.query.sort, + summaryStatsPeriod: validateSummaryStatsPeriod( + decodeScalar(location.query.statsPeriod) + ), + per_page: 20, + flatten: activeSort === ReleasesSortOption.DATE ? 0 : 1, + adoptionStages: 1, + status: + activeStatus === ReleasesStatusOption.ARCHIVED + ? ReleaseStatus.ARCHIVED + : ReleaseStatus.ACTIVE, }, - ); + staleTime: Infinity, + }); } const releasesFeedbackOptions = { - messagePlaceholder: t("How can we improve the Releases experience?"), + messagePlaceholder: t('How can we improve the Releases experience?'), tags: { - ["feedback.source"]: "releases-list-header", + ['feedback.source']: 'releases-list-header', }, }; export default function ReleasesList() { - const api = useApi({ persistInFlight: true }); + const api = useApi({persistInFlight: true}); const organization = useOrganization(); const hasPageFrameFeature = useHasPageFrameFeature(); - const { projects } = useProjects(); - const { selection } = usePageFilters(); + const {projects} = useProjects(); + const {selection} = usePageFilters(); const location = useLocation(); const navigate = useNavigate(); @@ -148,18 +139,18 @@ export default function ReleasesList() { statsPeriod: validatedStatsPeriod, }, }, - { replace: true }, + {replace: true} ); } }, [navigate, location.pathname, location.query]); const activeQuery = useMemo(() => { - const { query: locationQuery } = location.query; - return typeof locationQuery === "string" ? locationQuery : ""; + const {query: locationQuery} = location.query; + return typeof locationQuery === 'string' ? locationQuery : ''; }, [location.query]); const activeSort = useMemo(() => { - const { sort: locationSort } = location.query; + const {sort: locationSort} = location.query; // Require 1 environment for date adopted if ( @@ -170,7 +161,7 @@ export default function ReleasesList() { } const sortExists = Object.values(ReleasesSortOption).includes( - locationSort as ReleasesSortOption, + locationSort as ReleasesSortOption ); if (sortExists) { return locationSort as ReleasesSortOption; @@ -180,7 +171,7 @@ export default function ReleasesList() { }, [selection.environments, location.query]); const activeDisplay = useMemo(() => { - const { display: locationDisplay } = location.query; + const {display: locationDisplay} = location.query; switch (locationDisplay) { case ReleasesDisplayOption.USERS: @@ -191,7 +182,7 @@ export default function ReleasesList() { }, [location.query]); const activeStatus = useMemo(() => { - const { status } = location.query; + const {status} = location.query; switch (status) { case ReleasesStatusOption.ARCHIVED: @@ -238,40 +229,34 @@ export default function ReleasesList() { return projects[0]; } - const selectedProjectId = - selection.projects?.length === 1 && selection.projects[0]; - return projects?.find((p) => p.id === `${selectedProjectId}`); + const selectedProjectId = selection.projects?.length === 1 && selection.projects[0]; + return projects?.find(p => p.id === `${selectedProjectId}`); }, [selection.projects, projects]); // Get selected project IDs, handling "All Projects" case const selectedProjectIds = useMemo(() => { - const selectedIds = selection.projects.filter( - (id) => id !== ALL_ACCESS_PROJECTS, - ); + const selectedIds = selection.projects.filter(id => id !== ALL_ACCESS_PROJECTS); // If no specific projects selected, pass [-1] to represent "all projects" // This avoids expanding to hundreds of project IDs which causes URL length issues return selectedIds.length === 0 ? [`${ALL_ACCESS_PROJECTS}`] - : selectedIds.map((id) => `${id}`); + : selectedIds.map(id => `${id}`); }, [selection.projects]); - const hasSnapshotsFeature = - organization.features?.includes("preprod-snapshots"); + const hasSnapshotsFeature = organization.features?.includes('preprod-snapshots'); - const { statsPeriod, start, end, utc } = normalizeDateTimeParams( - location.query, - ); + const {statsPeriod, start, end, utc} = normalizeDateTimeParams(location.query); const buildsProbeQuery = useQuery({ ...buildDetailsApiOptions({ organization, queryParams: { per_page: 1, project: selectedProjectIds, - ...(statsPeriod && { statsPeriod }), - ...(start && { start }), - ...(end && { end }), - ...(utc && { utc }), + ...(statsPeriod && {statsPeriod}), + ...(start && {start}), + ...(end && {end}), + ...(utc && {utc}), }, }), staleTime: 60_000, @@ -283,39 +268,34 @@ export default function ReleasesList() { selectedProjectIds.length === 1 && selectedProjectIds[0] === `${ALL_ACCESS_PROJECTS}`; const projectIdsToCheck = isAllProjects - ? projects.map((p) => p.id) + ? projects.map(p => p.id) : selectedProjectIds; return projectIdsToCheck - .map((id) => ProjectsStore.getById(id)) + .map(id => ProjectsStore.getById(id)) .filter(Boolean) - .some( - (project) => - project?.platform && isMobileRelease(project.platform, false), - ); + .some(project => project?.platform && isMobileRelease(project.platform, false)); }, [selectedProjectIds, projects]); const hasBuildsData = !buildsProbeQuery.isPending && (buildsProbeQuery.data?.length ?? 0) > 0; - const shouldShowMobileBuildsTab = - hasBuildsData || hasAnyStrictlyMobileProject; + const shouldShowMobileBuildsTab = hasBuildsData || hasAnyStrictlyMobileProject; const shouldShowSnapshotsTab = !!hasSnapshotsFeature; - const shouldShowPreprodTabs = - shouldShowMobileBuildsTab || shouldShowSnapshotsTab; + const shouldShowPreprodTabs = shouldShowMobileBuildsTab || shouldShowSnapshotsTab; const selectedTab = useMemo(() => { if (!shouldShowPreprodTabs) { - return "releases"; + return 'releases'; } const tab = decodeScalar(location.query.tab) as ReleaseTab | undefined; - if (tab === "snapshots" && !shouldShowSnapshotsTab) { - return "releases"; + if (tab === 'snapshots' && !shouldShowSnapshotsTab) { + return 'releases'; } - if (tab === "mobile-builds" && !shouldShowMobileBuildsTab) { - return "releases"; + if (tab === 'mobile-builds' && !shouldShowMobileBuildsTab) { + return 'releases'; } - return tab || "releases"; + return tab || 'releases'; }, [ shouldShowPreprodTabs, shouldShowMobileBuildsTab, @@ -327,20 +307,20 @@ export default function ReleasesList() { (query: string) => { navigate({ ...location, - query: { ...location.query, cursor: undefined, query }, + query: {...location.query, cursor: undefined, query}, }); }, - [location, navigate], + [location, navigate] ); const handleSortBy = useCallback( (sort: string) => { navigate({ ...location, - query: { ...location.query, cursor: undefined, sort }, + query: {...location.query, cursor: undefined, sort}, }); }, - [location, navigate], + [location, navigate] ); const handleDisplay = useCallback( @@ -370,41 +350,41 @@ export default function ReleasesList() { navigate({ ...location, - query: { ...location.query, cursor: undefined, display, sort }, + query: {...location.query, cursor: undefined, display, sort}, }); }, - [location, navigate], + [location, navigate] ); const handleStatus = useCallback( (status: string) => { navigate({ ...location, - query: { ...location.query, cursor: undefined, status }, + query: {...location.query, cursor: undefined, status}, }); }, - [location, navigate], + [location, navigate] ); const handleTabChange = useCallback( (newTab: string) => { - if (newTab === "mobile-builds") { - trackAnalytics("preprod.releases.mobile-builds.tab-clicked", { + if (newTab === 'mobile-builds') { + trackAnalytics('preprod.releases.mobile-builds.tab-clicked', { organization, }); } }, - [organization], + [organization] ); const tagValueLoader = useCallback( (key: string, search: string) => { - const { project } = location.query; + const {project} = location.query; // Coerce the url param into an array const projectIds = Array.isArray(project) ? project - : typeof project === "string" + : typeof project === 'string' ? [project] : []; @@ -417,15 +397,15 @@ export default function ReleasesList() { endpointParams: normalizeDateTimeParams(location.query), }); }, - [api, location, organization], + [api, location, organization] ); const getTagValues = useCallback( async (tag, currentQuery) => { const values = await tagValueLoader(tag.key, currentQuery); - return values.map(({ value }) => value); + return values.map(({value}) => value); }, - [tagValueLoader], + [tagValueLoader] ); const hasAnyMobileProject = useMemo(() => { @@ -435,15 +415,13 @@ export default function ReleasesList() { selectedProjectIds.length === 1 && selectedProjectIds[0] === `${ALL_ACCESS_PROJECTS}`; const projectIdsToCheck = isAllProjects - ? projects.map((p) => p.id) + ? projects.map(p => p.id) : selectedProjectIds; return projectIdsToCheck - .map((id) => ProjectsStore.getById(id)) + .map(id => ProjectsStore.getById(id)) .filter(Boolean) - .some( - (project) => project?.platform && isMobileRelease(project.platform), - ); + .some(project => project?.platform && isMobileRelease(project.platform)); }, [selectedProjectIds, projects]); const showReleaseAdoptionStages = @@ -451,9 +429,9 @@ export default function ReleasesList() { const shouldShowQuickstart = Boolean( selectedProject && // Has not set up releases - !selectedProject?.features.includes("releases") && + !selectedProject?.features.includes('releases') && // Has no releases - !releases?.length, + !releases?.length ); const releasesPageLinks = data?.headers.Link; @@ -465,12 +443,12 @@ export default function ReleasesList() { // eslint-disable-next-line @typescript-eslint/no-base-to-string return String(releasesError.responseJSON?.detail); } - return t("There was an error loading releases"); + return t('There was an error loading releases'); }, [releasesError]); return ( - + @@ -478,11 +456,11 @@ export default function ReleasesList() { - {t("Releases")} + {t('Releases')} @@ -507,24 +485,20 @@ export default function ReleasesList() { {shouldShowPreprodTabs && ( - - + + - {t("Releases")} + {t('Releases')} - {t("Mobile Builds")} + {t('Mobile Builds')} @@ -565,13 +539,13 @@ export default function ReleasesList() { query: { ...location.query, query: undefined, - tab: "snapshots", + tab: 'snapshots', }, }} - textValue={t("Snapshots")} + textValue={t('Snapshots')} > - {t("Snapshots")} + {t('Snapshots')} @@ -583,14 +557,14 @@ export default function ReleasesList() { - {selectedTab === "mobile-builds" && ( + {selectedTab === 'mobile-builds' && ( )} - {selectedTab === "snapshots" && shouldShowSnapshotsTab && ( + {selectedTab === 'snapshots' && shouldShowSnapshotsTab && ( )} - {selectedTab === "releases" && ( + {selectedTab === 'releases' && ( - {(props) => ( + {props => (
)} @@ -685,27 +655,27 @@ export default function ReleasesList() { const ReleasesPageFilterBar = styled(PageFilterBar)<{ shouldShowPreprodTabs: boolean; }>` - ${(p) => !p.shouldShowPreprodTabs && `margin-bottom: ${p.theme.space.xl};`} + ${p => !p.shouldShowPreprodTabs && `margin-bottom: ${p.theme.space.xl};`} `; -const SortAndFilterWrapper = styled("div")` +const SortAndFilterWrapper = styled('div')` display: grid; grid-template-columns: 1fr repeat(3, max-content); - gap: ${(p) => p.theme.space.xl}; + gap: ${p => p.theme.space.xl}; - @media (max-width: ${(p) => p.theme.breakpoints.md}) { + @media (max-width: ${p => p.theme.breakpoints.md}) { grid-template-columns: repeat(3, 1fr); & > div { width: auto; } } - @media (max-width: ${(p) => p.theme.breakpoints.sm}) { + @media (max-width: ${p => p.theme.breakpoints.sm}) { grid-template-columns: minmax(0, 1fr); } `; const StyledSearchQueryBuilder = styled(SearchQueryBuilder)` - @media (max-width: ${(p) => p.theme.breakpoints.md}) { + @media (max-width: ${p => p.theme.breakpoints.md}) { grid-column: 1 / -1; } `;