diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 23a4ce67622a03..6c49ab28141498 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -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, @@ -37,6 +38,7 @@ LOG_LEVELS_MAP, MAX_TAG_VALUE_LENGTH, PLACEHOLDER_EVENT_TITLES, + VALID_PLATFORMS, DataCategory, InsightModules, ) @@ -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, @@ -137,6 +140,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 @@ -464,6 +468,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 @@ -690,6 +699,30 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: job["groups"] = [] +def _set_project_platform_if_needed(project: Project, event: Event) -> None: + if project.platform: + return + + if event.platform not in VALID_PLATFORMS or event.get_tag("sample_event") == "yes": + return + + try: + 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: diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index bee808feca92e8..5ec9466b1afc32 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -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, +) diff --git a/tests/sentry/event_manager/test_project_platform_infer.py b/tests/sentry/event_manager/test_project_platform_infer.py new file mode 100644 index 00000000000000..c1746ba64bee9a --- /dev/null +++ b/tests/sentry/event_manager/test_project_platform_infer.py @@ -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"