Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Adds conversation target and oncall service override options to signal instances

Revision ID: dfc8e213a2c4
Revises: 2d9e4d392ea4
Create Date: 2024-12-17 10:02:26.920568

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'dfc8e213a2c4'
down_revision = '2d9e4d392ea4'
branch_labels = None
depends_on = None


def upgrade():
op.add_column("signal_instance", sa.Column("conversation_target", sa.String(), nullable=True))
op.add_column("signal_instance", sa.Column("oncall_service_id", sa.Integer(), nullable=True))
op.create_foreign_key(
"oncall_service_id_fkey",
"signal_instance",
"service",
["oncall_service_id"],
["id"]
)


def downgrade():
op.drop_constraint("oncall_service_id_fkey", "signal_instance", type_="foreignkey")
op.drop_column("signal_instance", "oncall_service_id")
op.drop_column("signal_instance", "conversation_target")
47 changes: 27 additions & 20 deletions src/dispatch/signal/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,34 +79,38 @@ def signal_instance_create_flow(
if not signal_instance.signal.create_case:
return signal_instance

# processes overrides for case creation
# we want the following order of precedence:
# 1. signal instance overrides
# 2. signal definition overrides
# 3. case type defaults
# set signal instance attributes with priority given to signal instance specification, then signal, then case type.
if signal_instance.case_type:
case_type = signal_instance.case_type
else:
case_type = signal_instance.signal.case_type

if signal_instance.case_priority:
case_priority = signal_instance.case_priority
else:
case_priority = signal_instance.signal.case_priority

# if the signal has provided a case type use it's values instead of the definitions
conversation_target = None
if signal_instance.case_type:
case_type = signal_instance.case_type
if signal_instance.signal.conversation_target:
conversation_target = signal_instance.case_type.conversation_target
if signal_instance.oncall_service:
oncall_service = signal_instance.oncall_service
elif signal_instance.signal.oncall_service:
oncall_service = signal_instance.signal.oncall_service
elif case_type.oncall_service:
oncall_service = case_type.oncall_service
else:
case_type = signal_instance.signal.case_type

if signal_instance.signal.conversation_target:
conversation_target = signal_instance.signal.conversation_target
oncall_service = None

if signal_instance.conversation_target:
conversation_target = signal_instance.conversation_target
elif signal_instance.signal.conversation_target:
conversation_target = signal_instance.signal.conversation_target
elif case_type.conversation_target:
conversation_target = case_type.conversation_target
else:
conversation_target = None

assignee = None
if signal_instance.signal.oncall_service:
email = service_flows.resolve_oncall(
service=signal_instance.signal.oncall_service, db_session=db_session
)
if oncall_service:
email = service_flows.resolve_oncall(service=oncall_service, db_session=db_session)
assignee = {"individual": {"email": email}}

# create a case if not duplicate or snoozed and case creation is enabled
Expand Down Expand Up @@ -172,7 +176,10 @@ def create_signal_instance(
raise DispatchException("Signal definition is not enabled.")

signal_instance_in = SignalInstanceCreate(
raw=signal_instance_data, signal=signal, project=signal.project
**signal_instance_data,
raw=signal_instance_data,
signal=signal,
project=signal.project,
)

signal_instance = signal_service.create_instance(
Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/signal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
TimeStampMixin,
)
from dispatch.project.models import ProjectRead
from dispatch.service.models import Service, ServiceRead
from dispatch.tag.models import TagRead
from dispatch.workflow.models import WorkflowRead

Expand Down Expand Up @@ -228,8 +229,11 @@ class SignalInstance(Base, TimeStampMixin, ProjectMixin):
case_type = relationship("CaseType", backref="signal_instances")
case_priority_id = Column(Integer, ForeignKey(CasePriority.id))
case_priority = relationship("CasePriority", backref="signal_instances")
conversation_target = Column(String)
filter_action = Column(String)
canary = Column(Boolean, default=False)
oncall_service_id = Column(Integer, ForeignKey(Service.id))
oncall_service = relationship("Service", backref="signal_instances")
raw = Column(JSONB)
signal = relationship("Signal", backref="instances")
signal_id = Column(Integer, ForeignKey("signal.id"))
Expand Down Expand Up @@ -386,6 +390,8 @@ class SignalInstanceCreate(SignalInstanceBase):
signal: Optional[SignalRead]
case_priority: Optional[CasePriorityRead]
case_type: Optional[CaseTypeRead]
conversation_target: Optional[str]
oncall_service: Optional[ServiceRead]


class SignalInstanceRead(SignalInstanceBase):
Expand Down
16 changes: 15 additions & 1 deletion src/dispatch/signal/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,11 @@ def create_instance(

signal = get(db_session=db_session, signal_id=signal_instance_in.signal.id)

# remove non-serializable entities from the raw JSON:
signal_instance_in_raw = signal_instance_in.raw.copy()
if signal_instance_in.oncall_service:
signal_instance_in_raw.pop('oncall_service')

# we round trip the raw data to json-ify date strings
signal_instance = SignalInstance(
**signal_instance_in.dict(
Expand All @@ -580,12 +585,13 @@ def create_instance(
"case_type",
"entities",
"external_id",
"oncall_service",
"project",
"raw",
"signal",
}
),
raw=json.loads(json.dumps(signal_instance_in.raw)),
raw=json.loads(json.dumps(signal_instance_in_raw)),
project=project,
signal=signal,
)
Expand Down Expand Up @@ -619,6 +625,14 @@ def create_instance(
)
signal_instance.case_type = case_type

if signal_instance_in.oncall_service:
oncall_service = service_service.get_by_name(
db_session=db_session,
project_id=project.id,
name=signal_instance_in.oncall_service.name,
)
signal_instance.oncall_service = oncall_service

db_session.add(signal_instance)
db_session.commit()
return signal_instance
Expand Down
179 changes: 179 additions & 0 deletions tests/signal/test_signal_flow.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest import mock

import pytest

from dispatch.exceptions import DispatchException
Expand Down Expand Up @@ -59,3 +61,180 @@ def test_create_signal_instance_not_enabled(session, signal, case_severity, case
signal_instance_data=instance_data,
current_user=user,
)

def test_create_signal_instance_custom_conversation_target(session, signal, case_severity, case_priority, user, case_type):
from dispatch.signal.flows import create_signal_instance

case_priority.default = True
case_priority.project_id = signal.project_id

case_severity.default = True
case_severity.project_id = signal.project_id

instance_data = {"variant": signal.variant, "conversation_target": "instance-conversation-target"}
signal.conversation_target = "signal-conversation-target"

signal_instance = create_signal_instance(
db_session=session,
project=signal.project,
signal_instance_data=instance_data,
current_user=user,
)
assert signal_instance.conversation_target == 'instance-conversation-target'


def test_create_signal_instance_custom_oncall_service(session, signal, case_severity, case_priority, user, services):
from dispatch.signal.flows import create_signal_instance

case_priority.default = True
case_priority.project_id = signal.project_id

case_severity.default = True
case_severity.project_id = signal.project_id

service_0, service_1 = services
service_0.project_id = signal.project_id
service_1.project_id = signal.project_id

signal.oncall_service = service_0
instance_data = {"variant": signal.variant, "oncall_service": service_1}

signal_instance = create_signal_instance(
db_session=session,
project=signal.project,
signal_instance_data=instance_data,
current_user=user,
)
assert signal_instance.oncall_service.id == service_1.id

def test_signal_instance_create_flow_custom_attributes(session, signal, case_severity, case_priority, user, services, signal_instance, oncall_plugin, case_type, case):
from dispatch.signal.flows import signal_instance_create_flow
from dispatch.service import flows as service_flows
from dispatch.case import service as case_service

case_priority.default = True
case_priority.project_id = signal.project_id

case_severity.default = True
case_severity.project_id = signal.project_id

service_0, service_1 = services
service_0.project_id = signal.project_id
service_1.project_id = signal.project_id

signal_instance.oncall_service = service_0
signal_instance.signal.oncall_service = service_1
signal_instance.conversation_target = "instance-conversation-target"
signal_instance.signal.conversation_target = "signal-conversation-target"

with mock.patch.object(service_flows, "resolve_oncall") as mock_resolve_oncall, \
mock.patch.object(case_service, "create") as mock_case_create, \
mock.patch("dispatch.case.flows.case_new_create_flow") as mock_case_new_create_flow:
mock_resolve_oncall.side_effect = lambda service, db_session: "example@test.com" if service.id == service_0.id else None
mock_case_create.return_value = case

post_flow_instance = signal_instance_create_flow(
signal_instance_id=signal_instance.id,
db_session=session,
current_user=user
)
case_in_arg = mock_case_create.call_args[1]['case_in']
assert case_in_arg.assignee.individual.email == "example@test.com"
mock_case_new_create_flow.assert_called_once_with(
db_session=session,
organization_slug=None,
service_id=None,
conversation_target="instance-conversation-target",
case_id=post_flow_instance.case.id,
create_all_resources=False
)

def test_signal_instance_create_flow_use_signal_attributes(session, signal, case_severity, case_priority, user, services, signal_instance,
case_type, case):
"""
If the signal instance does not specify a conversation target or on-call service, use the signal's configurations
before the case type's configurations.
"""
from dispatch.signal.flows import signal_instance_create_flow
from dispatch.service import flows as service_flows
from dispatch.case import service as case_service

case_priority.default = True
case_priority.project_id = signal.project_id

case_severity.default = True
case_severity.project_id = signal.project_id

service_0, service_1 = services
service_0.project_id = signal.project_id
service_1.project_id = signal.project_id

signal_instance.signal.oncall_service = service_0
signal_instance.signal.conversation_target = "signal-conversation-target"
case_type.oncall_service = service_1
case_type.conversation_target = "case-type-conversation-target"
signal_instance.signal.case_type = case_type

with mock.patch.object(service_flows, "resolve_oncall") as mock_resolve_oncall, \
mock.patch.object(case_service, "create") as mock_case_create, \
mock.patch("dispatch.case.flows.case_new_create_flow") as mock_case_new_create_flow:

mock_resolve_oncall.side_effect = lambda service, db_session: "example@test.com" if service.id == service_0.id else None
mock_case_create.return_value = case

post_flow_instance = signal_instance_create_flow(
signal_instance_id=signal_instance.id,
db_session=session,
current_user=user
)
case_in_arg = mock_case_create.call_args[1]['case_in']
assert case_in_arg.assignee.individual.email == "example@test.com"
mock_case_new_create_flow.assert_called_once_with(
db_session=session,
organization_slug=None,
service_id=None,
conversation_target="signal-conversation-target",
case_id=post_flow_instance.case.id,
create_all_resources=False
)


def test_signal_instance_create_flow_use_case_type_attributes(session, signal, case_severity, case_priority, user, service, case, signal_instance, case_type):
"""
If the signal instance and the signal both do not specify conversation targets or on-call services, use the case type's configurations.
"""
from dispatch.signal.flows import signal_instance_create_flow
from dispatch.service import flows as service_flows
from dispatch.case import service as case_service

case_priority.default = True
case_priority.project_id = signal.project_id

case_severity.default = True
case_severity.project_id = signal.project_id

case_type.oncall_service = service
case_type.conversation_target = "case-type-conversation-target"
signal_instance.signal.case_type = case_type

with mock.patch.object(service_flows, "resolve_oncall") as mock_resolve_oncall, \
mock.patch.object(case_service, "create") as mock_case_create, \
mock.patch("dispatch.case.flows.case_new_create_flow") as mock_case_new_create_flow:
mock_resolve_oncall.side_effect = lambda service, db_session: "example@test.com"
mock_case_create.return_value = case

post_flow_instance = signal_instance_create_flow(
signal_instance_id=signal_instance.id,
db_session=session,
current_user=user
)
case_in_arg = mock_case_create.call_args[1]['case_in']
assert case_in_arg.assignee.individual.email == "example@test.com"
mock_case_new_create_flow.assert_called_once_with(
db_session=session,
organization_slug=None,
service_id=None,
conversation_target="case-type-conversation-target",
case_id=post_flow_instance.case.id,
create_all_resources=False
)
Loading