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()