Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
70fc053
feat(symsorter): Add platform-restricted builtin symbol sources with …
vaind Oct 23, 2025
584cd00
chore: reorder fields
vaind Oct 24, 2025
cc15866
formatting
vaind Oct 24, 2025
6f916cc
Update static/app/views/settings/projectDebugFiles/index.tsx
vaind Oct 28, 2025
54a82d5
Merge branch 'master' into feat/platform-restricted-symbol-sources
vaind Oct 28, 2025
b73221a
feat(symsorter): Add platform-restricted builtin symbol sources with …
vaind Oct 28, 2025
64eab65
cleanup
vaind Oct 28, 2025
fd46a7d
Merge remote-tracking branch 'upstream/master' into feat/platform-res…
vaind Oct 28, 2025
ffccdec
fix: Preserve non-existent symbol sources in platform defaults
vaind Oct 29, 2025
a36d188
Merge remote-tracking branch 'upstream/master' into feat/platform-res…
vaind Oct 29, 2025
921d8dc
fix(types): Add type casts for platform restrictions to fix mypy errors
vaind Oct 29, 2025
f7b555d
chore: Remove frontend changes (will be in separate PR)
vaind Oct 29, 2025
b77b919
perf: Address Seer bot review feedback
vaind Oct 29, 2025
868544d
Apply suggestions from code review
vaind Nov 19, 2025
6a4b9f9
Apply suggestions from code review
vaind Nov 19, 2025
efcb564
Merge branch 'master' into feat/platform-restricted-symbol-sources
vaind Nov 19, 2025
6076341
feat(symbol-sources): remove Nintendo Symbol Server configuration
vaind Nov 27, 2025
c94b5df
refactor(symbol-sources): extract console platform access check to sh…
vaind Nov 27, 2025
68a2c28
Merge branch 'master' into feat/platform-restricted-symbol-sources
vaind Nov 27, 2025
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
38 changes: 34 additions & 4 deletions src/sentry/api/endpoints/builtin_symbol_sources.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import cast

from django.conf import settings
from rest_framework.request import Request
from rest_framework.response import Response
Expand All @@ -6,6 +8,8 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.serializers import serialize
from sentry.models.organization import Organization
from sentry.utils.console_platforms import organization_has_console_platform_access


def normalize_symbol_source(key, source):
Expand All @@ -26,10 +30,36 @@ class BuiltinSymbolSourcesEndpoint(Endpoint):
permission_classes = ()

def get(self, request: Request, **kwargs) -> Response:
sources = [
normalize_symbol_source(key, source)
for key, source in settings.SENTRY_BUILTIN_SOURCES.items()
]
platform = request.GET.get("platform")

# Get organization if organization context is available
organization = None
organization_id_or_slug = kwargs.get("organization_id_or_slug")
if organization_id_or_slug:
try:
if str(organization_id_or_slug).isdecimal():
organization = Organization.objects.get_from_cache(id=organization_id_or_slug)
else:
organization = Organization.objects.get_from_cache(slug=organization_id_or_slug)
except Organization.DoesNotExist:
pass

sources = []
for key, source in settings.SENTRY_BUILTIN_SOURCES.items():
source_platforms: list[str] | None = cast("list[str] | None", source.get("platforms"))

# If source has platform restrictions, check if current platform matches
if source_platforms is not None:
if not platform or platform not in source_platforms:
continue

# Platform matches - now check if organization has access to this console platform
if not organization or not organization_has_console_platform_access(
organization, platform
):
continue

sources.append(normalize_symbol_source(key, source))

sources.sort(key=lambda s: s["name"])
return Response(serialize(sources))
74 changes: 69 additions & 5 deletions src/sentry/api/helpers/default_symbol_sources.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from typing import cast

from django.conf import settings

from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.projects.services.project import RpcProject

Expand All @@ -7,11 +13,69 @@
"unity": ["ios", "microsoft", "android", "nuget", "unity", "nvidia", "ubuntu"],
"unreal": ["ios", "microsoft", "android", "nvidia", "ubuntu"],
"godot": ["ios", "microsoft", "android", "nuget", "nvidia", "ubuntu"],
"nintendo-switch": ["nintendo"],
}


def set_default_symbol_sources(project: Project | RpcProject) -> None:
if project.platform and project.platform in DEFAULT_SYMBOL_SOURCES:
project.update_option(
"sentry:builtin_symbol_sources", DEFAULT_SYMBOL_SOURCES[project.platform]
)
def set_default_symbol_sources(
project: Project | RpcProject, organization: Organization | None = None
) -> None:
"""
Sets default symbol sources for a project based on its platform.
For sources with platform restrictions (e.g., console platforms), this function checks
if the organization has access to the required platform before adding the source.
Args:
project: The project to configure symbol sources for
organization: Optional organization (fetched from project if not provided)
"""
if not project.platform or project.platform not in DEFAULT_SYMBOL_SOURCES:
return

# Get organization from project if not provided
if organization is None:
if isinstance(project, Project):
organization = project.organization
else:
# For RpcProject, fetch organization by ID
try:
organization = Organization.objects.get_from_cache(id=project.organization_id)
except Organization.DoesNotExist:
# If organization doesn't exist, cannot set defaults
return

# Get default sources for this platform
source_keys = DEFAULT_SYMBOL_SOURCES[project.platform]

# Get enabled console platforms once (optimization to avoid repeated DB calls)
enabled_console_platforms = organization.get_option(
"sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT
)

# Filter sources based on platform restrictions and organization access
enabled_sources = []
for source_key in source_keys:
source_config = settings.SENTRY_BUILTIN_SOURCES.get(source_key)

# If source exists in config, check for platform restrictions
if source_config:
required_platforms: list[str] | None = cast(
"list[str] | None", source_config.get("platforms")
)
if required_platforms:
# Source is platform-restricted - check if org has access
# Only add source if org has access to at least one of the required platforms
has_access = any(
platform in enabled_console_platforms for platform in required_platforms
)
if not has_access:
continue

# Include the source (either it passed platform check or doesn't exist in config)
# Non-existent sources will be filtered out at runtime in sources.py
enabled_sources.append(source_key)

# Always update the option for recognized platforms, even if empty
# This ensures platform-specific defaults override epoch defaults
project.update_option("sentry:builtin_symbol_sources", enabled_sources)
2 changes: 1 addition & 1 deletion src/sentry/core/endpoints/team_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def apply_default_project_settings(organization: Organization, project: Project)

set_default_disabled_detectors(project)

set_default_symbol_sources(project)
set_default_symbol_sources(project, organization)

# Create project option to turn on ML similarity feature for new EA projects
if project_is_seer_eligible(project):
Expand Down
1 change: 1 addition & 0 deletions src/sentry/lang/native/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"filters": FILTERS_SCHEMA,
"is_public": {"type": "boolean"},
"has_index": {"type": "boolean"},
"platforms": {"type": "array", "items": {"type": "string"}},
}

APP_STORE_CONNECT_SCHEMA = {
Expand Down
7 changes: 4 additions & 3 deletions src/sentry/projectoptions/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
epoch_defaults={1: "4.x", 2: "5.x", 7: "6.x", 8: "7.x", 13: "8.x", 14: "9.x", 15: "10.x"},
)

# Default symbol sources. The ios source does not exist by default and
# will be skipped later. The microsoft source exists by default and is
# unlikely to be disabled.
# Default symbol sources. The ios source does not exist by default and
# will be skipped later. The microsoft source exists by default and is
# unlikely to be disabled. Platform-specific sources may be added via
# set_default_symbol_sources() when a project is created.
register(
key="sentry:builtin_symbol_sources",
epoch_defaults={
Expand Down
7 changes: 2 additions & 5 deletions src/sentry/tempest/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from sentry.models.organization import Organization
from sentry.utils.console_platforms import organization_has_console_platform_access


def has_tempest_access(organization: Organization | None) -> bool:

if not organization:
return False

enabled_platforms = organization.get_option("sentry:enabled_console_platforms", [])
has_playstation_access = "playstation" in enabled_platforms

return has_playstation_access
return organization_has_console_platform_access(organization, "playstation")
Copy link
Member

Choose a reason for hiding this comment

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

thank you 💜

19 changes: 19 additions & 0 deletions src/sentry/utils/console_platforms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sentry.constants import ENABLED_CONSOLE_PLATFORMS_DEFAULT
from sentry.models.organization import Organization


def organization_has_console_platform_access(organization: Organization, platform: str) -> bool:
"""
Check if an organization has access to a specific console platform.

Args:
organization: The organization to check
platform: The console platform (e.g., 'nintendo-switch', 'playstation', 'xbox')

Returns:
True if the organization has access to the console platform, False otherwise
"""
enabled_console_platforms = organization.get_option(
"sentry:enabled_console_platforms", ENABLED_CONSOLE_PLATFORMS_DEFAULT
)
return platform in enabled_console_platforms
102 changes: 102 additions & 0 deletions tests/sentry/api/endpoints/test_builtin_symbol_sources.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
from django.test import override_settings

from sentry.testutils.cases import APITestCase

SENTRY_BUILTIN_SOURCES_PLATFORM_TEST = {
"public-source-1": {
"id": "sentry:public-1",
"name": "Public Source 1",
"type": "http",
"url": "https://example.com/symbols/",
},
"public-source-2": {
"id": "sentry:public-2",
"name": "Public Source 2",
"type": "http",
"url": "https://example.com/symbols2/",
},
"nintendo": {
"id": "sentry:nintendo",
"name": "Nintendo SDK",
"type": "s3",
"bucket": "nintendo-symbols",
"region": "us-east-1",
"access_key": "test-key",
"secret_key": "test-secret",
"layout": {"type": "native"},
"platforms": ["nintendo-switch"],
},
}

Comment on lines +5 to +30
Copy link
Member

Choose a reason for hiding this comment

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

instead of defining this object, can't we just import the SENTRY_BUILTIN_SOURCES?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need it because

  • The "nintendo" source is not defined in this repo - it's in getsentry
  • The real SENTRY_BUILTIN_SOURCES contains public sources (ios, microsoft, etc.) that don't have platform restrictions
  • We need a controlled test fixture with both public sources AND a platform-restricted source to properly test the filtering logic


class BuiltinSymbolSourcesNoSlugTest(APITestCase):
endpoint = "sentry-api-0-builtin-symbol-sources"
Expand Down Expand Up @@ -39,3 +67,77 @@ def test_with_slug(self) -> None:
assert "id" in body[0]
assert "name" in body[0]
assert "hidden" in body[0]


class BuiltinSymbolSourcesPlatformFilteringTest(APITestCase):
endpoint = "sentry-api-0-organization-builtin-symbol-sources"

def setUp(self) -> None:
super().setUp()
self.organization = self.create_organization(owner=self.user)
self.login_as(user=self.user)

@override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST)
def test_platform_filtering_nintendo_switch_with_access(self) -> None:
"""Nintendo Switch platform should see nintendo source only if org has access"""
# Enable nintendo-switch for this organization
self.organization.update_option("sentry:enabled_console_platforms", ["nintendo-switch"])

resp = self.get_response(self.organization.slug, qs_params={"platform": "nintendo-switch"})
assert resp.status_code == 200

body = resp.data
source_keys = [source["sentry_key"] for source in body]

# Nintendo Switch with access should see nintendo
assert "nintendo" in source_keys
# Should also see public sources (no platform restriction)
assert "public-source-1" in source_keys
assert "public-source-2" in source_keys

@override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST)
def test_platform_filtering_nintendo_switch_without_access(self) -> None:
"""Nintendo Switch platform should NOT see nintendo if org lacks access"""
# Organization does not have nintendo-switch enabled (default is empty list)

resp = self.get_response(self.organization.slug, qs_params={"platform": "nintendo-switch"})
assert resp.status_code == 200

body = resp.data
source_keys = [source["sentry_key"] for source in body]

# Should NOT see nintendo without console platform access
assert "nintendo" not in source_keys
# Should still see public sources
assert "public-source-1" in source_keys
assert "public-source-2" in source_keys
Comment on lines +100 to +113
Copy link
Member

Choose a reason for hiding this comment

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

are we adding an additional security check here? Users should only be able to create a Nintendo Switch project if they have access. Or is the idea that access could later be revoked, in which case they would already have an existing Nintendo Switch project?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes we are. the platform is an API input so if we didn't check for access, anyone could add it just by overriding the request.


@override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST)
def test_platform_filtering_unity(self) -> None:
"""Unity platform should NOT see nintendo source"""
resp = self.get_response(self.organization.slug, qs_params={"platform": "unity"})
assert resp.status_code == 200

body = resp.data
source_keys = [source["sentry_key"] for source in body]

# Unity should see public sources (no platform restriction)
assert "public-source-1" in source_keys
assert "public-source-2" in source_keys
# Unity should NOT see nintendo (restricted to nintendo-switch)
assert "nintendo" not in source_keys

@override_settings(SENTRY_BUILTIN_SOURCES=SENTRY_BUILTIN_SOURCES_PLATFORM_TEST)
def test_no_platform_parameter(self) -> None:
"""Without platform parameter, should see public sources but not platform-restricted ones"""
resp = self.get_response(self.organization.slug)
assert resp.status_code == 200

body = resp.data
source_keys = [source["sentry_key"] for source in body]

# Should see public sources (no platform restriction)
assert "public-source-1" in source_keys
assert "public-source-2" in source_keys
# Should NOT see platform-restricted source when no platform is provided
assert "nintendo" not in source_keys
Loading
Loading