Skip to content

feat(issues): infer project platform if not set #95402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
58 changes: 58 additions & 0 deletions src/sentry/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from usageaccountant import UsageUnit

from sentry import (
audit_log,
eventstore,
eventstream,
eventtypes,
Expand Down Expand Up @@ -80,6 +81,7 @@
from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka
from sentry.killswitches import killswitch_matches_context
from sentry.lang.native.utils import STORE_CRASH_REPORTS_ALL, convert_crashreport_count
from sentry.locks import locks
from sentry.models.activity import Activity
from sentry.models.environment import Environment
from sentry.models.event import EventDict
Expand Down Expand Up @@ -127,6 +129,7 @@
from sentry.types.group import GroupSubStatus, PriorityLevel
from sentry.usage_accountant import record
from sentry.utils import metrics
from sentry.utils.audit import create_system_audit_entry
from sentry.utils.cache import cache_key_for_event
from sentry.utils.circuit_breaker import (
ERROR_COUNT_CACHE_KEY,
Expand Down Expand Up @@ -464,6 +467,10 @@ def save(
# After calling _pull_out_data we get some keys in the job like the platform
_pull_out_data([job], projects)

# Sometimes projects get created without a platform (e.g. through the API), in which case we
# attempt to set it based on the first event
_set_project_platform_if_needed(project, job["event"])

event_type = self._data.get("type")
if event_type == "transaction":
job["data"]["project"] = project.id
Expand Down Expand Up @@ -690,6 +697,57 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None:
job["groups"] = []


def _set_project_platform_if_needed(project: Project, event: Event) -> None:
# Only infer the platform if it's useful - if the event platform is "other" or null, there's
# no useful information for us to set the project platform
if not event.platform or event.platform == "other":
Copy link
Member

@mrduncan mrduncan Jul 14, 2025

Choose a reason for hiding this comment

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

We should exclude "other" here, users can explicitly select this during project creation by searching for it and we shouldn't overwrite it:

Screenshot 2025-07-14 at 12 51 54 PM

Copy link
Contributor Author

@cvxluo cvxluo Jul 14, 2025

Choose a reason for hiding this comment

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

here, we exclude events without a platform - we won't overwrite platform if it is set anywhere
the motivation to exclude 'other' events is that in cases where an event is missing a platform, it seems to get normalized to 'other'. in those cases, i'd rather ignore the event and do nothing rather than assign the project to 'other'

return

# Use a lock to prevent race conditions when multiple events are processed
# concurrently for a project with no initial platform
lock_key = f"project-platform-lock:{project.id}"

try:
with locks.get(lock_key, duration=60, name="project-platform-lock").acquire():
project.refresh_from_db(fields=["platform"])

if not project.platform:
with transaction.atomic(router.db_for_write(Project)):
project.update(platform=event.platform)
project.update_option("sentry:project_platform_inferred", event.platform)

create_system_audit_entry(
organization=project.organization,
target_object=project.id,
event=audit_log.get_event_id("PROJECT_EDIT"),
data={**project.get_audit_log_data(), "platform": event.platform},
)
return

if project.platform != event.platform:
inferred_platform = project.get_option("sentry:project_platform_inferred")

# If current platform matches what we inferred, we can safely continue inferring it
# since it hasn't been manually changed
if inferred_platform and project.platform == inferred_platform:
with transaction.atomic(router.db_for_write(Project)):
project.update(platform="other")
project.update_option("sentry:project_platform_inferred", "other")
create_system_audit_entry(
organization=project.organization,
target_object=project.id,
event=audit_log.get_event_id("PROJECT_EDIT"),
data={**project.get_audit_log_data(), "platform": "other"},
)

# Otherwise, we need to mark that we should stop inferring platform (unless it becomes null again)
else:
project.update_option("sentry:project_platform_inferred", None)

except Exception:
return


@sentry_sdk.tracing.trace
def _get_or_create_release_many(jobs: Sequence[Job], projects: ProjectsMapping) -> None:
for job in jobs:
Expand Down
1 change: 1 addition & 0 deletions src/sentry/models/options/project_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"sentry:uptime_autodetection",
"sentry:autofix_automation_tuning",
"sentry:seer_scanner_automation",
"sentry:project_platform_inferred",
"quotas:spike-protection-disabled",
"feedback:branding",
"digests:mail:minimum_delay",
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/projectoptions/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,6 @@

# Should seer scanner run automatically on new issues
register(key="sentry:seer_scanner_automation", default=True)

# Track the platform that was inferred from the events received
register(key="sentry:project_platform_inferred", default=None)
53 changes: 53 additions & 0 deletions tests/sentry/event_manager/test_project_platform_infer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.eventprocessing import save_new_event


class ProjectPlatformInferTest(TestCase):
def test_platform_inferred_on_event(self):
project = self.create_project()

save_new_event({"message": "test", "platform": "javascript"}, project)

project.refresh_from_db()
assert project.platform == "javascript"
assert project.get_option("sentry:project_platform_inferred") == "javascript"

def test_platform_inferred_other_when_mismatch(self):
project = self.create_project()

save_new_event({"message": "test", "platform": "javascript"}, project)
save_new_event({"message": "test", "platform": "python"}, project)

project.refresh_from_db()
assert project.platform == "other"
assert project.get_option("sentry:project_platform_inferred") == "other"

def test_platform_does_not_override_existing_platform(self):
project = self.create_project(platform="python")

save_new_event({"message": "test", "platform": "javascript"}, project)

project.refresh_from_db()
assert project.platform == "python"
assert project.get_option("sentry:project_platform_inferred") is None

def test_platform_stops_inferring_when_manually_set(self):
project = self.create_project()

save_new_event({"message": "test", "platform": "javascript"}, project)
project.refresh_from_db()

assert project.platform == "javascript"
assert project.get_option("sentry:project_platform_inferred") == "javascript"

project.update(platform="python")
project.refresh_from_db()

assert project.platform == "python"
assert project.get_option("sentry:project_platform_inferred") == "javascript"

save_new_event({"message": "test", "platform": "native"}, project)

project.refresh_from_db()
assert project.platform == "python"
assert project.get_option("sentry:project_platform_inferred") is None
Loading