IMPORTANT: AGENTS.md files are the source of truth for AI agent instructions. Always update the relevant AGENTS.md file when adding or modifying agent guidance. do not add to CLAUDE.md or cursor rules
Sentry is a developer-first error tracking and performance monitoring platform. This repository contains the main Sentry application, which is a large-scale Django application with a React frontend.
See static/CLAUDE.md for frontend development guide.
- Language: Python 3.13+
- Framework: Django 5.2+
- API: Django REST Framework with drf-spectacular for OpenAPI docs
- Task Queue: Celery 5.5+
- Databases: PostgreSQL (primary), Redis, ClickHouse (via Snuba)
- Message Queue: Kafka, RabbitMQ
- Stream Processing: Arroyo (Kafka consumer/producer framework)
- Cloud Services: Google Cloud Platform (Bigtable, Pub/Sub, Storage, KMS)
- Container: Docker (via devservices)
- Package Management: pnpm (Node.js), pip (Python)
- Node Version: 22 (managed by Volta)
sentry/
├── src/
│ ├── sentry/ # Main Django application
│ │ ├── api/ # REST API endpoints
│ │ ├── models/ # Django models
│ │ ├── tasks/ # Celery tasks
│ │ ├── integrations/ # Third-party integrations
│ │ ├── issues/ # Issue tracking logic
│ │ └── web/ # Web views and middleware
│ ├── sentry_plugins/ # Plugin system
│ └── social_auth/ # Social authentication
├── static/ # Frontend application (see static/CLAUDE.md)
├── tests/ # Test suite
├── fixtures/ # Test fixtures
├── devenv/ # Development environment config
├── migrations/ # Database migrations
└── config/ # Configuration files
# Install dependencies and setup development environment
make develop
# Or use the newer devenv command
devenv sync
# Start dev dependencies
devservices up
# Start the development server
devservices serve# Run Python tests
pytest
# Run specific test file
pytest tests/sentry/api/test_base.py# Preferred: Run pre-commit hooks on specific files
pre-commit run --files src/sentry/path/to/file.py
# Run all pre-commit hooks
pre-commit run --all-files
# Individual linting tools (use pre-commit instead when possible)
black --check # Run black first
isort --check
flake8# Run migrations
sentry django migrate
# Create new migration
sentry django makemigrations
# Reset database
make reset-dbSentry uses devservices to manage local development dependencies:
- PostgreSQL: Primary database
- Redis: Caching and queuing
- Snuba: ClickHouse-based event storage
- Relay: Event ingestion service
- Symbolicator: Debug symbol processing
- Taskbroker: Asynchronous task processing
- Spotlight: Local debugging tool
📖 Full devservices documentation: https://develop.sentry.dev/development-infrastructure/devservices.md
- Check if endpoint already exists:
grep -r "endpoint_name" src/sentry/api/ - Inherit from appropriate base:
- Organization-scoped:
OrganizationEndpoint - Project-scoped:
ProjectEndpoint - Region silo:
RegionSiloEndpoint
- Organization-scoped:
- File locations:
- Endpoint:
src/sentry/api/endpoints/{resource}.py - URL:
src/sentry/api/urls.py - Test:
tests/sentry/api/endpoints/test_{resource}.py - Serializer:
src/sentry/api/serializers/models/{model}.py
- Endpoint:
- Location:
src/sentry/tasks/{category}.py - Use
@instrumented_taskdecorator - Set appropriate
queueandmax_retries - Test location:
tests/sentry/tasks/test_{category}.py
Comments should not repeat what the code is saying. Instead, reserve comments for explaining why something is being done, or to provide context that is not obvious from the code itself.
Bad:
# Increment the retry count by 1
retries += 1Good:
# Some APIs occasionally return 500s on valid requests. We retry up to 3 times
# before surfacing an error.
retries += 1When to Comment
- To explain why a particular approach or workaround was chosen.
- To clarify intent when the code could be misread or misunderstood.
- To provide context from external systems, specs, or requirements.
- To document assumptions, edge cases, or limitations.
When Not to Comment
- Don't narrate what the code is doing — the code already says that.
- Don't duplicate function or variable names in plain English.
- Don't leave stale comments that contradict the code.
Avoid comments that reference removed or obsolete code paths (e.g. "No longer uses X format"). If compatibility code or legacy behavior is deleted, comments about it should also be deleted. The comment should describe the code that exists now, not what used to be there. Historic details belong in commit messages or documentation, not in-line comments.
# src/sentry/core/endpoints/organization_details.py
from rest_framework.request import Request
from rest_framework.response import Response
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint
from sentry.api.serializers import serialize
from sentry.api.serializers.models.organization import DetailedOrganizationSerializer
@region_silo_endpoint
class OrganizationDetailsEndpoint(OrganizationEndpoint):
publish_status = {
"GET": ApiPublishStatus.PUBLIC,
"PUT": ApiPublishStatus.PUBLIC,
}
def get(self, request: Request, organization: Organization) -> Response:
"""Get organization details."""
return Response(
serialize(
organization,
request.user,
DetailedOrganizationSerializer()
)
)
# Add to src/sentry/api/urls.py:
# path('organizations/<slug:organization_slug>/', OrganizationDetailsEndpoint.as_view()),# src/sentry/tasks/email.py
from sentry.tasks.base import instrumented_task
@instrumented_task(
name="sentry.tasks.send_email",
queue="email",
max_retries=3,
default_retry_delay=60,
)
def send_email(user_id: int, subject: str, body: str) -> None:
from sentry.models import User
try:
user = User.objects.get(id=user_id)
# Send email logic
except User.DoesNotExist:
# Don't retry if user doesn't exist
return- Create endpoint in
src/sentry/api/endpoints/ - Add URL pattern in
src/sentry/api/urls.py - Document with drf-spectacular decorators
- Add tests in
tests/sentry/api/endpoints/
- OpenAPI spec generation:
make build-api-docs - API ownership tracked in
src/sentry/apidocs/api_ownership_allowlist_dont_modify.py
- Route:
/api/0/organizations/{org}/projects/{project}/ - Use
snake_casefor URL params - Use
camelCasefor request/response bodies - Return strings for numeric IDs
- Implement pagination with
cursor - Use
GETfor read,POSTfor create,PUTfor update
- Use pytest fixtures
- Mock external services
- Test database isolation with transactions
- Use factories for test data
- For Kafka/Arroyo components: Use
LocalProducerwithMemoryMessageStorageinstead of mocks
# tests/sentry/core/endpoints/test_organization_details.py
from sentry.testutils.cases import APITestCase
class OrganizationDetailsTest(APITestCase):
endpoint = "sentry-api-0-organization-details"
def test_get_organization(self):
org = self.create_organization(owner=self.user)
self.login_as(self.user)
response = self.get_success_response(org.slug)
assert response.data["id"] == str(org.id)Notes:
- Tests should ALWAYS be procuderal with NO branching logic. It is very rare that you will need an if statement as part of a Frontend Jest test or backend pytest.
from sentry import features
if features.has('organizations:new-feature', organization):
# New feature codefrom sentry.api.permissions import SentryPermission
class MyPermission(SentryPermission):
scope_map = {
'GET': ['org:read'],
'POST': ['org:write'],
}import logging
from sentry import analytics
from sentry.analytics.events.feature_used import FeatureUsedEvent # does not exist, only for demonstration purposes
logger = logging.getLogger(__name__)
# Structured logging
logger.info(
"user.action.complete",
extra={
"user_id": user.id,
"action": "login",
"ip_address": request.META.get("REMOTE_ADDR"),
}
)
# Analytics event
analytics.record(
FeatureUsedEvent(
user_id=user.id,
organization_id=org.id,
feature="new-dashboard",
)
)# Using Arroyo for Kafka producers with dependency injection for testing
from arroyo.backends.abstract import Producer
from arroyo.backends.kafka import KafkaProducer, KafkaPayload
from arroyo.backends.local.backend import LocalBroker
from arroyo.backends.local.storages.memory import MemoryMessageStorage
# Production producer
def create_kafka_producer(config):
return KafkaProducer(build_kafka_configuration(default_config=config))
# Test producer using Arroyo's LocalProducer
def create_test_producer_factory():
storage = MemoryMessageStorage()
broker = LocalBroker(storage)
return lambda config: broker.get_producer(), storage
# Dependency injection pattern for testable Kafka producers
class MultiProducer:
def __init__(self, topic: Topic, producer_factory: Callable[[Mapping[str, object]], Producer[KafkaPayload]] | None = None):
self.producer_factory = producer_factory or self._default_producer_factory
# ... setup code
def _default_producer_factory(self, config) -> KafkaProducer:
return KafkaProducer(build_kafka_configuration(default_config=config))- Control Silo: User auth, billing, organization management
- Region Silo: Project data, events, issues
- Check model's silo in
src/sentry/models/outbox.py - Use
@region_silo_endpointor@control_silo_endpoint
- NEVER join across silos
- Use
outboxfor cross-silo updates - Migrations must be backwards compatible
- Add indexes for queries on 1M+ row tables
- Use
db_index=Trueordb_index_together
# WRONG: Direct model import in API
from sentry.models import Organization # NO!
# RIGHT: Use endpoint bases
from sentry.api.bases.organization import OrganizationEndpoint
# WRONG: Synchronous external calls
response = requests.get(url) # NO!
# RIGHT: Use Celery task
from sentry.tasks import fetch_external_data
fetch_external_data.delay(url)
# WRONG: N+1 queries
for org in organizations:
org.projects.all() # NO!
# RIGHT: Use prefetch_related
organizations.prefetch_related('projects')
# WRONG: Use hasattr() for unions
x: str | None = "hello"
if hasattr(x, "replace"):
x = x.replace("e", "a")
# RIGHT: Use isinstance()
x: str | None = "hello"
if isinstance(x, str):
x = x.replace("e", "a")
# WRONG: Importing inside function bodies.
# RIGHT: Import at the top of python modules. ONLY import in a function body if
# to avoid a circular import (very rare)
def my_function():
from sentry.models.project import Project # NO!
...- Avoid blanket exception handling (
except Exception:or bareexcept:) - Only catch specific exceptions when you have a meaningful way to handle them
- We have global exception handlers in tasks and endpoints that automatically log errors and report them to Sentry
- Let exceptions bubble up unless you need to:
- Add context to the error
- Perform cleanup operations
- Convert one exception type to another with additional information
- Recover from expected error conditions
- Use database indexing appropriately
- Implement pagination for list endpoints
- Cache expensive computations with Redis
- Use Celery for background tasks
- Optimize queries with
select_relatedandprefetch_related
- Always validate user input
- Use Django's CSRF protection
- Implement proper permission checks
- Sanitize data before rendering
- Follow OWASP guidelines
Indirect Object Reference vulnerabilities occur when an attacker can access resources they shouldn't by manipulating IDs passed in requests. This is one of the most critical security issues in multi-tenant applications like Sentry.
When querying resources, ALWAYS include organization_id and/or project_id in your query filters. Never trust user-supplied IDs alone.
# WRONG: Vulnerable to IDOR - user can access any resource by guessing IDs
resource = Resource.objects.get(id=request.data["resource_id"])
# RIGHT: Properly scoped to organization
resource = Resource.objects.get(
id=request.data["resource_id"],
organization_id=organization.id
)
# RIGHT: Properly scoped to project
resource = Resource.objects.get(
id=request.data["resource_id"],
project_id=project.id
)When project IDs are passed in the request (query string or body), NEVER directly access or trust request.data["project_id"] or request.GET["project_id"]. Instead, use the endpoint's self.get_projects() method which performs proper permission checks.
# WRONG: Direct access bypasses permission checks
project_ids = request.data.get("project_id")
projects = Project.objects.filter(id__in=project_ids)
# RIGHT: Use self.get_projects() which validates permissions
projects = self.get_projects(
request=request,
organization=organization,
project_ids=request.data.get("project_id")
)- Use
devservices servefor full stack debugging - Access Django shell:
sentry django shell - View Celery tasks: monitor RabbitMQ management UI
- Database queries: use Django Debug Toolbar
# Print SQL queries
from django.db import connection
print(connection.queries)
# Debug Celery task
from sentry.tasks import my_task
my_task.apply(args=[...]).get() # Run synchronously
# Check feature flag
from sentry import features
features.has('organizations:feature', org)
# Current silo mode
from sentry.silo import SiloMode
from sentry.services.hybrid_cloud import silo_mode_delegation
print(silo_mode_delegation.get_current_mode())pyproject.toml: Python project configurationsetup.cfg: Python package metadata.github/: CI/CD workflowsdevservices/config.yml: Local service configuration.pre-commit-config.yaml: Pre-commit hooks configurationcodecov.yml: Code coverage configuration
- Models:
src/sentry/models/{model}.py - API Endpoints:
src/sentry/api/endpoints/{resource}.py - Serializers:
src/sentry/api/serializers/models/{model}.py - Tasks:
src/sentry/tasks/{category}.py - Integrations:
src/sentry/integrations/{provider}/ - Permissions:
src/sentry/api/permissions.py - Feature Flags:
src/sentry/features/permanent.pyortemporary.py - Utils:
src/sentry/utils/{category}.py
- Python:
tests/mirrorssrc/structure - Fixtures:
fixtures/{type}/ - Factories:
tests/sentry/testutils/factories.py
- Create dir:
src/sentry/integrations/{name}/ - Required files:
__init__.pyintegration.py(inherit fromIntegration)client.py(API client)webhooks/(if needed)
- Register in
src/sentry/integrations/registry.py - Add feature flag in
temporary.py
# src/sentry/integrations/example/integration.py
from sentry.integrations import Integration, IntegrationProvider
class ExampleIntegration(Integration):
def get_client(self):
from .client import ExampleClient
return ExampleClient(self.metadata['access_token'])
class ExampleIntegrationProvider(IntegrationProvider):
key = "example"
name = "Example"
features = ["issue-basic", "alert-rule"]
def build_integration(self, state):
# OAuth flow handling
pass- Follow existing code style
- Write comprehensive tests
- Update documentation
- Add feature flags for experimental features
- Consider backwards compatibility
- Performance test significant changes
- Hybrid Cloud: Check silo mode before cross-silo queries
- Feature Flags: Always add for new features
- Migrations: Test rollback, never drop columns immediately
- Celery: Always handle task failures/retries
- API: Serializers can be expensive, use
@attach_scenarios - Tests: Use
self.create_*helpers, not direct model creation - Permissions: Check both RBAC and scopes
- Development Setup Guide: https://develop.sentry.dev/getting-started/
- Devservices Documentation: https://develop.sentry.dev/development-infrastructure/devservices
- Main Documentation: https://docs.sentry.io/
- Internal Contributing Guide: https://docs.sentry.io/internal/contributing/
- GitHub Discussions: https://github.com/getsentry/sentry/discussions
- Discord: https://discord.gg/PXa5Apfe7K
- This is a large, complex codebase with many interconnected systems
- Always consider the impact of changes on performance and scalability
- Many features are gated behind feature flags for gradual rollout
- The codebase follows Django patterns but with significant customization
- Database migrations require special care due to the scale of deployment
- ALWAYS use pre-commit for linting instead of individual tools
- Check silo mode before making cross-silo queries
- Use decision trees above for common user requests
- Follow the anti-patterns section to avoid common mistakes
ALWAYS activate the virtualenv before any Python operation: Before running any Python command (e.g. python -c), Python package (e.g. pytest, mypy), or Python script, you MUST first activate the virtualenv with source .venv/bin/activate. This applies to ALL Python operations without exception.
For function signatures, always use abstract types (e.g. Sequence over list) for input parameters and use specific return types (e.g. list over Sequence).
# Good: Abstract input types, specific return types
def process_items(items: Sequence[Item]) -> list[ProcessedItem]:
return [process(item) for item in items]
# Avoid: Specific input types, abstract return types
def process_items(items: list[Item]) -> Sequence[ProcessedItem]:
return [process(item) for item in items]Always import a type from the module collections.abc rather than the typing module if it is available (e.g. from collections.abc import Sequence rather than from typing import Sequence).
Always run pytest with these parameters: pytest -svv --reuse-db since it is faster to execute.
When fixing errors or adding functionality, you MUST add test cases to existing test files rather than creating new test files. Follow this pattern to locate the correct test file:
- Code location:
src/sentry/foo/bar.py - Test location:
tests/sentry/foo/test_bar.py
Notice that we prefix tests/ to the path and prefix test_ to the module name.
Exception: Tests ensuring Snuba compatibility MUST be placed in tests/snuba/. The tests in this folder will also run in Snuba's CI.
In Sentry Python tests, you MUST use factory methods in this priority order:
- Fixture methods (e.g.,
self.create_model) from base classes likesentry.testutils.fixtures.Fixtures - Factory methods from
sentry.testutils.factories.Factorieswhen fixtures aren't available
NEVER directly call Model.objects.create - this violates our testing standards and bypasses shared test setup logic.
For example, a diff that uses a fixture instead of directly calling Model.objects.create would look like:
- direct_project = Project.objects.create(
- organization=self.organization,
- name="Directly Created",
- slug="directly-created"
- )
+ direct_project = self.create_project(
+ organization=self.organization,
+ name="Directly Created",
+ slug="directly-created" # Note: Ensure factory args match
+ )In Sentry Python tests, always use pytest instead of unittest. This promotes consistency, reduces boilerplate, and leverages shared test setup logic defined in the factories.
For example, a diff that uses pytest instead of unittest would look like:
- self.assertRaises(ValueError, EffectiveGrantStatus.from_cache, None)
+ with pytest.raises(ValueError):
+ EffectiveGrantStatus.from_cache(None)These rules are MANDATORY for all Python development in the Sentry codebase. Violations will:
- Cause CI failures
- Require code review rejection
- Must be fixed before merging the pull request
Agents MUST follow these rules without exception to maintain code quality and consistency across the project.