diff --git a/.agents/skills/hybrid-cloud-rpc/SKILL.md b/.agents/skills/hybrid-cloud-rpc/SKILL.md index 9b306a8ce766ae..1d883d872f388f 100644 --- a/.agents/skills/hybrid-cloud-rpc/SKILL.md +++ b/.agents/skills/hybrid-cloud-rpc/SKILL.md @@ -41,7 +41,7 @@ The service's `local_mode` determines where the database-backed implementation r | Data lives in... | `local_mode` | Decorator on methods | Example | | ------------------------------------------------- | ------------------ | ------------------------------- | ---------------------------------- | -| Region silo (projects, events, issues, org data) | `SiloMode.CELL` | `@cell_rpc_method(resolve=...)` | `OrganizationService` | +| Cell silo (projects, events, issues, org data) | `SiloMode.CELL` | `@cell_rpc_method(resolve=...)` | `OrganizationService` | | Control silo (users, auth, billing, org mappings) | `SiloMode.CONTROL` | `@rpc_method` | `OrganizationMemberMappingService` | **Decision rule**: If the Django models you need to query live in the cell database, use `SiloMode.CELL`. If they live in the control database, use `SiloMode.CONTROL`. @@ -95,7 +95,7 @@ If your service doesn't fit any of these, add a new entry to the `service_packag ## Step 4: Add or Update Methods -### For REGION silo services +### For CELL silo services Load `references/resolvers.md` for resolver details. @@ -217,11 +217,11 @@ Every RPC service needs three categories of tests: **silo mode compatibility**, ### 7.1 Silo mode compatibility with `@all_silo_test` -Every service test class MUST use `@all_silo_test` so tests run in all three modes (MONOLITH, REGION, CONTROL). This ensures the delegation layer works for both local and remote dispatch paths. +Every service test class MUST use `@all_silo_test` so tests run in all three modes (MONOLITH, CELL, CONTROL). This ensures the delegation layer works for both local and remote dispatch paths. ```python from sentry.testutils.cases import TestCase, TransactionTestCase -from sentry.testutils.silo import all_silo_test, assume_test_silo_mode, create_test_regions +from sentry.testutils.silo import all_silo_test, assume_test_silo_mode, create_test_cells @all_silo_test class MyServiceTest(TestCase): @@ -234,8 +234,8 @@ class MyServiceTest(TestCase): For tests that need named cells (e.g., testing cell resolution): ```python -@all_silo_test(regions=create_test_regions("us", "eu")) -class MyServiceRegionTest(TransactionTestCase): +@all_silo_test(cells=create_test_cells("us", "eu")) +class MyServiceCellTest(TransactionTestCase): ... ``` @@ -403,7 +403,7 @@ from sentry.testutils.silo import ( cell_silo_test, assume_test_silo_mode, assume_test_silo_mode_of, - create_test_regions, + create_test_cells, ) from sentry.testutils.outbox import outbox_runner from sentry.testutils.hybrid_cloud import HybridCloudTestMixin @@ -423,7 +423,7 @@ Before submitting your PR, verify: - [ ] All RPC method parameters are keyword-only (`*` separator) - [ ] All parameters have explicit type annotations - [ ] All types are serializable (primitives, RpcModel, list, Optional, dict, Enum, datetime) -- [ ] Region service methods have `@cell_rpc_method` with appropriate resolver +- [ ] Cell service methods have `@cell_rpc_method` with appropriate resolver - [ ] Control service methods have `@rpc_method` - [ ] `@cell_rpc_method` / `@rpc_method` comes BEFORE `@abstractmethod` - [ ] `create_delegation()` is called at module level at the bottom of service.py diff --git a/.agents/skills/hybrid-cloud-test-gen/SKILL.md b/.agents/skills/hybrid-cloud-test-gen/SKILL.md index 95a2bccdb48d66..6507a24eeb3ecb 100644 --- a/.agents/skills/hybrid-cloud-test-gen/SKILL.md +++ b/.agents/skills/hybrid-cloud-test-gen/SKILL.md @@ -65,17 +65,17 @@ RPC service tests must cover: ### Quick Reference — Decorator & Base Class -| Scenario | Decorator | Base Class | -| ---------------------------------- | --------------------------------------------------- | -------------------------------- | -| Standard RPC service | `@all_silo_test` | `TestCase` | -| RPC with named regions | `@all_silo_test(regions=create_test_regions("us"))` | `TestCase` | -| RPC with member mapping assertions | `@all_silo_test` | `TestCase, HybridCloudTestMixin` | +| Scenario | Decorator | Base Class | +| ---------------------------------- | ----------------------------------------------- | -------------------------------- | +| Standard RPC service | `@all_silo_test` | `TestCase` | +| RPC with named cells | `@all_silo_test(cells=create_test_cells("us"))` | `TestCase` | +| RPC with member mapping assertions | `@all_silo_test` | `TestCase, HybridCloudTestMixin` | ## Step 4: Generate API Gateway Tests Load `references/api-gateway-tests.md` for complete templates and patterns. -API gateway tests verify that requests to control-silo endpoints are correctly proxied to the appropriate region. They must cover: +API gateway tests verify that requests to control-silo endpoints are correctly proxied to the appropriate cell. They must cover: - **Proxy pass-through**: Requests forwarded with correct params, headers, body - **Query parameter forwarding**: Multi-value params preserved @@ -84,9 +84,9 @@ API gateway tests verify that requests to control-silo endpoints are correctly p ### Quick Reference — Decorator & Base Class -| Scenario | Decorator | Base Class | -| --------------------- | ------------------------------------------------------------------------------------ | -------------------- | -| Standard gateway test | `@control_silo_test(regions=[ApiGatewayTestCase.REGION], include_monolith_run=True)` | `ApiGatewayTestCase` | +| Scenario | Decorator | Base Class | +| --------------------- | -------------------------------------------------------------------------------- | -------------------- | +| Standard gateway test | `@control_silo_test(cells=[ApiGatewayTestCase.CELL], include_monolith_run=True)` | `ApiGatewayTestCase` | ## Step 5: Generate Outbox Pattern Tests @@ -120,12 +120,12 @@ Endpoint silo tests verify that API endpoints work correctly under their declare ### Quick Reference — Decorator Mapping -| Endpoint Decorator | Test Decorator | -| ------------------------------------- | ------------------------------------------------------- | -| `@cell_silo_endpoint ` | `@cell_silo_test` | -| `@control_silo_endpoint` | `@control_silo_test` | -| `@control_silo_endpoint` (with proxy) | `@control_silo_test(regions=create_test_regions("us"))` | -| No decorator (monolith-only) | `@no_silo_test` | +| Endpoint Decorator | Test Decorator | +| ------------------------------------- | --------------------------------------------------- | +| `@cell_silo_endpoint ` | `@cell_silo_test` | +| `@control_silo_endpoint` | `@control_silo_test` | +| `@control_silo_endpoint` (with proxy) | `@control_silo_test(cells=create_test_cells("us"))` | +| No decorator (monolith-only) | `@no_silo_test` | ## Step 7: Validate @@ -153,7 +153,7 @@ from sentry.testutils.silo import ( no_silo_test, assume_test_silo_mode, assume_test_silo_mode_of, - create_test_regions, + create_test_cells, ) # Base classes diff --git a/.agents/skills/hybrid-cloud-test-gen/references/api-gateway-tests.md b/.agents/skills/hybrid-cloud-test-gen/references/api-gateway-tests.md index 8344544f10e949..ea999d7508ede8 100644 --- a/.agents/skills/hybrid-cloud-test-gen/references/api-gateway-tests.md +++ b/.agents/skills/hybrid-cloud-test-gen/references/api-gateway-tests.md @@ -26,7 +26,7 @@ from sentry.utils import json ## Template: Standard API Gateway Test ```python -@control_silo_test(regions=[ApiGatewayTestCase.REGION], include_monolith_run=True) +@control_silo_test(cells=[ApiGatewayTestCase.CELL], include_monolith_run=True) class Test{Feature}ApiGateway(ApiGatewayTestCase): @responses.activate @@ -36,7 +36,7 @@ class Test{Feature}ApiGateway(ApiGatewayTestCase): headers = dict(example="this") responses.add_callback( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/{endpoint_path}/", + f"{self.CELL.address}/organizations/{self.organization.slug}/{endpoint_path}/", verify_request_params(query_params, headers), ) @@ -58,7 +58,7 @@ class Test{Feature}ApiGateway(ApiGatewayTestCase): headers = {"content-type": "application/json"} responses.add_callback( responses.POST, - f"{self.REGION.address}/organizations/{self.organization.slug}/{endpoint_path}/", + f"{self.CELL.address}/organizations/{self.organization.slug}/{endpoint_path}/", verify_request_body(request_body, headers), ) @@ -80,7 +80,7 @@ class Test{Feature}ApiGateway(ApiGatewayTestCase): """Verify upstream errors are forwarded to the client.""" responses.add( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/{endpoint_path}/", + f"{self.CELL.address}/organizations/{self.organization.slug}/{endpoint_path}/", status=400, json={"detail": "Bad request"}, ) @@ -104,7 +104,7 @@ In CONTROL mode, proxied responses are streamed. Use `close_streaming_response() """Verify proxied response content is correct.""" responses.add_callback( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/{endpoint_path}/", + f"{self.CELL.address}/organizations/{self.organization.slug}/{endpoint_path}/", verify_request_params({}, {}), ) @@ -128,7 +128,7 @@ In CONTROL mode, proxied responses are streamed. Use `close_streaming_response() ## Template: SiloLimit Availability Check ```python - def test_control_only_endpoint_unavailable_in_region(self): + def test_control_only_endpoint_unavailable_in_cell(self): """Verify control-only endpoints raise AvailabilityError outside their silo.""" with pytest.raises(SiloLimit.AvailabilityError): self.client.get("/api/0/{control-only-path}/") @@ -136,12 +136,12 @@ In CONTROL mode, proxied responses are streamed. Use `close_streaming_response() ## Key Patterns -- **`ApiGatewayTestCase`** sets up a test region, mock HTTP callbacks, and the API gateway middleware. It extends `APITestCase`. -- **`@control_silo_test(regions=[...], include_monolith_run=True)`** runs the test in both CONTROL and MONOLITH modes. -- **Every test method MUST use `@responses.activate`** because gateway tests mock HTTP calls to the region address. +- **`ApiGatewayTestCase`** sets up a test cell, mock HTTP callbacks, and the API gateway middleware. It extends `APITestCase`. +- **`@control_silo_test(cells=[...], include_monolith_run=True)`** runs the test in both CONTROL and MONOLITH modes. +- **Every test method MUST use `@responses.activate`** because gateway tests mock HTTP calls to the cell address. - **`verify_request_params(params, headers)`** is a callback that asserts query params and headers match. - **`verify_request_body(body, headers)`** asserts POST body matches. - **`close_streaming_response(resp)`** reads a streaming response to bytes — required for proxied responses in CONTROL mode. - **`override_settings(MIDDLEWARE=tuple(self.middleware))`** ensures the API gateway middleware is active. -- **`self.REGION`** is a pre-configured `Region` object with address `http://us.internal.sentry.io`. -- **`self.organization`** is pre-created in `setUp` and bound to `self.REGION`. +- **`self.CELL`** is a pre-configured `Cell` object with address `http://us.internal.sentry.io`. +- **`self.organization`** is pre-created in `setUp` and bound to `self.CELL`. diff --git a/.agents/skills/hybrid-cloud-test-gen/references/endpoint-silo-tests.md b/.agents/skills/hybrid-cloud-test-gen/references/endpoint-silo-tests.md index 17c76397c53cee..7ce8e2a8140735 100644 --- a/.agents/skills/hybrid-cloud-test-gen/references/endpoint-silo-tests.md +++ b/.agents/skills/hybrid-cloud-test-gen/references/endpoint-silo-tests.md @@ -10,7 +10,7 @@ from sentry.testutils.silo import ( no_silo_test, assume_test_silo_mode, assume_test_silo_mode_of, - create_test_regions, + create_test_cells, ) from sentry.silo.base import SiloMode ``` @@ -19,14 +19,14 @@ from sentry.silo.base import SiloMode Match the endpoint's silo decorator to the test's silo decorator: -| Endpoint Decorator | Test Decorator | -| -------------------------------------------- | ------------------------------------------------------- | -| `@cell_silo_endpoint` | `@cell_silo_test` | -| `@control_silo_endpoint` | `@control_silo_test` | -| `@control_silo_endpoint` (proxies to region) | `@control_silo_test(regions=create_test_regions("us"))` | -| No silo decorator | `@no_silo_test` | +| Endpoint Decorator | Test Decorator | +| ------------------------------------------ | --------------------------------------------------- | +| `@cell_silo_endpoint` | `@cell_silo_test` | +| `@control_silo_endpoint` | `@control_silo_test` | +| `@control_silo_endpoint` (proxies to cell) | `@control_silo_test(cells=create_test_cells("us"))` | +| No silo decorator | `@no_silo_test` | -## Template: Region Silo Endpoint Test +## Template: Cell Silo Endpoint Test ```python @cell_silo_test diff --git a/.agents/skills/hybrid-cloud-test-gen/references/rpc-service-tests.md b/.agents/skills/hybrid-cloud-test-gen/references/rpc-service-tests.md index 80d543697ca654..14aa8a04691123 100644 --- a/.agents/skills/hybrid-cloud-test-gen/references/rpc-service-tests.md +++ b/.agents/skills/hybrid-cloud-test-gen/references/rpc-service-tests.md @@ -27,7 +27,7 @@ from sentry.testutils.silo import ( all_silo_test, assume_test_silo_mode, assume_test_silo_mode_of, - create_test_regions, + create_test_cells, ) ``` @@ -92,7 +92,7 @@ class Test{ServiceName}Service(TestCase): Prefer `assume_test_silo_mode_of(Model)` over `assume_test_silo_mode(SiloMode.X)` when checking a single model: ```python -@all_silo_test(regions=create_test_regions("us")) +@all_silo_test(cells=create_test_cells("us")) class Test{ServiceName}CrossSilo(TestCase, HybridCloudTestMixin): def test_{method_name}_creates_mapping(self): with outbox_runner(): diff --git a/src/sentry/apidocs/examples/replay_examples.py b/src/sentry/apidocs/examples/replay_examples.py index 7dc96ad4888b9e..f3950639384a70 100644 --- a/src/sentry/apidocs/examples/replay_examples.py +++ b/src/sentry/apidocs/examples/replay_examples.py @@ -200,6 +200,71 @@ class ReplayExamples: ) ] + GET_REPLAY_DELETION_JOBS = [ + OpenApiExample( + "List replay deletion jobs", + value={ + "data": [ + { + "id": 1, + "dateCreated": "2024-01-01T00:00:00Z", + "dateUpdated": "2024-01-01T00:05:00Z", + "rangeStart": "2023-12-01T00:00:00Z", + "rangeEnd": "2024-01-01T00:00:00Z", + "environments": ["production"], + "status": "pending", + "query": "user.email:test@example.com", + "countDeleted": 0, + } + ] + }, + status_codes=["200"], + response_only=True, + ) + ] + + CREATE_REPLAY_DELETION_JOB = [ + OpenApiExample( + "Create a replay deletion job", + value={ + "data": { + "id": 1, + "dateCreated": "2024-01-01T00:00:00Z", + "dateUpdated": "2024-01-01T00:05:00Z", + "rangeStart": "2023-12-01T00:00:00Z", + "rangeEnd": "2024-01-01T00:00:00Z", + "environments": ["production"], + "status": "pending", + "query": "user.email:test@example.com", + "countDeleted": 0, + } + }, + status_codes=["201"], + response_only=True, + ) + ] + + GET_REPLAY_DELETION_JOB = [ + OpenApiExample( + "Get a replay deletion job", + value={ + "data": { + "id": 1, + "dateCreated": "2024-01-01T00:00:00Z", + "dateUpdated": "2024-01-01T00:05:00Z", + "rangeStart": "2023-12-01T00:00:00Z", + "rangeEnd": "2024-01-01T00:00:00Z", + "environments": ["production"], + "status": "pending", + "query": "user.email:test@example.com", + "countDeleted": 0, + } + }, + status_codes=["200"], + response_only=True, + ) + ] + GET_REPLAY_VIEWED_BY = [ OpenApiExample( "Get list of users who have viewed a replay", diff --git a/src/sentry/apidocs/parameters.py b/src/sentry/apidocs/parameters.py index bebff27aef1a10..2cbb90a209ba93 100644 --- a/src/sentry/apidocs/parameters.py +++ b/src/sentry/apidocs/parameters.py @@ -927,6 +927,14 @@ class ReplayParams: description="""The ID of the segment you'd like to retrieve.""", ) + JOB_ID = OpenApiParameter( + name="job_id", + location="path", + required=True, + type=OpenApiTypes.INT, + description="""The ID of the replay deletion job you'd like to retrieve.""", + ) + class NotificationParams: TRIGGER_TYPE = OpenApiParameter( diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index b82748fba615b5..ebfd7086146fc2 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -116,6 +116,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:dynamic-sampling-custom", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable dynamic sampling minimum sample rate manager.add("organizations:dynamic-sampling-minimum-sample-rate", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable explore -> errors ui + manager.add("organizations:explore-errors", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable returning the migrated discover queries in explore saved queries manager.add("organizations:expose-migrated-discover-queries", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable GenAI features such as Autofix and Issue Summary diff --git a/src/sentry/hybridcloud/services/control_organization_provisioning/model.py b/src/sentry/hybridcloud/services/control_organization_provisioning/model.py index e72dfd93ef27ce..90806036796a35 100644 --- a/src/sentry/hybridcloud/services/control_organization_provisioning/model.py +++ b/src/sentry/hybridcloud/services/control_organization_provisioning/model.py @@ -10,16 +10,16 @@ class RpcOrganizationSlugReservation(RpcModel): organization_id: int user_id: int | None slug: str - region_name: str + cell_name: str reservation_type: int @root_validator(pre=True) @classmethod - def _accept_cell_name(cls, values: dict[str, Any]) -> dict[str, Any]: - if "cell_name" in values and "region_name" not in values: - values["region_name"] = values.pop("cell_name") + def _accept_region_name(cls, values: dict[str, Any]) -> dict[str, Any]: + if "region_name" in values and "cell_name" not in values: + values["cell_name"] = values.pop("region_name") return values @property - def cell_name(self) -> str: - return self.region_name + def region_name(self) -> str: + return self.cell_name diff --git a/src/sentry/hybridcloud/services/control_organization_provisioning/serial.py b/src/sentry/hybridcloud/services/control_organization_provisioning/serial.py index 515773d81a3240..626aa6dfc3ed69 100644 --- a/src/sentry/hybridcloud/services/control_organization_provisioning/serial.py +++ b/src/sentry/hybridcloud/services/control_organization_provisioning/serial.py @@ -11,7 +11,7 @@ def serialize_slug_reservation( id=slug_reservation.id, organization_id=slug_reservation.organization_id, slug=slug_reservation.slug, - region_name=slug_reservation.cell_name, + cell_name=slug_reservation.cell_name, user_id=slug_reservation.user_id, reservation_type=slug_reservation.reservation_type, ) diff --git a/src/sentry/replays/endpoints/project_replay_jobs_delete.py b/src/sentry/replays/endpoints/project_replay_jobs_delete.py index 18239a97f386ed..9326af6caf8991 100644 --- a/src/sentry/replays/endpoints/project_replay_jobs_delete.py +++ b/src/sentry/replays/endpoints/project_replay_jobs_delete.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +from typing import TypedDict + +from drf_spectacular.utils import extend_schema from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response @@ -10,12 +15,36 @@ from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import Serializer, serialize +from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND +from sentry.apidocs.examples.replay_examples import ReplayExamples +from sentry.apidocs.parameters import GlobalParams, ReplayParams +from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.replays.endpoints.project_replay_endpoint import ProjectReplayEndpoint from sentry.replays.models import ReplayDeletionJobModel from sentry.replays.permissions import has_replay_permission from sentry.replays.tasks import run_bulk_replay_delete_job +class ReplayDeletionJobResponseData(TypedDict): + id: int + dateCreated: str + dateUpdated: str + rangeStart: str + rangeEnd: str + environments: list[str] + status: str + query: str + countDeleted: int + + +class ReplayDeletionJobListResponse(TypedDict): + data: list[ReplayDeletionJobResponseData] + + +class ReplayDeletionJobDetailResponse(TypedDict): + data: ReplayDeletionJobResponseData + + class ReplayDeletionJobPermission(ProjectPermission): scope_map = { "GET": ["project:write", "project:admin"], @@ -57,14 +86,29 @@ class ReplayDeletionJobCreateSerializer(serializers.Serializer): @cell_silo_endpoint +@extend_schema(tags=["Replays"]) class ProjectReplayDeletionJobsIndexEndpoint(ProjectEndpoint): owner = ApiOwner.DATA_BROWSING publish_status = { - "GET": ApiPublishStatus.PRIVATE, - "POST": ApiPublishStatus.PRIVATE, + "GET": ApiPublishStatus.PUBLIC, + "POST": ApiPublishStatus.PUBLIC, } permission_classes = (ReplayDeletionJobPermission,) + @extend_schema( + operation_id="List Replay Deletion Jobs", + parameters=[ + GlobalParams.ORG_ID_OR_SLUG, + GlobalParams.PROJECT_ID_OR_SLUG, + ], + responses={ + 200: inline_sentry_response_serializer( + "ListReplayDeletionJobs", ReplayDeletionJobListResponse + ), + 403: RESPONSE_FORBIDDEN, + }, + examples=ReplayExamples.GET_REPLAY_DELETION_JOBS, + ) def get(self, request: Request, project) -> Response: """ Retrieve a collection of replay delete jobs. @@ -86,6 +130,22 @@ def get(self, request: Request, project) -> Response: paginator_cls=OffsetPaginator, ) + @extend_schema( + operation_id="Create a Replay Deletion Job", + parameters=[ + GlobalParams.ORG_ID_OR_SLUG, + GlobalParams.PROJECT_ID_OR_SLUG, + ], + request=ReplayDeletionJobCreateSerializer, + responses={ + 201: inline_sentry_response_serializer( + "CreateReplayDeletionJob", ReplayDeletionJobDetailResponse + ), + 400: RESPONSE_BAD_REQUEST, + 403: RESPONSE_FORBIDDEN, + }, + examples=ReplayExamples.CREATE_REPLAY_DELETION_JOB, + ) def post(self, request: Request, project) -> Response: """ Create a new replay deletion job. @@ -132,12 +192,29 @@ def post(self, request: Request, project) -> Response: @cell_silo_endpoint +@extend_schema(tags=["Replays"]) class ProjectReplayDeletionJobDetailEndpoint(ProjectReplayEndpoint): publish_status = { - "GET": ApiPublishStatus.PRIVATE, + "GET": ApiPublishStatus.PUBLIC, } permission_classes = (ReplayDeletionJobPermission,) + @extend_schema( + operation_id="Get a Replay Deletion Job", + parameters=[ + GlobalParams.ORG_ID_OR_SLUG, + GlobalParams.PROJECT_ID_OR_SLUG, + ReplayParams.JOB_ID, + ], + responses={ + 200: inline_sentry_response_serializer( + "GetReplayDeletionJob", ReplayDeletionJobDetailResponse + ), + 403: RESPONSE_FORBIDDEN, + 404: RESPONSE_NOT_FOUND, + }, + examples=ReplayExamples.GET_REPLAY_DELETION_JOB, + ) def get(self, request: Request, project, job_id: int) -> Response: """ Fetch a replay delete job instance. diff --git a/src/sentry/scm/actions.py b/src/sentry/scm/actions.py index b7689cd6bd25cc..c4d6c0041a79bc 100644 --- a/src/sentry/scm/actions.py +++ b/src/sentry/scm/actions.py @@ -13,8 +13,20 @@ map_repository_model_to_repository, ) from sentry.scm.private.ipc import record_count_metric -from sentry.scm.private.provider import ( +from sentry.scm.private.rate_limit import RateLimitProvider +from sentry.scm.types import ( ALL_PROTOCOLS, + SHA, + ActionResult, + ArchiveFormat, + ArchiveLink, + BranchName, + BuildConclusion, + BuildStatus, + CheckRun, + CheckRunOutput, + Comment, + Commit, CompareCommitsProtocol, CreateBranchProtocol, CreateCheckRunProtocol, @@ -38,6 +50,7 @@ DeletePullRequestCommentProtocol, DeletePullRequestCommentReactionProtocol, DeletePullRequestReactionProtocol, + FileContent, GetArchiveLinkProtocol, GetBranchProtocol, GetCheckRunProtocol, @@ -58,34 +71,15 @@ GetPullRequestReactionsProtocol, GetPullRequestsProtocol, GetTreeProtocol, - MinimizeCommentProtocol, - Provider, - RequestReviewProtocol, - UpdateBranchProtocol, - UpdateCheckRunProtocol, - UpdatePullRequestProtocol, -) -from sentry.scm.private.rate_limit import RateLimitProvider -from sentry.scm.types import ( - SHA, - ActionResult, - ArchiveFormat, - ArchiveLink, - BranchName, - BuildConclusion, - BuildStatus, - CheckRun, - CheckRunOutput, - Comment, - Commit, - FileContent, GitBlob, GitCommitObject, GitRef, GitTree, InputTreeEntry, + MinimizeCommentProtocol, PaginatedActionResult, PaginationParams, + Provider, PullRequest, PullRequestCommit, PullRequestFile, @@ -96,12 +90,16 @@ Repository, RepositoryId, RequestOptions, + RequestReviewProtocol, ResourceId, Review, ReviewComment, ReviewCommentInput, ReviewEvent, ReviewSide, + UpdateBranchProtocol, + UpdateCheckRunProtocol, + UpdatePullRequestProtocol, ) diff --git a/src/sentry/scm/private/facade.py b/src/sentry/scm/private/facade.py index e915adee68fa33..5d15cc7904ac9a 100644 --- a/src/sentry/scm/private/facade.py +++ b/src/sentry/scm/private/facade.py @@ -6,8 +6,7 @@ from sentry.scm.private.helpers import exec_provider_fn from sentry.scm.private.ipc import record_count_metric -from sentry.scm.private.provider import ALL_PROTOCOLS, Provider -from sentry.scm.types import Referrer +from sentry.scm.types import ALL_PROTOCOLS, Provider, Referrer def _delegating_method(name: str) -> Callable[..., Any]: diff --git a/src/sentry/scm/private/helpers.py b/src/sentry/scm/private/helpers.py index 4f0ae6f2f6509e..1ca94c1a9f9e07 100644 --- a/src/sentry/scm/private/helpers.py +++ b/src/sentry/scm/private/helpers.py @@ -8,11 +8,10 @@ from sentry.models.repository import Repository as RepositoryModel from sentry.scm.errors import SCMCodedError, SCMError, SCMUnhandledException from sentry.scm.private.ipc import record_count_metric -from sentry.scm.private.provider import Provider from sentry.scm.private.providers.github import GitHubProvider from sentry.scm.private.providers.gitlab import GitLabProvider from sentry.scm.private.rate_limit import RateLimitProvider, RedisRateLimitProvider -from sentry.scm.types import ExternalId, ProviderName, Referrer, Repository, RepositoryId +from sentry.scm.types import ExternalId, Provider, ProviderName, Referrer, Repository, RepositoryId def map_integration_to_provider( diff --git a/src/sentry/scm/private/provider.py b/src/sentry/scm/private/provider.py deleted file mode 100644 index 502494f5068b10..00000000000000 --- a/src/sentry/scm/private/provider.py +++ /dev/null @@ -1,611 +0,0 @@ -from typing import Protocol, runtime_checkable - -from sentry.scm.types import ( - SHA, - ActionResult, - ArchiveFormat, - ArchiveLink, - BranchName, - BuildConclusion, - BuildStatus, - CheckRun, - CheckRunOutput, - Comment, - Commit, - FileContent, - GitBlob, - GitCommitObject, - GitRef, - GitTree, - InputTreeEntry, - PaginatedActionResult, - PaginationParams, - PullRequest, - PullRequestCommit, - PullRequestFile, - PullRequestState, - Reaction, - ReactionResult, - Referrer, - Repository, - RequestOptions, - ResourceId, - Review, - ReviewComment, - ReviewCommentInput, - ReviewEvent, - ReviewSide, -) - -# Issue Comment Protocols - - -@runtime_checkable -class GetIssueCommentsProtocol(Protocol): - def get_issue_comments( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: ... - - -@runtime_checkable -class CreateIssueCommentProtocol(Protocol): - def create_issue_comment(self, issue_id: str, body: str) -> ActionResult[Comment]: ... - - -@runtime_checkable -class DeleteIssueCommentProtocol(Protocol): - def delete_issue_comment(self, issue_id: str, comment_id: str) -> None: ... - - -# Pull Request Comment Protocols - - -@runtime_checkable -class GetPullRequestCommentsProtocol(Protocol): - def get_pull_request_comments( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Comment]: ... - - -@runtime_checkable -class CreatePullRequestCommentProtocol(Protocol): - def create_pull_request_comment( - self, pull_request_id: str, body: str - ) -> ActionResult[Comment]: ... - - -@runtime_checkable -class DeletePullRequestCommentProtocol(Protocol): - def delete_pull_request_comment(self, pull_request_id: str, comment_id: str) -> None: ... - - -# Issue Comment Reaction Protocols - - -@runtime_checkable -class GetIssueCommentReactionsProtocol(Protocol): - def get_issue_comment_reactions( - self, - issue_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreateIssueCommentReactionProtocol(Protocol): - def create_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeleteIssueCommentReactionProtocol(Protocol): - def delete_issue_comment_reaction( - self, issue_id: str, comment_id: str, reaction_id: str - ) -> None: ... - - -# Pull Request Comment Reaction Protocols - - -@runtime_checkable -class GetPullRequestCommentReactionsProtocol(Protocol): - def get_pull_request_comment_reactions( - self, - pull_request_id: str, - comment_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreatePullRequestCommentReactionProtocol(Protocol): - def create_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeletePullRequestCommentReactionProtocol(Protocol): - def delete_pull_request_comment_reaction( - self, pull_request_id: str, comment_id: str, reaction_id: str - ) -> None: ... - - -# Issue Reaction Protocols - - -@runtime_checkable -class GetIssueReactionsProtocol(Protocol): - def get_issue_reactions( - self, - issue_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreateIssueReactionProtocol(Protocol): - def create_issue_reaction( - self, issue_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeleteIssueReactionProtocol(Protocol): - def delete_issue_reaction(self, issue_id: str, reaction_id: str) -> None: ... - - -# Pull Request Reaction Protocols - - -@runtime_checkable -class GetPullRequestReactionsProtocol(Protocol): - def get_pull_request_reactions( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[ReactionResult]: ... - - -@runtime_checkable -class CreatePullRequestReactionProtocol(Protocol): - def create_pull_request_reaction( - self, pull_request_id: str, reaction: Reaction - ) -> ActionResult[ReactionResult]: ... - - -@runtime_checkable -class DeletePullRequestReactionProtocol(Protocol): - def delete_pull_request_reaction(self, pull_request_id: str, reaction_id: str) -> None: ... - - -# Branch Protocols - - -@runtime_checkable -class GetBranchProtocol(Protocol): - def get_branch( - self, - branch: BranchName, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitRef]: ... - - -@runtime_checkable -class CreateBranchProtocol(Protocol): - def create_branch(self, branch: BranchName, sha: SHA) -> ActionResult[GitRef]: ... - - -@runtime_checkable -class UpdateBranchProtocol(Protocol): - def update_branch( - self, branch: BranchName, sha: SHA, force: bool = False - ) -> ActionResult[GitRef]: ... - - -# Commit Protocols - - -@runtime_checkable -class GetCommitProtocol(Protocol): - def get_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[Commit]: ... - - -@runtime_checkable -class GetCommitsProtocol(Protocol): - def get_commits( - self, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: ... - - -@runtime_checkable -class GetCommitsByPathProtocol(Protocol): - def get_commits_by_path( - self, - path: str, - ref: str | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: ... - - -@runtime_checkable -class CompareCommitsProtocol(Protocol): - def compare_commits( - self, - start_sha: SHA, - end_sha: SHA, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[Commit]: ... - - -# Pull Request Protocols - - -@runtime_checkable -class GetPullRequestProtocol(Protocol): - def get_pull_request( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class GetPullRequestsProtocol(Protocol): - def get_pull_requests( - self, - state: PullRequestState | None = "open", - head: BranchName | None = None, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequest]: ... - - -@runtime_checkable -class GetPullRequestFilesProtocol(Protocol): - def get_pull_request_files( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestFile]: ... - - -@runtime_checkable -class GetPullRequestCommitsProtocol(Protocol): - def get_pull_request_commits( - self, - pull_request_id: str, - pagination: PaginationParams | None = None, - request_options: RequestOptions | None = None, - ) -> PaginatedActionResult[PullRequestCommit]: ... - - -@runtime_checkable -class GetPullRequestDiffProtocol(Protocol): - def get_pull_request_diff( - self, - pull_request_id: str, - request_options: RequestOptions | None = None, - ) -> ActionResult[str]: ... - - -@runtime_checkable -class CreatePullRequestProtocol(Protocol): - def create_pull_request( - self, - title: str, - body: str, - head: BranchName, - base: BranchName, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class CreatePullRequestDraftProtocol(Protocol): - def create_pull_request_draft( - self, - title: str, - body: str, - head: BranchName, - base: BranchName, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class UpdatePullRequestProtocol(Protocol): - def update_pull_request( - self, - pull_request_id: str, - title: str | None = None, - body: str | None = None, - state: PullRequestState | None = None, - ) -> ActionResult[PullRequest]: ... - - -@runtime_checkable -class RequestReviewProtocol(Protocol): - def request_review(self, pull_request_id: str, reviewers: list[str]) -> None: ... - - -# Git Object Protocols - - -@runtime_checkable -class GetTreeProtocol(Protocol): - def get_tree( - self, - tree_sha: SHA, - recursive: bool = True, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitTree]: ... - - -@runtime_checkable -class GetGitCommitProtocol(Protocol): - def get_git_commit( - self, - sha: SHA, - request_options: RequestOptions | None = None, - ) -> ActionResult[GitCommitObject]: ... - - -@runtime_checkable -class CreateGitBlobProtocol(Protocol): - def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: ... - - -@runtime_checkable -class CreateGitTreeProtocol(Protocol): - def create_git_tree( - self, tree: list[InputTreeEntry], base_tree: SHA | None = None - ) -> ActionResult[GitTree]: ... - - -@runtime_checkable -class CreateGitCommitProtocol(Protocol): - def create_git_commit( - self, message: str, tree_sha: SHA, parent_shas: list[SHA] - ) -> ActionResult[GitCommitObject]: ... - - -# File Content Protocol - - -@runtime_checkable -class GetFileContentProtocol(Protocol): - def get_file_content( - self, - path: str, - ref: str | None = None, - request_options: RequestOptions | None = None, - ) -> ActionResult[FileContent]: ... - - -# Archive Protocols - - -@runtime_checkable -class GetArchiveLinkProtocol(Protocol): - def get_archive_link( - self, - ref: str, - archive_format: ArchiveFormat = "tarball", - ) -> ActionResult[ArchiveLink]: ... - - -# Check Run Protocols - - -@runtime_checkable -class GetCheckRunProtocol(Protocol): - def get_check_run( - self, - check_run_id: ResourceId, - request_options: RequestOptions | None = None, - ) -> ActionResult[CheckRun]: ... - - -@runtime_checkable -class CreateCheckRunProtocol(Protocol): - def create_check_run( - self, - name: str, - head_sha: SHA, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - external_id: str | None = None, - started_at: str | None = None, - completed_at: str | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: ... - - -@runtime_checkable -class UpdateCheckRunProtocol(Protocol): - def update_check_run( - self, - check_run_id: ResourceId, - status: BuildStatus | None = None, - conclusion: BuildConclusion | None = None, - output: CheckRunOutput | None = None, - ) -> ActionResult[CheckRun]: ... - - -# Review Protocols - - -@runtime_checkable -class CreateReviewCommentFileProtocol(Protocol): - def create_review_comment_file( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - side: ReviewSide, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewCommentLineProtocol(Protocol): - def create_review_comment_line( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - line: int, - side: ReviewSide, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewCommentMultilineProtocol(Protocol): - def create_review_comment_multiline( - self, - pull_request_id: str, - commit_id: SHA, - body: str, - path: str, - start_line: int, - start_side: ReviewSide, - end_line: int, - end_side: ReviewSide, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewCommentReplyProtocol(Protocol): - def create_review_comment_reply( - self, - pull_request_id: str, - body: str, - comment_id: str, - ) -> ActionResult[ReviewComment]: ... - - -@runtime_checkable -class CreateReviewProtocol(Protocol): - def create_review( - self, - pull_request_id: str, - commit_sha: SHA, - event: ReviewEvent, - comments: list[ReviewCommentInput], - body: str | None = None, - ) -> ActionResult[Review]: ... - - -# Moderation Protocols - - -@runtime_checkable -class MinimizeCommentProtocol(Protocol): - def minimize_comment(self, comment_node_id: str, reason: str) -> None: ... - - -@runtime_checkable -class ResolveReviewThreadProtocol(Protocol): - def resolve_review_thread(self, thread_node_id: str) -> None: ... - - -ALL_PROTOCOLS = ( - CompareCommitsProtocol, - CreateBranchProtocol, - CreateCheckRunProtocol, - CreateGitBlobProtocol, - CreateGitCommitProtocol, - CreateGitTreeProtocol, - CreateIssueCommentProtocol, - CreateIssueCommentReactionProtocol, - CreateIssueReactionProtocol, - CreatePullRequestCommentProtocol, - CreatePullRequestCommentReactionProtocol, - CreatePullRequestDraftProtocol, - CreatePullRequestProtocol, - CreatePullRequestReactionProtocol, - CreateReviewCommentFileProtocol, - CreateReviewCommentLineProtocol, - CreateReviewCommentMultilineProtocol, - CreateReviewCommentReplyProtocol, - CreateReviewProtocol, - DeleteIssueCommentProtocol, - DeleteIssueCommentReactionProtocol, - DeleteIssueReactionProtocol, - DeletePullRequestCommentProtocol, - DeletePullRequestCommentReactionProtocol, - DeletePullRequestReactionProtocol, - GetArchiveLinkProtocol, - GetBranchProtocol, - GetCheckRunProtocol, - GetCommitProtocol, - GetCommitsByPathProtocol, - GetCommitsProtocol, - GetFileContentProtocol, - GetGitCommitProtocol, - GetIssueCommentReactionsProtocol, - GetIssueCommentsProtocol, - GetIssueReactionsProtocol, - GetPullRequestCommentReactionsProtocol, - GetPullRequestCommentsProtocol, - GetPullRequestCommitsProtocol, - GetPullRequestDiffProtocol, - GetPullRequestFilesProtocol, - GetPullRequestProtocol, - GetPullRequestReactionsProtocol, - GetPullRequestsProtocol, - GetTreeProtocol, - MinimizeCommentProtocol, - RequestReviewProtocol, - ResolveReviewThreadProtocol, - UpdateBranchProtocol, - UpdateCheckRunProtocol, - UpdatePullRequestProtocol, -) - - -class Provider(Protocol): - """ - Providers abstract over an integration. They map generic commands to service-provider specific - commands and they map the results of those commands to generic result-types. - - Providers necessarily offer a larger API surface than what is available in an integration. Some - methods may be duplicates in some providers. This is intentional. Providers capture programmer - intent and translate it into a concrete interface. Therefore, providers provide a large range - of behaviors which may or may not be explicitly defined on a service-provider. - - Providers, also by necessity, offer a smaller API surface than what the SCM platform can - maximally provide. There are simply some operations which can not be adequately translated - between providers. None the less, we want to have a service-agnostic interface. This problem - is solved with capability-object-like system. Capabilities are progressively opted into using - structural sub-typing. As a provider's surface area expands the SourceCodeManager class will - automatically recognize that the provider has a particular capability and return "true" when - handling "can" requests. - """ - - organization_id: int - repository: Repository - - def is_rate_limited(self, referrer: Referrer) -> bool: ... diff --git a/src/sentry/scm/types.py b/src/sentry/scm/types.py index 7a5f6119db3ae9..d16c3b899b52b9 100644 --- a/src/sentry/scm/types.py +++ b/src/sentry/scm/types.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import datetime -from typing import Any, Literal, Required, TypedDict +from typing import Any, Literal, Protocol, Required, TypedDict, runtime_checkable type Action = Literal["check_run", "comment", "pull_request"] type EventType = "CheckRunEvent" | "CommentEvent" | "PullRequestEvent" @@ -584,3 +584,577 @@ class PullRequestEvent: the listener. In some cases, Sentry will query the database for information. This information is stored in the "sentry_meta" field and is accessible without performing redundant queries. """ + + +# Issue Comment Protocols + + +@runtime_checkable +class GetIssueCommentsProtocol(Protocol): + def get_issue_comments( + self, + issue_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[Comment]: ... + + +@runtime_checkable +class CreateIssueCommentProtocol(Protocol): + def create_issue_comment(self, issue_id: str, body: str) -> ActionResult[Comment]: ... + + +@runtime_checkable +class DeleteIssueCommentProtocol(Protocol): + def delete_issue_comment(self, issue_id: str, comment_id: str) -> None: ... + + +# Pull Request Comment Protocols + + +@runtime_checkable +class GetPullRequestCommentsProtocol(Protocol): + def get_pull_request_comments( + self, + pull_request_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[Comment]: ... + + +@runtime_checkable +class CreatePullRequestCommentProtocol(Protocol): + def create_pull_request_comment( + self, pull_request_id: str, body: str + ) -> ActionResult[Comment]: ... + + +@runtime_checkable +class DeletePullRequestCommentProtocol(Protocol): + def delete_pull_request_comment(self, pull_request_id: str, comment_id: str) -> None: ... + + +# Issue Comment Reaction Protocols + + +@runtime_checkable +class GetIssueCommentReactionsProtocol(Protocol): + def get_issue_comment_reactions( + self, + issue_id: str, + comment_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[ReactionResult]: ... + + +@runtime_checkable +class CreateIssueCommentReactionProtocol(Protocol): + def create_issue_comment_reaction( + self, issue_id: str, comment_id: str, reaction: Reaction + ) -> ActionResult[ReactionResult]: ... + + +@runtime_checkable +class DeleteIssueCommentReactionProtocol(Protocol): + def delete_issue_comment_reaction( + self, issue_id: str, comment_id: str, reaction_id: str + ) -> None: ... + + +# Pull Request Comment Reaction Protocols + + +@runtime_checkable +class GetPullRequestCommentReactionsProtocol(Protocol): + def get_pull_request_comment_reactions( + self, + pull_request_id: str, + comment_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[ReactionResult]: ... + + +@runtime_checkable +class CreatePullRequestCommentReactionProtocol(Protocol): + def create_pull_request_comment_reaction( + self, pull_request_id: str, comment_id: str, reaction: Reaction + ) -> ActionResult[ReactionResult]: ... + + +@runtime_checkable +class DeletePullRequestCommentReactionProtocol(Protocol): + def delete_pull_request_comment_reaction( + self, pull_request_id: str, comment_id: str, reaction_id: str + ) -> None: ... + + +# Issue Reaction Protocols + + +@runtime_checkable +class GetIssueReactionsProtocol(Protocol): + def get_issue_reactions( + self, + issue_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[ReactionResult]: ... + + +@runtime_checkable +class CreateIssueReactionProtocol(Protocol): + def create_issue_reaction( + self, issue_id: str, reaction: Reaction + ) -> ActionResult[ReactionResult]: ... + + +@runtime_checkable +class DeleteIssueReactionProtocol(Protocol): + def delete_issue_reaction(self, issue_id: str, reaction_id: str) -> None: ... + + +# Pull Request Reaction Protocols + + +@runtime_checkable +class GetPullRequestReactionsProtocol(Protocol): + def get_pull_request_reactions( + self, + pull_request_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[ReactionResult]: ... + + +@runtime_checkable +class CreatePullRequestReactionProtocol(Protocol): + def create_pull_request_reaction( + self, pull_request_id: str, reaction: Reaction + ) -> ActionResult[ReactionResult]: ... + + +@runtime_checkable +class DeletePullRequestReactionProtocol(Protocol): + def delete_pull_request_reaction(self, pull_request_id: str, reaction_id: str) -> None: ... + + +# Branch Protocols + + +@runtime_checkable +class GetBranchProtocol(Protocol): + def get_branch( + self, + branch: BranchName, + request_options: RequestOptions | None = None, + ) -> ActionResult[GitRef]: ... + + +@runtime_checkable +class CreateBranchProtocol(Protocol): + def create_branch(self, branch: BranchName, sha: SHA) -> ActionResult[GitRef]: ... + + +@runtime_checkable +class UpdateBranchProtocol(Protocol): + def update_branch( + self, branch: BranchName, sha: SHA, force: bool = False + ) -> ActionResult[GitRef]: ... + + +# Commit Protocols + + +@runtime_checkable +class GetCommitProtocol(Protocol): + def get_commit( + self, + sha: SHA, + request_options: RequestOptions | None = None, + ) -> ActionResult[Commit]: ... + + +@runtime_checkable +class GetCommitsProtocol(Protocol): + def get_commits( + self, + ref: str | None = None, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[Commit]: ... + + +@runtime_checkable +class GetCommitsByPathProtocol(Protocol): + def get_commits_by_path( + self, + path: str, + ref: str | None = None, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[Commit]: ... + + +@runtime_checkable +class CompareCommitsProtocol(Protocol): + def compare_commits( + self, + start_sha: SHA, + end_sha: SHA, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[Commit]: ... + + +# Pull Request Protocols + + +@runtime_checkable +class GetPullRequestProtocol(Protocol): + def get_pull_request( + self, + pull_request_id: str, + request_options: RequestOptions | None = None, + ) -> ActionResult[PullRequest]: ... + + +@runtime_checkable +class GetPullRequestsProtocol(Protocol): + def get_pull_requests( + self, + state: PullRequestState | None = "open", + head: BranchName | None = None, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[PullRequest]: ... + + +@runtime_checkable +class GetPullRequestFilesProtocol(Protocol): + def get_pull_request_files( + self, + pull_request_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[PullRequestFile]: ... + + +@runtime_checkable +class GetPullRequestCommitsProtocol(Protocol): + def get_pull_request_commits( + self, + pull_request_id: str, + pagination: PaginationParams | None = None, + request_options: RequestOptions | None = None, + ) -> PaginatedActionResult[PullRequestCommit]: ... + + +@runtime_checkable +class GetPullRequestDiffProtocol(Protocol): + def get_pull_request_diff( + self, + pull_request_id: str, + request_options: RequestOptions | None = None, + ) -> ActionResult[str]: ... + + +@runtime_checkable +class CreatePullRequestProtocol(Protocol): + def create_pull_request( + self, + title: str, + body: str, + head: BranchName, + base: BranchName, + ) -> ActionResult[PullRequest]: ... + + +@runtime_checkable +class CreatePullRequestDraftProtocol(Protocol): + def create_pull_request_draft( + self, + title: str, + body: str, + head: BranchName, + base: BranchName, + ) -> ActionResult[PullRequest]: ... + + +@runtime_checkable +class UpdatePullRequestProtocol(Protocol): + def update_pull_request( + self, + pull_request_id: str, + title: str | None = None, + body: str | None = None, + state: PullRequestState | None = None, + ) -> ActionResult[PullRequest]: ... + + +@runtime_checkable +class RequestReviewProtocol(Protocol): + def request_review(self, pull_request_id: str, reviewers: list[str]) -> None: ... + + +# Git Object Protocols + + +@runtime_checkable +class GetTreeProtocol(Protocol): + def get_tree( + self, + tree_sha: SHA, + recursive: bool = True, + request_options: RequestOptions | None = None, + ) -> ActionResult[GitTree]: ... + + +@runtime_checkable +class GetGitCommitProtocol(Protocol): + def get_git_commit( + self, + sha: SHA, + request_options: RequestOptions | None = None, + ) -> ActionResult[GitCommitObject]: ... + + +@runtime_checkable +class CreateGitBlobProtocol(Protocol): + def create_git_blob(self, content: str, encoding: str) -> ActionResult[GitBlob]: ... + + +@runtime_checkable +class CreateGitTreeProtocol(Protocol): + def create_git_tree( + self, tree: list[InputTreeEntry], base_tree: SHA | None = None + ) -> ActionResult[GitTree]: ... + + +@runtime_checkable +class CreateGitCommitProtocol(Protocol): + def create_git_commit( + self, message: str, tree_sha: SHA, parent_shas: list[SHA] + ) -> ActionResult[GitCommitObject]: ... + + +# File Content Protocol + + +@runtime_checkable +class GetFileContentProtocol(Protocol): + def get_file_content( + self, + path: str, + ref: str | None = None, + request_options: RequestOptions | None = None, + ) -> ActionResult[FileContent]: ... + + +# Archive Protocols + + +@runtime_checkable +class GetArchiveLinkProtocol(Protocol): + def get_archive_link( + self, + ref: str, + archive_format: ArchiveFormat = "tarball", + ) -> ActionResult[ArchiveLink]: ... + + +# Check Run Protocols + + +@runtime_checkable +class GetCheckRunProtocol(Protocol): + def get_check_run( + self, + check_run_id: ResourceId, + request_options: RequestOptions | None = None, + ) -> ActionResult[CheckRun]: ... + + +@runtime_checkable +class CreateCheckRunProtocol(Protocol): + def create_check_run( + self, + name: str, + head_sha: SHA, + status: BuildStatus | None = None, + conclusion: BuildConclusion | None = None, + external_id: str | None = None, + started_at: str | None = None, + completed_at: str | None = None, + output: CheckRunOutput | None = None, + ) -> ActionResult[CheckRun]: ... + + +@runtime_checkable +class UpdateCheckRunProtocol(Protocol): + def update_check_run( + self, + check_run_id: ResourceId, + status: BuildStatus | None = None, + conclusion: BuildConclusion | None = None, + output: CheckRunOutput | None = None, + ) -> ActionResult[CheckRun]: ... + + +# Review Protocols + + +@runtime_checkable +class CreateReviewCommentFileProtocol(Protocol): + def create_review_comment_file( + self, + pull_request_id: str, + commit_id: SHA, + body: str, + path: str, + side: ReviewSide, + ) -> ActionResult[ReviewComment]: ... + + +@runtime_checkable +class CreateReviewCommentLineProtocol(Protocol): + def create_review_comment_line( + self, + pull_request_id: str, + commit_id: SHA, + body: str, + path: str, + line: int, + side: ReviewSide, + ) -> ActionResult[ReviewComment]: ... + + +@runtime_checkable +class CreateReviewCommentMultilineProtocol(Protocol): + def create_review_comment_multiline( + self, + pull_request_id: str, + commit_id: SHA, + body: str, + path: str, + start_line: int, + start_side: ReviewSide, + end_line: int, + end_side: ReviewSide, + ) -> ActionResult[ReviewComment]: ... + + +@runtime_checkable +class CreateReviewCommentReplyProtocol(Protocol): + def create_review_comment_reply( + self, + pull_request_id: str, + body: str, + comment_id: str, + ) -> ActionResult[ReviewComment]: ... + + +@runtime_checkable +class CreateReviewProtocol(Protocol): + def create_review( + self, + pull_request_id: str, + commit_sha: SHA, + event: ReviewEvent, + comments: list[ReviewCommentInput], + body: str | None = None, + ) -> ActionResult[Review]: ... + + +# Moderation Protocols + + +@runtime_checkable +class MinimizeCommentProtocol(Protocol): + def minimize_comment(self, comment_node_id: str, reason: str) -> None: ... + + +@runtime_checkable +class ResolveReviewThreadProtocol(Protocol): + def resolve_review_thread(self, thread_node_id: str) -> None: ... + + +ALL_PROTOCOLS = ( + CompareCommitsProtocol, + CreateBranchProtocol, + CreateCheckRunProtocol, + CreateGitBlobProtocol, + CreateGitCommitProtocol, + CreateGitTreeProtocol, + CreateIssueCommentProtocol, + CreateIssueCommentReactionProtocol, + CreateIssueReactionProtocol, + CreatePullRequestCommentProtocol, + CreatePullRequestCommentReactionProtocol, + CreatePullRequestDraftProtocol, + CreatePullRequestProtocol, + CreatePullRequestReactionProtocol, + CreateReviewCommentFileProtocol, + CreateReviewCommentLineProtocol, + CreateReviewCommentMultilineProtocol, + CreateReviewCommentReplyProtocol, + CreateReviewProtocol, + DeleteIssueCommentProtocol, + DeleteIssueCommentReactionProtocol, + DeleteIssueReactionProtocol, + DeletePullRequestCommentProtocol, + DeletePullRequestCommentReactionProtocol, + DeletePullRequestReactionProtocol, + GetArchiveLinkProtocol, + GetBranchProtocol, + GetCheckRunProtocol, + GetCommitProtocol, + GetCommitsByPathProtocol, + GetCommitsProtocol, + GetFileContentProtocol, + GetGitCommitProtocol, + GetIssueCommentReactionsProtocol, + GetIssueCommentsProtocol, + GetIssueReactionsProtocol, + GetPullRequestCommentReactionsProtocol, + GetPullRequestCommentsProtocol, + GetPullRequestCommitsProtocol, + GetPullRequestDiffProtocol, + GetPullRequestFilesProtocol, + GetPullRequestProtocol, + GetPullRequestReactionsProtocol, + GetPullRequestsProtocol, + GetTreeProtocol, + MinimizeCommentProtocol, + RequestReviewProtocol, + ResolveReviewThreadProtocol, + UpdateBranchProtocol, + UpdateCheckRunProtocol, + UpdatePullRequestProtocol, +) + + +class Provider(Protocol): + """ + Providers abstract over an integration. They map generic commands to service-provider specific + commands and they map the results of those commands to generic result-types. + + Providers necessarily offer a larger API surface than what is available in an integration. Some + methods may be duplicates in some providers. This is intentional. Providers capture programmer + intent and translate it into a concrete interface. Therefore, providers provide a large range + of behaviors which may or may not be explicitly defined on a service-provider. + + Providers, also by necessity, offer a smaller API surface than what the SCM platform can + maximally provide. There are simply some operations which can not be adequately translated + between providers. None the less, we want to have a service-agnostic interface. This problem + is solved with capability-object-like system. Capabilities are progressively opted into using + structural sub-typing. As a provider's surface area expands the SourceCodeManager class will + automatically recognize that the provider has a particular capability and return "true" when + handling "can" requests. + """ + + organization_id: int + repository: Repository + + def is_rate_limited(self, referrer: Referrer) -> bool: ... diff --git a/src/sentry/testutils/helpers/apigateway.py b/src/sentry/testutils/helpers/apigateway.py index a96cf527434104..08e3c4ac4bf6f1 100644 --- a/src/sentry/testutils/helpers/apigateway.py +++ b/src/sentry/testutils/helpers/apigateway.py @@ -137,9 +137,9 @@ def provision_middleware(): @override_settings(ROOT_URLCONF=__name__) class ApiGatewayTestCase(APITestCase): # Subclasses will generally need to be decorated with - # @*_silo_test(regions=[ApiGatewayTestCase.REGION]) + # @*_silo_test(cells=[ApiGatewayTestCase.CELL]) - REGION = Cell( + CELL = Cell( name="us", snowflake_id=1, address="http://us.internal.sentry.io", @@ -150,21 +150,21 @@ def setUp(self): super().setUp() responses.add( responses.GET, - f"{self.REGION.address}/get", + f"{self.CELL.address}/get", body=json.dumps({"proxy": True}), content_type="application/json", adding_headers={"test": "header"}, ) responses.add( responses.GET, - f"{self.REGION.address}/error", + f"{self.CELL.address}/error", body=json.dumps({"proxy": True}), status=400, content_type="application/json", adding_headers={"test": "header"}, ) - self.organization = self.create_organization(region=self.REGION) + self.organization = self.create_organization(region=self.CELL) # Echos the request body and header back for verification def return_request_body(request): @@ -175,7 +175,7 @@ def return_request_params(request): params = parse_qs(request.url.split("?")[1]) return (200, request.headers, json.dumps(params).encode()) - responses.add_callback(responses.GET, f"{self.REGION.address}/echo", return_request_params) - responses.add_callback(responses.POST, f"{self.REGION.address}/echo", return_request_body) + responses.add_callback(responses.GET, f"{self.CELL.address}/echo", return_request_params) + responses.add_callback(responses.POST, f"{self.CELL.address}/echo", return_request_body) self.middleware = provision_middleware() diff --git a/static/app/components/guidedSteps/guidedSteps.tsx b/static/app/components/guidedSteps/guidedSteps.tsx index 730f217d20d0a8..113e2dc8497db6 100644 --- a/static/app/components/guidedSteps/guidedSteps.tsx +++ b/static/app/components/guidedSteps/guidedSteps.tsx @@ -371,6 +371,7 @@ const ChildrenWrapper = styled('div')<{isActive: boolean}>` `; const StepDetails = styled('div')` + overflow: hidden; grid-area: details; `; diff --git a/static/app/views/alerts/create.tsx b/static/app/views/alerts/create.tsx index 8230782cf304cf..7179f1f4db1bcf 100644 --- a/static/app/views/alerts/create.tsx +++ b/static/app/views/alerts/create.tsx @@ -137,7 +137,7 @@ export default function Create() { const title = t('New Alert Rule'); return ( - + @@ -228,6 +228,6 @@ export default function Create() { )} - + ); } diff --git a/static/app/views/alerts/edit.tsx b/static/app/views/alerts/edit.tsx index 9805c6bbfb5c1c..a5dced1453d396 100644 --- a/static/app/views/alerts/edit.tsx +++ b/static/app/views/alerts/edit.tsx @@ -61,7 +61,7 @@ export default function ProjectAlertsEditor() { const {teams, isLoading: teamsLoading} = useUserTeams(); return ( - + )} - + ); } diff --git a/static/app/views/alerts/list/incidents/index.tsx b/static/app/views/alerts/list/incidents/index.tsx index b3d75f8a7cf9ca..eb2d9e0b008c59 100644 --- a/static/app/views/alerts/list/incidents/index.tsx +++ b/static/app/views/alerts/list/incidents/index.tsx @@ -263,28 +263,30 @@ class IncidentsList extends DeprecatedAsyncComponent< return ( - - - - - {!this.tryRenderOnboarding() && ( - - - {t('This page only shows metric alerts.')} - - - - )} - {this.renderList()} - - - + + + + + + {!this.tryRenderOnboarding() && ( + + + {t('This page only shows metric alerts.')} + + + + )} + {this.renderList()} + + + + ); } @@ -303,15 +305,17 @@ export default function IncidentsListContainer() { }, []); const renderDisabled = () => ( - - - - - {t("You don't have access to this feature")} - - - - + + + + + + {t("You don't have access to this feature")} + + + + + ); return ( diff --git a/static/app/views/alerts/list/rules/alertRulesList.tsx b/static/app/views/alerts/list/rules/alertRulesList.tsx index 1c5e300230d21b..8d37a4e8d81516 100644 --- a/static/app/views/alerts/list/rules/alertRulesList.tsx +++ b/static/app/views/alerts/list/rules/alertRulesList.tsx @@ -210,132 +210,134 @@ export default function AlertRulesList() { - - - - - - {!hasMetricAlertsFeature && hasAnyMetricAlerts && ( - - - Your metric alerts have been disabled. Upgrade your plan to re-enable - them. - - - )} - - - {t('Alert Rule')} {sort.field === 'name' ? sortArrow : null} - , - - {t('Status')} {isAlertRuleSort ? sortArrow : null} - , - t('Project'), - t('Team'), - t('Actions'), - ]} - > - {isError ? ( - - ) : null} - 0} + + + + + + + {!hasMetricAlertsFeature && hasAnyMetricAlerts && ( + + + Your metric alerts have been disabled. Upgrade your plan to re-enable + them. + + + )} + + + {t('Alert Rule')} {sort.field === 'name' ? sortArrow : null} + , + + {t('Status')} {isAlertRuleSort ? sortArrow : null} + , + t('Project'), + t('Team'), + t('Actions'), + ]} > - - {({initiallyLoaded, projects}) => - ruleList.map(rule => { - const isIssueAlertInstance = isIssueAlert(rule); - const keyPrefix = isIssueAlertInstance - ? AlertRuleType.ISSUE - : rule.type === CombinedAlertType.UPTIME - ? AlertRuleType.UPTIME - : AlertRuleType.METRIC; + {isError ? ( + + ) : null} + 0} + > + + {({initiallyLoaded, projects}) => + ruleList.map(rule => { + const isIssueAlertInstance = isIssueAlert(rule); + const keyPrefix = isIssueAlertInstance + ? AlertRuleType.ISSUE + : rule.type === CombinedAlertType.UPTIME + ? AlertRuleType.UPTIME + : AlertRuleType.METRIC; - return ( - - ); - }) + return ( + + ); + }) + } + + + + { + let team = currentQuery.team; + // Keep team parameter, but empty to remove parameters + if (!team || team.length === 0) { + team = ''; } - - - - { - let team = currentQuery.team; - // Keep team parameter, but empty to remove parameters - if (!team || team.length === 0) { - team = ''; - } - navigate({ - pathname: path, - query: {...currentQuery, team, cursor}, - }); - }} - /> - - - + navigate({ + pathname: path, + query: {...currentQuery, team, cursor}, + }); + }} + /> + + + + ); } diff --git a/static/app/views/alerts/rules/issue/details/ruleDetails.tsx b/static/app/views/alerts/rules/issue/details/ruleDetails.tsx index a32f1eb4f7922a..2040b0d26c7628 100644 --- a/static/app/views/alerts/rules/issue/details/ruleDetails.tsx +++ b/static/app/views/alerts/rules/issue/details/ruleDetails.tsx @@ -383,139 +383,141 @@ export default function AlertRuleDetails() { const {period, start, end, utc} = getDataDatetime(); const cursor = decodeScalar(location.query.cursor); return ( - - - - - - - - + + + + + + - {rule.name} - - - - - - {({hasAccess}) => ( - - )} - - } - to={duplicateLink} - disabled={rule.status === 'disabled'} - > - {t('Duplicate')} - - } - to={makeAlertsPathname({ - path: `/rules/${projectSlug}/${ruleId}/`, - organization, - })} - onClick={() => - trackAnalytics('issue_alert_rule_details.edit_clicked', { + + + {rule.name} + + + + + + {({hasAccess}) => ( + + )} + + } + to={duplicateLink} + disabled={rule.status === 'disabled'} + > + {t('Duplicate')} + + } + to={makeAlertsPathname({ + path: `/rules/${projectSlug}/${ruleId}/`, organization, - rule_id: parseInt(ruleId, 10), - }) - } - > - {rule.status === 'disabled' ? t('Edit to enable') : t('Edit Rule')} - - - - - - - - {renderIncompatibleAlert()} - {renderDisabledAlertBanner()} - {rule.snooze && ( - - {rule.snoozeForEveryone ? ( - - {tct( - "[creator] muted this alert for everyone so you won't get these notifications in the future.", - { - creator: rule.snoozeCreatedBy, - } - )} - - ) : ( - - )} - - )} - - - + trackAnalytics('issue_alert_rule_details.edit_clicked', { + organization, + rule_id: parseInt(ruleId, 10), + }) + } + > + {rule.status === 'disabled' ? t('Edit to enable') : t('Edit Rule')} + + + + + + + + {renderIncompatibleAlert()} + {renderDisabledAlertBanner()} + {rule.snooze && ( + + {rule.snoozeForEveryone ? ( + + {tct( + "[creator] muted this alert for everyone so you won't get these notifications in the future.", + { + creator: rule.snoozeCreatedBy, + } + )} + + ) : ( + + )} + + )} + + + + + - - - - - - - - + + + + + + + ); } diff --git a/static/app/views/dashboards/hooks/useDuplicateDashboard.spec.tsx b/static/app/views/dashboards/hooks/useDuplicateDashboard.spec.tsx index 1bea2595c12150..ed4efde8da82e1 100644 --- a/static/app/views/dashboards/hooks/useDuplicateDashboard.spec.tsx +++ b/static/app/views/dashboards/hooks/useDuplicateDashboard.spec.tsx @@ -79,6 +79,74 @@ describe('useDuplicateDashboard', () => { expect(createMock).toHaveBeenCalled(); expect(onSuccess).toHaveBeenCalled(); }); + + it('copies saved filters and page filters when duplicating a prebuilt dashboard', async () => { + const savedFilters: DashboardFilters = { + globalFilter: [ + { + dataset: WidgetType.SPANS, + tag: {key: 'span.system', name: 'span.system'}, + value: 'postgresql', + }, + ], + }; + // Mock for fetchDashboard (saved instance with user's filters) + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/dashboards/55/`, + body: DashboardFixture([], { + id: '55', + prebuiltId: PrebuiltDashboardId.BACKEND_QUERIES, + filters: savedFilters, + projects: [1, 2], + environment: ['production'], + period: '7d', + }), + }); + // Mock for resolveLinkedDashboardIds (resolves linked summary dashboard) + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/dashboards/`, + method: 'GET', + body: [ + DashboardFixture([], { + id: '56', + prebuiltId: PrebuiltDashboardId.BACKEND_QUERIES_SUMMARY, + }), + ], + }); + const createMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/dashboards/`, + method: 'POST', + body: DashboardFixture([], {id: '200'}), + }); + + const onSuccess = jest.fn(); + const {result} = renderHookWithProviders(() => useDuplicateDashboard({onSuccess}), { + organization, + }); + + await act(async () => { + await result.current( + DashboardListItemFixture({ + id: '55', + prebuiltId: PrebuiltDashboardId.BACKEND_QUERIES, + }), + 'grid' + ); + }); + + expect(createMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + data: expect.objectContaining({ + filters: savedFilters, + projects: [1, 2], + environment: ['production'], + period: '7d', + }), + }) + ); + expect(onSuccess).toHaveBeenCalled(); + }); }); describe('useDuplicatePrebuiltDashboard', () => { @@ -88,7 +156,7 @@ describe('useDuplicatePrebuiltDashboard', () => { MockApiClient.clearMockResponses(); }); - it('fetches saved dashboard details and duplicates with saved filters', async () => { + it('fetches saved dashboard details and duplicates with saved filters and page filters', async () => { const savedFilters: DashboardFilters = { globalFilter: [ { @@ -104,6 +172,9 @@ describe('useDuplicatePrebuiltDashboard', () => { id: '55', prebuiltId: PrebuiltDashboardId.BACKEND_QUERIES_SUMMARY, filters: savedFilters, + projects: [3, 4], + environment: ['staging'], + period: '14d', }), }); const createMock = MockApiClient.addMockResponse({ @@ -125,7 +196,12 @@ describe('useDuplicatePrebuiltDashboard', () => { expect(createMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - data: expect.objectContaining({filters: savedFilters}), + data: expect.objectContaining({ + filters: savedFilters, + projects: [3, 4], + environment: ['staging'], + period: '14d', + }), }) ); expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({id: '300'})); diff --git a/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx b/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx index 0f016fa3cdcc41..7bebd573c56b69 100644 --- a/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx +++ b/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx @@ -27,13 +27,28 @@ export function useDuplicateDashboard({onSuccess}: UseDuplicateDashboardProps) { const duplicateDashboard = useCallback( async (dashboard: DashboardListItem, viewType: 'table' | 'grid') => { try { - const dashboardDetail = dashboard.prebuiltId - ? await resolveLinkedDashboardIds({ + let dashboardDetail: DashboardDetails; + if (dashboard.prebuiltId) { + // Widgets come from static config. If the dashboard has been saved + // (has a real ID), also fetch the saved instance to copy its filters. + const hasSavedInstance = dashboard.id && dashboard.id !== '-1'; + const [resolved, saved] = await Promise.all([ + resolveLinkedDashboardIds({ queryClient, orgSlug: organization.slug, dashboard: toPrebuiltDashboardDetails(dashboard.prebuiltId), - }) - : await fetchDashboard(api, organization.slug, dashboard.id); + }), + hasSavedInstance + ? fetchDashboard(api, organization.slug, dashboard.id) + : Promise.resolve(undefined), + ]); + dashboardDetail = resolved; + if (saved) { + copySavedFilters(dashboardDetail, saved); + } + } else { + dashboardDetail = await fetchDashboard(api, organization.slug, dashboard.id); + } const newDashboard = cloneDashboard(dashboardDetail); newDashboard.title = `${newDashboard.title} copy`; @@ -93,9 +108,7 @@ export function useDuplicatePrebuiltDashboard({onSuccess}: UseDuplicateDashboard delete newDashboard.prebuiltId; newDashboard.title = `${newDashboard.title} copy`; newDashboard.widgets.map(widget => (widget.id = undefined)); - if (savedDashboard.filters !== undefined) { - newDashboard.filters = savedDashboard.filters; - } + copySavedFilters(newDashboard, savedDashboard); const copiedDashboard = await createDashboard( api, organization.slug, @@ -115,6 +128,22 @@ export function useDuplicatePrebuiltDashboard({onSuccess}: UseDuplicateDashboard return {duplicatePrebuiltDashboard, isLoading}; } +/** + * Copies saved filter state from a persisted dashboard onto a target. + * This includes both the DashboardFilters object (globalFilter, release) and + * the page-level filters (projects, environment, date range) which are stored + * as top-level fields. + */ +function copySavedFilters(target: DashboardDetails, source: DashboardDetails): void { + target.filters = source.filters; + target.projects = source.projects; + target.environment = source.environment; + target.period = source.period; + target.start = source.start; + target.end = source.end; + target.utc = source.utc; +} + /** * Prebuilt dashboard configs don't have an `id` since they aren't persisted. * `cloneDashboard` and `createDashboard` require `DashboardDetails` (which includes `id`), diff --git a/static/app/views/dashboards/manage/index.tsx b/static/app/views/dashboards/manage/index.tsx index 53ff00990f0b39..4e90d37c651b40 100644 --- a/static/app/views/dashboards/manage/index.tsx +++ b/static/app/views/dashboards/manage/index.tsx @@ -170,6 +170,9 @@ function ManageDashboards() { const isOnlyPrebuilt = hasPrebuiltDashboards && urlFilter === DashboardFilter.ONLY_PREBUILT; + const areAiFeaturesAllowed = + !organization.hideAiFeatures && organization.features.includes('gen-ai-features'); + const [showTemplates, setShowTemplatesLocal] = useLocalStorageState( SHOW_TEMPLATES_KEY, shouldShowTemplates() @@ -208,7 +211,9 @@ function ManageDashboards() { pin: 'favorites', per_page: dashboardsLayout === GRID ? rowCount * columnCount : DASHBOARD_TABLE_NUM_ROWS, - ...(isOnlyPrebuilt ? {filter: DashboardFilter.ONLY_PREBUILT} : {}), + ...(isOnlyPrebuilt + ? {filter: DashboardFilter.ONLY_PREBUILT} + : {filter: DashboardFilter.EXCLUDE_PREBUILT}), }, }, ], @@ -655,9 +660,9 @@ function ManageDashboards() { )} - + {({hasFeature: hasAiGenerate}) => - hasAiGenerate ? ( + hasAiGenerate && areAiFeaturesAllowed ? ( {({ hasReachedDashboardLimit, @@ -680,7 +685,7 @@ function ManageDashboards() { label: ( {t('Generate dashboard')} - + ), onAction: () => onGenerateDashboard(), diff --git a/static/app/views/insights/pages/conversations/layout.tsx b/static/app/views/insights/pages/conversations/layout.tsx index 30020aca1eae4a..9df28e8b02b4b9 100644 --- a/static/app/views/insights/pages/conversations/layout.tsx +++ b/static/app/views/insights/pages/conversations/layout.tsx @@ -1,6 +1,6 @@ -import {Fragment} from 'react'; import {Outlet, useMatches} from 'react-router-dom'; +import * as Layout from 'sentry/components/layouts/thirds'; import {ConversationsPageHeader} from 'sentry/views/insights/pages/conversations/conversationsPageHeader'; import {ModuleName} from 'sentry/views/insights/types'; @@ -8,12 +8,12 @@ function ConversationsLayout() { const handle = useMatches().at(-1)?.handle as {module?: ModuleName} | undefined; return ( - + {handle && 'module' in handle ? ( ) : null} - + ); } diff --git a/static/app/views/issueDetails/useGroupDetailsRoute.tsx b/static/app/views/issueDetails/useGroupDetailsRoute.tsx index 5821725adced2e..63bb8322423103 100644 --- a/static/app/views/issueDetails/useGroupDetailsRoute.tsx +++ b/static/app/views/issueDetails/useGroupDetailsRoute.tsx @@ -1,18 +1,22 @@ -import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; +import type {PlainRoute} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; -import {useRouter} from 'sentry/utils/useRouter'; +import {useRoutes} from 'sentry/utils/useRoutes'; import {Tab, TabPaths} from 'sentry/views/issueDetails/types'; -type RouteProps = RouteComponentProps<{groupId: string; eventId?: string}>; - -function getCurrentTab({router}: {router: RouteProps['router']}) { - const currentRoute = router.routes[router.routes.length - 1]; +function getCurrentTab({ + routes, + params, +}: { + params: Record; + routes: PlainRoute[]; +}) { + const currentRoute = routes[routes.length - 1]; // If we're in the tag details page ("/distributions/:tagKey/") - if (router.params.tagKey) { + if (params.tagKey) { return Tab.DISTRIBUTIONS; } return ( @@ -24,21 +28,23 @@ function getCurrentRouteInfo({ groupId, eventId, organization, - router, + routes, + params, }: { eventId: string | undefined; groupId: string; organization: Organization; - router: RouteProps['router']; + params: Record; + routes: PlainRoute[]; }): { baseUrl: string; currentTab: Tab; } { - const currentTab = getCurrentTab({router}); + const currentTab = getCurrentTab({routes, params}); const baseUrl = normalizeUrl( `/organizations/${organization.slug}/issues/${groupId}/${ - router.params.eventId && eventId ? `events/${eventId}/` : '' + params.eventId && eventId ? `events/${eventId}/` : '' }` ); @@ -50,12 +56,17 @@ export function useGroupDetailsRoute(): { currentTab: Tab; } { const organization = useOrganization(); - const params = useParams<{groupId: string; eventId?: string}>(); - const router = useRouter(); + const params = useParams<{ + groupId: string; + eventId?: string; + tagKey?: string; + }>(); + const routes = useRoutes(); return getCurrentRouteInfo({ groupId: params.groupId, eventId: params.eventId, organization, - router, + routes, + params, }); } diff --git a/static/app/views/profiling/continuousProfileFlamegraph.tsx b/static/app/views/profiling/continuousProfileFlamegraph.tsx index 96f3fde4aae9a7..0aaa232b60861e 100644 --- a/static/app/views/profiling/continuousProfileFlamegraph.tsx +++ b/static/app/views/profiling/continuousProfileFlamegraph.tsx @@ -4,7 +4,6 @@ import * as qs from 'query-string'; import {Stack} from '@sentry/scraps/layout'; -import * as Layout from 'sentry/components/layouts/thirds'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {ContinuousFlamegraph} from 'sentry/components/profiling/flamegraph/continuousFlamegraph'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; @@ -104,27 +103,25 @@ export default function ContinuousProfileFlamegraphWrapper() { title={t('Profiling \u2014 Flamechart')} orgSlug={organization.slug} > - - - - - - - - {profiles.type === 'loading' ? ( - - - - ) : null} - - - - - - + + + + + + + {profiles.type === 'loading' ? ( + + + + ) : null} + + + + + ); } @@ -159,9 +156,3 @@ const FlamegraphContainer = styled('div')` flex-direction: column; flex: 1 1 100%; `; - -const LayoutPageWithHiddenFooter = styled(Layout.Page)` - ~ footer { - display: none; - } -`; diff --git a/static/app/views/profiling/continuousProfileProvider.tsx b/static/app/views/profiling/continuousProfileProvider.tsx index d19eee233ec8a3..bbd1aec47844e8 100644 --- a/static/app/views/profiling/continuousProfileProvider.tsx +++ b/static/app/views/profiling/continuousProfileProvider.tsx @@ -9,6 +9,7 @@ import {decodeScalar} from 'sentry/utils/queryString'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; +import {LayoutPageWithHiddenFooter} from 'sentry/views/profiling/layoutPageWithHiddenFooter'; import {ContinuousProfileProvider, ProfileTransactionContext} from './profilesProvider'; @@ -65,12 +66,14 @@ export default function ProfileAndTransactionProvider(): React.ReactElement { setProfile={setProfile} > - - + + + + ); diff --git a/static/app/views/profiling/differentialFlamegraph.tsx b/static/app/views/profiling/differentialFlamegraph.tsx index d0d5a91d143383..3d704d0e81bd63 100644 --- a/static/app/views/profiling/differentialFlamegraph.tsx +++ b/static/app/views/profiling/differentialFlamegraph.tsx @@ -40,6 +40,7 @@ import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphR import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL'; import {Rect} from 'sentry/utils/profiling/speedscope'; import {useLocation} from 'sentry/utils/useLocation'; +import {LayoutPageWithHiddenFooter} from 'sentry/views/profiling/layoutPageWithHiddenFooter'; import {LOADING_PROFILE_GROUP} from 'sentry/views/profiling/profileGroupProvider'; const PROFILE_TYPE = 'differential aggregate flamegraph' as const; @@ -348,26 +349,24 @@ const DifferentialFlamegraphContainer = styled('div')` display: flex; flex-direction: column; flex: 1; - - ~ footer { - display: none; - } `; function DifferentialFlamegraphWithProviders() { return ( - - - - - + + + + + + + ); } diff --git a/static/app/views/profiling/landing/slowestFunctionsWidget.tsx b/static/app/views/profiling/landing/slowestFunctionsWidget.tsx index 2c071319f63118..561fa6c3b9cdcb 100644 --- a/static/app/views/profiling/landing/slowestFunctionsWidget.tsx +++ b/static/app/views/profiling/landing/slowestFunctionsWidget.tsx @@ -47,7 +47,6 @@ 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 {useRouter} from 'sentry/utils/useRouter'; import type {DataState} from 'sentry/views/profiling/useLandingAnalytics'; import {getProfileTargetId} from 'sentry/views/profiling/utils'; @@ -88,7 +87,6 @@ export function SlowestFunctionsWidget({ onDataState, }: SlowestFunctionsWidgetProps) { const navigate = useNavigate(); - const router = useRouter(); const location = useLocation(); const organization = useOrganization(); @@ -219,11 +217,14 @@ export function SlowestFunctionsWidget({ onChange={option => { setSortFunction(option.value as SortOption); setExpandedIndex(0); - const newQuery = omit(router.location.query, [cursorName]); - router.replace({ - pathname: router.location.pathname, - query: newQuery, - }); + const newQuery = omit(location.query, [cursorName]); + navigate( + { + pathname: location.pathname, + query: newQuery, + }, + {replace: true} + ); }} trigger={triggerProps => ( - - - - - - - - {profiles.type === 'loading' || profiledTransaction.type === 'loading' ? ( - - - - ) : null} - - - - - - + + + + + + + {profiles.type === 'loading' || profiledTransaction.type === 'loading' ? ( + + + + ) : null} + + + + + ); } @@ -160,9 +157,3 @@ const FlamegraphContainer = styled('div')` flex-direction: column; flex: 1 1 100%; `; - -const LayoutPageWithHiddenFooter = styled(Layout.Page)` - ~ footer { - display: none; - } -`; diff --git a/static/app/views/profiling/profileSummary/index.tsx b/static/app/views/profiling/profileSummary/index.tsx index 1e56955c2e3f31..d4137d3717c93d 100644 --- a/static/app/views/profiling/profileSummary/index.tsx +++ b/static/app/views/profiling/profileSummary/index.tsx @@ -67,6 +67,7 @@ import { useFlamegraph, } from 'sentry/views/profiling/flamegraphProvider'; import {ProfilesSummaryChart} from 'sentry/views/profiling/landing/profilesSummaryChart'; +import {LayoutPageWithHiddenFooter} from 'sentry/views/profiling/layoutPageWithHiddenFooter'; import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider'; import {ProfilesTable} from 'sentry/views/profiling/profileSummary/profilesTable'; @@ -636,15 +637,6 @@ const ProfileSummaryContainer = styled('div')` display: flex; flex-direction: column; flex: 1 1 100%; - - /* - * The footer component is a sibling of this div. - * Remove it so the flamegraph can take up the - * entire screen. - */ - ~ footer { - display: none; - } `; const PROFILE_DIGEST_FIELDS = [ @@ -781,10 +773,12 @@ const ProfileDigestLabel = styled('span')` export default function ProfileSummaryPageToggle() { return ( - - - - - + + + + + + + ); } diff --git a/static/app/views/profiling/transactionProfileProvider.tsx b/static/app/views/profiling/transactionProfileProvider.tsx index f7fb3553b06802..34a810413dc3ff 100644 --- a/static/app/views/profiling/transactionProfileProvider.tsx +++ b/static/app/views/profiling/transactionProfileProvider.tsx @@ -8,6 +8,7 @@ import {isSchema, isSentrySampledProfile} from 'sentry/utils/profiling/guards/pr import {useSentryEvent} from 'sentry/utils/profiling/hooks/useSentryEvent'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; +import {LayoutPageWithHiddenFooter} from 'sentry/views/profiling/layoutPageWithHiddenFooter'; import {ProfileTransactionContext, TransactionProfileProvider} from './profilesProvider'; @@ -46,14 +47,16 @@ export default function ProfileAndTransactionProvider(): React.ReactElement { setProfile={setProfile} > - - + + + + ); diff --git a/static/app/views/projectInstall/newProject.tsx b/static/app/views/projectInstall/newProject.tsx index 57ccb755247948..19c5ea0d47685a 100644 --- a/static/app/views/projectInstall/newProject.tsx +++ b/static/app/views/projectInstall/newProject.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; +import * as Layout from 'sentry/components/layouts/thirds'; import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle'; import {CreateProject} from './createProject'; @@ -7,13 +8,15 @@ import {CreateProject} from './createProject'; function NewProject() { return ( - -
- - - -
-
+ + +
+ + + +
+
+
); } diff --git a/tests/sentry/api/endpoints/test_organization_fork.py b/tests/sentry/api/endpoints/test_organization_fork.py index d9d76fae630957..9953bdd548d4bc 100644 --- a/tests/sentry/api/endpoints/test_organization_fork.py +++ b/tests/sentry/api/endpoints/test_organization_fork.py @@ -25,7 +25,7 @@ @patch("sentry.analytics.record") @patch("sentry.relocation.tasks.process.uploading_start.apply_async") -@cell_silo_test(regions=SAAS_TO_SAAS_TEST_REGIONS) +@cell_silo_test(cells=SAAS_TO_SAAS_TEST_REGIONS) class OrganizationForkTest(APITestCase): endpoint = "sentry-api-0-organization-fork" method = "POST" diff --git a/tests/sentry/hybridcloud/apigateway/test_apigateway.py b/tests/sentry/hybridcloud/apigateway/test_apigateway.py index 992e91b217170f..723305c5ab8568 100644 --- a/tests/sentry/hybridcloud/apigateway/test_apigateway.py +++ b/tests/sentry/hybridcloud/apigateway/test_apigateway.py @@ -14,7 +14,7 @@ from sentry.utils import json -@control_silo_test(cells=[ApiGatewayTestCase.REGION], include_monolith_run=True) +@control_silo_test(cells=[ApiGatewayTestCase.CELL], include_monolith_run=True) class ApiGatewayTest(ApiGatewayTestCase): @responses.activate def test_simple(self) -> None: @@ -22,7 +22,7 @@ def test_simple(self) -> None: headers = dict(example="this") responses.add_callback( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/region/", + f"{self.CELL.address}/organizations/{self.organization.slug}/region/", verify_request_params(query_params, headers), ) @@ -44,7 +44,7 @@ def test_simple(self) -> None: def test_proxy_does_not_resolve_redirect(self) -> None: responses.add( responses.POST, - f"{self.REGION.address}/organizations/{self.organization.slug}/region/", + f"{self.CELL.address}/organizations/{self.organization.slug}/region/", headers={"Location": "https://zombo.com"}, status=302, ) @@ -78,12 +78,12 @@ def test_proxy_check_org_slug_url(self) -> None: """Test the logic of when a request should be proxied""" responses.add( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/region/", + f"{self.CELL.address}/organizations/{self.organization.slug}/region/", json={"proxy": True}, ) responses.add( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/control/", + f"{self.CELL.address}/organizations/{self.organization.slug}/control/", json={"proxy": True}, ) @@ -114,22 +114,22 @@ def test_proxy_check_org_id_or_slug_url_with_params(self) -> None: """Test the logic of when a request should be proxied""" responses.add( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/region/", + f"{self.CELL.address}/organizations/{self.organization.slug}/region/", json={"proxy": True}, ) responses.add( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.slug}/control/", + f"{self.CELL.address}/organizations/{self.organization.slug}/control/", json={"proxy": True}, ) responses.add( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.id}/region/", + f"{self.CELL.address}/organizations/{self.organization.id}/region/", json={"proxy": True}, ) responses.add( responses.GET, - f"{self.REGION.address}/organizations/{self.organization.id}/control/", + f"{self.CELL.address}/organizations/{self.organization.id}/control/", json={"proxy": True}, ) @@ -183,7 +183,7 @@ def test_proxy_check_region_pinned_url(self) -> None: project_key = self.create_project_key(self.project) responses.add( responses.GET, - f"{self.REGION.address}/js-sdk-loader/{project_key.public_key}.js", + f"{self.CELL.address}/js-sdk-loader/{project_key.public_key}.js", json={"proxy": True}, ) @@ -206,15 +206,15 @@ def test_proxy_check_region_pinned_url(self) -> None: assert resp.data["proxy"] is False @responses.activate - def test_proxy_check_region_pinned_url_with_params(self) -> None: + def test_proxy_check_cell_pinned_url_with_params(self) -> None: responses.add( responses.GET, - f"{self.REGION.address}/relays/register/", + f"{self.CELL.address}/relays/register/", json={"proxy": True}, ) responses.add( responses.GET, - f"{self.REGION.address}/relays/abc123/", + f"{self.CELL.address}/relays/abc123/", json={"proxy": True, "details": True}, ) @@ -231,16 +231,16 @@ def test_proxy_check_region_pinned_url_with_params(self) -> None: assert resp_json["details"] is True @responses.activate - def test_proxy_check_region_pinned_issue_urls(self) -> None: + def test_proxy_check_cell_pinned_issue_urls(self) -> None: issue = self.create_group() responses.add( responses.GET, - f"{self.REGION.address}/issues/{issue.id}/", + f"{self.CELL.address}/issues/{issue.id}/", json={"proxy": True, "id": issue.id}, ) responses.add( responses.GET, - f"{self.REGION.address}/issues/{issue.id}/events/", + f"{self.CELL.address}/issues/{issue.id}/events/", json={"proxy": True, "id": issue.id, "events": True}, ) @@ -266,7 +266,7 @@ def test_proxy_check_region_pinned_issue_urls(self) -> None: def test_proxy_error_embed_dsn(self) -> None: responses.add( responses.GET, - f"{self.REGION.address}/api/embed/error-page/", + f"{self.CELL.address}/api/embed/error-page/", json={"proxy": True, "name": "error-embed"}, ) with override_settings(SILO_MODE=SiloMode.CONTROL, MIDDLEWARE=tuple(self.middleware)): diff --git a/tests/sentry/hybridcloud/apigateway/test_apigateway_helpers.py b/tests/sentry/hybridcloud/apigateway/test_apigateway_helpers.py index 9eca3bec73ee23..f07005d6d73707 100644 --- a/tests/sentry/hybridcloud/apigateway/test_apigateway_helpers.py +++ b/tests/sentry/hybridcloud/apigateway/test_apigateway_helpers.py @@ -8,7 +8,7 @@ from sentry.utils import json -@no_silo_test(cells=[ApiGatewayTestCase.REGION]) +@no_silo_test(cells=[ApiGatewayTestCase.CELL]) class VerifyRequestBodyTest(ApiGatewayTestCase): @responses.activate def test_verify_request_body(self) -> None: diff --git a/tests/sentry/hybridcloud/apigateway/test_proxy.py b/tests/sentry/hybridcloud/apigateway/test_proxy.py index 47e5913de61139..c1458db1ba4f71 100644 --- a/tests/sentry/hybridcloud/apigateway/test_proxy.py +++ b/tests/sentry/hybridcloud/apigateway/test_proxy.py @@ -30,7 +30,7 @@ url_name = "sentry-api-0-projets" -@control_silo_test(cells=[ApiGatewayTestCase.REGION], include_monolith_run=True) +@control_silo_test(cells=[ApiGatewayTestCase.CELL], include_monolith_run=True) class ProxyTestCase(ApiGatewayTestCase): @responses.activate def test_simple(self) -> None: @@ -275,7 +275,7 @@ def test_strip_request_headers(self) -> None: } -@control_silo_test(regions=[ApiGatewayTestCase.REGION]) +@control_silo_test(cells=[ApiGatewayTestCase.CELL]) class ProxyCircuitBreakerTestCase(ApiGatewayTestCase): def _make_breaker_mock(self, *, allow_request: bool) -> MagicMock: mock_breaker = MagicMock() @@ -304,7 +304,7 @@ def test_circuit_breaker_keyed_per_cell(self) -> None: request = RequestFactory().get("http://sentry.io/get") proxy_request(request, self.organization.slug, url_name) key_used = mock_breaker_class.call_args.kwargs["key"] - assert key_used == f"apigateway.proxy.{self.REGION.name}" + assert key_used == f"apigateway.proxy.{self.CELL.name}" @responses.activate def test_circuit_breaker_disabled_by_default(self) -> None: @@ -339,7 +339,7 @@ def test_handles_invalid_config(self) -> None: def test_timeout_records_error(self) -> None: responses.add( responses.GET, - f"{self.REGION.address}/timeout", + f"{self.CELL.address}/timeout", body=Timeout(), ) with patch("sentry.hybridcloud.apigateway.proxy.CircuitBreaker") as mock_breaker_class: @@ -355,7 +355,7 @@ def test_timeout_records_error(self) -> None: def test_5xx_response_records_error(self) -> None: responses.add( responses.GET, - f"{self.REGION.address}/server-error", + f"{self.CELL.address}/server-error", status=500, body=json.dumps({"detail": "internal server error"}), content_type="application/json", diff --git a/tests/sentry/hybridcloud/services/test_cell_organization_provisioning.py b/tests/sentry/hybridcloud/services/test_cell_organization_provisioning.py index bbe89289ca5053..371e24ba2f8d5a 100644 --- a/tests/sentry/hybridcloud/services/test_cell_organization_provisioning.py +++ b/tests/sentry/hybridcloud/services/test_cell_organization_provisioning.py @@ -254,7 +254,7 @@ def create_rpc_organization_slug_reservation(self, slug: str) -> RpcOrganization slug=slug, organization_id=self.provisioned_org.id, user_id=self.provisioning_user.id, - region_name="us", + cell_name="us", reservation_type=OrganizationSlugReservationType.TEMPORARY_RENAME_ALIAS.value, ) diff --git a/tests/sentry/hybridcloud/test_cell.py b/tests/sentry/hybridcloud/test_cell.py index 3d652f0e0f633c..a1ddb07cc44b3f 100644 --- a/tests/sentry/hybridcloud/test_cell.py +++ b/tests/sentry/hybridcloud/test_cell.py @@ -21,7 +21,7 @@ ) -@control_silo_test(regions=_TEST_CELLS) +@control_silo_test(cells=_TEST_CELLS) class CellResolutionTest(TestCase): def setUp(self) -> None: self.target_cell = _TEST_CELLS[0] diff --git a/tests/sentry/integrations/jira/test_sentry_issue_details.py b/tests/sentry/integrations/jira/test_sentry_issue_details.py index 0db8dcddb54b95..7ccd484f9e2484 100644 --- a/tests/sentry/integrations/jira/test_sentry_issue_details.py +++ b/tests/sentry/integrations/jira/test_sentry_issue_details.py @@ -173,7 +173,7 @@ def test_simple_not_linked(self, mock_get_integration_from_request: MagicMock) - assert b"This Sentry issue is not linked to a Jira issue" in response.content -@control_silo_test(regions=region_config) +@control_silo_test(cells=region_config) class JiraIssueHookControlTest(APITestCase): def setUp(self) -> None: super().setUp() diff --git a/tests/sentry/relocation/tasks/test_process.py b/tests/sentry/relocation/tasks/test_process.py index 454defcbc67dfb..8fa4e5bd7a0d58 100644 --- a/tests/sentry/relocation/tasks/test_process.py +++ b/tests/sentry/relocation/tasks/test_process.py @@ -223,7 +223,7 @@ def mock_message_builder(self, fake_message_builder: Mock): @patch("sentry.backup.crypto.KeyManagementServiceClient") @patch("sentry.relocation.utils.MessageBuilder") @patch("sentry.relocation.tasks.process.uploading_complete.apply_async") -@cell_silo_test(regions=SAAS_TO_SAAS_TEST_REGIONS) +@cell_silo_test(cells=SAAS_TO_SAAS_TEST_REGIONS) class UploadingStartTest(RelocationTaskTestCase): def setUp(self) -> None: self.owner = self.create_user( diff --git a/tests/sentry/relocation/tasks/test_transfer.py b/tests/sentry/relocation/tasks/test_transfer.py index ec94f7692645f7..01b51a0a54d364 100644 --- a/tests/sentry/relocation/tasks/test_transfer.py +++ b/tests/sentry/relocation/tasks/test_transfer.py @@ -130,7 +130,7 @@ def test_purge_expired(self, mock_process: MagicMock) -> None: assert not RegionRelocationTransfer.objects.filter(id=transfer.id).exists() -@control_silo_test(regions=TEST_REGIONS) +@control_silo_test(cells=TEST_REGIONS) class ProcessRelocationTransferControlTest(TestCase): def test_missing_transfer(self) -> None: res = process_relocation_transfer_control(transfer_id=999) @@ -181,7 +181,7 @@ def test_transfer_reply_state(self, mock_uploading_complete: MagicMock) -> None: assert RelocationFile.objects.filter(relocation=relocation).exists() -@cell_silo_test(regions=TEST_REGIONS) +@cell_silo_test(cells=TEST_REGIONS) class ProcessRelocationTransferRegionTest(TestCase): def test_missing_transfer(self) -> None: res = process_relocation_transfer_region(transfer_id=999) diff --git a/tests/sentry/scm/test_fixtures.py b/tests/sentry/scm/test_fixtures.py index 9c4532b554f45d..191632a30daf90 100644 --- a/tests/sentry/scm/test_fixtures.py +++ b/tests/sentry/scm/test_fixtures.py @@ -4,7 +4,6 @@ from sentry.integrations.github.client import GitHubApiClient, GitHubReaction from sentry.integrations.models import Integration -from sentry.scm.private.provider import Provider from sentry.scm.types import ( ActionResult, BuildConclusion, @@ -25,6 +24,7 @@ PaginatedActionResult, PaginatedResponseMeta, PaginationParams, + Provider, PullRequest, PullRequestBranch, PullRequestCommit, diff --git a/tests/sentry/scm/unit/test_scm_actions.py b/tests/sentry/scm/unit/test_scm_actions.py index f4dd319643629b..cbbb2b231f0b60 100644 --- a/tests/sentry/scm/unit/test_scm_actions.py +++ b/tests/sentry/scm/unit/test_scm_actions.py @@ -60,8 +60,14 @@ SCMProviderException, SCMUnhandledException, ) -from sentry.scm.private.provider import GetBranchProtocol, GetIssueReactionsProtocol -from sentry.scm.types import PaginatedActionResult, ReactionResult, Referrer, Repository +from sentry.scm.types import ( + GetBranchProtocol, + GetIssueReactionsProtocol, + PaginatedActionResult, + ReactionResult, + Referrer, + Repository, +) from tests.sentry.scm.test_fixtures import BaseTestProvider diff --git a/tests/sentry/users/models/test_user.py b/tests/sentry/users/models/test_user.py index 5f9e837f9a18b3..a34ab72712ae1b 100644 --- a/tests/sentry/users/models/test_user.py +++ b/tests/sentry/users/models/test_user.py @@ -52,7 +52,7 @@ ) -@control_silo_test(regions=_TEST_CELLS) +@control_silo_test(cells=_TEST_CELLS) class UserHybridCloudDeletionTest(TestCase): def setUp(self) -> None: super().setUp()