Skip to content

Commit

Permalink
Adding additional oncall metrics (#5204)
Browse files Browse the repository at this point in the history
  • Loading branch information
whitdog47 authored Sep 18, 2024
1 parent 5d2fd54 commit 41fdcb3
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 8 deletions.
6 changes: 6 additions & 0 deletions src/dispatch/case/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ def get_all_by_status(
)


def get_all_last_x_hours(*, db_session, hours: int) -> List[Optional[Case]]:
"""Returns all cases in the last x hours."""
now = datetime.utcnow()
return db_session.query(Case).filter(Case.created_at >= now - timedelta(hours=hours)).all()


def get_all_last_x_hours_by_status(
*, db_session, project_id: int, status: str, hours: int
) -> List[Optional[Case]]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Adding details column to support additional feedback information
Revision ID: 47d616802f56
Revises: f729d61738b0
Create Date: 2024-09-13 10:28:16.823990
"""

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "47d616802f56"
down_revision = "f729d61738b0"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("service_feedback", sa.Column("details", sa.JSON(), nullable=True))
op.add_column("service_feedback_reminder", sa.Column("details", sa.JSON(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("service_feedback_reminder", "details")
op.drop_column("service_feedback", "details")
# ### end Alembic commands ###
5 changes: 4 additions & 1 deletion src/dispatch/feedback/service/messaging.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from datetime import datetime, timedelta
from typing import Optional
from typing import Optional, List

from sqlalchemy.orm import Session

Expand All @@ -26,6 +26,7 @@ def send_oncall_shift_feedback_message(
shift_end_at: str,
schedule_name: str,
reminder: Optional[ServiceFeedbackReminder] = None,
details: Optional[List[dict]] = None,
db_session: Session,
):
"""
Expand Down Expand Up @@ -66,6 +67,7 @@ def send_oncall_shift_feedback_message(
schedule_id=schedule_id,
schedule_name=schedule_name,
shift_end_at=shift_end_at,
details=[] if details is None else details,
),
)

Expand All @@ -79,6 +81,7 @@ def send_oncall_shift_feedback_message(
"project_id": project.id,
"shift_end_at": shift_end_clean,
"reminder_id": reminder.id,
"details": [] if details is None else details,
}
]

Expand Down
4 changes: 3 additions & 1 deletion src/dispatch/feedback/service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydantic import Field
from typing import Optional, List

from sqlalchemy import Column, Integer, ForeignKey, DateTime, String, Numeric
from sqlalchemy import Column, Integer, ForeignKey, DateTime, String, Numeric, JSON
from sqlalchemy_utils import TSVectorType
from sqlalchemy.orm import relationship

Expand All @@ -21,6 +21,7 @@ class ServiceFeedback(TimeStampMixin, FeedbackMixin, Base):
hours = Column(Numeric(precision=10, scale=2))
shift_start_at = Column(DateTime)
shift_end_at = Column(DateTime)
details = Column(JSON)

# Relationships
individual_contact_id = Column(Integer, ForeignKey("individual_contact.id"))
Expand Down Expand Up @@ -48,6 +49,7 @@ class ServiceFeedbackBase(DispatchBase):
shift_start_at: Optional[datetime]
project: Optional[ProjectRead]
created_at: Optional[datetime]
details: Optional[List[dict]] = Field([], nullable=True)


class ServiceFeedbackCreate(ServiceFeedbackBase):
Expand Down
7 changes: 5 additions & 2 deletions src/dispatch/feedback/service/reminder/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from datetime import datetime
from typing import Optional
from pydantic import Field
from typing import Optional, List

from sqlalchemy import Column, Integer, ForeignKey, DateTime, String
from sqlalchemy import Column, Integer, ForeignKey, DateTime, String, JSON

from dispatch.database.core import Base
from dispatch.individual.models import IndividualContactRead
Expand All @@ -16,6 +17,7 @@ class ServiceFeedbackReminder(TimeStampMixin, Base):
schedule_id = Column(String)
schedule_name = Column(String)
shift_end_at = Column(DateTime)
details = Column(JSON)

# Relationships
individual_contact_id = Column(Integer, ForeignKey("individual_contact.id"))
Expand All @@ -30,6 +32,7 @@ class ServiceFeedbackReminderBase(DispatchBase):
schedule_id: Optional[str]
schedule_name: Optional[str]
shift_end_at: Optional[datetime]
details: Optional[List[dict]] = Field([], nullable=True)


class ServiceFeedbackReminderCreate(ServiceFeedbackReminderBase):
Expand Down
50 changes: 50 additions & 0 deletions src/dispatch/feedback/service/scheduled.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from schedule import every
import logging
import datetime

from dispatch.database.core import SessionLocal
from dispatch.decorators import scheduled_project_task, timer
Expand All @@ -10,6 +11,8 @@
from dispatch.scheduler import scheduler
from dispatch.service import service as service_service
from .reminder import service as reminder_service
from dispatch.incident import service as incident_service
from dispatch.case import service as case_service

from .messaging import send_oncall_shift_feedback_message

Expand Down Expand Up @@ -56,6 +59,7 @@ def find_expired_reminders_and_send(*, db_session: SessionLocal, project: Projec
schedule_name=reminder.schedule_name,
reminder=reminder,
db_session=db_session,
details=reminder.details,
)


Expand All @@ -72,6 +76,11 @@ def find_schedule_and_send(
send the health metrics feedback request - note that if current and previous oncall is the
same, then did_oncall_just_go_off_shift will return None
"""

# Counts the number of participants with one or more active roles
def count_active_participants(participants):
return sum(1 for participant in participants if len(participant.active_roles) >= 1)

current_oncall = oncall_plugin.instance.did_oncall_just_go_off_shift(schedule_id, hour)

if current_oncall is None:
Expand All @@ -81,12 +90,53 @@ def find_schedule_and_send(
db_session=db_session, email=current_oncall["email"], project_id=project.id
)

# Calculate the number of hours in the shift
if current_oncall["shift_start"]:
shift_start_raw = current_oncall["shift_start"]
shift_start_at = (
datetime.strptime(shift_start_raw, "%Y-%m-%dT%H:%M:%SZ")
if "T" in shift_start_raw
else datetime.strptime(shift_start_raw, "%Y-%m-%d %H:%M:%S")
)
shift_end_raw = current_oncall["shift_end"]
shift_end_at = (
datetime.strptime(shift_end_raw, "%Y-%m-%dT%H:%M:%SZ")
if "T" in shift_end_raw
else datetime.strptime(shift_end_raw, "%Y-%m-%d %H:%M:%S")
)
hours_in_shift = (shift_end_at - shift_start_at).total_seconds() / 3600
else:
hours_in_shift = 7 * 24 # default to 7 days

num_incidents = 0
num_cases = 0
num_participants = 0
incidents = incident_service.get_all_last_x_hours(db_session=db_session, hours=hours_in_shift)
for incident in incidents:
if incident.commander.individual.email == current_oncall["email"]:
num_participants += count_active_participants(incident.participants)
num_incidents += 1

cases = case_service.get_all_last_x_hours(db_session=db_session, hours=hours_in_shift)
for case in cases:
if case.has_channel and case.assignee.individual.email == current_oncall["email"]:
num_participants += count_active_participants(case.participants)
num_cases += 1
details = [
{
"num_incidents": num_incidents,
"num_cases": num_cases,
"num_participants": num_participants,
}
]

send_oncall_shift_feedback_message(
project=project,
individual=individual,
schedule_id=schedule_id,
shift_end_at=current_oncall["shift_end"],
schedule_name=current_oncall["schedule_name"],
details=details,
db_session=db_session,
)

Expand Down
8 changes: 8 additions & 0 deletions src/dispatch/incident/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ def get_all_by_status(*, db_session, status: str, project_id: int) -> List[Optio
)


def get_all_last_x_hours(*, db_session, hours: int) -> List[Optional[Incident]]:
"""Returns all incidents in the last x hours."""
now = datetime.utcnow()
return (
db_session.query(Incident).filter(Incident.created_at >= now - timedelta(hours=hours)).all()
)


def get_all_last_x_hours_by_status(
*, db_session, status: str, hours: int, project_id: int
) -> List[Optional[Incident]]:
Expand Down
2 changes: 1 addition & 1 deletion src/dispatch/messaging/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ class MessageType(DispatchEnum):
ONCALL_SHIFT_FEEDBACK_BUTTONS = [
{
"button_text": "Provide Feedback",
"button_value": "{{organization_slug}}|{{project_id}}|{{oncall_schedule_id}}|{{shift_end_at}}|{{reminder_id}}",
"button_value": "{{organization_slug}}|{{project_id}}|{{oncall_schedule_id}}|{{shift_end_at}}|{{reminder_id}}|{{details}}",
"button_action": ConversationButtonActions.service_feedback,
}
]
Expand Down
12 changes: 12 additions & 0 deletions src/dispatch/plugins/dispatch_pagerduty/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,12 @@ def get_oncall_at_time(client: APISession, schedule_id: str, utctime: str) -> Op
user = get_user(client, user_id)
user_email = user["email"]
shift_end = oncalls[0]["end"]
shift_start = oncalls[0]["start"]
schedule_name = oncalls[0]["schedule"]["summary"]
return {
"email": user_email,
"shift_end": shift_end,
"shift_start": shift_start,
"schedule_name": schedule_name,
}

Expand Down Expand Up @@ -195,6 +197,16 @@ def oncall_shift_check(client: APISession, schedule_id: str, hour: int) -> Optio
next_oncall = get_oncall_at_time(client=client, schedule_id=schedule_id, utctime=next_shift)

if previous_oncall["email"] != next_oncall["email"]:
# find the beginning of previous_oncall's shift by going back in time in 24 hour increments
hours = 18
prior_oncall = previous_oncall
while prior_oncall["email"] == previous_oncall["email"]:
hours += 24
previous_shift = (now - timedelta(hours=hours)).isoformat(timespec="minutes") + "Z"
prior_oncall = get_oncall_at_time(
client=client, schedule_id=schedule_id, utctime=previous_shift
)
previous_oncall["shift_start"] = prior_oncall["shift_start"]
return previous_oncall


Expand Down
18 changes: 16 additions & 2 deletions src/dispatch/plugins/dispatch_slack/feedback/interactive.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import json
from blockkit import (
Checkboxes,
Context,
Expand Down Expand Up @@ -386,7 +387,7 @@ def handle_oncall_shift_feedback_submission_event(
feedback = form_data.get(ServiceFeedbackNotificationBlockIds.feedback_input, "")
rating = form_data.get(ServiceFeedbackNotificationBlockIds.rating_select, {}).get("value")

# metadata is organization_slug|project_id|schedule_id|shift_end_at|reminder_id
# metadata is organization_slug|project_id|schedule_id|shift_end_at|reminder_id|shift_start_at|details
metadata = body["view"]["private_metadata"].split("|")
project_id = metadata[1]
schedule_id = metadata[2]
Expand All @@ -401,6 +402,18 @@ def handle_oncall_shift_feedback_submission_event(
reminder_id = metadata[4]
if reminder_id.isnumeric():
reminder_service.delete(db_session=db_session, reminder_id=reminder_id)
if len(metadata) > 5:
shift_start_raw = metadata[5]
shift_start_at = (
datetime.strptime(shift_start_raw, "%Y-%m-%dT%H:%M:%SZ")
if "T" in shift_start_raw
else datetime.strptime(shift_start_raw, "%Y-%m-%d %H:%M:%S")
)
# if there are other details, store those
details = json.loads(metadata[5])
else:
shift_start_at = None
details = None

individual = (
None
Expand All @@ -419,8 +432,9 @@ def handle_oncall_shift_feedback_submission_event(
rating=ServiceFeedbackRating(rating),
schedule=schedule_id,
shift_end_at=shift_end_at,
shift_start_at=None,
shift_start_at=shift_start_at,
project=project,
details=details,
)

service_feedback = feedback_service.create(
Expand Down
10 changes: 9 additions & 1 deletion src/dispatch/static/dispatch/src/feedback/service/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<delete-dialog />
<v-row no-gutters>
<v-col>
<div class="text-h5">Feedback</div>
<div class="text-h5">Oncall Feedback</div>
</v-col>
<v-col class="text-right">
<table-filter-dialog :projects="defaultUserProjects" />
Expand Down Expand Up @@ -58,6 +58,13 @@
<span>{{ formatDate(item.created_at) }}</span>
</v-tooltip>
</template>
<template #item.details="{ item }">
<span v-if="item.details">
Incidents: {{ item.details[0].num_incidents }}<br />
Cases: {{ item.details[0].num_cases }}<br />
Participants: {{ item.details[0].num_participants }}<br />
</span>
</template>
<template #item.project.name="{ item }">
<v-chip size="small" :color="item.project.color">
{{ item.project.name }}
Expand Down Expand Up @@ -111,6 +118,7 @@ export default {
{ title: "Off-Shift Hours", value: "hours", sortable: true },
{ title: "Rating", value: "rating", sortable: true },
{ title: "Feedback", value: "feedback", sortable: true },
{ title: "Details", value: "details", sortable: true },
{ title: "Project", value: "project.name", sortable: false },
{ title: "Created At", value: "created_at", sortable: true },
{ title: "", key: "data-table-actions", sortable: false, align: "end" },
Expand Down

0 comments on commit 41fdcb3

Please sign in to comment.