getsentry / getsentry/sentry

Many org-scoped API endpoints reject project slugs in the ?project= parameter

Open
#107,832 2 comments 0 reactions 0 assignees View on GitHub
API Platform Feature Waiting for: Product Owner
Dominant language
Python
Stars
44.8k
Forks
4.9k
Avg merge
21h 10m
Merged PRs (30d)
635

Description

## Problem

Sentry's public API identifies projects by their **slug** — the human-readable identifier that appears in URLs and is how customers reference projects (e.g. `org-slug/project-slug`, similar to GitHub's `owner/repo`). While numeric project IDs are also accepted and sometimes preferred for performance, the slug is the primary external-facing identifier we encourage customers to use.

Despite this, many organization-scoped endpoints only accept **numeric project IDs** in the `?project=` query parameter (and in some request body fields), returning HTTP 400 if a slug is passed. This forces API consumers to first resolve a slug to a numeric ID before calling these endpoints — an unnecessary extra round-trip and a poor developer experience.

A separate `?projectSlug=` query parameter exists on some endpoints, but it is not widely documented and is inconsistent with the `?project=` parameter that most tooling already uses.

Since project slugs **cannot be entirely numeric** (enforced by `no_numeric_validator`), disambiguating between an ID and a slug is trivial and already implemented in utilities like `IdOrSlugLookup`.

## Where the inconsistency comes from

The majority of org-scoped endpoints go through a shared code path — `get_requested_project_ids_unchecked()` in `src/sentry/api/bases/organization.py` — which hard-casts `?project=` values to `int`:

```python
def get_requested_project_ids_unchecked(self, request):
try:
return set(map(int, request.GET.getlist("project")))
except ValueError:
raise ParseError(detail="Invalid project parameter. Values must be numbers.")
```

This is called by `get_projects()`, the standard way `OrganizationEndpoint` subclasses resolve the `?project=` query parameter. Beyond this shared path, several other endpoints do their own direct numeric-only lookups.

## Proposed Solution

Update `get_requested_project_ids_unchecked()` and `get_projects()` to partition `?project=` values into IDs (all-digit values) and slugs (everything else), then resolve both. This single change would fix the majority of affected endpoints. A handful of endpoints that do their own project lookups outside of `get_projects()` need individual fixes.

Existing utilities that already support this pattern:
- **`IdOrSlugLookup`** (`src/sentry/db/models/fields/slug.py`) — ORM lookup that routes to `id` or `slug` based on `.isdecimal()`
- **`ProjectField(id_allowed=True)`** (`src/sentry/api/serializers/rest_framework/project.py`) — DRF field accepting both

## Affected Endpoints

### Endpoints using `get_projects()` (shared code path)

All of these inherit from `OrganizationEndpoint` and use the standard `?project=` query parameter flow. Fixing `get_projects()` fixes all of them.

**Events / Discover**
- `OrganizationEventsEndpoint` — `src/sentry/api/endpoints/organization_events.py` — GET
- `OrganizationEventsTimeseriesEndpoint` — `src/sentry/api/endpoints/organization_events_timeseries.py` — GET

**Issues**
- `OrganizationGroupIndexEndpoint` — `src/sentry/issues/endpoints/organization_group_index.py` — GET, PUT, DELETE

**Releases**
- `OrganizationReleaseDetailsEndpoint` — `src/sentry/releases/endpoints/organization_release_details.py` — GET, PUT, DELETE
- `ReleaseDeploysEndpoint` — `src/sentry/releases/endpoints/release_deploys.py` — GET, POST
- `OrganizationReleasesEndpoint` — `src/sentry/api/endpoints/organization_releases.py` — GET, POST

**Sessions**
- `OrganizationSessionsEndpoint` — `src/sentry/api/endpoints/organization_sessions.py` — GET

**Monitors / Crons**
- `OrganizationMonitorIndexEndpoint` — `src/sentry/monitors/endpoints/organization_monitor_index.py` — GET, POST

**Alerts / Detectors / Workflows**
- `OrganizationAlertRuleIndexEndpoint` — `src/sentry/incidents/endpoints/organization_alert_rule_index.py` — GET, POST
- `OrganizationDetectorIndexEndpoint` — `src/sentry/workflow_engine/endpoints/organization_detector_index.py` — GET, POST, PUT, DELETE
- `OrganizationWorkflowIndexEndpoint` — `src/sentry/workflow_engine/endpoints/organization_workflow_index.py` — GET, POST, PUT, DELETE

**Replays**
- `OrganizationReplayIndexEndpoint` — `src/sentry/replays/endpoints/organization_replay_index.py` — GET
- `OrganizationReplaySelectorIndexEndpoint` — `src/sentry/replays/endpoints/organization_replay_selector_index.py` — GET
- `OrganizationReplayCountEndpoint` — `src/sentry/replays/endpoints/organization_replay_count.py` — GET

**Stats**
- `OrganizationStatsEndpointV2` — `src/sentry/api/endpoints/organization_stats_v2.py` — GET
- `OrganizationStatsSummaryEndpoint` — `src/sentry/api/endpoints/organization_stats_summary.py` — GET

**Notification Actions**
- `NotificationActionsIndexEndpoint` — `src/sentry/notifications/api/endpoints/notification_actions_index.py` — GET, POST

**Other**
- `OrganizationProjectKeysEndpoint` — `src/sentry/api/endpoints/organization_project_keys.py` — GET
- `OrganizationDashboardsEndpoint` — `src/sentry/dashboards/endpoints/organization_dashboards.py` — GET
- `DiscoverSavedQueriesEndpoint` — `src/sentry/discover/endpoints/discover_saved_queries.py` — POST (`projects` body field)

### Endpoints with their own numeric-only project lookups (need individual fixes)

These bypass `get_projects()` and do direct ID-only lookups:

- `OrganizationDetectorIndexEndpoint` POST — `src/sentry/workflow_engine/endpoints/organization_detector_index.py` — `projectId` body param uses `to_valid_int_id()` → `Project.objects.get(id=...)`
- `OrganizationReleaseDetailsEndpoint` GET — `src/sentry/releases/endpoints/organization_release_details.py` — `project` query param uses `int()` → `Project.objects.get_from_cache(id=...)`
- `OrganizationCodeMappingsEndpoint` POST — `src/sentry/integrations/api/endpoints/organization_code_mappings.py` — `projectId` body param → `Project.objects.get(id=...)`
- `OrganizationCodeMappingDetailsEndpoint` PUT — `src/sentry/integrations/api/endpoints/organization_code_mapping_details.py` — `projectId` body param → `Project.objects.get(id=...)`
- `OrganizationStatsEndpoint` — `src/sentry/api/endpoints/organization_stats.py` — `projectID` query param → `Project.objects.filter(id__in=...)`
- `OrganizationCombinedRuleIndexEndpoint` — `src/sentry/incidents/endpoints/organization_alert_rule_index.py` — `project` query param → `get_requested_project_ids_unchecked()`
- `OrganizationOnDemandRuleStatsEndpoint` — `src/sentry/incidents/endpoints/organization_alert_rule_index.py` — `project_id` query param → `to_valid_int_id()`
- `PromptsActivityEndpoint` — `src/sentry/api/endpoints/prompts_activity.py` — `project_id` query/body → `Project.objects.filter(id=...)`
- `OrganizationEventsAnomaliesEndpoint` — `src/sentry/seer/endpoints/organization_events_anomalies.py` — `project_id` body → `to_valid_int_id()`
- `CustomRulesEndpoint` — `src/sentry/api/endpoints/custom_rules.py` — `project` query / `projects` body → `int()` cast
- `OrganizationEventsRootCauseAnalysisEndpoint` — `src/sentry/api/endpoints/organization_events_root_cause_analysis.py` — `project` query → passed directly to snuba

### Excluded (no organization context — cannot resolve slugs)

- `AdminRelayProjectConfigsEndpoint` — internal admin, no org context
- `SentryAppInstallationExternalRequestsEndpoint` — uses installation context, no org endpoint

Contributor guide

Open the contributing guide

Research direction

Start with get_requested_project_ids_unchecked() and get_projects() in src/sentry/api/bases/organization.py, then inspect the listed endpoint files for direct numeric-only project lookups. Compare the existing IdOrSlugLookup and ProjectField(id_allowed=True) utilities, and verify that affected endpoints accept both project IDs and slugs while excluded endpoints remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, backend-api-design
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.