diff --git a/api/.claude/context/api-structure.md b/api/.claude/context/api-structure.md new file mode 100644 index 000000000000..063292fae5c7 --- /dev/null +++ b/api/.claude/context/api-structure.md @@ -0,0 +1,157 @@ +# API Structure + +## URL Patterns + +### Features +- **Project Features**: `/api/v1/projects/{project_pk}/features/` + - List/create features for a project + - ViewSet: `FeatureViewSet` + +- **Feature Detail**: `/api/v1/projects/{project_pk}/features/{id}/` + - Retrieve/update/delete specific feature + - ViewSet: `FeatureViewSet` + +### Feature States + +#### Environment Feature States +- **List/Create**: `/api/v1/environments/{api_key}/featurestates/` + - Get feature states for environment (no segment/identity overrides) + - ViewSet: `EnvironmentFeatureStateViewSet` + - Filters: `feature`, `feature_name`, `anyIdentity` (deprecated) + - New: `segment` parameter to filter segment overrides + +- **Detail**: `/api/v1/environments/{api_key}/featurestates/{id}/` + - Retrieve/update/delete specific feature state + - ViewSet: `EnvironmentFeatureStateViewSet` + +#### Identity Feature States +- **List/Create**: `/api/v1/environments/{api_key}/identities/{identity_pk}/featurestates/` + - Feature states for specific identity + - ViewSet: `IdentityFeatureStateViewSet` + +- **All States**: `/api/v1/environments/{api_key}/identities/{identity_pk}/featurestates/all/` + - Get all feature states for identity (including environment defaults) + - ViewSet: `IdentityFeatureStateViewSet.all` action + +#### Simple Feature States (Alternative Endpoint) +- **List/Create/Update**: `/api/v1/features/featurestates/` + - Simpler endpoint for creating feature states + - ViewSet: `SimpleFeatureStateViewSet` + - Required param: `environment` (ID) + +### Segments + +#### Feature Segments (Segment Override Associations) +- **List/Create**: `/api/v1/features/feature-segments/` + - Links segments to features with priority + - ViewSet: `FeatureSegmentViewSet` + - Required params: `environment` (ID), `feature` (ID) + +- **Update Priorities**: `/api/v1/features/feature-segments/update-priorities/` + - Batch update segment override priorities + - ViewSet: `FeatureSegmentViewSet.update_priorities` action + +#### Segments +- **List/Create**: `/api/v1/projects/{project_pk}/segments/` + - Manage segments for a project + +### Segment Overrides (Legacy Endpoint) +- **Create**: `/api/v1/environments/{api_key}/features/{feature_pk}/create-segment-override/` + - Create segment override for a feature + - Function: `create_segment_override` + +## Key ViewSets + +### FeatureViewSet +- **Location**: `api/features/views.py` +- **Purpose**: Manage features at project level +- **Permissions**: `FeaturePermissions` +- **Filters**: `environment`, `search`, `tags`, `is_archived`, `owners`, `group_owners`, `value_search`, `is_enabled` + +### EnvironmentFeatureStateViewSet +- **Location**: `api/features/views.py` +- **Base Class**: `BaseFeatureStateViewSet` +- **Purpose**: Manage environment-level feature states (base states + segment overrides) +- **Permissions**: `EnvironmentFeatureStatePermissions` +- **Default Filter**: `feature_segment=None, identity=None` (base states only) +- **New**: Can filter by `segment` parameter to get segment overrides + +### IdentityFeatureStateViewSet +- **Location**: `api/features/views.py` +- **Base Class**: `BaseFeatureStateViewSet` +- **Purpose**: Manage identity-specific feature state overrides +- **Permissions**: `IdentityFeatureStatePermissions` +- **Filter**: `identity__pk={identity_pk}` + +### FeatureSegmentViewSet +- **Location**: `api/features/feature_segments/views.py` +- **Purpose**: Manage feature-segment associations (which segments override which features) +- **Permissions**: `FeatureSegmentPermissions` +- **Required Filters**: `environment` (ID), `feature` (ID) + +## Feature Versioning + +### v1 (Default) +- Feature states directly on environment +- No versioning or scheduling +- Filter: `environment=` + +### v2 (Optional) +- Feature states via `EnvironmentFeatureVersion` +- Supports versioning and scheduling of changes +- Enabled per environment: `environment.use_v2_feature_versioning = True` +- Uses `get_current_live_environment_feature_version()` to get active version +- Filter: `environment_feature_version=` + +### Checking Version in Views +```python +if environment.use_v2_feature_versioning: + queryset = queryset.filter( + environment_feature_version=get_current_live_environment_feature_version( + environment_id=environment_id, + feature_id=feature_id, + ) + ) +``` + +## Common Query Patterns + +### Get Environment Feature States (Base States Only) +```python +GET /api/v1/environments/{api_key}/featurestates/ +# Returns: Feature states where feature_segment=None and identity=None +``` + +### Get Segment Override States +```python +GET /api/v1/environments/{api_key}/featurestates/?segment={segment_id} +# Returns: Feature states where feature_segment.segment_id={segment_id} +``` + +### Get Feature State by Feature +```python +GET /api/v1/environments/{api_key}/featurestates/?feature={feature_id} +# Returns: Feature states for specific feature +``` + +### Get All Identity States +```python +GET /api/v1/environments/{api_key}/identities/{identity_pk}/featurestates/all/ +# Returns: All feature states with identity overrides + environment defaults +``` + +## Authentication + +### User Authentication +- Django session auth +- Token auth +- Required for dashboard/admin endpoints + +### Environment Key Authentication +- Header: `X-Environment-Key: {api_key}` +- Used for SDK endpoints +- Limited to environment-specific operations + +### Master API Key Authentication +- Full access to organization +- Used for automation/integrations diff --git a/api/.claude/context/models-relationships.md b/api/.claude/context/models-relationships.md new file mode 100644 index 000000000000..5f5e859e452b --- /dev/null +++ b/api/.claude/context/models-relationships.md @@ -0,0 +1,134 @@ +# Key Model Relationships + +## Core Models + +### Feature (`features/models.py`) +- `project`: FK to Project +- `name`: Feature name (unique per project) +- `type`: STANDARD, MULTIVARIATE +- `default_enabled`: Default enabled state +- `is_server_key_only`: Hide from client-side SDKs + +### FeatureState (`features/models.py`) +The value/enabled state of a feature in a specific context. + +**Key Fields**: +- `feature`: FK to Feature (required) +- `environment`: FK to Environment (required) +- `identity`: FK to Identity (nullable - for identity overrides) +- `feature_segment`: FK to FeatureSegment (nullable - for segment overrides) +- `feature_state_value`: OneToOne to FeatureStateValue +- `enabled`: Boolean enabled state +- `environment_feature_version`: FK to EnvironmentFeatureVersion (nullable - v2 only) + +**Three Types**: +1. **Base State**: `identity=None`, `feature_segment=None` +2. **Segment Override**: `feature_segment=`, `identity=None` +3. **Identity Override**: `identity=`, `feature_segment=None` + +### FeatureStateValue (`features/models.py`) +Stores the actual value (string, int, or boolean). +- `feature_state`: OneToOne to FeatureState +- `type`: STRING, INTEGER, BOOLEAN +- `string_value`, `integer_value`, `boolean_value` + +### FeatureSegment (`features/feature_segments/models.py`) +Links Feature + Segment + Environment with priority. + +- `feature`: FK to Feature +- `segment`: FK to Segment +- `environment`: FK to Environment +- `priority`: Integer (0 = highest priority) +- `environment_feature_version`: FK to EnvironmentFeatureVersion (nullable - v2 only) + +**Important**: The override values are in FeatureState with `feature_segment=` + +### Segment (`segments/models.py`) +- `project`: FK to Project +- `name`: Segment name +- `rules`: JSON field with segment rules + +### Environment (`environments/models.py`) +- `project`: FK to Project +- `api_key`: Unique API key for SDK access +- `use_v2_feature_versioning`: Boolean (enables v2 versioning) + +## Segment Override Structure + +``` +FeatureSegment (links Feature + Segment + Environment) + └─> FeatureState (the actual override) + ├─> feature_segment = + ├─> identity = None + └─> FeatureStateValue (actual value) +``` + +## Common Query Patterns + +### Base Environment Feature States +```python +FeatureState.objects.filter( + environment=environment, + feature_segment=None, + identity=None +) +``` + +### Segment Override Feature States +```python +FeatureState.objects.filter( + environment=environment, + feature_segment__segment=segment, + identity=None +) +``` + +### Identity Override Feature States +```python +FeatureState.objects.filter( + environment=environment, + identity=identity +) +``` + +## Versioning (v2) + +### EnvironmentFeatureVersion (`features/versioning/models.py`) +Version container for feature states (enables scheduling/versioning). +- `environment`: FK to Environment +- `feature`: FK to Feature +- `published_at`: DateTime (when version went live) + +### Querying with v2 +```python +from features.versioning.versioning_service import ( + get_current_live_environment_feature_version +) + +if environment.use_v2_feature_versioning: + version = get_current_live_environment_feature_version( + environment_id=environment.id, + feature_id=feature.id + ) + feature_states = FeatureState.objects.filter( + environment_feature_version=version + ) +else: + feature_states = FeatureState.objects.filter( + environment=environment, + environment_feature_version=None + ) +``` + +## Useful Select Related + +```python +# Feature State with full context +FeatureState.objects.select_related( + 'feature', + 'environment', + 'feature_state_value', + 'identity', + 'feature_segment__segment' +) +``` diff --git a/api/.claude/context/testing.md b/api/.claude/context/testing.md new file mode 100644 index 000000000000..a655f0287c89 --- /dev/null +++ b/api/.claude/context/testing.md @@ -0,0 +1,195 @@ +# Testing Guide + +## Test Organization + +### Directory Structure +- **Unit tests**: `api/tests/unit/` - Mock dependencies, test logic in isolation +- **Integration tests**: `api/tests/integration/` - Real DB, test full flow with dependencies +- View tests typically go in: `tests/unit//test_unit__views.py` +- Model tests: `tests/unit//test_unit__models.py` +- Serializer tests: `tests/unit//test_unit__serializers.py` + +### When to Use Each Type +- **Unit tests**: Testing view logic, permissions, query filtering, serialization +- **Integration tests**: Testing full request/response cycle, DB transactions, complex workflows + +## Running Tests + +### Commands (from api/ directory) + +```bash +# Run all tests +make test + +# Run a single test file +make test opts="path/to/test_file.py" + +# Run a single test case +make test opts="path/to/test_file.py::test_function_name" + +# Run tests matching a pattern +make test opts="-k test_pattern" + +# Run with verbose output +make test opts="-v" + +# Run with print statements visible +make test opts="-s" +``` + +### Examples + +```bash +# Run all feature view tests +make test opts="tests/unit/features/test_unit_features_views.py" + +# Run a specific test +make test opts="tests/unit/features/test_unit_features_views.py::test_list_feature_states_for_segment" + +# Run all segment-related tests +make test opts="-k segment" +``` + +## Common Testing Patterns + +### URL Reversal +```python +from django.urls import reverse + +# Environment feature states list +url = reverse( + "api-v1:environments:environment-featurestates-list", + args=[environment_api_key] +) + +# With detail +url = reverse( + "api-v1:environments:environment-featurestates-detail", + args=[environment_api_key, feature_state_id] +) +``` + +### Parametrized Tests +```python +import pytest +from pytest_lazyfixture import lazy_fixture + +@pytest.mark.parametrize( + "client", + [(lazy_fixture("admin_master_api_key_client")), (lazy_fixture("admin_client"))], +) +def test_something(client, environment): + # Test with both regular admin and master API key + response = client.get(url) + assert response.status_code == 200 +``` + +### Using Fixtures +Common fixtures available in conftest.py: +- `admin_client` - Authenticated admin user client +- `admin_master_api_key_client` - Master API key authenticated client +- `environment` - Test environment +- `project` - Test project +- `organisation` - Test organisation +- `feature` - Test feature +- `segment` - Test segment + +## Important Testing Rules + +### Mock External Calls +Always mock external API calls, network requests, and third-party services: + +```python +from unittest.mock import patch + +@patch('requests.post') +def test_webhook_call(mock_post, environment): + mock_post.return_value.status_code = 200 + # Test code that triggers webhook +``` + +### Versioning Tests +Test both v1 (default) and v2 feature versioning paths when relevant: + +```python +def test_with_v1_versioning(environment, feature): + # v1 is default + assert environment.use_v2_feature_versioning is False + # test v1 behavior + +def test_with_v2_versioning(environment, feature): + environment.use_v2_feature_versioning = True + environment.save() + # test v2 behavior with EnvironmentFeatureVersion +``` + +### Permission Tests +Test permission boundaries: + +```python +from common.environments.permissions import UPDATE_FEATURE_STATE, VIEW_ENVIRONMENT + +def test_user_without_permission_cannot_update( + organisation_one_user, + organisation_one_project_one_environment_one, +): + client = get_environment_user_client( + user=organisation_one_user, + environment=organisation_one_project_one_environment_one, + permission_keys=[VIEW_ENVIRONMENT], # No UPDATE_FEATURE_STATE + ) + + response = client.patch(url, data=data) + assert response.status_code == status.HTTP_403_FORBIDDEN +``` + +## Test Data Patterns + +### Creating Test Data +```python +from features.models import Feature, FeatureState, FeatureSegment +from segments.models import Segment + +# Create a segment override +segment = Segment.objects.create(name="test_segment", project=project) +feature_segment = FeatureSegment.objects.create( + feature=feature, + segment=segment, + environment=environment, + priority=0, +) +segment_feature_state = FeatureState.objects.create( + feature=feature, + environment=environment, + feature_segment=feature_segment, + enabled=True, +) +``` + +### Asserting Response Data +```python +response = client.get(url) +assert response.status_code == status.HTTP_200_OK + +response_json = response.json() +assert response_json["count"] == 2 +assert len(response_json["results"]) == 2 +assert response_json["results"][0]["feature"] == feature.id +``` + +## Debugging Tests + +### Running with pdb +```bash +make test opts="-s --pdb path/to/test.py::test_name" +``` + +### Print Debugging +```bash +make test opts="-s path/to/test.py::test_name" +``` + +### Checking Test Coverage +```bash +make test opts="--cov=features --cov-report=html tests/unit/features/" +```