diff --git a/fixtures/backup/model_dependencies/detailed.json b/fixtures/backup/model_dependencies/detailed.json index 5c9ef066ce257..b4cf567d9e76b 100644 --- a/fixtures/backup/model_dependencies/detailed.json +++ b/fixtures/backup/model_dependencies/detailed.json @@ -6218,6 +6218,39 @@ "table_name": "workflow_engine_datasource", "uniques": [] }, + "workflow_engine.detector": { + "dangling": false, + "foreign_keys": { + "organization": { + "kind": "FlexibleForeignKey", + "model": "sentry.organization", + "nullable": false + }, + "owner_team": { + "kind": "FlexibleForeignKey", + "model": "sentry.team", + "nullable": true + }, + "owner_user_id": { + "kind": "HybridCloudForeignKey", + "model": "sentry.user", + "nullable": true + } + }, + "model": "workflow_engine.detector", + "relocation_dependencies": [], + "relocation_scope": "Organization", + "silos": [ + "Region" + ], + "table_name": "workflow_engine_detector", + "uniques": [ + [ + "name", + "organization" + ] + ] + }, "workflow_engine.workflow": { "dangling": false, "foreign_keys": { diff --git a/fixtures/backup/model_dependencies/flat.json b/fixtures/backup/model_dependencies/flat.json index 565a7b2511a32..d3ebeea510f3d 100644 --- a/fixtures/backup/model_dependencies/flat.json +++ b/fixtures/backup/model_dependencies/flat.json @@ -854,6 +854,11 @@ "workflow_engine.datasource": [ "sentry.organization" ], + "workflow_engine.detector": [ + "sentry.organization", + "sentry.team", + "sentry.user" + ], "workflow_engine.workflow": [ "sentry.organization" ] diff --git a/fixtures/backup/model_dependencies/sorted.json b/fixtures/backup/model_dependencies/sorted.json index 27ee4dcbc104b..c44da7830e751 100644 --- a/fixtures/backup/model_dependencies/sorted.json +++ b/fixtures/backup/model_dependencies/sorted.json @@ -50,6 +50,7 @@ "social_auth.usersocialauth", "uptime.uptimesubscription", "workflow_engine.datasource", + "workflow_engine.detector", "workflow_engine.workflow", "sentry.savedsearch", "sentry.relocation", diff --git a/fixtures/backup/model_dependencies/truncate.json b/fixtures/backup/model_dependencies/truncate.json index 0efd005f723e1..79fd55dab93ab 100644 --- a/fixtures/backup/model_dependencies/truncate.json +++ b/fixtures/backup/model_dependencies/truncate.json @@ -50,6 +50,7 @@ "social_auth_usersocialauth", "uptime_uptimesubscription", "workflow_engine_datasource", + "workflow_engine_detector", "workflow_engine_workflow", "sentry_savedsearch", "sentry_relocation", diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 2bdda8ac814fb..2c268d78fb8fe 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -13,4 +13,4 @@ replays: 0004_index_together sentry: 0759_remove_spanattributeextraction_tables social_auth: 0002_default_auto_field uptime: 0008_uptime_url_suffix -workflow_engine: 0002_data_source +workflow_engine: 0003_detector diff --git a/src/sentry/backup/comparators.py b/src/sentry/backup/comparators.py index 52e6ddaa5590a..a17e2a6fbb423 100644 --- a/src/sentry/backup/comparators.py +++ b/src/sentry/backup/comparators.py @@ -866,6 +866,7 @@ def get_default_comparators() -> dict[str, list[JSONScrubbingComparator]]: "sentry.userrole": [DateUpdatedComparator("date_updated")], "sentry.userroleuser": [DateUpdatedComparator("date_updated")], "workflow_engine.datasource": [DateUpdatedComparator("date_updated", "date_added")], + "workflow_engine.detector": [DateUpdatedComparator("date_updated", "date_added")], "workflow_engine.workflow": [DateUpdatedComparator("date_updated", "date_added")], }, ) diff --git a/src/sentry/models/owner_base.py b/src/sentry/models/owner_base.py new file mode 100644 index 0000000000000..f1c117eeb6e72 --- /dev/null +++ b/src/sentry/models/owner_base.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from django.conf import settings +from django.db import models +from django.db.models import BaseConstraint + +from sentry.db.models import FlexibleForeignKey, Model +from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey +from sentry.types.actor import Actor + + +class OwnerModel(Model): + """ + A base model that adds ownership fields to existing models. + """ + + owner_user_id = HybridCloudForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete="SET_NULL") + owner_team = FlexibleForeignKey("sentry.Team", null=True, on_delete=models.SET_NULL) + + class Meta: + abstract = True + + constraints: list[BaseConstraint] = [ + models.CheckConstraint( + condition=( + models.Q(owner_user_id__isnull=True, owner_team__isnull=False) + | models.Q(owner_user_id__isnull=False, owner_team__isnull=True) + | models.Q(owner_user_id__isnull=True, owner_team__isnull=True) + ), + name="%(app_label)s_%(class)s_owner_constraints", + ), + ] + + @property + def owner(self) -> Actor | None: + return Actor.from_id(user_id=self.owner_user_id, team_id=self.owner_team_id) diff --git a/src/sentry/testutils/factories.py b/src/sentry/testutils/factories.py index 4f2a29b2b975b..4be5bf44d37bd 100644 --- a/src/sentry/testutils/factories.py +++ b/src/sentry/testutils/factories.py @@ -170,7 +170,7 @@ from sentry.users.services.user import RpcUser from sentry.utils import loremipsum from sentry.utils.performance_issues.performance_problem import PerformanceProblem -from sentry.workflow_engine.models import DataSource, Workflow +from sentry.workflow_engine.models import DataSource, Detector, Workflow from social_auth.models import UserSocialAuth @@ -2038,7 +2038,7 @@ def create_workflow( name: str | None = None, organization: Organization | None = None, **kwargs, - ): + ) -> Workflow: if organization is None: organization = Factories.create_organization() if name is None: @@ -2052,7 +2052,7 @@ def create_datasource( query_id: int | None = None, type: DataSource.Type | None = None, **kwargs, - ): + ) -> DataSource: if organization is None: organization = Factories.create_organization() if query_id is None: @@ -2060,3 +2060,20 @@ def create_datasource( if type is None: type = DataSource.Type.SNUBA_QUERY_SUBSCRIPTION return DataSource.objects.create(organization=organization, query_id=query_id, type=type) + + @staticmethod + @assume_test_silo_mode(SiloMode.REGION) + def create_detector( + organization: Organization | None = None, + name: str | None = None, + owner_user_id: int | None = None, + owner_team: Team | None = None, + **kwargs, + ) -> Detector: + if organization is None: + organization = Factories.create_organization() + if name is None: + name = petname.generate(2, " ", letters=10).title() + return Detector.objects.create( + organization=organization, name=name, owner_user_id=owner_user_id, owner_team=owner_team + ) diff --git a/src/sentry/testutils/fixtures.py b/src/sentry/testutils/fixtures.py index 3dcd35bd324d9..f30fcc3e9371a 100644 --- a/src/sentry/testutils/fixtures.py +++ b/src/sentry/testutils/fixtures.py @@ -639,6 +639,9 @@ def create_workflow(self, *args, **kwargs): def create_datasource(self, *args, **kwargs): return Factories.create_datasource(*args, **kwargs) + def create_detector(self, *args, **kwargs): + return Factories.create_detector(*args, **kwargs) + def create_uptime_subscription( self, type: str = "test", diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index 5ad294412ad71..e27bbfa11d858 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -610,6 +610,7 @@ def create_exhaustive_organization( self.create_workflow(organization=org) self.create_datasource(organization=org) + self.create_detector(organization=org) return org diff --git a/src/sentry/workflow_engine/migrations/0003_detector.py b/src/sentry/workflow_engine/migrations/0003_detector.py new file mode 100644 index 0000000000000..a41eed7073a06 --- /dev/null +++ b/src/sentry/workflow_engine/migrations/0003_detector.py @@ -0,0 +1,86 @@ +# Generated by Django 5.1.1 on 2024-09-11 21:13 + +import django.db.models.deletion +from django.db import migrations, models + +import sentry.db.models.fields.bounded +import sentry.db.models.fields.foreignkey +import sentry.db.models.fields.hybrid_cloud_foreign_key +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("sentry", "0759_remove_spanattributeextraction_tables"), + ("workflow_engine", "0002_data_source"), + ] + + operations = [ + migrations.CreateModel( + name="Detector", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ("date_updated", models.DateTimeField(auto_now=True)), + ("date_added", models.DateTimeField(auto_now_add=True)), + ( + "owner_user_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.User", db_index=True, null=True, on_delete="SET_NULL" + ), + ), + ("name", models.CharField(max_length=200)), + ( + "organization", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sentry.organization" + ), + ), + ( + "owner_team", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to="sentry.team" + ), + ), + ], + options={ + "abstract": False, + "constraints": [ + models.CheckConstraint( + condition=models.Q( + models.Q( + ("owner_team__isnull", False), ("owner_user_id__isnull", True) + ), + models.Q( + ("owner_team__isnull", True), ("owner_user_id__isnull", False) + ), + models.Q(("owner_team__isnull", True), ("owner_user_id__isnull", True)), + _connector="OR", + ), + name="workflow_engine_detector_owner_constraints", + ), + models.UniqueConstraint( + fields=("organization", "name"), name="workflow_engine_detector_org_name" + ), + ], + }, + ), + ] diff --git a/src/sentry/workflow_engine/models/__init__.py b/src/sentry/workflow_engine/models/__init__.py index aeb02aaf42ee9..413a61b4d85cf 100644 --- a/src/sentry/workflow_engine/models/__init__.py +++ b/src/sentry/workflow_engine/models/__init__.py @@ -1,4 +1,5 @@ -__all__ = ["DataSource", "Workflow"] +__all__ = ["DataSource", "Detector", "Workflow"] from .data_source import DataSource +from .detector import Detector from .workflow import Workflow diff --git a/src/sentry/workflow_engine/models/detector.py b/src/sentry/workflow_engine/models/detector.py new file mode 100644 index 0000000000000..c08c410e45e5c --- /dev/null +++ b/src/sentry/workflow_engine/models/detector.py @@ -0,0 +1,22 @@ +from django.db import models +from django.db.models import UniqueConstraint + +from sentry.backup.scopes import RelocationScope +from sentry.db.models import DefaultFieldsModel, FlexibleForeignKey, region_silo_model +from sentry.models.owner_base import OwnerModel + + +@region_silo_model +class Detector(DefaultFieldsModel, OwnerModel): + __relocation_scope__ = RelocationScope.Organization + + organization = FlexibleForeignKey("sentry.Organization") + name = models.CharField(max_length=200) + + class Meta(OwnerModel.Meta): + constraints = OwnerModel.Meta.constraints + [ + UniqueConstraint( + fields=["organization", "name"], + name="workflow_engine_detector_org_name", + ) + ] diff --git a/tests/sentry/backup/snapshots/SanitizationExhaustiveTests/test_clean_pks.pysnap b/tests/sentry/backup/snapshots/SanitizationExhaustiveTests/test_clean_pks.pysnap index 8ee0e15fe3b1d..20e7e1d40c702 100644 --- a/tests/sentry/backup/snapshots/SanitizationExhaustiveTests/test_clean_pks.pysnap +++ b/tests/sentry/backup/snapshots/SanitizationExhaustiveTests/test_clean_pks.pysnap @@ -1,5 +1,5 @@ --- -created: '2024-09-10T22:26:24.920905+00:00' +created: '2024-09-10T23:34:32.337196+00:00' creator: sentry source: tests/sentry/backup/test_sanitize.py --- @@ -217,6 +217,12 @@ source: tests/sentry/backup/test_sanitize.py sanitized_fields: - date_added - date_updated +- model_name: workflow_engine.detector + ordinal: 1 + sanitized_fields: + - date_added + - date_updated + - name - model_name: workflow_engine.workflow ordinal: 1 sanitized_fields: diff --git a/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap b/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap index 21f3633ec30e7..12fb6cc18ee6e 100644 --- a/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap +++ b/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap @@ -1,5 +1,5 @@ --- -created: '2024-09-10T22:25:01.144002+00:00' +created: '2024-09-10T23:35:41.229560+00:00' creator: sentry source: tests/sentry/backup/test_comparators.py --- @@ -1616,6 +1616,17 @@ source: tests/sentry/backup/test_comparators.py fields: - organization model_name: workflow_engine.datasource +- comparators: + - class: DateUpdatedComparator + fields: + - date_added + - date_updated + - class: ForeignKeyComparator + fields: + - organization + - owner_team + - owner_user_id + model_name: workflow_engine.detector - comparators: - class: DateUpdatedComparator fields: diff --git a/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap b/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap index 53e62695542e7..92c102de82629 100644 --- a/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap +++ b/tests/sentry/tasks/snapshots/PreprocessingTransferTest/test_success.pysnap @@ -1,5 +1,5 @@ --- -created: '2024-09-10T22:24:28.399110+00:00' +created: '2024-09-10T23:35:05.993533+00:00' creator: sentry source: tests/sentry/tasks/test_relocation.py --- @@ -96,7 +96,7 @@ steps: - -U - postgres - -c - - TRUNCATE sentry_controloption,sentry_integration,sentry_option,sentry_organization,sentry_organizationintegration,sentry_organizationoptions,sentry_projecttemplate,sentry_projecttemplateoption,sentry_relay,sentry_relayusage,sentry_repository,sentry_team,auth_user,sentry_userip,sentry_userpermission,sentry_userrole,sentry_userrole_users,workflow_engine_datasource,workflow_engine_workflow,sentry_savedsearch,sentry_recentsearch,sentry_project,sentry_orgauthtoken,sentry_organizationmember,sentry_organizationaccessrequest,sentry_monitor,sentry_groupsearchview,sentry_environment,sentry_email,sentry_datasecrecywaiver,sentry_dashboardtombstone,sentry_dashboard,sentry_customdynamicsamplingrule,sentry_projectcounter,sentry_authprovider,sentry_authidentity,auth_authenticator,sentry_apikey,sentry_apiapplication,sentry_useroption,sentry_useremail,sentry_snubaquery,sentry_sentryapp,sentry_rule,sentry_querysubscription,sentry_projectteam,sentry_projectredirect,sentry_projectownership,sentry_projectoptions,sentry_projectkey,sentry_projectintegration,sentry_projectbookmark,sentry_organizationmember_teams,sentry_notificationaction,sentry_neglectedrule,sentry_environmentproject,sentry_dashboardwidget,sentry_customdynamicsamplingruleproject,sentry_apitoken,sentry_apigrant,sentry_apiauthorization,sentry_alertrule,sentry_snubaqueryeventtype,sentry_sentryappinstallation,sentry_sentryappcomponent,sentry_rulesnooze,sentry_ruleactivity,sentry_notificationactionproject,sentry_dashboardwidgetquery,sentry_alertruletrigger,sentry_alertruleprojects,sentry_alertruleexcludedprojects,sentry_alertruleactivity,sentry_alertruleactivationcondition,sentry_servicehook,sentry_incident,sentry_dashboardwidgetqueryondemand,sentry_alertruletriggerexclusion,sentry_alertruletriggeraction,sentry_timeseriessnapshot,sentry_pendingincidentsnapshot,sentry_incidenttrigger,sentry_incidentsubscription,sentry_incidentsnapshot,sentry_incidentactivity + - TRUNCATE sentry_controloption,sentry_integration,sentry_option,sentry_organization,sentry_organizationintegration,sentry_organizationoptions,sentry_projecttemplate,sentry_projecttemplateoption,sentry_relay,sentry_relayusage,sentry_repository,sentry_team,auth_user,sentry_userip,sentry_userpermission,sentry_userrole,sentry_userrole_users,workflow_engine_datasource,workflow_engine_detector,workflow_engine_workflow,sentry_savedsearch,sentry_recentsearch,sentry_project,sentry_orgauthtoken,sentry_organizationmember,sentry_organizationaccessrequest,sentry_monitor,sentry_groupsearchview,sentry_environment,sentry_email,sentry_datasecrecywaiver,sentry_dashboardtombstone,sentry_dashboard,sentry_customdynamicsamplingrule,sentry_projectcounter,sentry_authprovider,sentry_authidentity,auth_authenticator,sentry_apikey,sentry_apiapplication,sentry_useroption,sentry_useremail,sentry_snubaquery,sentry_sentryapp,sentry_rule,sentry_querysubscription,sentry_projectteam,sentry_projectredirect,sentry_projectownership,sentry_projectoptions,sentry_projectkey,sentry_projectintegration,sentry_projectbookmark,sentry_organizationmember_teams,sentry_notificationaction,sentry_neglectedrule,sentry_environmentproject,sentry_dashboardwidget,sentry_customdynamicsamplingruleproject,sentry_apitoken,sentry_apigrant,sentry_apiauthorization,sentry_alertrule,sentry_snubaqueryeventtype,sentry_sentryappinstallation,sentry_sentryappcomponent,sentry_rulesnooze,sentry_ruleactivity,sentry_notificationactionproject,sentry_dashboardwidgetquery,sentry_alertruletrigger,sentry_alertruleprojects,sentry_alertruleexcludedprojects,sentry_alertruleactivity,sentry_alertruleactivationcondition,sentry_servicehook,sentry_incident,sentry_dashboardwidgetqueryondemand,sentry_alertruletriggerexclusion,sentry_alertruletriggeraction,sentry_timeseriessnapshot,sentry_pendingincidentsnapshot,sentry_incidenttrigger,sentry_incidentsubscription,sentry_incidentsnapshot,sentry_incidentactivity RESTART IDENTITY CASCADE; id: clear-database name: gcr.io/cloud-builders/docker