Skip to content

Commit

Permalink
Add ability to partition report-only incidents (#154)
Browse files Browse the repository at this point in the history
Some incidents are not currently in-progress, but rather after-the-fact reports (e.g. a near-miss or breach of procedures).

It's useful to be able to send these incident reports to a separate channel thereby reducing noise.

If an incident is report-only, no comms channel is created.

Squashed commits:

* Add report_only field to Incident

* Add 'report only' dialog option

* Add migration for report_only field

* Add report_only field to IncidentManager

* Fix order of incident report dialog fields

* Handle incident_type field in dialog submission

* Add report settings to demo app

* Exclude comms channel from report-only headline post

* Add new config to pytest

* Add report_only to tests

* Fix migration conflict with master

* Make report-only field configurable
  • Loading branch information
mattrco authored Sep 26, 2019
1 parent fe89ec1 commit fc86a9e
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 14 deletions.
4 changes: 4 additions & 0 deletions demo/demo/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
SLACK_TOKEN = get_env_var("SLACK_TOKEN")
SLACK_SIGNING_SECRET = get_env_var("SLACK_SIGNING_SECRET")
INCIDENT_CHANNEL_NAME = get_env_var("INCIDENT_CHANNEL_NAME")
INCIDENT_REPORT_CHANNEL_NAME = get_env_var("INCIDENT_REPORT_CHANNEL_NAME")
INCIDENT_BOT_NAME = get_env_var("INCIDENT_BOT_NAME")

SLACK_API_MOCK = os.getenv("SLACK_API_MOCK", None)
Expand All @@ -56,3 +57,6 @@
INCIDENT_CHANNEL_ID = os.getenv("INCIDENT_CHANNEL_ID") or SLACK_CLIENT.get_channel_id(
INCIDENT_CHANNEL_NAME
)
INCIDENT_REPORT_CHANNEL_ID = os.getenv(
"INCIDENT_REPORT_CHANNEL_ID"
) or SLACK_CLIENT.get_channel_id(INCIDENT_REPORT_CHANNEL_NAME)
4 changes: 4 additions & 0 deletions demo/demo/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
SLACK_TOKEN = get_env_var("SLACK_TOKEN")
SLACK_SIGNING_SECRET = get_env_var("SLACK_SIGNING_SECRET")
INCIDENT_CHANNEL_NAME = get_env_var("INCIDENT_CHANNEL_NAME")
INCIDENT_REPORT_CHANNEL_NAME = get_env_var("INCIDENT_REPORT_CHANNEL_NAME")
INCIDENT_BOT_NAME = get_env_var("INCIDENT_BOT_NAME")

INCIDENT_BOT_ID = os.getenv("INCIDENT_BOT_ID") or SLACK_CLIENT.get_user_id(
Expand All @@ -56,3 +57,6 @@
INCIDENT_CHANNEL_ID = os.getenv("INCIDENT_CHANNEL_ID") or SLACK_CLIENT.get_channel_id(
INCIDENT_CHANNEL_NAME
)
INCIDENT_REPORT_CHANNEL_ID = os.getenv(
"INCIDENT_REPORT_CHANNEL_ID"
) or SLACK_CLIENT.get_channel_id(INCIDENT_REPORT_CHANNEL_NAME)
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ env =
SLACK_TOKEN=xoxp-foo
SLACK_SIGNING_SECRET=shhdonttellanyone
INCIDENT_CHANNEL_NAME=incidents
INCIDENT_REPORT_CHANNEL_NAME=incident-reports
INCIDENT_BOT_NAME=responsetestincidents
INCIDENT_CHANNEL_ID=incident-channel-id
INCIDENT_REPORT_CHANNEL_ID=incident-report-channel-id
INCIDENT_BOT_ID=incident-bot-id
3 changes: 3 additions & 0 deletions response/core/models/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def create_incident(
report,
reporter,
report_time,
report_only,
summary=None,
impact=None,
lead=None,
Expand All @@ -21,6 +22,7 @@ def create_incident(
report=report,
reporter=reporter,
report_time=report_time,
report_only=report_only,
start_time=report_time,
summary=summary,
impact=impact,
Expand All @@ -44,6 +46,7 @@ class Incident(models.Model):
null=True,
)
report_time = models.DateTimeField()
report_only = models.BooleanField(default=False)

start_time = models.DateTimeField(null=False)
end_time = models.DateTimeField(blank=True, null=True)
Expand Down
16 changes: 16 additions & 0 deletions response/migrations/0010_incident_report_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 2.2.3 on 2019-09-24 15:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [("response", "0009_commschannel_channel_name")]

operations = [
migrations.AddField(
model_name="incident",
name="report_only",
field=models.BooleanField(default=False),
)
]
17 changes: 15 additions & 2 deletions response/slack/dialog_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ def report_incident(
lead_id = submission["lead"]
severity = submission["severity"]

if "incident_type" in submission:
report_only = submission["incident_type"] == "report"
else:
report_only = False

name = settings.SLACK_CLIENT.get_user_profile(user_id)["name"]
reporter, _ = ExternalUser.objects.get_or_create_slack(
external_id=user_id, display_name=name
Expand All @@ -38,14 +43,22 @@ def report_incident(
report=report,
reporter=reporter,
report_time=datetime.now(),
report_only=report_only,
summary=summary,
impact=impact,
lead=lead,
severity=severity,
)

incidents_channel_ref = channel_reference(settings.INCIDENT_CHANNEL_ID)
text = f"Thanks for raising the incident 🙏\n\nHead over to {incidents_channel_ref} to complete the report and/or help deal with the issue"
if report_only and hasattr(settings, "INCIDENT_REPORT_CHANNEL_ID"):
incidents_channel_ref = channel_reference(settings.INCIDENT_REPORT_CHANNEL_ID)
else:
incidents_channel_ref = channel_reference(settings.INCIDENT_CHANNEL_ID)

text = (
f"Thanks for raising the incident 🙏\n\nHead over to {incidents_channel_ref} "
f"to complete the report and/or help deal with the issue"
)
settings.SLACK_CLIENT.send_ephemeral_message(channel_id, user_id, text)


Expand Down
35 changes: 23 additions & 12 deletions response/slack/models/headline_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,19 @@ def update_in_slack(self):
)
)

channel_ref = (
channel_reference(self.comms_channel.channel_id)
if self.comms_channel
else None
)
if channel_ref:
msg.add_block(
Section(
block_id="comms_channel",
text=Text(f"🗣 Comms Channel: {channel_ref or '-'}"),
)
if not self.incident.report_only:
channel_ref = (
channel_reference(self.comms_channel.channel_id)
if self.comms_channel
else None
)
if channel_ref:
msg.add_block(
Section(
block_id="comms_channel",
text=Text(f"🗣 Comms Channel: {channel_ref or '-'}"),
)
)

# Add buttons (if the incident is open)
if not self.incident.is_closed():
Expand All @@ -136,7 +137,14 @@ def update_in_slack(self):
msg.add_block(actions)

# Post / update the slack message
response = msg.send(settings.INCIDENT_CHANNEL_ID, self.message_ts)
if self.incident.report_only and hasattr(
settings, "INCIDENT_REPORT_CHANNEL_ID"
):
channel_id = settings.INCIDENT_REPORT_CHANNEL_ID
else:
channel_id = settings.INCIDENT_CHANNEL_ID

response = msg.send(channel_id, self.message_ts)
logging.info(
f"Got response back from Slack after updating headline post: {response}"
)
Expand All @@ -157,6 +165,9 @@ def post_to_thread(self, message):

@headline_post_action(order=100)
def create_comms_channel_action(headline_post):
if headline_post.incident.report_only:
# Reports don't have a comms channel
return None
if headline_post.comms_channel:
# No need to create an action, channel already exists
return None
Expand Down
14 changes: 14 additions & 0 deletions response/slack/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging

from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

Expand Down Expand Up @@ -73,6 +74,19 @@ def slash_command(request):
],
)

if hasattr(settings, "INCIDENT_REPORT_CHANNEL_ID"):
dialog.add_element(
SelectWithOptions(
[
("Yes - this is a live incident happening right now", "live"),
("No - this is just a report of something that happened", "report"),
],
label="Is this a live incident?",
name="incident_type",
optional=False,
)
)

logger.info(
f"Handling Slack slash command for user {user_id}, report {report} - opening dialog"
)
Expand Down
1 change: 1 addition & 0 deletions tests/factories/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Meta:
report_time = factory.LazyFunction(
lambda: faker.date_time_between(start_date="-6m", end_date="now", tzinfo=None)
)
report_only = random.choice([True, False])

reporter = factory.SubFactory("tests.factories.ExternalUserFactory")
lead = factory.SubFactory("tests.factories.ExternalUserFactory")
Expand Down
61 changes: 61 additions & 0 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,65 @@ def test_submit_dialog_creates_incident(post_from_slack_api, mock_slack):
mock_slack.send_or_update_message_block.return_value = {"ts": "123"}
mock_slack.get_user_profile.return_value = {"name": "Opsy McOpsface"}

summary = "testing dialog submission"
data = {
"payload": json.dumps(
{
"type": "dialog_submission",
"callback_id": "incident-report-dialog",
"user": {"id": "U123"},
"channel": {"id": "channel-posted-from"},
"response_url": "https://fake-response-url",
"submission": {
"report": "",
"summary": summary,
"impact": "",
"lead": "U123",
"severity": "",
"incident_type": "live",
},
"state": "foo",
}
)
}
r = post_from_slack_api("action", data)

assert r.status_code == 200

# Check if incident has been created

start_time = datetime.now()
timeout_secs = 2
backoff = 0.1
q = Incident.objects.filter(summary=summary)

while True:
d = datetime.now() - start_time
if d.total_seconds() > timeout_secs:
pytest.fail(f"waited {timeout_secs}s for condition")
return
time.sleep(backoff)

if q.exists():
break

# Check that headline post got created
mock_slack.send_or_update_message_block.assert_called_with(
"incident-channel-id", blocks=ANY, fallback_text=ANY, ts="123"
)

# Check that we sent an ephemeral message to the reporting user
mock_slack.send_ephemeral_message.assert_called_with(
"channel-posted-from", "U123", ANY
)


@pytest.mark.django_db(transaction=True)
def test_submit_dialog_without_incident_type(post_from_slack_api, mock_slack):

mock_slack.send_or_update_message_block.return_value = {"ts": "123"}
mock_slack.get_user_profile.return_value = {"name": "Opsy McOpsface"}

summary = "testing dialog submission"
data = {
"payload": json.dumps(
Expand Down Expand Up @@ -104,6 +163,7 @@ def test_edit_incident(post_from_slack_api, mock_slack):
report="Something happened",
reporter=user,
report_time=datetime.now(),
report_only=False,
summary="Testing editing incidents - before",
impact="Lots",
lead=user,
Expand All @@ -125,6 +185,7 @@ def test_edit_incident(post_from_slack_api, mock_slack):
"impact": "",
"lead": "U123",
"severity": "",
"incident_type": "",
},
"state": incident.id,
}
Expand Down

0 comments on commit fc86a9e

Please sign in to comment.