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 the development server
pnpm run dev# 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
- 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
# 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)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
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(
"feature.used",
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")- 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
- Use
sentry devserverfor 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/
- 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