Skip to content
Draft
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 migrations_lockfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ tempest: 0001_squashed_0003_use_encrypted_char_field

uptime: 0056_encrypt_uptime_subscription_headers_body

workflow_engine: 0121_add_activation_id_to_detector_state
workflow_engine: 0122_cleanup_invalid_data_condition_comparisons
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import logging
from decimal import Decimal, InvalidOperation
from typing import Any

from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.db.models import Q

from sentry.new_migrations.migrations import CheckedMigration
from sentry.utils.query import RangeQuerySetWrapperWithProgressBar

logger = logging.getLogger(__name__)

EVENT_SEEN_COUNT = "event_seen_count"
EVENT_CREATED_BY_DETECTOR = "event_created_by_detector"
ISSUE_RESOLUTION_CHANGE = "issue_resolution_change"
ISSUE_PRIORITY_DEESCALATING = "issue_priority_deescalating"
ISSUE_PRIORITY_EQUALS = "issue_priority_equals"
ISSUE_PRIORITY_GREATER_OR_EQUAL = "issue_priority_greater_or_equal"

CONDITION_TYPES = (
EVENT_SEEN_COUNT,
EVENT_CREATED_BY_DETECTOR,
ISSUE_RESOLUTION_CHANGE,
ISSUE_PRIORITY_DEESCALATING,
)
PRIORITY_CONDITION_TYPES = (
ISSUE_PRIORITY_EQUALS,
ISSUE_PRIORITY_GREATER_OR_EQUAL,
)
PRIORITY_VALUES = {25, 50, 75}
PRIORITY_NAMES = {"low", "medium", "high"}
RESOLVED = 1


def positive_integer(value: object) -> int | None:
if isinstance(value, bool):
return None
if isinstance(value, int):
return value if value >= 1 else None
if not isinstance(value, (float, str)):
return None

try:
decimal = Decimal(str(value))
except InvalidOperation:
return None
if not decimal.is_finite() or decimal < 1 or decimal != decimal.to_integral_value():
return None
return int(decimal)


def priority_integer(value: object) -> int | None:
comparison = positive_integer(value)
return comparison if comparison in PRIORITY_VALUES else None


def get_replacement(condition: Any) -> int | None:
comparison = condition.comparison
if condition.type == EVENT_SEEN_COUNT:
return RESOLVED if comparison is True else positive_integer(comparison)

if condition.type == EVENT_CREATED_BY_DETECTOR:
return positive_integer(comparison)

if condition.type == ISSUE_RESOLUTION_CHANGE:
if comparison is True or comparison == "resolved":
return RESOLVED
resolved = positive_integer(comparison)
return resolved if resolved == RESOLVED else None

if condition.type in PRIORITY_CONDITION_TYPES:
return priority_integer(comparison)

if condition.type == ISSUE_PRIORITY_DEESCALATING:
return priority_integer(comparison)

return None


def cleanup_invalid_data_condition_comparisons(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
DataCondition = apps.get_model("workflow_engine", "DataCondition")
conditions = DataCondition.objects.filter(
Q(type__in=CONDITION_TYPES)
| Q(type__in=PRIORITY_CONDITION_TYPES) & ~Q(comparison__in=PRIORITY_NAMES)
)

for condition in RangeQuerySetWrapperWithProgressBar(conditions):
old_comparison = condition.comparison
replacement = get_replacement(condition)
logging_extra = {
"data_condition_id": condition.id,
"data_condition_type": condition.type,
"condition_group_id": condition.condition_group_id,
"old_comparison": old_comparison,
}

if replacement is None:
logger.warning(
"Data condition comparison could not be safely converted and was left unchanged",
extra=logging_extra,
)
continue

if type(old_comparison) is int and old_comparison == replacement:
continue

condition.comparison = replacement
condition.save(update_fields=["comparison"])
logger.info(
"Data condition comparison converted",
extra={**logging_extra, "new_comparison": replacement},
)


class Migration(CheckedMigration):
is_post_deployment = True

dependencies = [
("workflow_engine", "0121_add_activation_id_to_detector_state"),
]

operations = [
migrations.RunPython(
cleanup_invalid_data_condition_comparisons,
reverse_code=migrations.RunPython.noop,
hints={"tables": ["workflow_engine_datacondition"]},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from sentry.silo.safety import unguarded_write
from sentry.testutils.cases import TestMigrations


class CleanupInvalidDataConditionComparisonsTest(TestMigrations):
app = "workflow_engine"
migrate_from = "0121_add_activation_id_to_detector_state"
migrate_to = "0122_cleanup_invalid_data_condition_comparisons"

def setup_before_migration(self, apps):
Organization = apps.get_model("sentry", "Organization")
DataCondition = apps.get_model("workflow_engine", "DataCondition")
DataConditionGroup = apps.get_model("workflow_engine", "DataConditionGroup")

with unguarded_write(using="default"):
organization = Organization.objects.create(
slug="cleanup-data-condition-comparisons",
name="Cleanup Data Condition Comparisons",
)
default_group = DataConditionGroup.objects.create(organization_id=organization.id)
values: dict[str, tuple[str, object, int]] = {
"seen-string": ("event_seen_count", "2", default_group.id),
"seen-boolean": ("event_seen_count", True, default_group.id),
"detector-string": ("event_created_by_detector", "42", default_group.id),
"resolution-string": ("issue_resolution_change", "resolved", default_group.id),
"priority-string": ("issue_priority_equals", "25", default_group.id),
"deescalation-string": (
"issue_priority_deescalating",
"50",
default_group.id,
),
"invalid-detector": (
"event_created_by_detector",
"detector",
default_group.id,
),
"existing-priority-name": ("issue_priority_equals", "high", default_group.id),
}
self.condition_ids = {}
for name, (condition_type, comparison, group_id) in values.items():
condition = DataCondition.objects.create(
condition_group_id=group_id,
type=condition_type,
comparison=comparison,
condition_result=True,
)
self.condition_ids[name] = condition.id

def test(self):
DataCondition = self.apps.get_model("workflow_engine", "DataCondition")
comparisons = {
name: DataCondition.objects.get(id=condition_id).comparison
for name, condition_id in self.condition_ids.items()
}

assert comparisons == {
"seen-string": 2,
"seen-boolean": 1,
"detector-string": 42,
"resolution-string": 1,
"priority-string": 25,
"deescalation-string": 50,
"invalid-detector": "detector",
"existing-priority-name": "high",
}
Loading