Skip to content

Commit 7654f09

Browse files
authored
feat(profiling): Add way to reject bad sdk versions for select orgs (#97574)
8.49.2 of the cocoa sdk fixed a bug where you might get profiles even if it was never configured. This adds a way to reject profiles from these older sdks on a per org basis. We can't turn this on for all orgs as it means unaffected orgs will lose all their profiles.
1 parent 95baadd commit 7654f09

File tree

5 files changed

+168
-15
lines changed

5 files changed

+168
-15
lines changed

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ def register_temporary_features(manager: FeatureManager):
304304
manager.add("organizations:profiling-sdks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
305305
# Enables dropping of deprecated profiling sdks used
306306
manager.add("organizations:profiling-deprecate-sdks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
307+
# Enables dropping of profiles that may come from buggy sdks
308+
manager.add("organizations:profiling-reject-sdks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
307309
# Enables production profiling in sentry browser application
308310
manager.add("organizations:profiling-browser", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
309311
# Enables separate differential flamegraph page

src/sentry/models/projectsdk.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sentry.db.models.base import DefaultFieldsModel
1616
from sentry.locks import locks
1717
from sentry.models.project import Project
18+
from sentry.options import UnknownOption
1819
from sentry.sdk_updates import get_sdk_index
1920
from sentry.utils import metrics
2021
from sentry.utils.cache import cache
@@ -175,11 +176,36 @@ def normalize_sdk_name(sdk_name: str) -> str | None:
175176

176177

177178
MINIMUM_SDK_VERSION_OPTIONS: dict[tuple[int, str], str] = {
179+
(EventType.PROFILE.value, "sentry.cocoa"): "sdk-deprecation.profile.cocoa",
178180
(EventType.PROFILE_CHUNK.value, "sentry.cocoa"): "sdk-deprecation.profile-chunk.cocoa",
179181
(EventType.PROFILE_CHUNK.value, "sentry.python"): "sdk-deprecation.profile-chunk.python",
180182
}
181183

182184

185+
def get_rejected_sdk_version(event_type: int, sdk_name: str) -> Version | None:
186+
parts = sdk_name.split(".", 2)
187+
if len(parts) < 2:
188+
return None
189+
190+
normalized_sdk_name = ".".join(parts[:2])
191+
192+
sdk_version_option = MINIMUM_SDK_VERSION_OPTIONS.get((event_type, normalized_sdk_name))
193+
if sdk_version_option is None:
194+
return None
195+
196+
try:
197+
sdk_version = options.get(f"{sdk_version_option}.reject")
198+
except UnknownOption:
199+
sdk_version = None
200+
201+
if sdk_version:
202+
try:
203+
return parse_version(sdk_version)
204+
except InvalidVersion as e:
205+
sentry_sdk.capture_exception(e)
206+
return None
207+
208+
183209
def get_minimum_sdk_version(event_type: int, sdk_name: str, hard_limit: bool) -> Version | None:
184210
parts = sdk_name.split(".", 2)
185211
if len(parts) < 2:
@@ -191,10 +217,13 @@ def get_minimum_sdk_version(event_type: int, sdk_name: str, hard_limit: bool) ->
191217
if sdk_version_option is None:
192218
return None
193219

194-
if hard_limit:
195-
sdk_version = options.get(f"{sdk_version_option}.hard")
196-
else:
197-
sdk_version = options.get(sdk_version_option)
220+
try:
221+
if hard_limit:
222+
sdk_version = options.get(f"{sdk_version_option}.hard")
223+
else:
224+
sdk_version = options.get(sdk_version_option)
225+
except UnknownOption:
226+
sdk_version = None
198227

199228
if sdk_version:
200229
try:

src/sentry/options/defaults.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3392,6 +3392,16 @@
33923392
default="8.49.0",
33933393
flags=FLAG_AUTOMATOR_MODIFIABLE,
33943394
)
3395+
register(
3396+
"sdk-deprecation.profile-chunk.cocoa.reject",
3397+
default="8.49.2",
3398+
flags=FLAG_AUTOMATOR_MODIFIABLE,
3399+
)
3400+
register(
3401+
"sdk-deprecation.profile.cocoa.reject",
3402+
default="8.49.2",
3403+
flags=FLAG_AUTOMATOR_MODIFIABLE,
3404+
)
33953405

33963406

33973407
# Orgs for which compression should be disabled in the chunk upload endpoint.

src/sentry/profiles/task.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
from sentry.models.files.utils import get_profiles_storage
3232
from sentry.models.organization import Organization
3333
from sentry.models.project import Project
34-
from sentry.models.projectsdk import EventType, ProjectSDK, get_minimum_sdk_version
34+
from sentry.models.projectsdk import (
35+
EventType,
36+
ProjectSDK,
37+
get_minimum_sdk_version,
38+
get_rejected_sdk_version,
39+
)
3540
from sentry.objectstore.metrics import measure_storage_operation
3641
from sentry.profiles.java import (
3742
convert_android_methods_to_jvm_frames,
@@ -310,6 +315,9 @@ def _is_deprecated(profile: Profile, project: Project, organization: Organizatio
310315
try:
311316
sdk_name, sdk_version = determine_client_sdk(profile, event_type)
312317
except UnknownClientSDKException:
318+
# unknown SDKs happen because older sdks didn't send the sdk version
319+
# in the payload, so if we cant determine the client sdk, we assume
320+
# it's one of the deprecated versions
313321
_track_outcome(
314322
profile=profile,
315323
project=project,
@@ -331,18 +339,31 @@ def _is_deprecated(profile: Profile, project: Project, organization: Organizatio
331339
# update the version so we can skip the update from this event
332340
return False
333341

334-
if not is_sdk_deprecated(event_type, sdk_name, sdk_version):
335-
return False
342+
if features.has("organizations:profiling-reject-sdks", organization) and is_sdk_rejected(
343+
organization, event_type, sdk_name, sdk_version
344+
):
345+
_track_outcome(
346+
profile=profile,
347+
project=project,
348+
outcome=Outcome.FILTERED,
349+
categories=[category],
350+
reason="rejected sdk",
351+
)
352+
return True
336353

337-
_track_outcome(
338-
profile=profile,
339-
project=project,
340-
outcome=Outcome.FILTERED,
341-
categories=[category],
342-
reason="deprecated sdk",
343-
)
354+
if features.has("organizations:profiling-deprecate-sdks", organization) and is_sdk_deprecated(
355+
event_type, sdk_name, sdk_version
356+
):
357+
_track_outcome(
358+
profile=profile,
359+
project=project,
360+
outcome=Outcome.FILTERED,
361+
categories=[category],
362+
reason="deprecated sdk",
363+
)
364+
return True
344365

345-
return features.has("organizations:profiling-deprecate-sdks", organization)
366+
return False
346367

347368

348369
JS_PLATFORMS = ["javascript", "node"]
@@ -1306,6 +1327,36 @@ def is_sdk_deprecated(event_type: EventType, sdk_name: str, sdk_version: str) ->
13061327
return True
13071328

13081329

1330+
def is_sdk_rejected(
1331+
organization: Organization, event_type: EventType, sdk_name: str, sdk_version: str
1332+
) -> bool:
1333+
rejected_version = get_rejected_sdk_version(event_type.value, sdk_name)
1334+
1335+
# no rejected sdk version was specified
1336+
if rejected_version is None:
1337+
return False
1338+
1339+
try:
1340+
version = parse_version(sdk_version)
1341+
except InvalidVersion:
1342+
return False
1343+
1344+
# satisfies the rejected sdk version
1345+
if version >= rejected_version:
1346+
return False
1347+
1348+
parts = sdk_name.split(".", 2)
1349+
if len(parts) >= 2:
1350+
normalized_sdk_name = ".".join(parts[:2])
1351+
metrics.incr(
1352+
"process_profile.sdk.rejected",
1353+
tags={"sdk_name": normalized_sdk_name},
1354+
sample_rate=1.0,
1355+
)
1356+
1357+
return True
1358+
1359+
13091360
@metrics.wraps("process_profile.process_vroomrs_profile")
13101361
def _process_vroomrs_profile(profile: Profile, project: Project) -> bool:
13111362
if "profiler_id" in profile:

tests/sentry/profiles/test_task.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,67 @@ def test_deprecated_sdks(
11341134
_push_profile_to_vroom.assert_called()
11351135

11361136

1137+
@patch("sentry.profiles.task._track_outcome")
1138+
@patch("sentry.profiles.task._push_profile_to_vroom")
1139+
@django_db_all
1140+
@pytest.mark.parametrize(
1141+
["profile", "category", "sdk_version", "dropped"],
1142+
[
1143+
pytest.param("sample_v1_profile", DataCategory.PROFILE, "2.23.0", True),
1144+
pytest.param("sample_v2_profile", DataCategory.PROFILE_CHUNK, "2.23.0", True),
1145+
pytest.param("sample_v2_profile", DataCategory.PROFILE_CHUNK, "2.24.0", False),
1146+
pytest.param("sample_v2_profile", DataCategory.PROFILE_CHUNK, "2.24.1", False),
1147+
],
1148+
)
1149+
def test_rejected_sdks(
1150+
_push_profile_to_vroom,
1151+
_track_outcome,
1152+
profile,
1153+
category,
1154+
sdk_version,
1155+
dropped,
1156+
organization,
1157+
project,
1158+
request,
1159+
):
1160+
profile = request.getfixturevalue(profile)
1161+
profile["organization_id"] = organization.id
1162+
profile["project_id"] = project.id
1163+
profile["client_sdk"] = {
1164+
"name": "sentry.cocoa",
1165+
"version": sdk_version,
1166+
}
1167+
1168+
with Feature(
1169+
[
1170+
"organizations:profiling-sdks",
1171+
"organizations:profiling-deprecate-sdks",
1172+
"organizations:profiling-reject-sdks",
1173+
]
1174+
):
1175+
with override_options(
1176+
{
1177+
"sdk-deprecation.profile-chunk.cocoa": "2.24.1",
1178+
"sdk-deprecation.profile-chunk.cocoa.hard": "2.24.0",
1179+
"sdk-deprecation.profile-chunk.cocoa.reject": "2.23.1",
1180+
"sdk-deprecation.profile.cocoa.reject": "2.23.1",
1181+
}
1182+
):
1183+
process_profile_task(profile=profile)
1184+
1185+
if dropped:
1186+
_push_profile_to_vroom.assert_not_called()
1187+
_track_outcome.assert_called_with(
1188+
profile=profile,
1189+
project=project,
1190+
outcome=Outcome.FILTERED,
1191+
categories=[category],
1192+
reason="rejected sdk",
1193+
)
1194+
else:
1195+
_push_profile_to_vroom.assert_called()
1196+
1197+
11371198
@patch("sentry.profiles.task._symbolicate_profile")
11381199
@patch("sentry.profiles.task._deobfuscate_profile")
11391200
@patch("sentry.profiles.task._push_profile_to_vroom")

0 commit comments

Comments
 (0)