Skip to content

Instantly share code, notes, and snippets.

@shivendrasoni
Last active April 19, 2024 10:51
Show Gist options
  • Save shivendrasoni/9c16ffa1c7c9ba3524259295644c1604 to your computer and use it in GitHub Desktop.
Save shivendrasoni/9c16ffa1c7c9ba3524259295644c1604 to your computer and use it in GitHub Desktop.
import json
import dateutil.parser
from datetime import date
import numpy as np
import pandas as pd
import dateutil.parser
import streamlit as st
def load_json(filename):
with open(filename, 'r') as f:
return json.load(f)['data']['response']
PROJECT_SPECIFIC_CONFIGS = {
'P1T': {
'initial_state': '3',
'final_state': '10060',
'review_state': '10017',
'bugfix_state': '10071',
'story_point_field': 'customfield_10033',
'bug_issue_type': '10009',
'sanity_bug_issue_type': '10080'
},
'P1D': {
'initial_state': '10085',
'final_state': '10090',
'review_state': '10089',
'bugfix_state': '10088',
'story_point_field': 'customfield_10016',
'bug_issue_type': '10058',
'sanity_bug_issue_type': '10068'
},
'POD3': {
'initial_state': '3',
'final_state': '10060',
'review_state': '10017',
'bugfix_state': 'IN BUG FIX',
'story_point_field': 'customfield_10033',
'bug_issue_type': '10009',
'sanity_bug_issue_type': '10080'
},
'P2': {
'initial_state': '3',
'final_state': '10006',
'review_state': '10017',
'bugfix_state': 'IN BUG FIX',
'story_point_field': 'customfield_10033',
'bug_issue_type': '10009',
'sanity_bug_issue_type': '10080'
},
'PLAT': {
'initial_state': 'In Progress',
'final_state': 'Ready For QA',
'review_state': 'In code review',
'bugfix_state': 'IN BUG FIX',
'story_point_field': 'customfield_10016',
'bug_issue_type': '10058',
'sanity_bug_issue_type': '10068'
},
'PLG': {
'initial_state': '10054',
'final_state': '10066',
'review_state': '10059',
'bugfix_state': 'IN BUG FIX',
'story_point_field': 'customfield_10016',
'bug_issue_type': '10045',
'sanity_bug_issue_type': '10079'
},
'PA': {
'initial_state': '10050',
'final_state': '10062',
'review_state': '10061',
'bugfix_state': '10121',
'story_point_field': 'customfield_10016',
'bug_issue_type': '10040',
'sanity_bug_issue_type': '10050'
},
}
def calculate_average_cycle_time_per_user(jira_data_list=[], initial_state='In Progress',
final_state='Ready For QA') -> list:
total_durations = []
for jira_data in jira_data_list:
histories = jira_data['jiraIssueHistory']['changelog']['histories']
if jira_data['jiraIssueField']['fields']['assignee'] is None:
continue
initial_state_time = None
final_state_time = None
histories.reverse()
for history in histories:
for item in history['items']:
if item['field'] == 'status':
if item['to'] == initial_state and initial_state_time is None: #set the first instance
initial_state_time = dateutil.parser.parse(history['created'])
elif item['to'] == final_state:
if final_state_time is None:
final_state_time = dateutil.parser.parse(history['created'])
elif dateutil.parser.parse(history['created']) > final_state_time:
final_state_time = dateutil.parser.parse(history['created'])
# Stop searching once both times are set
if final_state_time and initial_state_time:
break
if initial_state_time and final_state_time:
duration = final_state_time - initial_state_time
total_durations.append({
'user_id': jira_data['jiraIssueField']['fields']['assignee']['accountId'],
'duration': int(duration.seconds)//60,
'issue': f"https://amberhq.atlassian.net/browse/{jira_data['key']}"
})
return total_durations
def sanity_bug_count(jira_data_list, config):
total_counts = 0
sanity_bug_issue_type = config['sanity_bug_issue_type']
for jira_data in jira_data_list:
if jira_data['jiraIssueField']['fields']['issuetype']["id"] == sanity_bug_issue_type:
total_counts += 1
return total_counts
def total_story_points_delivered(jira_data_list, config):
sum = 0
story_point_field = config['story_point_field']
for jira_data in jira_data_list:
if jira_data['jiraIssueField']['fields'][story_point_field]:
sum += jira_data['jiraIssueField']['fields'][story_point_field]
return sum
def count_bugs_individual(jira_data_list, config):
count = 0
bugs = []
bug_issue_type = config['bug_issue_type']
for jira_data in jira_data_list:
if jira_data['jiraIssueField']['fields']['issuetype']["id"] == bug_issue_type:
count += 1
bugs.append(f"https://amberhq.atlassian.net/browse/{jira_data['key']}")
return bugs
def tickets_going_to_bugfix(jira_data_list, bugfix_state=None):
total_tickets = []
total_count = 0 #counts total instances all issues moved to bugfix (incase one issue moved multiple times)
for jira_data in jira_data_list:
histories = jira_data['jiraIssueHistory']['changelog']['histories']
if jira_data['jiraIssueField']['fields']['assignee'] is None:
continue
moved_to_bugfix_count = 0
for history in histories:
for item in history['items']:
if item['field'] == 'status':
if item['to'] == bugfix_state:
moved_to_bugfix_count += 1 # count of times this issue moved to bugfix
total_count += 1
total_tickets.append(
{'issue': f"https://amberhq.atlassian.net/browse/{jira_data['key']}", 'count': moved_to_bugfix_count})
return total_tickets, total_count
def get_filtered_data(user_id=None, start_date=None, end_date=None, jira_data=[], initial_state = ''):
if not user_id or not start_date or not end_date:
return jira_data
filtered_data_by_user = list(filter(lambda obj: (obj['jiraIssueField']['fields']['assignee'] is not None and
obj['jiraIssueField']['fields']['assignee'][
'accountId'] == user_id), jira_data))
user_name = filtered_data_by_user[0]['jiraIssueField']['fields']['assignee']['displayName']
return filtered_data_by_user, user_name #return all tasks done by user in the quarter (created in the quarter),
# has a small caveat for tickets created in prev quarter and done in this one.
# Pick only tickets moved to in-progress by user in the quarter
# filtered_data = []
# for jira_data in filtered_data_by_user:
# histories = jira_data['jiraIssueHistory']['changelog']['histories']
#
# for history in histories:
# for item in history['items']:
# if 'field' in item and item['field'] == 'status':
# if item['to'] == initial_state:
# if dateutil.parser.parse(history['created']).date() >= start_date and dateutil.parser.parse(
# history['created']).date() <= end_date:
# filtered_data.append(jira_data)
# return filtered_data
def get_pod_specific_config_and_data(project_name):
data = None
config = {}
if project_name.startswith('P1T'):
data = load_json('./JIRA_DUMP/P1T-q4.json')
q3_data = load_json('./JIRA_DUMP/P1T-q3.json')
config = PROJECT_SPECIFIC_CONFIGS['P1T']
elif project_name.startswith('P1D'):
data = load_json('./JIRA_DATA/P1D-q4.json')
q3_data = load_json('./JIRA_DATA/P1D-q3.json')
config = PROJECT_SPECIFIC_CONFIGS['P1D']
elif project_name.startswith('P2'):
data = load_json('./JIRA_DATA/P2-q4.json')
q3_data = load_json('./JIRA_DATA/P2-q3.json')
config = PROJECT_SPECIFIC_CONFIGS['P2']
elif project_name.startswith('POD3'):
data = load_json('./JIRA_DATA/POD3-q4.json')
q3_data = load_json('./JIRA_DATA/POD3-q3.json')
config = PROJECT_SPECIFIC_CONFIGS['POD3']
elif project_name.startswith('PA'):
data = load_json('./JIRA_DATA/PA-q4.json')
q3_data = load_json('./JIRA_DATA/PA-q3.json')
config = PROJECT_SPECIFIC_CONFIGS['PA']
elif project_name.startswith('PLAT'):
data = load_json('./JIRA_DATA/PLAT-q4.json')
q3_data = load_json('./JIRA_DATA/PLAT-q3.json')
config = PROJECT_SPECIFIC_CONFIGS['PLAT']
elif project_name.startswith('PLG'):
data = load_json('./JIRA_DATA/PLG-q4.json')
q3_data = load_json('./JIRA_DATA/PLG-q3.json')
config = PROJECT_SPECIFIC_CONFIGS['PLG']
return data, q3_data, config
def percentage_change(A, B):
"""
Calculate the percentage change from the value A to the value B.
Parameters:
A (float or int): Initial value at the first time interval.
B (float or int): Value at the later time interval.
Returns:
float: Percentage change from A to B.
"""
if A == 0:
raise ValueError("Initial value A cannot be zero because division by zero is undefined.")
return ((B - A) / A) * 100
# def get_kpi_metrics_delta(new_kpis, old_kpis):
# # Each KPI should improve by 10%
#
#
# return {
# 'mean_in_progress_to_qa_ready_time': percentage_change(old_kpis['story_points'], new_kpis['story_points']),
# 'story_points': percentage_change(new_kpis['story_points'], old_kpis['story_points']),
# 'mean_in_progress_to_review_time': percentage_change(new_kpis['mean_in_progress_to_review_time'],old_kpis['mean_in_progress_to_review_time']),
# 'tickets_going_to_bugfix_count': percentage_change(new_kpis['tickets_going_to_bugfix_count'],old_kpis['tickets_going_to_bugfix_count']),
# 'sanity_bugs': percentage_change(new_kpis['sanity_bugs'],old_kpis['sanity_bugs']),
# 'individual_bugs_count': percentage_change(new_kpis['individual_bugs_count'],old_kpis['individual_bugs_count']),
# 'total_tickets_done': percentage_change(new_kpis['total_tickets_done'],old_kpis['total_tickets_done'])
# }
def get_user_level_kpis(user_id=None, jira_data_list=[], start_date=None, end_date=None, project_config={}):
if not user_id or not start_date or not end_date:
return {
'mean_in_progress_to_qa_ready_time': "",
'progress_to_qa_ready_time_issues': [],
'story_points': 0,
'mean_in_progress_to_review_time': "",
'progress_to_review_time_issues': [],
'tickets_going_to_bugfix': [],
'tickets_going_to_bugfix_count': 0,
'individual_bugs': [],
'sanity_bugs': 0,
'individual_bugs_count': 0,
'total_tickets_done': 0,
'name': "N/A"
}
initial_state = project_config['initial_state']
final_state = project_config['final_state']
review_state = project_config['review_state']
bugfix_state = project_config['bugfix_state']
user_specific_data, name = get_filtered_data(user_id, jira_data=jira_data_list, start_date=start_date, end_date=end_date, initial_state = initial_state)
cyle_times_1 = calculate_average_cycle_time_per_user(user_specific_data, initial_state=initial_state,
final_state=final_state)
cyle_times_2 = calculate_average_cycle_time_per_user(user_specific_data, initial_state=initial_state,
final_state=review_state)
story_points = total_story_points_delivered(user_specific_data, project_config)
average_cycle_1 = int(np.mean([d['duration'] for d in cyle_times_1])) if cyle_times_1 else 0
average_cycle_2 = int(np.mean([d['duration'] for d in cyle_times_2])) if cyle_times_2 else 0
tickets_going_to_bugfix_list, tickets_going_to_bugfix_count = tickets_going_to_bugfix(user_specific_data,
bugfix_state=bugfix_state)
sanity_bugs = sanity_bug_count(jira_data_list, project_config)
individual_bugs = count_bugs_individual(user_specific_data, project_config)
individual_bugs_count = len(individual_bugs)
return {
'name': name,
'mean_in_progress_to_qa_ready_time': average_cycle_1,
'progress_to_qa_ready_time_issues': cyle_times_1,
'story_points': story_points,
'mean_in_progress_to_review_time': average_cycle_2,
'progress_to_review_time_issues': cyle_times_2,
'tickets_going_to_bugfix_count': tickets_going_to_bugfix_count,
'sanity_bugs': sanity_bugs,
'individual_bugs_count': individual_bugs_count,
'individual_bugs': individual_bugs,
'tickets_going_to_bugfix': tickets_going_to_bugfix_list,
'total_tickets_done': len(user_specific_data)
}
st.header("KPI Dashboard")
user = st.text_input("Enter user id")
start_date = st.date_input(label="start date", value=date(2014, 1, 1))
end_date = st.date_input(label='end date')
pod = st.selectbox(
label="Select the JIRA Project or Pod",
key="pod_select",
index=None,
options=("P1T", "P1D", "POD3", "P2", "PA", "PLG", "PLAT"),
placeholder="Select Pod/Project...",
)
if user != 'sd' and pod != None:
jira_data_list_per_pod, q3_jira_data_list_per_pod, project_config = get_pod_specific_config_and_data(pod)
res = get_user_level_kpis(user, jira_data_list_per_pod, start_date, end_date, project_config=project_config)
res_q3 = get_user_level_kpis(user, q3_jira_data_list_per_pod, start_date, end_date, project_config=project_config)
if res and res_q3:
# Get delta change in percent for current quarter as compared to previous quarter.
# print(get_kpi_metrics_delta(res, res_q3))
st.subheader(f"User: {res['name']}")
st.header("Speed of Delivery")
q4, q3 = st.columns(2)
with q4:
st.subheader("Q4")
with q3:
st.subheader("Q3")
st.divider()
st_points, st_points_q3 = st.columns(2)
with st_points:
st.metric(label="Story Points", value=res['story_points'])
with st_points_q3:
st.metric(label="Story Points", value=res_q3['story_points'])
mean_in_progress_to_qa_ready_time, mean_in_progress_to_qa_ready_time_q3 = st.columns(2)
with mean_in_progress_to_qa_ready_time:
st.metric(label="Mean In Progress to QA Ready Time", value=f"{res['mean_in_progress_to_qa_ready_time']} mins")
with mean_in_progress_to_qa_ready_time_q3:
st.metric(label="Mean In Progress to QA Ready Time", value=f"{res_q3['mean_in_progress_to_qa_ready_time']} mins")
mean_in_progress_to_review_time, mean_in_progress_to_review_time_q3 = st.columns(2)
with mean_in_progress_to_review_time:
st.metric(label="Mean In Progress to Review Time", value=f" {res['mean_in_progress_to_review_time']} mins")
with mean_in_progress_to_review_time_q3:
st.metric(label="Mean In Progress to Review Time", value=f" {res_q3['mean_in_progress_to_review_time']} mins")
st.subheader("Quality of Delivery")
bg_fix, bg_fix_q3 = st.columns(2)
with bg_fix:
st.metric(label="Tickets Going to Bug Fix", value=res['tickets_going_to_bugfix_count'])
with bg_fix_q3:
st.metric(label="Tickets Going to Bug Fix", value=res_q3['tickets_going_to_bugfix_count'])
sanity_bugs, sanity_bugs_q3 = st.columns(2)
with sanity_bugs:
st.metric(label="Sanity Bugs", value=res['sanity_bugs'])
with sanity_bugs_q3:
st.metric(label="Sanity Bugs", value=res_q3['sanity_bugs'])
individual_bugs, individual_bugs_q3 = st.columns(2)
with individual_bugs:
st.metric(label="Individual Bugs", value=res['individual_bugs_count'])
with individual_bugs_q3:
st.metric(label="Individual Bugs", value=res_q3['individual_bugs_count'])
total_tickets, total_tickets_q3 = st.columns(2)
with total_tickets:
st.metric(label="Total Tickets", value=res['total_tickets_done'])
with total_tickets_q3:
st.metric(label="Total Tickets", value=res_q3['total_tickets_done'])
st.divider()
st.divider()
st.header("Detailed Ticket Views")
st.subheader("In Progress to Ready for QA Issues")
st.table(res['progress_to_qa_ready_time_issues'])
st.divider()
st.subheader("In Progress to Code Review Issues")
st.table(res['progress_to_review_time_issues'])
st.divider()
st.subheader("Tickets Going to Bug Fix")
st.table(res['tickets_going_to_bugfix'])
st.divider()
st.subheader("Individual Bugs List")
st.table(res['individual_bugs'])
# def debug_mode():
# user = '637492759960988ef6bc5f6a'
# pod = 'P1T'
# start_date = date(2024, 1, 1)
# end_date = date.today()
#
# jira_data_list_per_pod, jira_data_list_per_pod_q3, project_config = get_pod_specific_config_and_data(pod)
# res = get_user_level_kpis(user, jira_data_list_per_pod, start_date, end_date, project_config=project_config)
# print(res)
# #
# debug_mode()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment