@beauraines/sprint-tracker 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +16 -0
- package/.github/dependabot.yml +21 -0
- package/CHANGELOG.md +52 -0
- package/R/HeloSprintBurndown.R +138 -0
- package/R/ProjectHealth.R +109 -0
- package/R/backlogByTeam.R +52 -0
- package/R/backlogHealth.R +163 -0
- package/R/burndDownChart.R +144 -0
- package/R/carryOverAdjustedVelocity.R +154 -0
- package/R/featureTeamBacklog.R +73 -0
- package/R/projectHealth.Rmd +111 -0
- package/R/sprintOutcomes.R +142 -0
- package/R/sprintOutcomesCli.R +103 -0
- package/R/sprintOutcomesFeatureTeam.R +143 -0
- package/R/timeInCodeReview.R +157 -0
- package/README.md +181 -0
- package/cli.js +55 -0
- package/cmds/addOutcomes.js +98 -0
- package/cmds/addProject.js +53 -0
- package/cmds/addSprint.js +117 -0
- package/cmds/config.js +33 -0
- package/cmds/getSprintDetails.js +86 -0
- package/cmds/visualizations.js +116 -0
- package/docker-compose.yml +11 -0
- package/migrations/20230222011333-create-health-table.sql +10 -0
- package/migrations/20230222011334-outcomes.sql +42 -0
- package/migrations/20231206093700-seed-outcomes-table.sql +9 -0
- package/migrations.js +20 -0
- package/package.json +50 -0
- package/scripts/generateBacklogByTeam.sh +20 -0
- package/scripts/generateBacklogHealth.sh +12 -0
- package/scripts/generateChart.sh +7 -0
- package/scripts/generateFeatureTeamBacklog.sh +16 -0
- package/scripts/generateSprintOutcomePlot.sh +12 -0
- package/scripts/sprintOutcomes.sh +29 -0
- package/sql/bugsOpenedDuringSprint.sql +14 -0
- package/sql/bugsOpenedDuringSprintFeature.sql +14 -0
- package/sql/capacityAdjustedVelocity.sql +48 -0
- package/sql/carryOver.sql +33 -0
- package/sql/commitment.sql +30 -0
- package/sql/commitmentMet.sql +60 -0
- package/sql/completed.sql +40 -0
- package/sql/descoped.sql +21 -0
- package/sql/pullForward.sql +19 -0
- package/sql/pullForwardFeature.sql +10 -0
- package/sql/sprintOutcomes.sql +25 -0
- package/sql/sprint_outcomes.sql +23 -0
- package/src/addOutcomes.js +94 -0
- package/src/addSprint.js +108 -0
- package/src/getSprintDetails.js +88 -0
- package/utils/display.js +68 -0
- package/utils/input.js +24 -0
- package/utils/readConfig.js +74 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
library(tidyverse)
|
|
2
|
+
library(ggplot2)
|
|
3
|
+
|
|
4
|
+
# sqlite3 --header --csv tracker.db < sql/sprintOutcomes.sql > /tmp/outcomes.csv
|
|
5
|
+
|
|
6
|
+
outcomes = read_csv('/tmp/outcomes.csv')
|
|
7
|
+
|
|
8
|
+
outcomes <- outcomes %>%
|
|
9
|
+
tibble() %>%
|
|
10
|
+
filter(story_points != 0) %>%
|
|
11
|
+
mutate(
|
|
12
|
+
end_date = as.Date(end_date)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# compute velocity statistics
|
|
16
|
+
|
|
17
|
+
entire_project_velocity = (outcomes %>%
|
|
18
|
+
filter(outcome == 'B-Delivered' ) %>%
|
|
19
|
+
summarize( average_velocity = mean(story_points)))$average_velocity
|
|
20
|
+
|
|
21
|
+
average_velocity = (outcomes %>%
|
|
22
|
+
filter(outcome == 'B-Delivered') %>%
|
|
23
|
+
filter(!sprint_name %in% c('Accessibility Improvement 0','Accessibility Improvements 1')) %>%
|
|
24
|
+
summarize( average_velocity = mean(story_points)))$average_velocity
|
|
25
|
+
|
|
26
|
+
# Compute 95% band velocity
|
|
27
|
+
|
|
28
|
+
outcomeColors = c("A-Commitment" = "#0B4F6C",
|
|
29
|
+
"Carryover" = "#D34E24",
|
|
30
|
+
"Carryover - External" = "#d3248d",
|
|
31
|
+
"Descoped" ="#A39BA8",
|
|
32
|
+
"Unplanned - Bug" ="#F28123",
|
|
33
|
+
"Unplanned - Story" = "#F7F052",
|
|
34
|
+
"B-Delivered" ="#00A878")
|
|
35
|
+
outcomeFills = c("A-Commitment" = "#0B4F6C",
|
|
36
|
+
"Carryover" = "#D34E24",
|
|
37
|
+
"Carryover - External" = "#d3248d",
|
|
38
|
+
"Descoped" ="#A39BA8",
|
|
39
|
+
"Unplanned - Bug" ="#F28123",
|
|
40
|
+
"Unplanned - Story" = "#F7F052",
|
|
41
|
+
"B-Delivered" ="#00A878")
|
|
42
|
+
|
|
43
|
+
chartTitle = str_c("Sprint Outcomes - ",unique(outcomes$project_name))
|
|
44
|
+
|
|
45
|
+
plot<-ggplot(outcomes,
|
|
46
|
+
aes(fill=outcome,
|
|
47
|
+
y=story_points,
|
|
48
|
+
# x=end_date
|
|
49
|
+
#x = str_wrap(sprint_name,15)
|
|
50
|
+
x = reorder(str_wrap(sprint_name,15),end_date)
|
|
51
|
+
)
|
|
52
|
+
)+
|
|
53
|
+
geom_bar(position="dodge", stat="identity",color = "black") +
|
|
54
|
+
# add bar labels
|
|
55
|
+
geom_text(data = outcomes %>% mutate(story_points = na_if(story_points,0)),
|
|
56
|
+
aes(label=story_points, y = story_points + .5),
|
|
57
|
+
position = position_dodge(0.9)) +
|
|
58
|
+
# trendline
|
|
59
|
+
geom_smooth(aes(#x=factor(end_date),
|
|
60
|
+
x=factor(reorder(str_wrap(sprint_name,15),end_date)),
|
|
61
|
+
y=story_points,group=outcome),
|
|
62
|
+
data = outcomes%>%filter(outcome=="B-Delivered"),
|
|
63
|
+
#method = "lm", # no method = curved line
|
|
64
|
+
se=FALSE,
|
|
65
|
+
show.legend = FALSE,
|
|
66
|
+
linetype = "dashed",
|
|
67
|
+
color = "#00A878") +
|
|
68
|
+
# TODO better position this annotation
|
|
69
|
+
annotate(geom="text",
|
|
70
|
+
x=length(unique(outcomes$sprint_name))+.25,
|
|
71
|
+
y=average_velocity + 1.5,
|
|
72
|
+
label="Trend Line") +
|
|
73
|
+
# average velocity line
|
|
74
|
+
geom_hline(yintercept=average_velocity,linetype = "dotted", color = "#00A878") +
|
|
75
|
+
annotate(geom="text", x=.75, y=average_velocity + 1.5, label="Average\nVelocity") +
|
|
76
|
+
# plot labels
|
|
77
|
+
labs(title=chartTitle,
|
|
78
|
+
# subtitle = 'Excluding the Data Science, Product Ownership and QA Teams',
|
|
79
|
+
x ="",
|
|
80
|
+
y = "Story Points",
|
|
81
|
+
fill='Outcomes') +
|
|
82
|
+
#colors
|
|
83
|
+
scale_colour_manual(values = outcomeColors) +
|
|
84
|
+
scale_fill_manual(values = outcomeFills) +
|
|
85
|
+
#themes
|
|
86
|
+
theme(
|
|
87
|
+
plot.title = element_text(family = "roboto"),
|
|
88
|
+
axis.title.x = element_text(family = "roboto"),
|
|
89
|
+
axis.text.x = element_text(angle = 45, vjust = 1, hjust=1),
|
|
90
|
+
axis.title.y = element_text(family = "roboto"),
|
|
91
|
+
panel.grid.major.y = element_line(colour = "#D9D9D9"), panel.grid.minor.y = element_line(colour = "#D9D9D9"),
|
|
92
|
+
panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank(),
|
|
93
|
+
# background color
|
|
94
|
+
panel.background = element_rect(fill = "#D9D9D920", colour = NA),
|
|
95
|
+
legend.position = "bottom",
|
|
96
|
+
plot.background = element_rect(colour = "#D9D9D9", fill=NA)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# plot # commented out for docker
|
|
100
|
+
|
|
101
|
+
ggsave(str_c("~/projects/sprint-tracker/",str_replace(unique(outcomes$project_name)," ",""),"SprintOutcomesCli.png"),plot=plot,bg="white", width = 2882, height = 1700, units = "px")
|
|
102
|
+
|
|
103
|
+
print(str_c("Saving to ",str_c("~/projects/sprint-tracker/",str_replace(unique(outcomes$project_name)," ",""),"SprintOutcomesCli.png"),sep = " "))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
library(tidyverse)
|
|
2
|
+
library(ggplot2)
|
|
3
|
+
|
|
4
|
+
# sqlite3 --header --csv tracker.db < sprint_outcomes.sql > /tmp/outcomes.csv
|
|
5
|
+
|
|
6
|
+
library(DBI)
|
|
7
|
+
library(RSQLite)
|
|
8
|
+
con <- dbConnect(RSQLite::SQLite(),"~/projects/sprint-tracker/tracker.db")
|
|
9
|
+
|
|
10
|
+
res <- dbSendQuery(con, "-- SQLite
|
|
11
|
+
SELECT
|
|
12
|
+
p.name project_name,
|
|
13
|
+
s.id sprint_id,
|
|
14
|
+
s.sprint_name,
|
|
15
|
+
s.end_date,
|
|
16
|
+
case
|
|
17
|
+
when o.name = 'Commitment' then 'A-Commitment'
|
|
18
|
+
when o.name = 'Delivered' then 'B-Delivered'
|
|
19
|
+
else o.name
|
|
20
|
+
end outcome,
|
|
21
|
+
-- s.commited_points,
|
|
22
|
+
-- so.issue_count,
|
|
23
|
+
-- so.story_points,
|
|
24
|
+
ifnull(so.story_points, so.issue_count) AS story_points -- accounts for unpointed bug stories
|
|
25
|
+
|
|
26
|
+
from projects p
|
|
27
|
+
|
|
28
|
+
JOIN sprints s on s.project_id = p.id
|
|
29
|
+
JOIN sprint_outcomes so on so.sprint_id = s.id
|
|
30
|
+
JOIN outcomes o on o.id = so.outcome_id
|
|
31
|
+
|
|
32
|
+
WHERE
|
|
33
|
+
-- p.id = 10 -- Helo Project Team
|
|
34
|
+
p.id = 11 -- Feature Team One
|
|
35
|
+
|
|
36
|
+
")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# TODO query sqlite directly
|
|
40
|
+
outcomes <- dbFetch(res)
|
|
41
|
+
|
|
42
|
+
# Clear the result
|
|
43
|
+
dbClearResult(res)
|
|
44
|
+
|
|
45
|
+
# Disconnect from the database
|
|
46
|
+
dbDisconnect(con)
|
|
47
|
+
|
|
48
|
+
outcomes <- outcomes %>%
|
|
49
|
+
tibble() %>%
|
|
50
|
+
filter(story_points != 0) %>%
|
|
51
|
+
mutate(
|
|
52
|
+
end_date = as.Date(end_date)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# compute velocity statistics
|
|
56
|
+
|
|
57
|
+
entire_project_velocity = (outcomes %>%
|
|
58
|
+
filter(outcome == 'B-Delivered' ) %>%
|
|
59
|
+
summarize( average_velocity = mean(story_points)))$average_velocity
|
|
60
|
+
|
|
61
|
+
average_velocity = (outcomes %>%
|
|
62
|
+
filter(outcome == 'B-Delivered') %>%
|
|
63
|
+
filter(!sprint_name %in% c('Accessibility Improvement 0','Accessibility Improvements 1')) %>%
|
|
64
|
+
summarize( average_velocity = mean(story_points)))$average_velocity
|
|
65
|
+
|
|
66
|
+
# Compute 95% band velocity
|
|
67
|
+
|
|
68
|
+
outcomeColors = c("A-Commitment" = "#0B4F6C",
|
|
69
|
+
"Carryover" = "#D34E24",
|
|
70
|
+
"Carryover - External" = "#d3248d",
|
|
71
|
+
"Descoped" ="#A39BA8",
|
|
72
|
+
"Unplanned - Bug" ="#F28123",
|
|
73
|
+
"Unplanned - Story" = "#F7F052",
|
|
74
|
+
"B-Delivered" ="#00A878")
|
|
75
|
+
outcomeFills = c("A-Commitment" = "#0B4F6C",
|
|
76
|
+
"Carryover" = "#D34E24",
|
|
77
|
+
"Carryover - External" = "#d3248d",
|
|
78
|
+
"Descoped" ="#A39BA8",
|
|
79
|
+
"Unplanned - Bug" ="#F28123",
|
|
80
|
+
"Unplanned - Story" = "#F7F052",
|
|
81
|
+
"B-Delivered" ="#00A878")
|
|
82
|
+
|
|
83
|
+
chartTitle = str_c("Sprint Outcomes - ",unique(outcomes$project_name))
|
|
84
|
+
|
|
85
|
+
plot<-ggplot(outcomes,
|
|
86
|
+
aes(fill=outcome,
|
|
87
|
+
y=story_points,
|
|
88
|
+
# x=end_date
|
|
89
|
+
#x = str_wrap(sprint_name,15)
|
|
90
|
+
x = reorder(str_wrap(sprint_name,15),end_date)
|
|
91
|
+
)
|
|
92
|
+
)+
|
|
93
|
+
geom_bar(position="dodge", stat="identity",color = "black") +
|
|
94
|
+
# add bar labels
|
|
95
|
+
geom_text(data = outcomes %>% mutate(story_points = na_if(story_points,0)),
|
|
96
|
+
aes(label=story_points, y = story_points + .5),
|
|
97
|
+
position = position_dodge(0.9)) +
|
|
98
|
+
# trendline
|
|
99
|
+
geom_smooth(aes(#x=factor(end_date),
|
|
100
|
+
x=factor(reorder(str_wrap(sprint_name,15),end_date)),
|
|
101
|
+
y=story_points,group=outcome),
|
|
102
|
+
data = outcomes%>%filter(outcome=="B-Delivered"),
|
|
103
|
+
#method = "lm", # no method = curved line
|
|
104
|
+
se=FALSE,
|
|
105
|
+
show.legend = FALSE,
|
|
106
|
+
linetype = "dashed",
|
|
107
|
+
color = "#00A878") +
|
|
108
|
+
# TODO better position this annotation
|
|
109
|
+
annotate(geom="text",
|
|
110
|
+
x=length(unique(outcomes$sprint_name))+.25,
|
|
111
|
+
y=average_velocity + 1.5,
|
|
112
|
+
label="Trend Line") +
|
|
113
|
+
# average velocity line
|
|
114
|
+
geom_hline(yintercept=average_velocity,linetype = "dotted", color = "#00A878") +
|
|
115
|
+
annotate(geom="text", x=.75, y=average_velocity + 1.5, label="Average\nVelocity") +
|
|
116
|
+
# plot labels
|
|
117
|
+
labs(title=chartTitle,
|
|
118
|
+
# subtitle = 'Excluding the Data Science, Product Ownership and QA Teams',
|
|
119
|
+
x ="",
|
|
120
|
+
y = "Story Points",
|
|
121
|
+
fill='Outcomes') +
|
|
122
|
+
#colors
|
|
123
|
+
scale_colour_manual(values = outcomeColors) +
|
|
124
|
+
scale_fill_manual(values = outcomeFills) +
|
|
125
|
+
#themes
|
|
126
|
+
theme(
|
|
127
|
+
plot.title = element_text(family = "roboto"),
|
|
128
|
+
axis.title.x = element_text(family = "roboto"),
|
|
129
|
+
axis.text.x = element_text(angle = 45, vjust = 1, hjust=1),
|
|
130
|
+
axis.title.y = element_text(family = "roboto"),
|
|
131
|
+
panel.grid.major.y = element_line(colour = "#D9D9D9"), panel.grid.minor.y = element_line(colour = "#D9D9D9"),
|
|
132
|
+
panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank(),
|
|
133
|
+
# background color
|
|
134
|
+
panel.background = element_rect(fill = "#D9D9D920", colour = NA),
|
|
135
|
+
legend.position = "bottom",
|
|
136
|
+
plot.background = element_rect(colour = "#D9D9D9", fill=NA)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# plot # commented out for docker
|
|
140
|
+
|
|
141
|
+
ggsave(str_c("~/projects/sprint-tracker/",str_replace(unique(outcomes$project_name)," ",""),"SprintOutcomes.png"),plot=plot,bg="white", width = 2882, height = 1700, units = "px")
|
|
142
|
+
|
|
143
|
+
print(str_c("Saving to ",str_c("~/projects/sprint-tracker/",str_replace(unique(outcomes$project_name)," ",""),"SprintOutcomes.png"),sep = " "))
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
library(tidyverse)
|
|
2
|
+
library(lubridate)
|
|
3
|
+
library(ggplot2)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# json2csv -i Sprint12Transitions.json --unwind changelog --flatten-objects -o Sprint12Transitions.csv
|
|
7
|
+
|
|
8
|
+
# transitions = read_csv('~/projects/sprint-tracker/Sprint12Transitions.csv') %>%
|
|
9
|
+
transitions = read_csv('~/projects/sprint-tracker/code-review/transitions.csv') %>%
|
|
10
|
+
rename(timestamp = changelog.timestamp,
|
|
11
|
+
from = changelog.transition.from,
|
|
12
|
+
to = changelog.transition.to) %>%
|
|
13
|
+
mutate(transition = str_c(from,to,sep="-")) %>%
|
|
14
|
+
filter(str_detect(key,"ACDC"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# single = transitions %>%
|
|
18
|
+
# filter(key == 'ACDC-250') %>%
|
|
19
|
+
# filter(transition %in% c("In Progress-Code Review","Code Review-QA Review")) %>%
|
|
20
|
+
# pivot_wider(id_cols = c(key,summary),
|
|
21
|
+
# names_from = transition,
|
|
22
|
+
# values_from = timestamp) %>%
|
|
23
|
+
# mutate(
|
|
24
|
+
# duration = `Code Review-QA Review` - `In Progress-Code Review`
|
|
25
|
+
# )
|
|
26
|
+
|
|
27
|
+
time_in_code_review = transitions %>%
|
|
28
|
+
filter(transition %in% c("In Progress-Code Review","Code Review-QA Review")) %>%
|
|
29
|
+
pivot_wider(id_cols = c(sprint,key,summary),
|
|
30
|
+
names_from = transition,
|
|
31
|
+
values_from = timestamp,
|
|
32
|
+
# the min function is to get the earliest transition
|
|
33
|
+
values_fn = min) %>%
|
|
34
|
+
relocate(`In Progress-Code Review`,.after=summary) %>%
|
|
35
|
+
mutate(
|
|
36
|
+
duration = difftime(`Code Review-QA Review`,`In Progress-Code Review`,units = "days")
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
summary = time_in_code_review %>%
|
|
40
|
+
group_by(sprint) %>%
|
|
41
|
+
summarise(mean = mean(duration,na.rm=TRUE),
|
|
42
|
+
min = min(duration,na.rm=TRUE),
|
|
43
|
+
max=max(duration,na.rm=TRUE),
|
|
44
|
+
sd=sd(duration,na.rm=TRUE),
|
|
45
|
+
count = n()
|
|
46
|
+
)
|
|
47
|
+
print(summary)
|
|
48
|
+
|
|
49
|
+
summary %>%
|
|
50
|
+
filter(sprint != 'Bulk Assign API') %>%
|
|
51
|
+
pivot_longer(
|
|
52
|
+
# cols = c(mean_time,max_time,min_time),
|
|
53
|
+
cols = c(mean,max,min),
|
|
54
|
+
names_to = "statistic",
|
|
55
|
+
values_to = "days",
|
|
56
|
+
values_transform = as.numeric
|
|
57
|
+
) %>%
|
|
58
|
+
select(sprint,statistic,days) %>%
|
|
59
|
+
# filter(statistic=="mean") %>%
|
|
60
|
+
ggplot(
|
|
61
|
+
aes(x=reorder(sprint,as.numeric(str_extract(sprint,"[0-9]+"))),
|
|
62
|
+
y=days,
|
|
63
|
+
color=statistic,
|
|
64
|
+
group = statistic)
|
|
65
|
+
) +
|
|
66
|
+
geom_point() +
|
|
67
|
+
geom_line() +
|
|
68
|
+
labs(title="Time in Code Review", x ="", y = "Days") +
|
|
69
|
+
theme(
|
|
70
|
+
plot.title = element_text(family = "roboto"),
|
|
71
|
+
axis.title.x = element_text(family = "roboto"),
|
|
72
|
+
axis.text.x = element_text(angle = 45, vjust = 1, hjust=1),
|
|
73
|
+
axis.title.y = element_text(family = "roboto"),
|
|
74
|
+
panel.grid.major.y = element_line(colour = "#D9D9D9"), panel.grid.minor.y = element_line(colour = "#D9D9D9"),
|
|
75
|
+
panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank(),
|
|
76
|
+
# background color
|
|
77
|
+
panel.background = element_rect(fill = "#D9D9D920", colour = NA),
|
|
78
|
+
legend.position = "bottom",
|
|
79
|
+
plot.background = element_rect(colour = "#D9D9D9", fill=NA)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
mean_code_review = mean(time_in_code_review$duration,na.rm=TRUE)
|
|
84
|
+
meet_SLA = time_in_code_review %>% filter(duration <= 1 ) %>% count() %>% pull()
|
|
85
|
+
percent_meet_SLA = (meet_SLA/count(time_in_code_review)) %>% pull()
|
|
86
|
+
|
|
87
|
+
time_in_code_review %>%
|
|
88
|
+
filter(sprint != 'Bulk Assign API') %>%
|
|
89
|
+
ggplot(
|
|
90
|
+
aes(x=reorder(sprint,as.numeric(str_extract(sprint,"[0-9]+"))),
|
|
91
|
+
y=duration)
|
|
92
|
+
) +
|
|
93
|
+
geom_point()+
|
|
94
|
+
geom_jitter()+
|
|
95
|
+
geom_hline(yintercept = 1, color = "red") +
|
|
96
|
+
labs(title="Time in Code Review",
|
|
97
|
+
subtitle = "1 day SLA",
|
|
98
|
+
x ="",
|
|
99
|
+
y = "Days") +
|
|
100
|
+
annotate(geom="text",
|
|
101
|
+
x=1,
|
|
102
|
+
y=9,
|
|
103
|
+
hjust = 0,
|
|
104
|
+
color = "red",
|
|
105
|
+
label=str_c(scales::label_percent()(percent_meet_SLA)," meet 1 day SLA for review")) +
|
|
106
|
+
theme(
|
|
107
|
+
plot.title = element_text(family = "roboto"),
|
|
108
|
+
axis.title.x = element_text(family = "roboto"),
|
|
109
|
+
axis.text.x = element_text(angle = 45, vjust = 1, hjust=1),
|
|
110
|
+
axis.title.y = element_text(family = "roboto"),
|
|
111
|
+
panel.grid.major.y = element_line(colour = "#D9D9D9"), panel.grid.minor.y = element_line(colour = "#D9D9D9"),
|
|
112
|
+
panel.grid.major.x = element_blank(), panel.grid.minor.x = element_blank(),
|
|
113
|
+
# background color
|
|
114
|
+
panel.background = element_rect(fill = "#D9D9D920", colour = NA),
|
|
115
|
+
legend.position = "bottom",
|
|
116
|
+
plot.background = element_rect(colour = "#D9D9D9", fill=NA)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
ggsave("~/projects/sprint-tracker/time_in_code_review.png",bg="white", width = 2882, height = 1700, units = "px")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
time_in_code_review %>%
|
|
123
|
+
filter(sprint != 'Bulk Assign API') %>%
|
|
124
|
+
ggplot(aes(x=duration)) +
|
|
125
|
+
geom_histogram(binwidth=1,color="black", fill="white") +
|
|
126
|
+
stat_bin(binwidth=1,
|
|
127
|
+
geom="text",
|
|
128
|
+
colour="black",
|
|
129
|
+
size=3.5,
|
|
130
|
+
aes(label=..count..),
|
|
131
|
+
position=position_stack(vjust=0.5)) +
|
|
132
|
+
# geom_density(alpha=.2, fill="#FF6666") +
|
|
133
|
+
scale_x_continuous(minor_breaks = seq(1, 20, 1),
|
|
134
|
+
# limits=c(0,15)
|
|
135
|
+
) +
|
|
136
|
+
labs(title="Time in Code Review",
|
|
137
|
+
subtitle = "Histogram of 1 day buckets",
|
|
138
|
+
x ="",
|
|
139
|
+
y = "Days") +
|
|
140
|
+
theme(
|
|
141
|
+
plot.title = element_text(family = "roboto"),
|
|
142
|
+
axis.title.x = element_text(family = "roboto"),
|
|
143
|
+
# axis.text.x = element_text(angle = 45, vjust = 1, hjust=1),
|
|
144
|
+
axis.title.y = element_text(family = "roboto"),
|
|
145
|
+
panel.grid.major.y = element_line(colour = "#D9D9D9"),
|
|
146
|
+
panel.grid.minor.y = element_line(colour = "#D9D9D9"),
|
|
147
|
+
panel.grid.major.x = element_line(colour = "#D9D9D9"),
|
|
148
|
+
panel.grid.minor.x = element_line(colour = "#D9D9D9"),
|
|
149
|
+
# background color
|
|
150
|
+
panel.background = element_rect(fill = "#D9D9D920", colour = NA),
|
|
151
|
+
legend.position = "bottom",
|
|
152
|
+
plot.background = element_rect(colour = "#D9D9D9", fill=NA)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
ggsave("~/projects/sprint-tracker/time_in_code_review_histogram.png",bg="white", width = 2882, height = 1700, units = "px")
|
|
156
|
+
|
|
157
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Sprint Tracker
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
Collects sprint outcome information and generates a visualization of sprint outcomes.
|
|
5
|
+
|
|
6
|
+
## Visualizations
|
|
7
|
+
|
|
8
|
+
Using the `visualization` command, you can generate a sprint outcome and velocity visualization. An example is shown below, with sprint names across the bottom. For each sprint, it compares the commitment and delivered story points, as well as some information about stories added and removed from the sprint.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Command Information
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
sprint-tracker <cmd> [args]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
sprint-tracker addOutcome Menu driven process to add outcomes to a
|
|
20
|
+
sprint
|
|
21
|
+
sprint-tracker addProject Prompts to add a new project to the database
|
|
22
|
+
sprint-tracker addSprint Prompts to add a new sprint to an existing
|
|
23
|
+
project
|
|
24
|
+
sprint-tracker config Creates a config file, prompting for required
|
|
25
|
+
inputs
|
|
26
|
+
sprint-tracker getSprint Gets sprint details
|
|
27
|
+
sprint-tracker visualizations Creates Feature Team visualizations with R.
|
|
28
|
+
Must have docker, rstudio-tidyverse container
|
|
29
|
+
and more installed
|
|
30
|
+
sprint-tracker create-database Creates and initializes the empty database
|
|
31
|
+
specified in the config
|
|
32
|
+
sprint-tracker upgrade-database Upgrades database with new structure
|
|
33
|
+
sprint-tracker completion Outputs bash/zsh-completion shortcuts for
|
|
34
|
+
commands and options to add to .bashrc or
|
|
35
|
+
.bash_profile
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--version Show version number [boolean]
|
|
39
|
+
--help Show help [boolean]
|
|
40
|
+
```
|
|
41
|
+
## Dependencies
|
|
42
|
+
|
|
43
|
+
This CLI application relies on a local [sqlite](https://www.sqlite.org/download.html) database, [docker](https://docs.docker.com/desktop/?_gl=1*ogftl2*_ga*OTE5MjI3MDc1LjE3MDE5MDA5NTE.*_ga_XJWPQMJYHQ*MTcwMTkwMDk1MS4xLjEuMTcwMTkwMDk5My4xOC4wLjA.), and the [rocker](https://hub.docker.com/r/rocker/tidyverse)/tidyverse](https://hub.docker.com/r/rocker/tidyverse) image to function. Installation of these dependencies is outside of the scope of this project as the steps vary by OS.
|
|
44
|
+
|
|
45
|
+
On Mac, these can be installed with homebrew.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
brew install docker sqlite
|
|
49
|
+
docker pull rocker/tidyverse
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## General Usage
|
|
53
|
+
|
|
54
|
+
1. Update the config file with the path to the database `sprint-tracker config`. The database doesn't need to exist yet. This step only needs to be done the first time.
|
|
55
|
+
7. Create the database and run the migrations `sprint-tracker create-database`. This step only needs to be done the first time.
|
|
56
|
+
8. Run `sprint-tracker addProject` to add a project to the database. This step can be repeated as new projects are started.
|
|
57
|
+
9. Add the first sprint with `sprint-tracker addSprint` optionally, adding the sprint commitment. This step can be repeated for subsequent sprints.
|
|
58
|
+
10. At the end of the sprint, record sprint outcomes `sprint-tracker addOutcome`. This will allow you to log information about stories and points completed, committed, pulled forward, carried over, and bugs.
|
|
59
|
+
11. Generate the cumulative sprint outcomes visualization with `sprint-tracker visualizations`. If you do not have the [rocker/tidyverse](https://hub.docker.com/r/rocker/tidyverse) image, it will automatically pull it, which will take a little bit of time
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
A configuration file `~/sprintTracker.json` is required with the full path to your tracker database
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"database":"/Users/beauraines/projects/sprint-tracker/tracker.db"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Installation and Setup for Development
|
|
73
|
+
|
|
74
|
+
1. Clone the repository
|
|
75
|
+
2. Ensure you have sqlite3 and docker installed. See [Dependencies](#dependencies).
|
|
76
|
+
3. Install npm dependencies `npm i`
|
|
77
|
+
4. Globally install the sprint tracker `npm -g i`
|
|
78
|
+
5. Follow the [General Usage](#general-usage) instructions.
|
|
79
|
+
|
|
80
|
+
## Tracking Outcomes
|
|
81
|
+
|
|
82
|
+
### Project
|
|
83
|
+
|
|
84
|
+
This table will store project master data.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
### Sprint
|
|
88
|
+
Sprint details including the dates and original commitment. The original commitment will not change and can be sourced from the Jira "Velocity Report" for the sprint.
|
|
89
|
+
|
|
90
|
+
Non-working days and PTO will be recorded to be able to normalize Sprints with different durations.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### Sprint Outcomes
|
|
94
|
+
|
|
95
|
+
1. Commitment : Initial sprint commitment
|
|
96
|
+
1. Delivered : Work completed this sprint
|
|
97
|
+
1. Unplanned - Story : All unplanned work brought into the sprint, whether completed or not
|
|
98
|
+
1. Unplanned - Bug : Unplanned work without a pre-existing story that was deemed critical enough to add to the sprint
|
|
99
|
+
1. Descoped : Committed work, removed from the sprint
|
|
100
|
+
1. Carryover : Incomplete, started or unstarted, work from this sprint, assumed that it will carryover to the next sprint
|
|
101
|
+
1. Carryover - External : Work that was carried over due to blockers external to the scrum team. Use of this is optional, as an agile team should be fully self-sufficient.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### ERD
|
|
105
|
+
|
|
106
|
+
```mermaid
|
|
107
|
+
erDiagram
|
|
108
|
+
PROJECTS {
|
|
109
|
+
int id
|
|
110
|
+
string name
|
|
111
|
+
string pm_name
|
|
112
|
+
string pm_email
|
|
113
|
+
string client
|
|
114
|
+
string project_number
|
|
115
|
+
date start_date
|
|
116
|
+
date end_date
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
SPRINTS {
|
|
120
|
+
int id
|
|
121
|
+
int project_id
|
|
122
|
+
string sprint_name
|
|
123
|
+
date start_date
|
|
124
|
+
date end_date
|
|
125
|
+
number committed_points
|
|
126
|
+
number committed_stories
|
|
127
|
+
number non_working_days
|
|
128
|
+
number developers
|
|
129
|
+
number developer_pto_days
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
SPRINT_OUTCOMES {
|
|
133
|
+
int id
|
|
134
|
+
int sprint_id
|
|
135
|
+
int outcome_id
|
|
136
|
+
int issue_count
|
|
137
|
+
int story_points
|
|
138
|
+
text notes
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
OUTCOMES {
|
|
142
|
+
int id
|
|
143
|
+
string name
|
|
144
|
+
text description
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
PROJECTS ||--o{ SPRINTS : ""
|
|
148
|
+
SPRINTS ||--|{ SPRINT_OUTCOMES : ""
|
|
149
|
+
OUTCOMES ||--o{ SPRINT_OUTCOMES: ""
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Generating Visualizations with R and Docker
|
|
155
|
+
|
|
156
|
+
There are several R scripts included in the project repository, more than just the sprint outcomes. They can be run from the command line using docker and the `rocker/tidyverse` image as shown below.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# PROJECT_DIR must have trailing slash
|
|
160
|
+
PROJECT_DIR=/Users/beauraines/projects/
|
|
161
|
+
IMAGE=rocker/tidyverse
|
|
162
|
+
docker run -it --rm -v ${PROJECT_DIR}:/home/rstudio/projects/ ${IMAGE} su -c 'Rscript /home/rstudio/projects/sprint-tracker/R/sprintOutcomes.R' rstudio
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Roadmap
|
|
166
|
+
|
|
167
|
+
1. ~~Build local database using sqlite3~~
|
|
168
|
+
2. ~~Script visualizations, possibly with R, graph-cli, chartsjs, PowerBI or AWS QuickSight~~
|
|
169
|
+
3. [Backlog health tracking](https://github.com/beauraines/sprint-tracker/issues/36)
|
|
170
|
+
4. Write API for adding data
|
|
171
|
+
5. Write API for getting visualizations
|
|
172
|
+
6. ~~Create database migrations~~
|
|
173
|
+
7. Deploy to cloud
|
|
174
|
+
8. Add sprint metrics
|
|
175
|
+
1. Average velocity over life of project
|
|
176
|
+
2. Recent average velocity
|
|
177
|
+
3. 90% confidence range. See <https://resources.scrumalliance.org/Article/its-target-forecast>
|
|
178
|
+
4. Daily points per developer
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
package/cli.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
require('yargs')
|
|
4
|
+
.scriptName("sprint-tracker")
|
|
5
|
+
.usage('$0 <cmd> [args]')
|
|
6
|
+
.commandDir('cmds')
|
|
7
|
+
.command({
|
|
8
|
+
command: 'create-database',
|
|
9
|
+
desc: 'Creates and initializes the empty database specified in the config',
|
|
10
|
+
handler: async () => {
|
|
11
|
+
var sqlite3 = require('sqlite3').verbose();
|
|
12
|
+
let {CommandsRunner, SQLite3Driver} = require('node-db-migration');
|
|
13
|
+
const {homedir} = require('os');
|
|
14
|
+
const {readConfig} = require('./utils/readConfig.js')
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const filename = 'sprintTracker.json'
|
|
18
|
+
const configFile = path.join(homedir(),filename)
|
|
19
|
+
const config = await readConfig(configFile)
|
|
20
|
+
const database = config.database
|
|
21
|
+
var db = new sqlite3.Database(database);
|
|
22
|
+
let migrations = new CommandsRunner({
|
|
23
|
+
driver: new SQLite3Driver(db),
|
|
24
|
+
directoryWithScripts: __dirname + '/migrations',
|
|
25
|
+
});
|
|
26
|
+
await migrations.run('init')
|
|
27
|
+
await migrations.run('migrate')
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.command({
|
|
31
|
+
command: 'upgrade-database',
|
|
32
|
+
desc: 'Upgrades database with new structure',
|
|
33
|
+
handler: async () => {
|
|
34
|
+
var sqlite3 = require('sqlite3').verbose();
|
|
35
|
+
let {CommandsRunner, SQLite3Driver} = require('node-db-migration');
|
|
36
|
+
const {homedir} = require('os');
|
|
37
|
+
const {readConfig} = require('./utils/readConfig.js')
|
|
38
|
+
const path = require('path');
|
|
39
|
+
|
|
40
|
+
const filename = 'sprintTracker.json'
|
|
41
|
+
const configFile = path.join(homedir(),filename)
|
|
42
|
+
const config = await readConfig(configFile)
|
|
43
|
+
const database = config.database
|
|
44
|
+
var db = new sqlite3.Database(database);
|
|
45
|
+
let migrations = new CommandsRunner({
|
|
46
|
+
driver: new SQLite3Driver(db),
|
|
47
|
+
directoryWithScripts: __dirname + '/migrations',
|
|
48
|
+
});
|
|
49
|
+
await migrations.run('migrate')
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.completion('completion', 'Outputs bash/zsh-completion shortcuts for commands and options to add to .bashrc or .bash_profile')
|
|
53
|
+
.demandCommand()
|
|
54
|
+
.help()
|
|
55
|
+
.argv
|