Skip to content

Commit 6234116

Browse files
committed
Revert "chore(dynamic-sampling): remove dynamic sampling minimum samplerate project option (#95104)"
This reverts commit 885e367.
1 parent ca88475 commit 6234116

File tree

7 files changed

+176
-1
lines changed

7 files changed

+176
-1
lines changed

src/sentry/api/endpoints/project_details.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
3737
from sentry.dynamic_sampling import get_supported_biases_ids, get_user_biases
3838
from sentry.dynamic_sampling.types import DynamicSamplingMode
39-
from sentry.dynamic_sampling.utils import has_custom_dynamic_sampling, has_dynamic_sampling
39+
from sentry.dynamic_sampling.utils import (
40+
has_custom_dynamic_sampling,
41+
has_dynamic_sampling,
42+
has_dynamic_sampling_minimum_sample_rate,
43+
)
4044
from sentry.grouping.enhancer import Enhancements
4145
from sentry.grouping.enhancer.exceptions import InvalidEnhancerConfig
4246
from sentry.grouping.fingerprinting import FingerprintingRules, InvalidFingerprintingConfig
@@ -127,6 +131,7 @@ class ProjectMemberSerializer(serializers.Serializer):
127131
"copy_from_project",
128132
"targetSampleRate",
129133
"dynamicSamplingBiases",
134+
"dynamicSamplingMinimumSampleRate",
130135
"tempestFetchScreenshots",
131136
"tempestFetchDumps",
132137
"autofixAutomationTuning",
@@ -220,6 +225,7 @@ class ProjectAdminSerializer(ProjectMemberSerializer):
220225
copy_from_project = serializers.IntegerField(required=False)
221226
targetSampleRate = serializers.FloatField(required=False, min_value=0, max_value=1)
222227
dynamicSamplingBiases = DynamicSamplingBiasSerializer(required=False, many=True)
228+
dynamicSamplingMinimumSampleRate = serializers.BooleanField(required=False)
223229
tempestFetchScreenshots = serializers.BooleanField(required=False)
224230
tempestFetchDumps = serializers.BooleanField(required=False)
225231
autofixAutomationTuning = serializers.ChoiceField(
@@ -428,6 +434,15 @@ def validate_targetSampleRate(self, value):
428434

429435
return value
430436

437+
def validate_dynamicSamplingMinimumSampleRate(self, value):
438+
organization = self.context["project"].organization
439+
actor = self.context["request"].user
440+
if not has_dynamic_sampling_minimum_sample_rate(organization, actor=actor):
441+
raise serializers.ValidationError(
442+
"Organization does not have the dynamic sampling minimum sample rate feature enabled."
443+
)
444+
return value
445+
431446
def validate_tempestFetchScreenshots(self, value):
432447
organization = self.context["project"].organization
433448
actor = self.context["request"].user
@@ -758,6 +773,14 @@ def put(self, request: Request, project) -> Response:
758773
changed_proj_settings["sentry:dynamic_sampling_biases"] = result[
759774
"dynamicSamplingBiases"
760775
]
776+
if result.get("dynamicSamplingMinimumSampleRate") is not None:
777+
if project.update_option(
778+
"sentry:dynamic_sampling_minimum_sample_rate",
779+
result["dynamicSamplingMinimumSampleRate"],
780+
):
781+
changed_proj_settings["sentry:dynamic_sampling_minimum_sample_rate"] = result[
782+
"dynamicSamplingMinimumSampleRate"
783+
]
761784

762785
if result.get("autofixAutomationTuning") is not None:
763786
if project.update_option(

src/sentry/api/serializers/models/project.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from sentry.dynamic_sampling.utils import (
2626
has_custom_dynamic_sampling,
2727
has_dynamic_sampling,
28+
has_dynamic_sampling_minimum_sample_rate,
2829
is_project_mode_sampling,
2930
)
3031
from sentry.eventstore.models import DEFAULT_SUBJECT_TEMPLATE
@@ -949,6 +950,7 @@ class DetailedProjectResponse(ProjectWithTeamResponseDict):
949950
relayPiiConfig: str | None
950951
builtinSymbolSources: list[str]
951952
dynamicSamplingBiases: list[dict[str, str | bool]]
953+
dynamicSamplingMinimumSampleRate: bool
952954
eventProcessing: dict[str, bool]
953955
symbolSources: str
954956
isDynamicallySampled: bool
@@ -1099,6 +1101,9 @@ def serialize(
10991101
"dynamicSamplingBiases": self.get_value_with_default(
11001102
attrs, "sentry:dynamic_sampling_biases"
11011103
),
1104+
"dynamicSamplingMinimumSampleRate": self.get_value_with_default(
1105+
attrs, "sentry:dynamic_sampling_minimum_sample_rate"
1106+
),
11021107
"eventProcessing": {
11031108
"symbolicationDegraded": False,
11041109
},
@@ -1118,6 +1123,11 @@ def serialize(
11181123
)
11191124
data["tempestFetchDumps"] = attrs["options"].get("sentry:tempest_fetch_dumps", False)
11201125

1126+
if has_dynamic_sampling_minimum_sample_rate(obj.organization, user):
1127+
data["dynamicSamplingMinimumSampleRate"] = bool(
1128+
obj.get_option("sentry:dynamic_sampling_minimum_sample_rate")
1129+
)
1130+
11211131
return data
11221132

11231133
def format_options(self, attrs: Mapping[str, Any]) -> dict[str, Any]:

src/sentry/apidocs/examples/project_examples.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
"filters:releases": "",
153153
"filters:error_messages": "",
154154
"feedback:branding": True,
155+
"sentry:dynamic_sampling_minimum_sample_rate": True,
155156
},
156157
"digestsMinDelay": 180,
157158
"digestsMaxDelay": 600,
@@ -259,6 +260,7 @@
259260
{"id": "boostReplayId", "active": True},
260261
{"id": "recalibrationRule", "active": True},
261262
],
263+
"dynamicSamplingMinimumSampleRate": True,
262264
"eventProcessing": {"symbolicationDegraded": False},
263265
"symbolSources": "[]",
264266
"tempestFetchScreenshots": False,

src/sentry/dynamic_sampling/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ def has_custom_dynamic_sampling(
2525
)
2626

2727

28+
def has_dynamic_sampling_minimum_sample_rate(
29+
organization: Organization | None, actor: User | RpcUser | AnonymousUser | None = None
30+
) -> bool:
31+
return (
32+
organization is not None
33+
and features.has(
34+
"organizations:dynamic-sampling-minimum-sample-rate", organization, actor=actor
35+
)
36+
and has_custom_dynamic_sampling(organization, actor=actor)
37+
)
38+
39+
2840
def is_project_mode_sampling(organization: Organization | None) -> bool:
2941
return (
3042
organization is not None

src/sentry/models/options/project_option.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"sentry:relay_pii_config",
6060
"sentry:dynamic_sampling",
6161
"sentry:dynamic_sampling_biases",
62+
"sentry:dynamic_sampling_minimum_sample_rate",
6263
"sentry:target_sample_rate",
6364
"sentry:tempest_fetch_screenshots",
6465
"sentry:tempest_fetch_dumps",

src/sentry/projectoptions/defaults.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@
193193
# Dynamic sampling rate in project-level "manual" configuration mode
194194
register(key="sentry:target_sample_rate", default=TARGET_SAMPLE_RATE_DEFAULT)
195195

196+
# Dynamic sampling minimum sample rate
197+
register(key="sentry:dynamic_sampling_minimum_sample_rate", default=False)
198+
196199
# Should tempest fetch screenshots for this project
197200
register(key="sentry:tempest_fetch_screenshots", default=False)
198201

tests/sentry/api/endpoints/test_project_details.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,6 +1874,130 @@ def test_put_new_dynamic_sampling_incorrect_rules_with_correct_flags(self):
18741874
"Error: Only 'id' and 'active' fields are allowed for bias."
18751875
]
18761876

1877+
@with_feature(
1878+
{
1879+
"organizations:dynamic-sampling-minimum-sample-rate": True,
1880+
"organizations:dynamic-sampling-custom": True,
1881+
}
1882+
)
1883+
def test_dynamic_sampling_minimum_sample_rate_with_feature(self):
1884+
"""Test setting and getting dynamicSamplingMinimumSampleRate with feature flag enabled"""
1885+
# Test setting to True
1886+
response = self.get_success_response(
1887+
self.organization.slug,
1888+
self.project.slug,
1889+
method="put",
1890+
dynamicSamplingMinimumSampleRate=True,
1891+
)
1892+
assert response.data["dynamicSamplingMinimumSampleRate"] is True
1893+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is True
1894+
1895+
# Test getting the field after setting it
1896+
get_response = self.get_success_response(
1897+
self.organization.slug, self.project.slug, method="get"
1898+
)
1899+
assert "dynamicSamplingMinimumSampleRate" in get_response.data
1900+
assert get_response.data["dynamicSamplingMinimumSampleRate"] is True
1901+
1902+
# Test setting to False
1903+
response = self.get_success_response(
1904+
self.organization.slug,
1905+
self.project.slug,
1906+
method="put",
1907+
dynamicSamplingMinimumSampleRate=False,
1908+
)
1909+
assert response.data["dynamicSamplingMinimumSampleRate"] is False
1910+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is False
1911+
1912+
# Test getting the field after setting it to False
1913+
get_response = self.get_success_response(
1914+
self.organization.slug, self.project.slug, method="get"
1915+
)
1916+
assert "dynamicSamplingMinimumSampleRate" in get_response.data
1917+
assert get_response.data["dynamicSamplingMinimumSampleRate"] is False
1918+
1919+
def test_dynamic_sampling_minimum_sample_rate_without_feature(self):
1920+
"""Test setting and getting dynamicSamplingMinimumSampleRate without feature flag"""
1921+
# Test setting the field without feature flag - should fail
1922+
self.get_error_response(
1923+
self.organization.slug,
1924+
self.project.slug,
1925+
method="put",
1926+
dynamicSamplingMinimumSampleRate=True,
1927+
status_code=400,
1928+
)
1929+
1930+
# Test that the field is not present in GET response without feature flag
1931+
get_response = self.get_success_response(
1932+
self.organization.slug, self.project.slug, method="get"
1933+
)
1934+
assert not get_response.data["dynamicSamplingMinimumSampleRate"]
1935+
1936+
@with_feature(
1937+
{
1938+
"organizations:dynamic-sampling-minimum-sample-rate": True,
1939+
"organizations:dynamic-sampling-custom": True,
1940+
}
1941+
)
1942+
def test_dynamic_sampling_minimum_sample_rate_validation(self):
1943+
"""Test validation of dynamicSamplingMinimumSampleRate parameter types"""
1944+
# Ensure initial state is False
1945+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is False
1946+
1947+
# Test with valid boolean value
1948+
response = self.get_success_response(
1949+
self.organization.slug,
1950+
self.project.slug,
1951+
method="put",
1952+
dynamicSamplingMinimumSampleRate=True,
1953+
)
1954+
assert response.data["dynamicSamplingMinimumSampleRate"] is True
1955+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is True
1956+
1957+
# Test with valid string value
1958+
response = self.get_success_response(
1959+
self.organization.slug,
1960+
self.project.slug,
1961+
method="put",
1962+
dynamicSamplingMinimumSampleRate="true",
1963+
)
1964+
assert response.data["dynamicSamplingMinimumSampleRate"] is True
1965+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is True
1966+
1967+
# Test with valid number value
1968+
response = self.get_success_response(
1969+
self.organization.slug,
1970+
self.project.slug,
1971+
method="put",
1972+
dynamicSamplingMinimumSampleRate=1,
1973+
)
1974+
assert response.data["dynamicSamplingMinimumSampleRate"] is True
1975+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is True
1976+
1977+
# Test with invalid float value
1978+
response = self.get_error_response(
1979+
self.organization.slug,
1980+
self.project.slug,
1981+
method="put",
1982+
dynamicSamplingMinimumSampleRate=0.5,
1983+
status_code=400,
1984+
)
1985+
assert "Must be a valid boolean." in response.data["dynamicSamplingMinimumSampleRate"][0]
1986+
# Ensure the project option wasn't changed by invalid request
1987+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is True
1988+
1989+
# Test with null/None value
1990+
response = self.get_error_response(
1991+
self.organization.slug,
1992+
self.project.slug,
1993+
method="put",
1994+
dynamicSamplingMinimumSampleRate=None,
1995+
status_code=400,
1996+
)
1997+
assert "This field may not be null." in response.data["dynamicSamplingMinimumSampleRate"][0]
1998+
# Ensure the project option wasn't changed by invalid request
1999+
assert self.project.get_option("sentry:dynamic_sampling_minimum_sample_rate") is True
2000+
18772001
@with_feature("organizations:tempest-access")
18782002
def test_put_tempest_fetch_screenshots(self):
18792003
# assert default value is False, and that put request updates the value

0 commit comments

Comments
 (0)