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 18 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
47 changes: 46 additions & 1 deletion src/sentry/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import IntegrityError, OperationalError, connection, router, transaction
from django.db.models import Max
from django.db.models import Max, Q
from django.db.models.signals import post_save
from django.utils.encoding import force_str
from urllib3.exceptions import MaxRetryError, TimeoutError
from usageaccountant import UsageUnit

from sentry import (
audit_log,
eventstore,
eventstream,
eventtypes,
Expand All @@ -37,6 +38,7 @@
LOG_LEVELS_MAP,
MAX_TAG_VALUE_LENGTH,
PLACEHOLDER_EVENT_TITLES,
VALID_PLATFORMS,
DataCategory,
InsightModules,
)
Expand Down Expand Up @@ -80,6 +82,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 +130,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 All @@ -137,6 +141,7 @@
from sentry.utils.event import has_event_minified_stack_trace, has_stacktrace, is_handled
from sentry.utils.eventuser import EventUser
from sentry.utils.metrics import MutableTags
from sentry.utils.options import sample_modulo
from sentry.utils.outcomes import Outcome, track_outcome
from sentry.utils.projectflags import set_project_flag_and_signal
from sentry.utils.safe import get_path, safe_execute, setdefault_path, trim
Expand Down Expand Up @@ -464,6 +469,11 @@ 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
if sample_modulo("sentry:infer_project_platform", project.id):
_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 +700,41 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None:
job["groups"] = []


def _set_project_platform_if_needed(project: Project, event: Event) -> None:
if event.platform not in VALID_PLATFORMS or event.get_tag("sample_event") == "yes":
return

# Use a lock to prevent race conditions when multiple events are processed
# concurrently for a project with no initial platform
cache_key = f"project-platform-cache:{project.id}"
lock_key = f"project-platform-lock:{project.id}"
if cache.get(cache_key) is not None:
return

try:
with locks.get(lock_key, duration=60, name="project-platform-lock").acquire():
if cache.get(cache_key) is not None:
return
else:
cache.set(cache_key, "1", 60 * 5)

if not project.platform:
updated = Project.objects.filter(
Q(id=project.id) & (Q(platform__isnull=True) | Q(platform=""))
).update(platform=event.platform)

if updated:
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},
)

except Exception:
logger.exception("Failed to infer and set project platform")


@sentry_sdk.tracing.trace
def _get_or_create_release_many(jobs: Sequence[Job], projects: ProjectsMapping) -> None:
for job in jobs:
Expand Down
8 changes: 8 additions & 0 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -3480,3 +3480,11 @@
default=False,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)

# Rollout for inferring project platform from events received
register(
"sentry:infer_project_platform",
type=Float,
default=0.0,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)
23 changes: 23 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,23 @@
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers import override_options
from sentry.testutils.helpers.eventprocessing import save_new_event


class ProjectPlatformInferTest(TestCase):
@override_options({"sentry:infer_project_platform": 1.0})
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"

@override_options({"sentry:infer_project_platform": 1.0})
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"
Loading