Skip to content
22 changes: 22 additions & 0 deletions src/sentry/api/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
SAFE_FIELDS_DEFAULT,
SCRAPE_JAVASCRIPT_DEFAULT,
SENSITIVE_FIELDS_DEFAULT,
TARGET_SAMPLE_RATE_DEFAULT,
UPTIME_AUTODETECTION,
)
from sentry.datascrubbing import validate_pii_config_update, validate_pii_selectors
Expand Down Expand Up @@ -215,6 +216,7 @@
METRICS_ACTIVATE_LAST_FOR_GAUGES_DEFAULT,
),
("uptimeAutodetection", "sentry:uptime_autodetection", bool, UPTIME_AUTODETECTION),
("targetSampleRate", "sentry:target_sample_rate", float, TARGET_SAMPLE_RATE_DEFAULT),
)

DELETION_STATUSES = frozenset(
Expand Down Expand Up @@ -276,6 +278,7 @@ class OrganizationSerializer(BaseOrganizationSerializer):
relayPiiConfig = serializers.CharField(required=False, allow_blank=True, allow_null=True)
apdexThreshold = serializers.IntegerField(min_value=1, required=False)
uptimeAutodetection = serializers.BooleanField(required=False)
targetSampleRate = serializers.FloatField(required=False)

@cached_property
def _has_legacy_rate_limits(self):
Expand Down Expand Up @@ -365,6 +368,25 @@ def validate_projectRateLimit(self, value):
)
return value

def validate_targetSampleRate(self, value):
from sentry import features

organization = self.context["organization"]
request = self.context["request"]
has_dynamic_sampling_custom = features.has(
"organizations:dynamic-sampling-custom", organization, actor=request.user
)
if not has_dynamic_sampling_custom:
raise serializers.ValidationError(
"Organization does not have the custom dynamic sample rate feature enabled."
)

if not 0.0 <= value <= 1.0:
raise serializers.ValidationError(
"The targetSampleRate option must be in the range [0:1]"
)
return value

def validate(self, attrs):
attrs = super().validate(attrs)
if attrs.get("avatarType") == "upload":
Expand Down
1 change: 1 addition & 0 deletions src/sentry/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ class InsightModules(Enum):
METRICS_ACTIVATE_LAST_FOR_GAUGES_DEFAULT = False
DATA_CONSENT_DEFAULT = False
UPTIME_AUTODETECTION = True
TARGET_SAMPLE_RATE_DEFAULT = 1.0

# `sentry:events_member_admin` - controls whether the 'member' role gets the event:admin scope
EVENTS_MEMBER_ADMIN_DEFAULT = True
Expand Down
33 changes: 32 additions & 1 deletion tests/sentry/api/endpoints/test_organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ def test_upload_avatar(self):
"sentry.integrations.github.integration.GitHubApiClient.get_repositories",
return_value=[{"name": "cool-repo", "full_name": "testgit/cool-repo"}],
)
@with_feature("organizations:codecov-integration")
@with_feature(["organizations:codecov-integration", "organizations:dynamic-sampling-custom"])
def test_various_options(self, mock_get_repositories):
initial = self.organization.get_audit_log_data()
with assume_test_silo_mode_of(AuditLogEntry):
Expand Down Expand Up @@ -455,6 +455,7 @@ def test_various_options(self, mock_get_repositories):
"metricsActivatePercentiles": False,
"metricsActivateLastForGauges": True,
"uptimeAutodetection": False,
"targetSampleRate": 0.1,
}

# needed to set require2FA
Expand Down Expand Up @@ -493,6 +494,7 @@ def test_various_options(self, mock_get_repositories):
assert options.get("sentry:metrics_activate_percentiles") is False
assert options.get("sentry:metrics_activate_last_for_gauges") is True
assert options.get("sentry:uptime_autodetection") is False
assert options.get("sentry:target_sample_rate") == 0.1

# log created
with assume_test_silo_mode_of(AuditLogEntry):
Expand Down Expand Up @@ -940,6 +942,35 @@ def test_org_mapping_already_taken(self):
self.create_organization(slug="taken")
self.get_error_response(self.organization.slug, slug="taken", status_code=400)

def test_target_sample_rate_feature(self):
with self.feature("organizations:dynamic-sampling-custom"):
data = {"targetSampleRate": 0.1}
self.get_success_response(self.organization.slug, **data)

with self.feature({"organizations:dynamic-sampling-custom": False}):
data = {"targetSampleRate": 0.1}
self.get_error_response(self.organization.slug, status_code=400, **data)

@with_feature("organizations:dynamic-sampling-custom")
def test_target_sample_rate_range(self):
# low, within and high
data = {"targetSampleRate": 0.0}
self.get_success_response(self.organization.slug, **data)

data = {"targetSampleRate": 0.1}
self.get_success_response(self.organization.slug, **data)

data = {"targetSampleRate": 1.0}
self.get_success_response(self.organization.slug, **data)

# below range
data = {"targetSampleRate": -0.1}
self.get_error_response(self.organization.slug, status_code=400, **data)

# above range
data = {"targetSampleRate": 1.1}
self.get_error_response(self.organization.slug, status_code=400, **data)


class OrganizationDeleteTest(OrganizationDetailsTestBase):
method = "delete"
Expand Down
Loading