diff --git a/src/sentry/api/endpoints/organization_sdk_deprecations.py b/src/sentry/api/endpoints/organization_sdk_deprecations.py index 9c20478c89a17c..6748dc760e512a 100644 --- a/src/sentry/api/endpoints/organization_sdk_deprecations.py +++ b/src/sentry/api/endpoints/organization_sdk_deprecations.py @@ -2,19 +2,18 @@ from typing import DefaultDict, TypedDict import sentry_sdk -from packaging.version import InvalidVersion, Version +from packaging.version import InvalidVersion from packaging.version import parse as parse_version from rest_framework import serializers from rest_framework.request import Request from rest_framework.response import Response -from sentry import options from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.models.project import Project -from sentry.models.projectsdk import EventType, ProjectSDK +from sentry.models.projectsdk import EventType, ProjectSDK, get_minimum_sdk_version class SDKDeprecationsSerializer(serializers.Serializer): @@ -93,32 +92,6 @@ def get_event_types(raw_event_type: str) -> list[EventType]: raise ValueError(f"Unknown event type: {raw_event_type}") -MINIMUM_SDK_VERSION_OPTIONS: dict[tuple[int, str], str] = { - (EventType.PROFILE_CHUNK.value, "sentry.cocoa"): "sdk-deprecation.profile-chunk.cocoa", - (EventType.PROFILE_CHUNK.value, "sentry.python"): "sdk-deprecation.profile-chunk.python", -} - - -def get_minimum_sdk_version(project_sdk: ProjectSDK) -> Version | None: - parts = project_sdk.sdk_name.split(".", 2) - if len(parts) < 2: - return None - - sdk_name = ".".join(parts[:2]) - - sdk_version_option = MINIMUM_SDK_VERSION_OPTIONS.get((project_sdk.event_type, sdk_name)) - if sdk_version_option is None: - return None - - sdk_version = options.get(sdk_version_option) - if sdk_version: - try: - return parse_version(sdk_version) - except InvalidVersion as e: - sentry_sdk.capture_exception(e) - return None - - def get_deprecation_status(project_sdk: ProjectSDK) -> SDKDeprecation | None: try: sdk_version = parse_version(project_sdk.sdk_version) @@ -126,7 +99,11 @@ def get_deprecation_status(project_sdk: ProjectSDK) -> SDKDeprecation | None: sentry_sdk.capture_exception(e) return None - minimum_sdk_version = get_minimum_sdk_version(project_sdk) + minimum_sdk_version = get_minimum_sdk_version( + project_sdk.event_type, + project_sdk.sdk_name, + hard_limit=False, + ) # no minimum sdk version was specified if minimum_sdk_version is None: diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index eb00aa169d371f..7d7d782a50f118 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -273,6 +273,8 @@ def register_temporary_features(manager: FeatureManager): manager.add("organizations:profiling-beta", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True) # Enables monitoring for latest profiling sdk used manager.add("organizations:profiling-sdks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enables dropping of deprecated profiling sdks used + manager.add("organizations:profiling-deprecate-sdks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enables production profiling in sentry browser application manager.add("organizations:profiling-browser", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) # Enables separate differential flamegraph page diff --git a/src/sentry/models/projectsdk.py b/src/sentry/models/projectsdk.py index 16849475d620ad..01397dc06d1a26 100644 --- a/src/sentry/models/projectsdk.py +++ b/src/sentry/models/projectsdk.py @@ -4,10 +4,12 @@ from collections.abc import Sequence from enum import Enum +import sentry_sdk from django.db import models from packaging.version import InvalidVersion, Version from packaging.version import parse as parse_version +from sentry import options from sentry.backup.scopes import RelocationScope from sentry.db.models import BoundedIntegerField, FlexibleForeignKey, region_silo_model, sane_repr from sentry.db.models.base import DefaultFieldsModel @@ -170,3 +172,33 @@ def normalize_sdk_name(sdk_name: str) -> str | None: return sdk_name return None + + +MINIMUM_SDK_VERSION_OPTIONS: dict[tuple[int, str], str] = { + (EventType.PROFILE_CHUNK.value, "sentry.cocoa"): "sdk-deprecation.profile-chunk.cocoa", + (EventType.PROFILE_CHUNK.value, "sentry.python"): "sdk-deprecation.profile-chunk.python", +} + + +def get_minimum_sdk_version(event_type: int, sdk_name: str, hard_limit: bool) -> Version | None: + parts = sdk_name.split(".", 2) + if len(parts) < 2: + return None + + normalized_sdk_name = ".".join(parts[:2]) + + sdk_version_option = MINIMUM_SDK_VERSION_OPTIONS.get((event_type, normalized_sdk_name)) + if sdk_version_option is None: + return None + + if hard_limit: + sdk_version = options.get(f"{sdk_version_option}.hard") + else: + sdk_version = options.get(sdk_version_option) + + if sdk_version: + try: + return parse_version(sdk_version) + except InvalidVersion as e: + sentry_sdk.capture_exception(e) + return None diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 4f65cb1bbf93ec..770ebea943c40f 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -3194,8 +3194,18 @@ default="2.24.1", flags=FLAG_AUTOMATOR_MODIFIABLE, ) +register( + "sdk-deprecation.profile-chunk.python.hard", + default="2.24.1", + flags=FLAG_AUTOMATOR_MODIFIABLE, +) register( "sdk-deprecation.profile-chunk.cocoa", + default="8.49.2", + flags=FLAG_AUTOMATOR_MODIFIABLE, +) +register( + "sdk-deprecation.profile-chunk.cocoa.hard", default="8.49.0", flags=FLAG_AUTOMATOR_MODIFIABLE, ) diff --git a/src/sentry/profiles/task.py b/src/sentry/profiles/task.py index 9eb58e6b394f3e..bb62ca665743ea 100644 --- a/src/sentry/profiles/task.py +++ b/src/sentry/profiles/task.py @@ -15,6 +15,8 @@ from arroyo import Topic as ArroyoTopic from arroyo.backends.kafka import KafkaPayload, KafkaProducer, build_kafka_configuration from django.conf import settings +from packaging.version import InvalidVersion +from packaging.version import parse as parse_version from sentry import features, options, quotas from sentry.conf.types.kafka_definition import Topic @@ -27,7 +29,7 @@ from sentry.models.files.utils import get_profiles_storage from sentry.models.organization import Organization from sentry.models.project import Project -from sentry.models.projectsdk import EventType, ProjectSDK +from sentry.models.projectsdk import EventType, ProjectSDK, get_minimum_sdk_version from sentry.profiles.java import ( convert_android_methods_to_jvm_frames, deobfuscate_signature, @@ -159,6 +161,45 @@ def process_profile_task( sentry_sdk.set_tag("project", project.id) sentry_sdk.set_tag("project.slug", project.slug) + if sampled: + if features.has("organizations:profiling-sdks", organization): + try: + event_type = determine_profile_type(profile) + sdk_name, sdk_version = determine_client_sdk(profile, event_type) + + ProjectSDK.update_with_newest_version_or_create( + project=project, + event_type=event_type, + sdk_name=sdk_name, + sdk_version=sdk_version, + ) + + # Check to see if the data is coming from an deprecated SDK + # and drop it if needed + if is_sdk_deprecated(event_type, sdk_name, sdk_version): + if features.has("organizations:profiling-deprecate-sdks", organization): + category = ( + DataCategory.PROFILE_CHUNK + if event_type == EventType.PROFILE_CHUNK + else DataCategory.PROFILE + ) + _track_outcome( + profile=profile, + project=project, + outcome=Outcome.FILTERED, + categories=[category], + reason="deprecated sdk", + ) + return + except UnableToAcquireLock: + # unable to acquire the lock means another event is trying to + # update the version so we can skip the update from this event + pass + except (UnknownClientSDKException, UnknownProfileTypeException): + pass + except Exception as e: + sentry_sdk.capture_exception(e) + profile_context = { "organization_id": profile["organization_id"], "project_id": profile["project_id"], @@ -236,14 +277,6 @@ def process_profile_task( if sampled: with metrics.timer("process_profile.track_outcome.accepted"): - if features.has("organizations:profiling-sdks", organization): - try: - track_latest_sdk(project, profile) - except (UnknownClientSDKException, UnknownProfileTypeException): - pass - except Exception as e: - sentry_sdk.capture_exception(e) - if not project.flags.has_profiles: first_profile_received.send_robust(project=project, sender=Project) try: @@ -1212,21 +1245,32 @@ def determine_client_sdk(profile: Profile, event_type: EventType) -> tuple[str, raise UnknownClientSDKException -def track_latest_sdk(project: Project, profile: Profile) -> None: - event_type = determine_profile_type(profile) - sdk_name, sdk_version = determine_client_sdk(profile, event_type) +def is_sdk_deprecated(event_type: EventType, sdk_name: str, sdk_version: str) -> bool: + minimum_version = get_minimum_sdk_version(event_type.value, sdk_name, hard_limit=True) + + # no minimum sdk version was specified + if minimum_version is None: + return False try: - ProjectSDK.update_with_newest_version_or_create( - project=project, - event_type=event_type, - sdk_name=sdk_name, - sdk_version=sdk_version, + version = parse_version(sdk_version) + except InvalidVersion: + return False + + # satisfies the minimum sdk version + if version >= minimum_version: + return False + + parts = sdk_name.split(".", 2) + if len(parts) >= 2: + normalized_sdk_name = ".".join(parts[:2]) + metrics.incr( + "process_profile.sdk.deprecated", + tags={"sdk_name": normalized_sdk_name}, + sample_rate=1.0, ) - except UnableToAcquireLock: - # unable to acquire the lock means another event is trying to update the version - # so we can skip the update from this event - pass + + return True @metrics.wraps("process_profile.process_vroomrs_profile") diff --git a/tests/sentry/api/endpoints/test_organization_sdk_deprecations.py b/tests/sentry/api/endpoints/test_organization_sdk_deprecations.py index a7dd342b199241..bc2fafcf1a0c47 100644 --- a/tests/sentry/api/endpoints/test_organization_sdk_deprecations.py +++ b/tests/sentry/api/endpoints/test_organization_sdk_deprecations.py @@ -139,7 +139,7 @@ def test_mixed_sdks(self): project=self.project, event_type=EventType.PROFILE_CHUNK.value, sdk_name="sentry.cocoa", - sdk_version="8.49.0", + sdk_version="8.49.2", ) response = self.client.get( self.url, diff --git a/tests/sentry/profiles/test_task.py b/tests/sentry/profiles/test_task.py index 237cc816d947bb..3a4fd46af44305 100644 --- a/tests/sentry/profiles/test_task.py +++ b/tests/sentry/profiles/test_task.py @@ -33,7 +33,7 @@ from sentry.profiles.utils import Profile from sentry.testutils.cases import TransactionTestCase from sentry.testutils.factories import Factories, get_fixture_path -from sentry.testutils.helpers import Feature +from sentry.testutils.helpers import Feature, override_options from sentry.testutils.pytest.fixtures import django_db_all from sentry.testutils.skips import requires_symbolicator from sentry.utils import json @@ -206,7 +206,7 @@ def sample_v1_profile(): }, "client_sdk": { "name": "sentry.python", - "version": "2.23.0" + "version": "2.24.1" } }""" ) @@ -273,7 +273,7 @@ def generate_sample_v2_profile(): }, "client_sdk": { "name": "sentry.python", - "version": "2.23.0" + "version": "2.24.1" } }""" ) @@ -976,7 +976,7 @@ def test_track_latest_sdk( project=project, event_type=event_type.value, sdk_name="sentry.python", - sdk_version="2.23.0", + sdk_version="2.24.1", ) is not None ) @@ -1079,7 +1079,65 @@ def test_track_latest_sdk_with_payload( project=project, event_type=EventType.PROFILE.value, sdk_name="sentry.python", - sdk_version="2.23.0", + sdk_version="2.24.1", ) is not None ) + + +@patch("sentry.profiles.task._track_outcome") +@patch("sentry.profiles.task._push_profile_to_vroom") +@django_db_all +@pytest.mark.parametrize( + ["profile", "category", "sdk_version", "dropped"], + [ + pytest.param("sample_v1_profile", DataCategory.PROFILE, "2.23.0", False), + pytest.param("sample_v2_profile", DataCategory.PROFILE_CHUNK, "2.23.0", True), + pytest.param("sample_v2_profile", DataCategory.PROFILE_CHUNK, "2.24.0", False), + pytest.param("sample_v2_profile", DataCategory.PROFILE_CHUNK, "2.24.1", False), + ], +) +def test_deprecated_sdks( + _push_profile_to_vroom, + _track_outcome, + profile, + category, + sdk_version, + dropped, + organization, + project, + request, +): + profile = request.getfixturevalue(profile) + profile["organization_id"] = organization.id + profile["project_id"] = project.id + profile["client_sdk"] = { + "name": "sentry.python", + "version": sdk_version, + } + + with Feature( + [ + "organizations:profiling-sdks", + "organizations:profiling-deprecate-sdks", + ] + ): + with override_options( + { + "sdk-deprecation.profile-chunk.python": "2.24.1", + "sdk-deprecation.profile-chunk.python.hard": "2.24.0", + } + ): + process_profile_task(profile=profile) + + if dropped: + _push_profile_to_vroom.assert_not_called() + _track_outcome.assert_called_with( + profile=profile, + project=project, + outcome=Outcome.FILTERED, + categories=[category], + reason="deprecated sdk", + ) + else: + _push_profile_to_vroom.assert_called()