From 36b63c73e302c77cbd89243130462003e7b6a3be Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Fri, 11 Jul 2025 14:04:43 -0700 Subject: [PATCH 01/19] add helper to set project platform --- src/sentry/event_manager.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 23a4ce67622a03..3d2341f3e1af43 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -464,6 +464,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, in which case we 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 @@ -690,6 +694,17 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: job["groups"] = [] +def _set_project_platform_if_needed(project: Project, event: Event) -> None: + if not project.platform and event.platform: + project.update(platform=event.platform) + + elif project.platform != event.platform: + # Do we want to update to `other` here? Do we only want to do that if we were the ones to + # set the platform value? (We could track this in a project option). Maybe we should only do + # that if we add some FE indicator? + pass + + @sentry_sdk.tracing.trace def _get_or_create_release_many(jobs: Sequence[Job], projects: ProjectsMapping) -> None: for job in jobs: @@ -1284,6 +1299,9 @@ def assign_event_to_group( project, primary.config, primary.hashes, secondary.config, secondary.hashes, result ) + # BELOW IS WHERE WE DO IT FOR GROUPING CONFIG - FOR PROJECT PLATFORM IT MAKES SENSE TO DO IT + # EARLIER IN THE CALL STACK SO IT APPLIES TO TRANSACTION EVENTS, ETC ALSO + # Now that we've used the current and possibly secondary grouping config(s) to calculate the # hashes, we're free to perform a config update if needed. Future events will use the new # config, but will also be grandfathered into the current config for a week, so as not to From 88faf1b457885d1cf9ac67a8d0a77cc600257492 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Sat, 12 Jul 2025 21:32:51 -0700 Subject: [PATCH 02/19] infer project platform if not set --- src/sentry/event_manager.py | 42 ++++++++++++++----- src/sentry/models/options/project_option.py | 1 + src/sentry/projectoptions/defaults.py | 3 ++ .../event_manager/test_platform_auto_set.py | 32 ++++++++++++++ 4 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 tests/sentry/event_manager/test_platform_auto_set.py diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 3d2341f3e1af43..579cb59533f52d 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -21,6 +21,7 @@ from usageaccountant import UsageUnit from sentry import ( + audit_log, eventstore, eventstream, eventtypes, @@ -127,6 +128,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, @@ -464,8 +466,8 @@ 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, in which case we set it based on the - # first event + # 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") @@ -695,14 +697,35 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: def _set_project_platform_if_needed(project: Project, event: Event) -> None: - if not project.platform and event.platform: + + if not event.platform: + return + + if not project.platform: project.update(platform=event.platform) + project.update_option("sentry:project_platform_inferred", event.platform) - elif project.platform != event.platform: - # Do we want to update to `other` here? Do we only want to do that if we were the ones to - # set the platform value? (We could track this in a project option). Maybe we should only do - # that if we add some FE indicator? - pass + 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, it hasn't been manually changed, and we should set it to other + if inferred_platform and project.platform == inferred_platform: + 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"}, + ) @sentry_sdk.tracing.trace @@ -1299,9 +1322,6 @@ def assign_event_to_group( project, primary.config, primary.hashes, secondary.config, secondary.hashes, result ) - # BELOW IS WHERE WE DO IT FOR GROUPING CONFIG - FOR PROJECT PLATFORM IT MAKES SENSE TO DO IT - # EARLIER IN THE CALL STACK SO IT APPLIES TO TRANSACTION EVENTS, ETC ALSO - # Now that we've used the current and possibly secondary grouping config(s) to calculate the # hashes, we're free to perform a config update if needed. Future events will use the new # config, but will also be grandfathered into the current config for a week, so as not to diff --git a/src/sentry/models/options/project_option.py b/src/sentry/models/options/project_option.py index 3a461689d96892..36370d74cd35df 100644 --- a/src/sentry/models/options/project_option.py +++ b/src/sentry/models/options/project_option.py @@ -67,6 +67,7 @@ "sentry:uptime_autodetection", "sentry:autofix_automation_tuning", "sentry:seer_scanner_automation", + "sentry:project_platform_auto_set", "quotas:spike-protection-disabled", "feedback:branding", "digests:mail:minimum_delay", diff --git a/src/sentry/projectoptions/defaults.py b/src/sentry/projectoptions/defaults.py index dc6ae56e535df8..78974a942d0096 100644 --- a/src/sentry/projectoptions/defaults.py +++ b/src/sentry/projectoptions/defaults.py @@ -204,3 +204,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) diff --git a/tests/sentry/event_manager/test_platform_auto_set.py b/tests/sentry/event_manager/test_platform_auto_set.py new file mode 100644 index 00000000000000..fe21373325c301 --- /dev/null +++ b/tests/sentry/event_manager/test_platform_auto_set.py @@ -0,0 +1,32 @@ +from sentry.testutils.cases import TestCase +from sentry.testutils.helpers.eventprocessing import save_new_event + + +class PlatformAutoSetTest(TestCase): + 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_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" From 6fb23e536adb5f0c0bc733533757821feee3da4c Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Sun, 13 Jul 2025 09:16:31 -0700 Subject: [PATCH 03/19] stop inferring project if changed manually --- src/sentry/event_manager.py | 7 +++- ..._set.py => test_project_platform_infer.py} | 33 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) rename tests/sentry/event_manager/{test_platform_auto_set.py => test_project_platform_infer.py} (71%) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 579cb59533f52d..286d2bfcbf43fd 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -716,7 +716,8 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: if project.platform != event.platform: inferred_platform = project.get_option("sentry:project_platform_inferred") - # If current platform matches what we inferred, it hasn't been manually changed, and we should set it to other + # 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: project.update(platform="other") project.update_option("sentry:project_platform_inferred", "other") @@ -727,6 +728,10 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: 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) + @sentry_sdk.tracing.trace def _get_or_create_release_many(jobs: Sequence[Job], projects: ProjectsMapping) -> None: diff --git a/tests/sentry/event_manager/test_platform_auto_set.py b/tests/sentry/event_manager/test_project_platform_infer.py similarity index 71% rename from tests/sentry/event_manager/test_platform_auto_set.py rename to tests/sentry/event_manager/test_project_platform_infer.py index fe21373325c301..6c0f52c9223d68 100644 --- a/tests/sentry/event_manager/test_platform_auto_set.py +++ b/tests/sentry/event_manager/test_project_platform_infer.py @@ -2,16 +2,7 @@ from sentry.testutils.helpers.eventprocessing import save_new_event -class PlatformAutoSetTest(TestCase): - 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 - +class ProjectPlatformInferTest(TestCase): def test_platform_inferred_on_event(self): project = self.create_project() @@ -30,3 +21,25 @@ def test_platform_inferred_other_when_mismatch(self): 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.update(platform="python") + + save_new_event({"message": "test", "platform": "ios"}, project) + + project.refresh_from_db() + assert project.platform == "python" + assert project.get_option("sentry:project_platform_inferred") is None From afc89076ee74d3e7de50ff345630913b9602609a Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Sun, 13 Jul 2025 11:21:58 -0700 Subject: [PATCH 04/19] fix tests --- tests/sentry/event_manager/test_event_manager_grouping.py | 3 +++ tests/sentry/receivers/test_transactions.py | 1 + 2 files changed, 4 insertions(+) diff --git a/tests/sentry/event_manager/test_event_manager_grouping.py b/tests/sentry/event_manager/test_event_manager_grouping.py index 888c138874dce4..486752aff2d96f 100644 --- a/tests/sentry/event_manager/test_event_manager_grouping.py +++ b/tests/sentry/event_manager/test_event_manager_grouping.py @@ -150,6 +150,9 @@ def test_auto_updates_grouping_config_even_if_config_is_gone(self): def test_auto_updates_grouping_config(self): self.project.update_option("sentry:grouping_config", LEGACY_GROUPING_CONFIG) + # Set a platform to prevent platform update audit log entry + self.project.update(platform="asdf") + save_new_event({"message": "Adopt don't shop"}, self.project) assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG diff --git a/tests/sentry/receivers/test_transactions.py b/tests/sentry/receivers/test_transactions.py index 0d90c156332861..699eb1c703913d 100644 --- a/tests/sentry/receivers/test_transactions.py +++ b/tests/sentry/receivers/test_transactions.py @@ -66,6 +66,7 @@ def test_event_processed(self): @patch("sentry.analytics.record") def test_analytics_event(self, mock_record): + self.project.update(platform="python") assert not self.project.flags.has_transactions event = self.store_event( data={ From c559e3876ffaf3487c2585ab117cac5cf87ed0ec Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Sun, 13 Jul 2025 14:30:47 -0700 Subject: [PATCH 05/19] handle 'other' events --- src/sentry/event_manager.py | 4 +++- .../event_manager/test_project_platform_infer.py | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 286d2bfcbf43fd..7de061d3fda0e6 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -698,7 +698,9 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: def _set_project_platform_if_needed(project: Project, event: Event) -> None: - if not event.platform: + # 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": return if not project.platform: diff --git a/tests/sentry/event_manager/test_project_platform_infer.py b/tests/sentry/event_manager/test_project_platform_infer.py index 6c0f52c9223d68..9485819f6201dc 100644 --- a/tests/sentry/event_manager/test_project_platform_infer.py +++ b/tests/sentry/event_manager/test_project_platform_infer.py @@ -35,10 +35,18 @@ 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": "ios"}, project) + save_new_event({"message": "test", "platform": "native"}, project) project.refresh_from_db() assert project.platform == "python" From 2d2ec4d3d94831dd4a69a343896991a37df4f183 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Sun, 13 Jul 2025 14:35:31 -0700 Subject: [PATCH 06/19] clean up typos + tests --- src/sentry/models/options/project_option.py | 2 +- tests/sentry/event_manager/test_event_manager_grouping.py | 3 --- tests/sentry/receivers/test_transactions.py | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/sentry/models/options/project_option.py b/src/sentry/models/options/project_option.py index 36370d74cd35df..bc933559716dd9 100644 --- a/src/sentry/models/options/project_option.py +++ b/src/sentry/models/options/project_option.py @@ -67,7 +67,7 @@ "sentry:uptime_autodetection", "sentry:autofix_automation_tuning", "sentry:seer_scanner_automation", - "sentry:project_platform_auto_set", + "sentry:project_platform_inferred", "quotas:spike-protection-disabled", "feedback:branding", "digests:mail:minimum_delay", diff --git a/tests/sentry/event_manager/test_event_manager_grouping.py b/tests/sentry/event_manager/test_event_manager_grouping.py index 486752aff2d96f..888c138874dce4 100644 --- a/tests/sentry/event_manager/test_event_manager_grouping.py +++ b/tests/sentry/event_manager/test_event_manager_grouping.py @@ -150,9 +150,6 @@ def test_auto_updates_grouping_config_even_if_config_is_gone(self): def test_auto_updates_grouping_config(self): self.project.update_option("sentry:grouping_config", LEGACY_GROUPING_CONFIG) - # Set a platform to prevent platform update audit log entry - self.project.update(platform="asdf") - save_new_event({"message": "Adopt don't shop"}, self.project) assert self.project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG diff --git a/tests/sentry/receivers/test_transactions.py b/tests/sentry/receivers/test_transactions.py index 699eb1c703913d..0d90c156332861 100644 --- a/tests/sentry/receivers/test_transactions.py +++ b/tests/sentry/receivers/test_transactions.py @@ -66,7 +66,6 @@ def test_event_processed(self): @patch("sentry.analytics.record") def test_analytics_event(self, mock_record): - self.project.update(platform="python") assert not self.project.flags.has_transactions event = self.store_event( data={ From 05c536d098d407221cc4976d50979aa89d8e8fce Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Sun, 13 Jul 2025 20:46:18 -0700 Subject: [PATCH 07/19] add lock + atomic transaction --- src/sentry/event_manager.py | 69 ++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 7de061d3fda0e6..f3d7901cf69529 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -81,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 @@ -697,42 +698,54 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: 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": return - if not project.platform: - project.update(platform=event.platform) - project.update_option("sentry:project_platform_inferred", event.platform) + # 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}" - 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 + 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"}, + ) - 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: - 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) - # 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 From 597b3c4ef6d14705b71a68dd04805937b928a2eb Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 14 Jul 2025 13:57:53 -0700 Subject: [PATCH 08/19] stop inferring to 'other' + options gating + filter out sample events --- src/sentry/event_manager.py | 42 +++++++------------ src/sentry/options/defaults.py | 8 ++++ src/sentry/projectoptions/defaults.py | 3 -- .../test_project_platform_infer.py | 36 ++-------------- 4 files changed, 26 insertions(+), 63 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index f3d7901cf69529..29b5f2bc4a68ff 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -140,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 @@ -469,7 +470,8 @@ def save( # 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"]) + 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": @@ -698,23 +700,30 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: 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": + # Only infer the platform if it's useful - if the event platform is "other", null or a sample + # event, there's no useful information for us to set the project platform + if not event.platform or event.platform == "other" 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}:{event.platform}" 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) + 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, @@ -722,30 +731,9 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: 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 + logger.exception("Failed to infer and set project platform") @sentry_sdk.tracing.trace 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/src/sentry/projectoptions/defaults.py b/src/sentry/projectoptions/defaults.py index 05e687822d8735..9f9e9d1ca56364 100644 --- a/src/sentry/projectoptions/defaults.py +++ b/src/sentry/projectoptions/defaults.py @@ -197,6 +197,3 @@ # 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) diff --git a/tests/sentry/event_manager/test_project_platform_infer.py b/tests/sentry/event_manager/test_project_platform_infer.py index 9485819f6201dc..c1746ba64bee9a 100644 --- a/tests/sentry/event_manager/test_project_platform_infer.py +++ b/tests/sentry/event_manager/test_project_platform_infer.py @@ -1,8 +1,10 @@ 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() @@ -10,18 +12,8 @@ def test_platform_inferred_on_event(self): 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" + @override_options({"sentry:infer_project_platform": 1.0}) def test_platform_does_not_override_existing_platform(self): project = self.create_project(platform="python") @@ -29,25 +21,3 @@ def test_platform_does_not_override_existing_platform(self): 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 From 9bfc4ba08337092b152732bfb92558e1ca07d29f Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 14 Jul 2025 14:10:52 -0700 Subject: [PATCH 09/19] clean up option key --- src/sentry/models/options/project_option.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry/models/options/project_option.py b/src/sentry/models/options/project_option.py index bc933559716dd9..3a461689d96892 100644 --- a/src/sentry/models/options/project_option.py +++ b/src/sentry/models/options/project_option.py @@ -67,7 +67,6 @@ "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", From 29f6f79f41a6e9524e20b67ddd2224762f84bc43 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 14 Jul 2025 14:19:26 -0700 Subject: [PATCH 10/19] wrap refresh in transaction --- src/sentry/event_manager.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 29b5f2bc4a68ff..a7aa8a567086f3 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -719,10 +719,9 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: else: cache.set(cache_key, "1", 60 * 5) - project.refresh_from_db(fields=["platform"]) - - if not project.platform: - with transaction.atomic(router.db_for_write(Project)): + with transaction.atomic(router.db_for_write(Project)): + project.refresh_from_db(fields=["platform"]) + if not project.platform: project.update(platform=event.platform) create_system_audit_entry( From 1a95c89863628768562b440d8085f235576d307e Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 14 Jul 2025 14:21:59 -0700 Subject: [PATCH 11/19] adjust cache key --- src/sentry/event_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index a7aa8a567086f3..a014f420ba4e59 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -707,7 +707,7 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: # 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}:{event.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 From 1e25c0afc2debd89aa1bf5078bdcca387c8da4f7 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 14 Jul 2025 17:09:24 -0700 Subject: [PATCH 12/19] use queryset update instead of transaction --- src/sentry/event_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index a014f420ba4e59..4300605f866705 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -719,11 +719,12 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: else: cache.set(cache_key, "1", 60 * 5) - with transaction.atomic(router.db_for_write(Project)): - project.refresh_from_db(fields=["platform"]) - if not project.platform: - project.update(platform=event.platform) + if not project.platform: + updated = Project.objects.filter(id=project.id, platform__isnull=True).update( + platform=event.platform + ) + if updated: create_system_audit_entry( organization=project.organization, target_object=project.id, From b0e8ec801e56b477580b5b85fdab3108b9448336 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 14 Jul 2025 17:12:12 -0700 Subject: [PATCH 13/19] use VALID_PLATFORMS instead of ad-hoc event platform filtering --- src/sentry/event_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 4300605f866705..38405b26bca401 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -164,6 +164,8 @@ SEER_ERROR_COUNT_KEY = ERROR_COUNT_CACHE_KEY("sentry.seer.severity-failures") +from sentry.constants import VALID_PLATFORMS + @dataclass class GroupInfo: @@ -702,7 +704,7 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: 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", null or a sample # event, there's no useful information for us to set the project platform - if not event.platform or event.platform == "other" or event.get_tag("sample_event") == "yes": + if event.platform in VALID_PLATFORMS: return # Use a lock to prevent race conditions when multiple events are processed From 26c7fe37ac7babfc66e7ff8818c0dd48eb93e08a Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 14 Jul 2025 17:19:06 -0700 Subject: [PATCH 14/19] save changes --- src/sentry/event_manager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 38405b26bca401..5f3928c3b56a1a 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -38,6 +38,7 @@ LOG_LEVELS_MAP, MAX_TAG_VALUE_LENGTH, PLACEHOLDER_EVENT_TITLES, + VALID_PLATFORMS, DataCategory, InsightModules, ) @@ -164,8 +165,6 @@ SEER_ERROR_COUNT_KEY = ERROR_COUNT_CACHE_KEY("sentry.seer.severity-failures") -from sentry.constants import VALID_PLATFORMS - @dataclass class GroupInfo: @@ -704,7 +703,7 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: 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", null or a sample # event, there's no useful information for us to set the project platform - if event.platform in VALID_PLATFORMS: + 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 From 7bde68e08f739b435b556af6137ddadf0c244cf3 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Tue, 15 Jul 2025 11:00:07 -0700 Subject: [PATCH 15/19] better filtering --- src/sentry/event_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 5f3928c3b56a1a..a3b7faaae3a5c2 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -721,7 +721,7 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: cache.set(cache_key, "1", 60 * 5) if not project.platform: - updated = Project.objects.filter(id=project.id, platform__isnull=True).update( + updated = Project.objects.filter(id=project.id, platform__in=[None, ""]).update( platform=event.platform ) From 054c0b22ec14d6f12b4b1680c7d926390760af8f Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Tue, 15 Jul 2025 12:06:47 -0700 Subject: [PATCH 16/19] fix query --- src/sentry/event_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index a3b7faaae3a5c2..eaae38ded0d5ef 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -14,7 +14,7 @@ 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 @@ -721,9 +721,9 @@ def _set_project_platform_if_needed(project: Project, event: Event) -> None: cache.set(cache_key, "1", 60 * 5) if not project.platform: - updated = Project.objects.filter(id=project.id, platform__in=[None, ""]).update( - platform=event.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( From 89d40a7aada6078332fff1e27dd11f99445512f9 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Tue, 15 Jul 2025 12:55:24 -0700 Subject: [PATCH 17/19] fix comment --- src/sentry/event_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index eaae38ded0d5ef..692e9915149bbf 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -701,8 +701,6 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: 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", null or a sample - # event, there's no useful information for us to set the project platform if event.platform not in VALID_PLATFORMS or event.get_tag("sample_event") == "yes": return From 553895bf8e339c9030f1a16494f770a9182e7688 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Wed, 16 Jul 2025 10:48:47 -0700 Subject: [PATCH 18/19] remove extra lock + cache --- src/sentry/event_manager.py | 40 +++++++++++-------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 692e9915149bbf..4f7c6c79181236 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -82,7 +82,6 @@ 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 @@ -701,38 +700,23 @@ def _pull_out_data(jobs: Sequence[Job], projects: ProjectsMapping) -> None: 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": + if project.platform: 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: + if event.platform not in VALID_PLATFORMS or event.get_tag("sample_event") == "yes": 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}, - ) + updated = Project.objects.filter( + Q(id=project.id) & (Q(platform__isnull=True) | Q(platform="")) + ).update(platform=event.platform) - except Exception: - logger.exception("Failed to infer and set project 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}, + ) @sentry_sdk.tracing.trace From 6f7ee89ae0bb9b11fea38ce05524ea8c3ddcdf66 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Wed, 16 Jul 2025 12:44:37 -0700 Subject: [PATCH 19/19] try except --- src/sentry/event_manager.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 4f7c6c79181236..6c49ab28141498 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -706,17 +706,21 @@ 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 - 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}, - ) + 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