Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(streamline): Add an option to remove opt-out for new users #83205

Merged
merged 7 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 63 additions & 58 deletions src/sentry/api/endpoints/organization_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from sentry.auth.superuser import is_active_superuser
from sentry.db.models.query import in_iexact
from sentry.hybridcloud.rpc import IDEMPOTENCY_KEY_LENGTH
from sentry.issues.streamline import apply_streamline_rollout_group
from sentry.models.organization import Organization, OrganizationStatus
from sentry.models.organizationmember import OrganizationMember
from sentry.models.projectplatform import ProjectPlatform
Expand Down Expand Up @@ -238,70 +239,74 @@ def post(self, request: Request) -> Response:

serializer = OrganizationPostSerializer(data=request.data)

if serializer.is_valid():
result = serializer.validated_data

try:
create_default_team = bool(result.get("defaultTeam"))
provision_args = OrganizationProvisioningOptions(
provision_options=OrganizationOptions(
name=result["name"],
slug=result.get("slug") or result["name"],
owning_user_id=request.user.id,
create_default_team=create_default_team,
),
post_provision_options=PostProvisionOptions(
getsentry_options=None, sentry_options=None
),
)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

result = serializer.validated_data

try:
create_default_team = bool(result.get("defaultTeam"))
provision_args = OrganizationProvisioningOptions(
provision_options=OrganizationOptions(
name=result["name"],
slug=result.get("slug") or result["name"],
owning_user_id=request.user.id,
create_default_team=create_default_team,
),
post_provision_options=PostProvisionOptions(
getsentry_options=None, sentry_options=None
),
)

rpc_org = organization_provisioning_service.provision_organization_in_region(
region_name=settings.SENTRY_REGION or settings.SENTRY_MONOLITH_REGION,
provisioning_options=provision_args,
)
org = Organization.objects.get(id=rpc_org.id)
rpc_org = organization_provisioning_service.provision_organization_in_region(
region_name=settings.SENTRY_REGION or settings.SENTRY_MONOLITH_REGION,
provisioning_options=provision_args,
)
org = Organization.objects.get(id=rpc_org.id)

org_setup_complete.send_robust(
instance=org, user=request.user, sender=self.__class__, referrer="in-app"
)
org_setup_complete.send_robust(
instance=org, user=request.user, sender=self.__class__, referrer="in-app"
)

self.create_audit_entry(
request=request,
organization=org,
target_object=org.id,
event=audit_log.get_event_id("ORG_ADD"),
data=org.get_audit_log_data(),
)
self.create_audit_entry(
request=request,
organization=org,
target_object=org.id,
event=audit_log.get_event_id("ORG_ADD"),
data=org.get_audit_log_data(),
)

analytics.record(
"organization.created",
org,
actor_id=request.user.id if request.user.is_authenticated else None,
)
analytics.record(
"organization.created",
org,
actor_id=request.user.id if request.user.is_authenticated else None,
)

# TODO(hybrid-cloud): We'll need to catch a more generic error
# when the internal RPC is implemented.
except IntegrityError:
return Response(
{"detail": "An organization with this slug already exists."}, status=409
)
# TODO(hybrid-cloud): We'll need to catch a more generic error
# when the internal RPC is implemented.
except IntegrityError:
return Response(
{"detail": "An organization with this slug already exists."}, status=409
)

# failure on sending this signal is acceptable
if result.get("agreeTerms"):
terms_accepted.send_robust(
user=request.user,
organization_id=org.id,
ip_address=request.META["REMOTE_ADDR"],
sender=type(self),
)
# failure on sending this signal is acceptable
if result.get("agreeTerms"):
terms_accepted.send_robust(
user=request.user,
organization_id=org.id,
ip_address=request.META["REMOTE_ADDR"],
sender=type(self),
)

if result.get("aggregatedDataConsent"):
org.update_option("sentry:aggregated_data_consent", True)
if result.get("aggregatedDataConsent"):
org.update_option("sentry:aggregated_data_consent", True)

analytics.record(
"aggregated_data_consent.organization_created",
organization_id=org.id,
)
analytics.record(
"aggregated_data_consent.organization_created",
organization_id=org.id,
)

return Response(serialize(org, request.user), status=201)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
apply_streamline_rollout_group(organization=org)
# If unset, the organization's users can opt-in/out.
# If true, they only receive the Streamline UI. If false, they only receive the Legacy UI.
return Response(serialize(org, request.user), status=201)
3 changes: 3 additions & 0 deletions src/sentry/api/serializers/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
SAMPLING_MODE_DEFAULT,
SCRAPE_JAVASCRIPT_DEFAULT,
SENSITIVE_FIELDS_DEFAULT,
STREAMLINE_UI_ONLY,
TARGET_SAMPLE_RATE_DEFAULT,
UPTIME_AUTODETECTION,
ObjectStatus,
Expand Down Expand Up @@ -494,6 +495,7 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp
metricsActivateLastForGauges: bool
requiresSso: bool
rollbackEnabled: bool
streamlineOnly: bool
leeandher marked this conversation as resolved.
Show resolved Hide resolved


class DetailedOrganizationSerializer(OrganizationSerializer):
Expand Down Expand Up @@ -635,6 +637,7 @@ def serialize( # type: ignore[explicit-override, override]
"rollbackEnabled": bool(
obj.get_option("sentry:rollback_enabled", ROLLBACK_ENABLED_DEFAULT)
),
"streamlineOnly": obj.get_option("sentry:streamline_ui_only", STREAMLINE_UI_ONLY),
}
)

Expand Down
1 change: 1 addition & 0 deletions src/sentry/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ class InsightModules(Enum):
TARGET_SAMPLE_RATE_DEFAULT = 1.0
SAMPLING_MODE_DEFAULT = "organization"
ROLLBACK_ENABLED_DEFAULT = True
STREAMLINE_UI_ONLY = None

# `sentry:events_member_admin` - controls whether the 'member' role gets the event:admin scope
EVENTS_MEMBER_ADMIN_DEFAULT = True
Expand Down
14 changes: 14 additions & 0 deletions src/sentry/issues/streamline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sentry.models.group import Organization
from sentry.utils.options import sample_modulo


def apply_streamline_rollout_group(organization: Organization):
# If unset, the organization's users can opt-in/out.
if organization.get_option("sentry:streamline_ui_only") is not None:
leeandher marked this conversation as resolved.
Show resolved Hide resolved
return

result = sample_modulo("issues.details.streamline_rollout_rate", organization.id)
# If result is true, they only receive the Streamline UI.
# If result is false, they only receive the Legacy UI.
# This behaviour is controlled on the frontend.
organization.update_option("sentry:streamline_ui_only", result)
8 changes: 8 additions & 0 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,14 @@
)


register(
"issues.details.streamline_rollout_rate",
type=Float,
default=0.5,
flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
)


# Killswitch for issue priority
register(
"issues.priority.enabled",
Expand Down
15 changes: 15 additions & 0 deletions tests/sentry/api/endpoints/test_organization_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from sentry.silo.base import SiloMode
from sentry.slug.patterns import ORG_SLUG_PATTERN
from sentry.testutils.cases import APITestCase, TwoFactorAPITestCase
from sentry.testutils.helpers import override_options
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
from sentry.testutils.silo import assume_test_silo_mode, create_test_regions, region_silo_test
from sentry.users.models.authenticator import Authenticator
Expand Down Expand Up @@ -316,6 +317,20 @@ def test_data_consent(self):
assert org.name == data["name"]
assert OrganizationOption.objects.get_value(org, "sentry:aggregated_data_consent") is True

@override_options({"issues.details.streamline_rollout_rate": 1.0})
def test_streamline_only_is_true(self):
self.login_as(user=self.user)
response = self.get_success_response(name="acme")
organization = Organization.objects.get(id=response.data["id"])
assert OrganizationOption.objects.get_value(organization, "sentry:streamline_ui_only")

@override_options({"issues.details.streamline_rollout_rate": 0})
def test_streamline_only_is_false(self):
self.login_as(user=self.user)
response = self.get_success_response(name="acme")
organization = Organization.objects.get(id=response.data["id"])
assert not OrganizationOption.objects.get_value(organization, "sentry:streamline_ui_only")


@region_silo_test(regions=create_test_regions("de", "us"))
class OrganizationsCreateInRegionTest(OrganizationIndexTest, HybridCloudTestMixin):
Expand Down
Loading