Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .agents/skills/hybrid-cloud-rpc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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):
Expand All @@ -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):
...
```

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
32 changes: 16 additions & 16 deletions .agents/skills/hybrid-cloud-test-gen/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
)

Expand All @@ -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),
)

Expand All @@ -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"},
)
Expand All @@ -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({}, {}),
)

Expand All @@ -128,20 +128,20 @@ 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}/")
```

## 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`.
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
```

Expand Down Expand Up @@ -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():
Expand Down
65 changes: 65 additions & 0 deletions src/sentry/apidocs/examples/replay_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions src/sentry/apidocs/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Loading
Loading