Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/sentry/api/serializers/models/project_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class DSN(TypedDict):
playstation: str
otlp_traces: str
otlp_logs: str
endpoint: str


class BrowserSDK(TypedDict):
Expand Down Expand Up @@ -100,6 +101,7 @@ def serialize(
"playstation": obj.playstation_endpoint,
"otlp_traces": obj.otlp_traces_endpoint,
"otlp_logs": obj.otlp_logs_endpoint,
"endpoint": obj.get_endpoint(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Serializing multiple ProjectKey objects triggers N+1 database queries to fetch project.organization due to missing select_related.
Severity: HIGH | Confidence: 0.95

🔍 Detailed Analysis

When the API endpoint /api/0/projects/{org}/{project}/keys/ returns multiple ProjectKey objects, the ProjectKeySerializer.serialize() method (line 104) calls obj.get_endpoint() for each key. Inside get_endpoint() (line 280), self.project.organization is accessed to check a feature flag. This access triggers a database query to load the organization if it's not already prefetched. Since the queryset used by ProjectKeysEndpoint.get() does not include select_related('project__organization'), this results in an N+1 query problem, where N is the number of ProjectKey objects. This issue is exacerbated as other properties like obj.csp_endpoint also call get_endpoint() internally, potentially leading to multiple organization lookups per key.

💡 Suggested Fix

Add select_related('project__organization') to the queryset in ProjectKeysEndpoint.get() or within the ProjectKey.objects.for_request() method to prefetch the organization.

🤖 Prompt for AI Agent
Fix this bug. In src/sentry/api/serializers/models/project_key.py at line 104: When the
API endpoint `/api/0/projects/{org}/{project}/keys/` returns multiple `ProjectKey`
objects, the `ProjectKeySerializer.serialize()` method (line 104) calls
`obj.get_endpoint()` for each key. Inside `get_endpoint()` (line 280),
`self.project.organization` is accessed to check a feature flag. This access triggers a
database query to load the organization if it's not already prefetched. Since the
queryset used by `ProjectKeysEndpoint.get()` does not include
`select_related('project__organization')`, this results in an N+1 query problem, where N
is the number of `ProjectKey` objects. This issue is exacerbated as other properties
like `obj.csp_endpoint` also call `get_endpoint()` internally, potentially leading to
multiple organization lookups per key.

Did we get this right? 👍 / 👎 to inform future reviews.

},
"browserSdkVersion": get_selected_browser_sdk_version(obj),
"browserSdk": {"choices": get_browser_sdk_version_choices(obj.project)},
Expand Down
1 change: 1 addition & 0 deletions src/sentry/apidocs/examples/project_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"playstation": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/playstation/?sentry_key=a785682ddda719b7a8a4011110d75598",
"otlp_traces": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/integration/otlp/v1/traces",
"otlp_logs": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/integration/otlp/v1/logs",
"endpoint": "https://o4504765715316736.ingest.sentry.io",
"nel": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/nel/?sentry_key=a785682ddda719b7a8a4011110d75598",
"unreal": "https://o4504765715316736.ingest.sentry.io/api/4505281256090153/unreal/a785682ddda719b7a8a4011110d75598/",
"cdn": "https://js.sentry-cdn.com/a785682ddda719b7a8a4011110d75598.min.js",
Expand Down
15 changes: 15 additions & 0 deletions tests/sentry/core/endpoints/test_project_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ def test_otlp_logs_endpoint(self) -> None:
assert response.data[0]["dsn"]["otlp_logs"] == key.otlp_logs_endpoint
assert "integration/otlp/v1/logs" in response.data[0]["dsn"]["otlp_logs"]

def test_endpoint(self) -> None:
project = self.create_project()
key = ProjectKey.objects.get_or_create(project=project)[0]
self.login_as(user=self.user)
url = reverse(
"sentry-api-0-project-keys",
kwargs={
"organization_id_or_slug": project.organization.slug,
"project_id_or_slug": project.slug,
},
)
response = self.client.get(url)
assert response.status_code == 200
assert response.data[0]["dsn"]["endpoint"] == key.get_endpoint()

def test_use_case(self) -> None:
"""Regular user can access user DSNs but not internal DSNs"""
project = self.create_project()
Expand Down
Loading