Skip to content

Commit ac0301e

Browse files
authored
feat(workflow): Add new experiment for issue alert fallback (#46124)
1 parent 4690914 commit ac0301e

File tree

6 files changed

+83
-6
lines changed

6 files changed

+83
-6
lines changed

src/sentry/conf/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,8 @@ def SOCIAL_AUTH_DEFAULT_USERNAME():
12071207
"organizations:invite-members": True,
12081208
# Enable rate limits for inviting members.
12091209
"organizations:invite-members-rate-limits": True,
1210+
# Test 10 member fallback vs 10 members
1211+
"organizations:issue-alert-fallback-experiment": False,
12101212
# Enable new issue alert "issue owners" fallback
12111213
"organizations:issue-alert-fallback-targeting": False,
12121214
# Enable removing issue from issue list if action taken.

src/sentry/features/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
default_manager.add("organizations:higher-ownership-limit", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
8383
default_manager.add("organizations:invite-members", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
8484
default_manager.add("organizations:invite-members-rate-limits", OrganizationFeature, FeatureHandlerStrategy.INTERNAL)
85+
default_manager.add("organizations:issue-alert-fallback-experiment", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
8586
default_manager.add("organizations:issue-alert-fallback-targeting", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
8687
default_manager.add("organizations:issue-alert-incompatible-rules", OrganizationFeature, FeatureHandlerStrategy.REMOTE)
8788
default_manager.add("organizations:issue-alert-preview", OrganizationFeature, FeatureHandlerStrategy.REMOTE)

src/sentry/mail/analytics.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class EmailNotificationSent(analytics.Event):
1212
analytics.Attribute("actor_id"),
1313
analytics.Attribute("user_id", required=False),
1414
analytics.Attribute("group_id", required=False),
15+
# Remove after IssueAlertFallbackExperiment
16+
analytics.Attribute("fallback_experiment", required=False),
1517
)
1618

1719

src/sentry/notifications/notifications/rules.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
has_alert_integration,
2727
has_integrations,
2828
)
29-
from sentry.notifications.utils.participants import get_owner_reason, get_send_to
29+
from sentry.notifications.utils.participants import (
30+
get_owner_reason,
31+
get_send_to,
32+
should_use_smaller_issue_alert_fallback,
33+
)
3034
from sentry.plugins.base.structs import Notification
3135
from sentry.services.hybrid_cloud.actor import ActorType, RpcActor
3236
from sentry.types.integrations import ExternalProviders
@@ -108,12 +112,19 @@ def get_context(self) -> MutableMapping[str, Any]:
108112
event=self.event,
109113
fallthrough_choice=self.fallthrough_choice,
110114
)
115+
fallback_params: MutableMapping[str, str] = {}
116+
# Piggybacking off of notification_reason that already determines if we're using the fallback
117+
if notification_reason and self.fallthrough_choice == FallthroughChoiceType.ACTIVE_MEMBERS:
118+
_, fallback_experiment = should_use_smaller_issue_alert_fallback(org=self.organization)
119+
fallback_params = {"ref_fallback": fallback_experiment}
111120

112121
context = {
113122
"project_label": self.project.get_full_name(),
114123
"group": self.group,
115124
"event": self.event,
116-
"link": get_group_settings_link(self.group, environment, rule_details),
125+
"link": get_group_settings_link(
126+
self.group, environment, rule_details, None, **fallback_params
127+
),
117128
"rules": rule_details,
118129
"has_integrations": has_integrations(self.organization, self.project),
119130
"enhanced_privacy": enhanced_privacy,
@@ -214,8 +225,10 @@ def send(self) -> None:
214225
notify(provider, self, participants, shared_context)
215226

216227
def get_log_params(self, recipient: RpcActor) -> Mapping[str, Any]:
228+
_, fallback_experiment = should_use_smaller_issue_alert_fallback(org=self.organization)
217229
return {
218230
"target_type": self.target_type,
219231
"target_identifier": self.target_identifier,
232+
"fallback_experiment": fallback_experiment,
220233
**super().get_log_params(recipient),
221234
}

src/sentry/notifications/utils/participants.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import TYPE_CHECKING, Any, Iterable, List, Mapping, MutableMapping, Sequence, Tuple
66

77
from sentry import features
8+
from sentry.experiments import manager as expt_manager
89
from sentry.models import (
910
ActorTuple,
1011
Group,
@@ -51,7 +52,8 @@
5152
ExternalProviders.SLACK,
5253
}
5354

54-
FALLTHROUGH_NOTIFICATION_LIMIT_EA = 20
55+
FALLTHROUGH_NOTIFICATION_LIMIT_EA = 10
56+
FALLTHROUGH_NOTIFICATION_LIMIT = 20
5557

5658

5759
class ParticipantMap:
@@ -385,6 +387,25 @@ def get_send_to(
385387
return get_recipients_by_provider(project, recipients, notification_type)
386388

387389

390+
def should_use_smaller_issue_alert_fallback(org: Organization) -> Tuple[bool, str]:
391+
"""
392+
Remove after IssueAlertFallbackExperiment experiment
393+
Returns a tuple of (enabled, analytics_label)
394+
"""
395+
if not org.flags.early_adopter.is_set:
396+
# Not early access, not in experiment
397+
return (False, "ga")
398+
399+
# Disabled
400+
if not features.has("organizations:issue-alert-fallback-experiment", org, actor=None):
401+
return (False, "disabled")
402+
403+
org_exposed = expt_manager.get("IssueAlertFallbackExperiment", org=org) == 1
404+
if org_exposed:
405+
return (True, "expt")
406+
return (False, "ctrl")
407+
408+
388409
def get_fallthrough_recipients(
389410
project: Project, fallthrough_choice: FallthroughChoiceType | None
390411
) -> Iterable[RpcUser]:
@@ -408,13 +429,19 @@ def get_fallthrough_recipients(
408429
)
409430

410431
elif fallthrough_choice == FallthroughChoiceType.ACTIVE_MEMBERS:
432+
use_smaller_limit, _ = should_use_smaller_issue_alert_fallback(org=project.organization)
433+
limit = (
434+
FALLTHROUGH_NOTIFICATION_LIMIT_EA
435+
if use_smaller_limit
436+
else FALLTHROUGH_NOTIFICATION_LIMIT
437+
)
411438
return user_service.get_many(
412439
filter={
413440
"user_ids": project.member_set.order_by("-user__last_active").values_list(
414441
"user_id", flat=True
415442
)
416443
}
417-
)[:FALLTHROUGH_NOTIFICATION_LIMIT_EA]
444+
)[:limit]
418445

419446
raise NotImplementedError(f"Unknown fallthrough choice: {fallthrough_choice}")
420447

tests/sentry/notifications/utils/test_participants.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Iterable, Mapping, Optional, Sequence, Set, Union
2+
from unittest import mock
23

34
import pytest
45

@@ -15,6 +16,7 @@
1516
NotificationSettingTypes,
1617
)
1718
from sentry.notifications.utils.participants import (
19+
FALLTHROUGH_NOTIFICATION_LIMIT,
1820
FALLTHROUGH_NOTIFICATION_LIMIT_EA,
1921
get_fallthrough_recipients,
2022
get_owner_reason,
@@ -902,7 +904,7 @@ def test_fallthrough_admin_or_recent_under_20(self):
902904
@with_feature("organizations:issue-alert-fallback-targeting")
903905
def test_fallthrough_admin_or_recent_over_20(self):
904906
notifiable_users = [self.user, self.user2]
905-
for i in range(FALLTHROUGH_NOTIFICATION_LIMIT_EA + 5):
907+
for i in range(FALLTHROUGH_NOTIFICATION_LIMIT + 5):
906908
new_user = self.create_user(email=f"user_{i}@example.com", is_active=True)
907909
self.create_member(
908910
user=new_user, organization=self.organization, role="owner", teams=[self.team2]
@@ -923,5 +925,35 @@ def test_fallthrough_admin_or_recent_over_20(self):
923925
event, self.project, FallthroughChoiceType.ACTIVE_MEMBERS
924926
)[ExternalProviders.EMAIL]
925927

926-
assert len(notified_users) == FALLTHROUGH_NOTIFICATION_LIMIT_EA
928+
assert len(notified_users) == FALLTHROUGH_NOTIFICATION_LIMIT
927929
assert notified_users.issubset(expected_notified_users)
930+
931+
@mock.patch("sentry.experiments.manager.get", return_value=1)
932+
@with_feature("organizations:issue-alert-fallback-targeting")
933+
@with_feature("organizations:issue-alert-fallback-experiment")
934+
def test_fallthrough_ea_experiment_limit(self, mock_func):
935+
self.organization.flags.early_adopter = True
936+
937+
notifiable_users = [self.user, self.user2]
938+
for i in range(FALLTHROUGH_NOTIFICATION_LIMIT_EA + 5):
939+
new_user = self.create_user(email=f"user_{i}@example.com", is_active=True)
940+
self.create_member(
941+
user=new_user, organization=self.organization, role="owner", teams=[self.team2]
942+
)
943+
notifiable_users.append(new_user)
944+
945+
for user in notifiable_users:
946+
NotificationSetting.objects.update_settings(
947+
ExternalProviders.SLACK,
948+
NotificationSettingTypes.ISSUE_ALERTS,
949+
NotificationSettingOptionValues.NEVER,
950+
user=user,
951+
)
952+
953+
event = self.store_event("admin.lol", self.project)
954+
notified_users = self.get_send_to_fallthrough(
955+
event, self.project, FallthroughChoiceType.ACTIVE_MEMBERS
956+
)[ExternalProviders.EMAIL]
957+
958+
# Check that we notify all possible folks with the EA experiment limit.
959+
assert len(notified_users) == FALLTHROUGH_NOTIFICATION_LIMIT_EA

0 commit comments

Comments
 (0)