Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def register_temporary_features(manager: FeatureManager) -> None:
# Enable processing activity updates in workflow engine
manager.add("organizations:workflow-engine-process-activity", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable dual writing for issue alert issues (see: alerts create issues)
manager.add("organizations:workflow-engine-issue-alert-dual-write", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
manager.add("organizations:workflow-engine-issue-alert-dual-write", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False, default=True)
# Enable workflow processing for metric issues
manager.add("organizations:workflow-engine-process-metric-issue-workflows", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False, default=True)
# Enable single processing through workflow engine for issue alerts
Expand Down
10 changes: 9 additions & 1 deletion src/sentry/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from django.utils.translation import gettext_lazy as _

from bitfield import TypedClassBitField
from sentry.backup.dependencies import PrimaryKeyMap
from sentry.backup.dependencies import ImportKind, PrimaryKeyMap
from sentry.backup.helpers import ImportFlags
from sentry.backup.scopes import ImportScope, RelocationScope
from sentry.constants import PROJECT_SLUG_MAX_LENGTH, RESERVED_PROJECT_SLUGS, ObjectStatus
Expand Down Expand Up @@ -788,6 +788,14 @@ def normalize_before_relocation_import(

return old_pk

def write_relocation_import(
self, scope: ImportScope, flags: ImportFlags
) -> tuple[int, ImportKind] | None:
from sentry.receivers.project_detectors import disable_default_detector_creation

with disable_default_detector_creation():
return super().write_relocation_import(scope, flags)

# pending deletion implementation
_pending_fields = ("slug",)

Expand Down
26 changes: 9 additions & 17 deletions src/sentry/projects/project_rules/creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from django.db import router, transaction
from rest_framework.request import Request

from sentry import features
from sentry.models.project import Project
from sentry.models.rule import Rule, RuleSource
from sentry.types.actor import Actor
Expand All @@ -31,26 +30,19 @@ class ProjectRuleCreator:
request: Request | None = None

def run(self) -> Rule:
if features.has(
"organizations:workflow-engine-issue-alert-dual-write", self.project.organization
):
ensure_default_detectors(self.project)
ensure_default_detectors(self.project)

with transaction.atomic(router.db_for_write(Rule)):
self.rule = self._create_rule()

if features.has(
"organizations:workflow-engine-issue-alert-dual-write",
self.project.organization,
):
# uncaught errors will rollback the transaction
workflow = IssueAlertMigrator(
self.rule, self.request.user.id if self.request else None
).run()
logger.info(
"workflow_engine.issue_alert.migrated",
extra={"rule_id": self.rule.id, "workflow_id": workflow.id},
)
# uncaught errors will rollback the transaction
workflow = IssueAlertMigrator(
self.rule, self.request.user.id if self.request else None
).run()
logger.info(
"workflow_engine.issue_alert.migrated",
extra={"rule_id": self.rule.id, "workflow_id": workflow.id},
)

return self.rule

Expand Down
18 changes: 7 additions & 11 deletions src/sentry/projects/project_rules/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from django.db import router, transaction
from rest_framework.request import Request

from sentry import features
from sentry.models.project import Project
from sentry.models.rule import Rule
from sentry.types.actor import Actor
Expand Down Expand Up @@ -43,16 +42,13 @@ def run(self) -> Rule:
self._update_frequency()
self.rule.save()

if features.has(
"organizations:workflow-engine-issue-alert-dual-write", self.project.organization
):
# uncaught errors will rollback the transaction
workflow = update_migrated_issue_alert(self.rule)
if workflow:
logger.info(
"workflow_engine.issue_alert.updated",
extra={"rule_id": self.rule.id, "workflow_id": workflow.id},
)
# uncaught errors will rollback the transaction
workflow = update_migrated_issue_alert(self.rule)
if workflow:
logger.info(
"workflow_engine.issue_alert.updated",
extra={"rule_id": self.rule.id, "workflow_id": workflow.id},
)
return self.rule

def _update_name(self) -> None:
Expand Down
30 changes: 25 additions & 5 deletions src/sentry/receivers/project_detectors.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from contextlib import contextmanager

import sentry_sdk
from django.db.models.signals import post_save

from sentry import features
from sentry.models.project import Project
from sentry.workflow_engine.processors.detector import (
UnableToAcquireLockApiError,
Expand All @@ -13,13 +13,33 @@
logger = logging.getLogger(__name__)


@contextmanager
def disable_default_detector_creation():
"""
Context manager that temporarily disconnects the create_project_detectors
signal handler, preventing default detectors from being created when a
project is saved.
"""
# Disconnect the signal
post_save.disconnect(
create_project_detectors, sender=Project, dispatch_uid="create_project_detectors"
)
try:
yield
finally:
# Always reconnect the signal, even if an exception occurred
post_save.connect(
create_project_detectors,
sender=Project,
dispatch_uid="create_project_detectors",
weak=False,
)


def create_project_detectors(instance, created, **kwargs):
if created:
try:
if features.has(
"organizations:workflow-engine-issue-alert-dual-write", instance.organization
):
ensure_default_detectors(instance)
ensure_default_detectors(instance)
except UnableToAcquireLockApiError as e:
sentry_sdk.capture_exception(e)

Expand Down
14 changes: 5 additions & 9 deletions src/sentry/receivers/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from django.db import router, transaction

from sentry import features
from sentry.models.project import Project
from sentry.models.rule import Rule
from sentry.notifications.types import FallthroughChoiceType
Expand Down Expand Up @@ -44,14 +43,11 @@ def create_default_rules(project: Project, default_rules=True, RuleModel=Rule, *
with transaction.atomic(router.db_for_write(RuleModel)):
rule = RuleModel.objects.create(project=project, label=DEFAULT_RULE_LABEL, data=rule_data)

if features.has(
"organizations:workflow-engine-issue-alert-dual-write", project.organization
):
workflow = IssueAlertMigrator(rule).run()
logger.info(
"workflow_engine.default_issue_alert.migrated",
extra={"rule_id": rule.id, "workflow_id": workflow.id},
)
workflow = IssueAlertMigrator(rule).run()
logger.info(
"workflow_engine.default_issue_alert.migrated",
extra={"rule_id": rule.id, "workflow_id": workflow.id},
)

try:
user: RpcUser = project.organization.get_default_owner()
Expand Down
42 changes: 32 additions & 10 deletions src/sentry/testutils/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from sentry.constants import SentryAppInstallationStatus, SentryAppStatus
from sentry.data_secrecy.models.data_access_grant import DataAccessGrant
from sentry.event_manager import EventManager
from sentry.grouping.grouptype import ErrorGroupType
from sentry.hybridcloud.models.outbox import RegionOutbox, outbox_context
from sentry.hybridcloud.models.webhookpayload import WebhookPayload
from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope
Expand Down Expand Up @@ -188,6 +189,7 @@
)
from sentry.workflow_engine.models.detector_group import DetectorGroup
from sentry.workflow_engine.registry import data_source_type_registry
from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType
from social_auth.models import UserSocialAuth


Expand Down Expand Up @@ -539,24 +541,36 @@ def create_environment(project, **kwargs):
@staticmethod
@assume_test_silo_mode(SiloMode.REGION)
def create_project(
organization=None, teams=None, fire_project_created=False, **kwargs
organization=None,
teams=None,
fire_project_created=False,
create_default_detectors=False,
**kwargs,
) -> Project:
from sentry.receivers.project_detectors import disable_default_detector_creation

if not kwargs.get("name"):
kwargs["name"] = petname.generate(2, " ", letters=10).title()
if not kwargs.get("slug"):
kwargs["slug"] = slugify(str(kwargs["name"]))
if not organization and teams:
organization = teams[0].organization

with transaction.atomic(router.db_for_write(Project)):
project = Project.objects.create(organization=organization, **kwargs)
if teams:
for team in teams:
project.add_team(team)
if fire_project_created:
project_created.send(
project=project, user=AnonymousUser(), default_rules=True, sender=Factories
)
with (
disable_default_detector_creation()
if not create_default_detectors
else contextlib.nullcontext()
):
with transaction.atomic(router.db_for_write(Project)):
project = Project.objects.create(organization=organization, **kwargs)
if teams:
for team in teams:
project.add_team(team)
if fire_project_created:
project_created.send(
project=project, user=AnonymousUser(), default_rules=True, sender=Factories
)

return project

@staticmethod
Expand Down Expand Up @@ -2271,6 +2285,14 @@ def create_detector(
name = petname.generate(2, " ", letters=10).title()
if config is None:
config = default_detector_config_data.get(kwargs["type"], {})
if kwargs.get("type") in (ErrorGroupType.slug, IssueStreamGroupType.slug):
detector, _ = Detector.objects.get_or_create(
type=kwargs["type"],
project=kwargs["project"],
defaults={"config": {}, "name": name},
)
detector.update(config=config, name=name, **kwargs)
return detector

return Detector.objects.create(
name=name,
Expand Down
10 changes: 4 additions & 6 deletions src/sentry/testutils/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,7 @@ def team(self):

@cached_property
def project(self):
return self.create_project(
name="Bar", slug="bar", teams=[self.team], fire_project_created=True
)
return self.create_project(name="Bar", slug="bar", teams=[self.team])

@cached_property
def release(self):
Expand Down Expand Up @@ -696,15 +694,15 @@ def create_data_condition(

def create_detector(
self,
project: Project | None = None,
type: str | None = ErrorGroupType.slug,
*args,
project=None,
type=ErrorGroupType.slug,
**kwargs,
) -> Detector:
if project is None:
project = self.create_project(organization=self.organization)

return Factories.create_detector(*args, project=project, type=type, **kwargs)
return Factories.create_detector(project=project, type=type, *args, **kwargs)

def create_detector_state(self, *args, **kwargs) -> DetectorState:
return Factories.create_detector_state(*args, **kwargs)
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/testutils/helpers/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
ExploreSavedQueryProject,
ExploreSavedQueryStarred,
)
from sentry.incidents.grouptype import MetricIssue
from sentry.incidents.models.incident import IncidentActivity, IncidentTrigger
from sentry.insights.models import InsightsStarredSegment
from sentry.integrations.models.data_forwarder import DataForwarder
Expand Down Expand Up @@ -677,7 +678,7 @@ def create_exhaustive_organization(

# Setup a test 'Issue Rule' and 'Automation'
workflow = self.create_workflow(organization=org)
detector = self.create_detector(project=project)
detector = self.create_detector(project=project, type=MetricIssue.slug)
self.create_detector_workflow(detector=detector, workflow=workflow)
self.create_detector_state(detector=detector)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def _connect_default_detectors(self, workflow: Workflow) -> None:
default_detectors = self._create_detector_lookups()
for detector in default_detectors:
if detector:
DetectorWorkflow.objects.create(detector=detector, workflow=workflow)
DetectorWorkflow.objects.get_or_create(detector=detector, workflow=workflow)

def _bulk_create_data_conditions(
self,
Expand Down
1 change: 1 addition & 0 deletions tests/acceptance/test_organization_alert_rule_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class OrganizationAlertRuleDetailsTest(AcceptanceTestCase, SnubaTestCase):
def setUp(self) -> None:
super().setUp()
self.login_as(self.user)
self.project = self.create_project(fire_project_created=True)
self.rule = Rule.objects.get(project=self.project)
self.path = f"/organizations/{self.organization.slug}/alerts/rules/{self.project.slug}/{self.rule.id}/details/"

Expand Down
2 changes: 0 additions & 2 deletions tests/sentry/api/endpoints/test_project_rule_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from sentry.testutils.helpers import install_slack
from sentry.testutils.helpers.analytics import assert_any_analytics_event
from sentry.testutils.helpers.datetime import freeze_time
from sentry.testutils.helpers.features import with_feature
from sentry.testutils.silo import assume_test_silo_mode
from sentry.types.actor import Actor
from sentry.workflow_engine.migration_helpers.issue_alert_migration import IssueAlertMigrator
Expand Down Expand Up @@ -1450,7 +1449,6 @@ def test_simple(self) -> None:
id=self.rule.id, project=self.project, status=ObjectStatus.PENDING_DELETION
).exists()

@with_feature("organizations:workflow-engine-issue-alert-dual-write")
def test_dual_delete_workflow_engine(self) -> None:
rule = self.create_project_rule(
self.project,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ProjectRuleTaskDetailsTest(APITestCase):
def setUp(self) -> None:
super().setUp()
self.login_as(user=self.user)

self.project = self.create_project(fire_project_created=True)
self.rule = self.project.rule_set.all()[0]
self.uuid = uuid4().hex

Expand Down
5 changes: 5 additions & 0 deletions tests/sentry/digests/backends/test_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
from sentry.digests.backends.base import InvalidState
from sentry.digests.backends.redis import RedisBackend
from sentry.digests.types import Notification, Record
from sentry.models.project import Project
from sentry.testutils.cases import TestCase


class RedisBackendTestCase(TestCase):
@cached_property
def project(self) -> Project:
return self.create_project(fire_project_created=True)

@cached_property
def notification(self) -> Notification:
rule = self.event.project.rule_set.all()[0]
Expand Down
Loading
Loading