mirror of
https://github.com/System-End/nephthys.git
synced 2026-04-19 18:35:14 +00:00
Add a "resolution time" statistic (#151)
* Add a resolution time stat * Add resolution times to API * Return None if no resolution time can be calculated * Optionally calculate hang time for all tickets, not just in-progress ones * Add avg_hang_time_all_minutes to API * Store reopenedAt timestamp
This commit is contained in:
parent
6afc28a27c
commit
e77a60e1ec
5 changed files with 59 additions and 28 deletions
|
|
@ -29,6 +29,7 @@ async def stats(req: Request):
|
|||
for entry in total_stats.helpers_leaderboard[:3]
|
||||
],
|
||||
"average_hang_time_minutes": total_stats.avg_hang_time_minutes,
|
||||
"mean_resolution_time_minutes": total_stats.mean_resolution_time_minutes,
|
||||
"prev_day_total": prev_day_stats.new_tickets_total,
|
||||
"prev_day_open": prev_day_stats.new_tickets_still_open,
|
||||
"prev_day_in_progress": prev_day_stats.new_tickets_in_progress,
|
||||
|
|
@ -41,6 +42,8 @@ async def stats(req: Request):
|
|||
}
|
||||
for entry in prev_day_stats.helpers_leaderboard[:3]
|
||||
],
|
||||
"prev_day_average_hang_time_minutes": prev_day_stats.avg_hang_time_minutes,
|
||||
"prev_day_average_hang_time_minutes": prev_day_stats.avg_hang_time_current_minutes,
|
||||
"prev_day_average_hang_time_all_minutes": prev_day_stats.avg_hang_time_all_minutes,
|
||||
"prev_day_mean_resolution_time_minutes": prev_day_stats.mean_resolution_time_minutes,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ class Reopen(Macro):
|
|||
"status": TicketStatus.OPEN,
|
||||
"closedBy": {"disconnect": True},
|
||||
"reopenedBy": {"connect": {"id": helper.id}},
|
||||
"reopenedAt": datetime.now(),
|
||||
"closedAt": None,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from statistics import fmean
|
||||
from typing import TypedDict
|
||||
|
||||
from nephthys.utils.env import env
|
||||
|
|
@ -21,17 +22,29 @@ class OverallStatsResult:
|
|||
tickets_in_progress: int
|
||||
helpers_leaderboard: list[LeaderboardEntry]
|
||||
avg_hang_time_minutes: float | None
|
||||
mean_resolution_time_minutes: float | None
|
||||
|
||||
|
||||
def calculate_avg_hang_time(tickets: list[Ticket]) -> float | None:
|
||||
def calculate_hang_times(
|
||||
tickets: list[Ticket], include_closed_tickets: bool
|
||||
) -> list[float]:
|
||||
hang_times = []
|
||||
for tkt in tickets:
|
||||
if tkt.status == TicketStatus.CLOSED:
|
||||
if not include_closed_tickets and tkt.status == TicketStatus.CLOSED:
|
||||
continue
|
||||
if not tkt.assignedAt:
|
||||
continue
|
||||
hang_times.append((tkt.assignedAt - tkt.createdAt).total_seconds() / 60)
|
||||
return sum(hang_times) / len(hang_times) if hang_times else None
|
||||
return hang_times
|
||||
|
||||
|
||||
def calculate_resolution_times(tickets: list[Ticket]) -> list[float]:
|
||||
resolution_times = []
|
||||
for tkt in tickets:
|
||||
if not tkt.closedAt:
|
||||
continue
|
||||
resolution_times.append((tkt.closedAt - tkt.createdAt).total_seconds() / 60)
|
||||
return resolution_times
|
||||
|
||||
|
||||
async def calculate_overall_stats() -> OverallStatsResult:
|
||||
|
|
@ -56,18 +69,27 @@ async def calculate_overall_stats() -> OverallStatsResult:
|
|||
reverse=True,
|
||||
)
|
||||
|
||||
hang_times = calculate_hang_times(tickets, include_closed_tickets=False)
|
||||
resolution_times = calculate_resolution_times(tickets)
|
||||
|
||||
return OverallStatsResult(
|
||||
tickets_total=total,
|
||||
tickets_open=total_open,
|
||||
tickets_closed=total_closed,
|
||||
tickets_in_progress=total_in_progress,
|
||||
helpers_leaderboard=helpers_leaderboard,
|
||||
avg_hang_time_minutes=calculate_avg_hang_time(tickets),
|
||||
avg_hang_time_minutes=fmean(hang_times) if hang_times else None,
|
||||
mean_resolution_time_minutes=fmean(resolution_times)
|
||||
if resolution_times
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DailyStatsResult:
|
||||
"""Processed statistics for a time interval
|
||||
(usually 24h but doesn't have to be)"""
|
||||
|
||||
new_tickets_total: int
|
||||
new_tickets_now_closed: int
|
||||
new_tickets_still_open: int
|
||||
|
|
@ -76,45 +98,39 @@ class DailyStatsResult:
|
|||
closed_today_from_today: int
|
||||
assigned_today_in_progress: int
|
||||
helpers_leaderboard: list[LeaderboardEntry]
|
||||
avg_hang_time_minutes: float | None
|
||||
# Mean time to response for tickets created today and currently in-progress
|
||||
avg_hang_time_current_minutes: float | None
|
||||
# Mean time to response for all tickets created today
|
||||
avg_hang_time_all_minutes: float | None
|
||||
# Mean time to resolution for tickets created today
|
||||
mean_resolution_time_minutes: float | None
|
||||
|
||||
|
||||
async def calculate_daily_stats(
|
||||
start_time: datetime, end_time: datetime
|
||||
) -> DailyStatsResult:
|
||||
tickets = await env.db.ticket.find_many() or []
|
||||
tickets_created_today = [t for t in tickets if start_time <= t.createdAt < end_time]
|
||||
users_with_closed_tickets = await env.db.user.find_many(
|
||||
include={"closedTickets": True},
|
||||
where={"helper": True, "closedTickets": {"some": {}}},
|
||||
)
|
||||
|
||||
new_tickets_total = len(
|
||||
[t for t in tickets if start_time <= t.createdAt < end_time]
|
||||
)
|
||||
new_tickets_total = len(tickets_created_today)
|
||||
new_tickets_now_closed = len(
|
||||
[
|
||||
t
|
||||
for t in tickets
|
||||
for t in tickets_created_today
|
||||
if t.status == TicketStatus.CLOSED
|
||||
and t.closedAt
|
||||
and start_time <= t.closedAt < end_time
|
||||
and start_time <= t.createdAt < end_time
|
||||
]
|
||||
)
|
||||
new_tickets_still_open = len(
|
||||
[
|
||||
t
|
||||
for t in tickets
|
||||
if start_time <= t.createdAt < end_time and t.status == TicketStatus.OPEN
|
||||
]
|
||||
[t for t in tickets_created_today if t.status == TicketStatus.OPEN]
|
||||
)
|
||||
new_tickets_in_progress = len(
|
||||
[
|
||||
t
|
||||
for t in tickets
|
||||
if start_time <= t.createdAt < end_time
|
||||
and t.status == TicketStatus.IN_PROGRESS
|
||||
]
|
||||
[t for t in tickets_created_today if t.status == TicketStatus.IN_PROGRESS]
|
||||
)
|
||||
tickets_closed_today = [
|
||||
t
|
||||
|
|
@ -152,9 +168,16 @@ async def calculate_daily_stats(
|
|||
reverse=True,
|
||||
)
|
||||
|
||||
hang_time = calculate_avg_hang_time(
|
||||
[t for t in tickets if start_time <= t.createdAt < end_time]
|
||||
hang_times_current = calculate_hang_times(
|
||||
tickets_created_today, include_closed_tickets=False
|
||||
)
|
||||
hang_times_all = calculate_hang_times(
|
||||
tickets_created_today, include_closed_tickets=True
|
||||
)
|
||||
resolution_times = calculate_resolution_times(tickets_created_today)
|
||||
hang_time_current = fmean(hang_times_current) if hang_times_current else None
|
||||
hang_time_all = fmean(hang_times_all) if hang_times_all else None
|
||||
resolution_time = fmean(resolution_times) if resolution_times else None
|
||||
|
||||
return DailyStatsResult(
|
||||
closed_today=closed_today,
|
||||
|
|
@ -165,5 +188,7 @@ async def calculate_daily_stats(
|
|||
new_tickets_now_closed=new_tickets_now_closed,
|
||||
new_tickets_in_progress=new_tickets_in_progress,
|
||||
new_tickets_still_open=new_tickets_still_open,
|
||||
avg_hang_time_minutes=hang_time,
|
||||
avg_hang_time_current_minutes=hang_time_current,
|
||||
avg_hang_time_all_minutes=hang_time_all,
|
||||
mean_resolution_time_minutes=resolution_time,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ async def get_leaderboard_view():
|
|||
else "No hang time data available"
|
||||
)
|
||||
avg_prev_day_hang_time_str = (
|
||||
f"{prev_day.avg_hang_time_minutes:.2f} minutes"
|
||||
if prev_day.avg_hang_time_minutes is not None
|
||||
f"{prev_day.avg_hang_time_current_minutes:.2f} minutes"
|
||||
if prev_day.avg_hang_time_current_minutes is not None
|
||||
else "No hang time data available"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ model Ticket {
|
|||
openedBy User @relation("OpenedTickets", fields: [openedById], references: [id])
|
||||
openedById Int
|
||||
|
||||
reopenedBy User? @relation("ReopenedTickets", fields: [reopenedById], references: [id])
|
||||
reopenedBy User? @relation("ReopenedTickets", fields: [reopenedById], references: [id])
|
||||
reopenedById Int?
|
||||
reopenedAt DateTime?
|
||||
|
||||
closedBy User? @relation("ClosedTickets", fields: [closedById], references: [id])
|
||||
closedById Int?
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue