jirametrics 2.6 → 2.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +6 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +6 -6
- data/lib/jirametrics/aging_work_in_progress_chart.rb +1 -1
- data/lib/jirametrics/aging_work_table.rb +4 -5
- data/lib/jirametrics/blocked_stalled_change.rb +1 -1
- data/lib/jirametrics/board.rb +14 -12
- data/lib/jirametrics/chart_base.rb +16 -10
- data/lib/jirametrics/cycletime_config.rb +26 -7
- data/lib/jirametrics/cycletime_histogram.rb +1 -1
- data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
- data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
- data/lib/jirametrics/daily_wip_chart.rb +7 -9
- data/lib/jirametrics/data_quality_report.rb +166 -7
- data/lib/jirametrics/dependency_chart.rb +3 -4
- data/lib/jirametrics/discard_changes_before.rb +1 -1
- data/lib/jirametrics/downloader.rb +14 -13
- data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
- data/lib/jirametrics/examples/aggregated_project.rb +1 -3
- data/lib/jirametrics/examples/standard_project.rb +10 -9
- data/lib/jirametrics/expedited_chart.rb +1 -2
- data/lib/jirametrics/exporter.rb +25 -0
- data/lib/jirametrics/file_system.rb +1 -1
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html/index.css +10 -3
- data/lib/jirametrics/html_report_config.rb +2 -1
- data/lib/jirametrics/issue.rb +63 -11
- data/lib/jirametrics/jira_gateway.rb +1 -1
- data/lib/jirametrics/project_config.rb +27 -19
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/sprint_burndown.rb +2 -2
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/status_collection.rb +4 -0
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics.rb +15 -5
- metadata +8 -7
- data/lib/jirametrics/html/data_quality_report.erb +0 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5bb71d8f3c6ab4cf1fa17ad93ad1c00ba1b8381c196bc15a054095a5a91917a
|
4
|
+
data.tar.gz: 790abb0404719d6b55d833ad76d34121875997f3ec3967f69f5d467fae203752
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f71c2ae123a13435a1147ceaadf7f10364a6cdf1421b4f8491633936189e3744e58da4d99be6c43c6e8034d5f27afc470498532157ac15a7d4459a0399f6fc12
|
7
|
+
data.tar.gz: f33eb0ea450d6002b09d84311823fc20847327bb78dca5fa218c3f724a23f02d49f4ef0183e9dc02499e0eea1f5916147396538751192974926e1c197f4f0bb5
|
@@ -62,7 +62,12 @@ class AggregateConfig
|
|
62
62
|
'the first file section'
|
63
63
|
end
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
|
+
if issues.nil?
|
67
|
+
log "No issues found for #{project_name}"
|
68
|
+
else
|
69
|
+
@project_config.add_issues issues
|
70
|
+
end
|
66
71
|
end
|
67
72
|
|
68
73
|
def find_time_range projects:
|
@@ -53,8 +53,8 @@ class AgingWorkBarChart < ChartBase
|
|
53
53
|
percentage_line_x = date_range.end - calculate_percent_line if percentage
|
54
54
|
|
55
55
|
if aging_issues.empty?
|
56
|
-
@description_text =
|
57
|
-
return render_top_text(binding)
|
56
|
+
@description_text = '<p>There is no aging work</p>'
|
57
|
+
return render_top_text(binding)
|
58
58
|
end
|
59
59
|
|
60
60
|
wrap_and_render(binding, __FILE__)
|
@@ -62,7 +62,7 @@ class AgingWorkBarChart < ChartBase
|
|
62
62
|
|
63
63
|
def data_sets_for_one_issue issue:, today:
|
64
64
|
cycletime = issue.board.cycletime
|
65
|
-
issue_start_time = cycletime.
|
65
|
+
issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
|
66
66
|
issue_start_date = issue_start_time.to_date
|
67
67
|
issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
|
68
68
|
[
|
@@ -92,8 +92,8 @@ class AgingWorkBarChart < ChartBase
|
|
92
92
|
|
93
93
|
def select_aging_issues issues:
|
94
94
|
issues.select do |issue|
|
95
|
-
|
96
|
-
|
95
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
96
|
+
started_time && stopped_time.nil?
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
@@ -107,7 +107,7 @@ class AgingWorkBarChart < ChartBase
|
|
107
107
|
def status_data_sets issue:, label:, today:
|
108
108
|
cycletime = issue.board.cycletime
|
109
109
|
|
110
|
-
issue_started_time = cycletime.
|
110
|
+
issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
|
111
111
|
|
112
112
|
previous_start = nil
|
113
113
|
previous_status = nil
|
@@ -114,7 +114,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
114
114
|
def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
|
115
115
|
issues.filter_map do |issue|
|
116
116
|
stop = issue.first_time_in_status(*status_ids)
|
117
|
-
start = issue.board.cycletime.
|
117
|
+
start, = issue.board.cycletime.started_stopped_times(issue)
|
118
118
|
|
119
119
|
# Skip if either it hasn't crossed the boundary or we can't tell when it started.
|
120
120
|
next if stop.nil? || start.nil?
|
@@ -36,16 +36,15 @@ class AgingWorkTable < ChartBase
|
|
36
36
|
|
37
37
|
def expedited_but_not_started
|
38
38
|
@issues.select do |issue|
|
39
|
-
|
40
|
-
|
39
|
+
started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
40
|
+
started_time.nil? && stopped_time.nil? && issue.expedited?
|
41
41
|
end.sort_by(&:created)
|
42
42
|
end
|
43
43
|
|
44
44
|
def select_aging_issues
|
45
45
|
aging_issues = @issues.select do |issue|
|
46
46
|
cycletime = issue.board.cycletime
|
47
|
-
started = cycletime.
|
48
|
-
stopped = cycletime.stopped_time(issue)
|
47
|
+
started, stopped = cycletime.started_stopped_times(issue)
|
49
48
|
next false if started.nil? || stopped
|
50
49
|
next true if issue.blocked_on_date?(@today, end_time: time_range.end) || issue.expedited?
|
51
50
|
|
@@ -64,7 +63,7 @@ class AgingWorkTable < ChartBase
|
|
64
63
|
end
|
65
64
|
|
66
65
|
def blocked_text issue
|
67
|
-
started_time = issue.board.cycletime.
|
66
|
+
started_time, _stopped_time = issue.board.cycletime.started_stopped_times(issue)
|
68
67
|
return nil if started_time.nil?
|
69
68
|
|
70
69
|
current = issue.blocked_stalled_changes(end_time: time_range.end)[-1]
|
data/lib/jirametrics/board.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Board
|
4
|
-
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :
|
4
|
+
attr_reader :visible_columns, :raw, :possible_statuses, :sprints, :board_type
|
5
5
|
attr_accessor :cycletime, :project_config
|
6
6
|
|
7
7
|
def initialize raw:, possible_statuses: StatusCollection.new
|
@@ -15,24 +15,26 @@ class Board
|
|
15
15
|
# For a Kanban board, the first column here will always be called 'Backlog' and will NOT be
|
16
16
|
# visible on the board. If the board is configured to have a kanban backlog then it will have
|
17
17
|
# statuses matched to it and otherwise, there will be no statuses.
|
18
|
-
if kanban?
|
19
|
-
@backlog_statuses = @possible_statuses.expand_statuses(status_ids_from_column columns[0]) do |unknown_status|
|
20
|
-
# There is a status defined as being 'backlog' that is no longer being returned in statuses.
|
21
|
-
# We used to display a warning for this but honestly, there is nothing that anyone can do about it
|
22
|
-
# so now we just quietly ignore it.
|
23
|
-
end
|
24
|
-
columns = columns[1..]
|
25
|
-
else
|
26
|
-
# We currently don't know how to get the backlog status for a Scrum board
|
27
|
-
@backlog_statuses = []
|
28
|
-
end
|
18
|
+
columns = columns[1..] if kanban?
|
29
19
|
|
20
|
+
@backlog_statuses = []
|
30
21
|
@visible_columns = columns.filter_map do |column|
|
31
22
|
# It's possible for a column to be defined without any statuses and in this case, it won't be visible.
|
32
23
|
BoardColumn.new column unless status_ids_from_column(column).empty?
|
33
24
|
end
|
34
25
|
end
|
35
26
|
|
27
|
+
def backlog_statuses
|
28
|
+
if @backlog_statuses.empty? && kanban?
|
29
|
+
status_ids = status_ids_from_column raw['columnConfig']['columns'].first
|
30
|
+
@backlog_statuses = @possible_statuses.expand_statuses(status_ids) do |unknown_status|
|
31
|
+
# If a status is returned here that is no longer in the system then there's nothing useful
|
32
|
+
# we can do about it. Ignore it.
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@backlog_statuses
|
36
|
+
end
|
37
|
+
|
36
38
|
def server_url_prefix
|
37
39
|
raise "Cannot parse self: #{@raw['self'].inspect}" unless @raw['self'] =~ /^(https?:\/\/.+)\/rest\//
|
38
40
|
|
@@ -25,6 +25,14 @@ class ChartBase
|
|
25
25
|
@aggregated_project
|
26
26
|
end
|
27
27
|
|
28
|
+
def html_directory
|
29
|
+
pathname = Pathname.new(File.realpath(__FILE__))
|
30
|
+
# basename = pathname.basename.to_s
|
31
|
+
# raise "Unexpected filename #{basename.inspect}" unless basename.match?(/^(.+)\.rb$/)
|
32
|
+
|
33
|
+
"#{pathname.dirname}/html"
|
34
|
+
end
|
35
|
+
|
28
36
|
def render caller_binding, file
|
29
37
|
pathname = Pathname.new(File.realpath(file))
|
30
38
|
basename = pathname.basename.to_s
|
@@ -33,8 +41,8 @@ class ChartBase
|
|
33
41
|
# Insert a incrementing chart_id so that all the chart names on the page are unique
|
34
42
|
caller_binding.eval "chart_id='chart#{next_id}'" # chart_id=chart3
|
35
43
|
|
36
|
-
@html_directory = "#{pathname.dirname}/html"
|
37
|
-
erb = ERB.new file_system.load "#{
|
44
|
+
# @html_directory = "#{pathname.dirname}/html"
|
45
|
+
erb = ERB.new file_system.load "#{html_directory}/#{$1}.erb"
|
38
46
|
erb.result(caller_binding)
|
39
47
|
end
|
40
48
|
|
@@ -100,7 +108,7 @@ class ChartBase
|
|
100
108
|
issues_id = next_id
|
101
109
|
|
102
110
|
issue_descriptions.sort! { |a, b| a[0].key_as_i <=> b[0].key_as_i }
|
103
|
-
erb = ERB.new file_system.load
|
111
|
+
erb = ERB.new file_system.load File.join(html_directory, 'collapsible_issues_panel.erb')
|
104
112
|
erb.result(binding)
|
105
113
|
end
|
106
114
|
|
@@ -144,8 +152,7 @@ class ChartBase
|
|
144
152
|
def completed_issues_in_range include_unstarted: false
|
145
153
|
issues.select do |issue|
|
146
154
|
cycletime = issue.board.cycletime
|
147
|
-
stopped_time = cycletime.
|
148
|
-
started_time = cycletime.started_time(issue)
|
155
|
+
started_time, stopped_time = cycletime.started_stopped_times(issue)
|
149
156
|
|
150
157
|
stopped_time &&
|
151
158
|
date_range.include?(stopped_time.to_date) && # Remove outside range
|
@@ -179,7 +186,7 @@ class ChartBase
|
|
179
186
|
def format_status name_or_id, board:, is_category: false
|
180
187
|
begin
|
181
188
|
statuses = board.possible_statuses.expand_statuses([name_or_id])
|
182
|
-
rescue StatusNotFoundError
|
189
|
+
rescue StatusNotFoundError
|
183
190
|
return "<span style='color: red'>#{name_or_id}</span>"
|
184
191
|
end
|
185
192
|
|
@@ -189,10 +196,9 @@ class ChartBase
|
|
189
196
|
visibility = ''
|
190
197
|
if is_category == false && board.visible_columns.none? { |column| column.status_ids.include? status.id }
|
191
198
|
visibility = icon_span(
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
199
|
+
title: "Not visible: The status #{status.name.inspect} is not mapped to any column and will not be visible",
|
200
|
+
icon: ' 👀'
|
201
|
+
)
|
196
202
|
end
|
197
203
|
text = is_category ? status.category_name : status.name
|
198
204
|
"<span title='Category: #{status.category_name}'>#{color_block color.name} #{text}</span>#{visibility}"
|
@@ -26,31 +26,50 @@ class CycleTimeConfig
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def in_progress? issue
|
29
|
-
started_time
|
29
|
+
started_time, stopped_time = started_stopped_times(issue)
|
30
|
+
started_time && stopped_time.nil?
|
30
31
|
end
|
31
32
|
|
32
33
|
def done? issue
|
33
|
-
|
34
|
+
started_stopped_times(issue).last
|
34
35
|
end
|
35
36
|
|
36
37
|
def started_time issue
|
37
|
-
|
38
|
+
deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
|
39
|
+
started_stopped_times(issue).first
|
38
40
|
end
|
39
41
|
|
40
42
|
def stopped_time issue
|
41
|
-
|
43
|
+
deprecated date: '2024-10-16', message: 'Use started_stopped_times() instead'
|
44
|
+
started_stopped_times(issue).last
|
45
|
+
end
|
46
|
+
|
47
|
+
def started_stopped_times issue
|
48
|
+
started = @start_at.call(issue)
|
49
|
+
stopped = @stop_at.call(issue)
|
50
|
+
|
51
|
+
# In the case where started and stopped are exactly the same time, we pretend that
|
52
|
+
# it just stopped and never started. This allows us to have logic like 'in or right of'
|
53
|
+
# for the start and not have it conflict.
|
54
|
+
started = nil if started == stopped
|
55
|
+
|
56
|
+
[started, stopped]
|
57
|
+
end
|
58
|
+
|
59
|
+
def started_stopped_dates issue
|
60
|
+
started_time, stopped_time = started_stopped_times(issue)
|
61
|
+
[started_time&.to_date, stopped_time&.to_date]
|
42
62
|
end
|
43
63
|
|
44
64
|
def cycletime issue
|
45
|
-
start =
|
46
|
-
stop = stopped_time(issue)
|
65
|
+
start, stop = started_stopped_times(issue)
|
47
66
|
return nil if start.nil? || stop.nil?
|
48
67
|
|
49
68
|
(stop.to_date - start.to_date).to_i + 1
|
50
69
|
end
|
51
70
|
|
52
71
|
def age issue, today: nil
|
53
|
-
start =
|
72
|
+
start = started_stopped_times(issue).first
|
54
73
|
stop = today || @today || Date.today
|
55
74
|
return nil if start.nil? || stop.nil?
|
56
75
|
|
@@ -30,7 +30,7 @@ class CycletimeHistogram < ChartBase
|
|
30
30
|
stopped_issues = completed_issues_in_range include_unstarted: true
|
31
31
|
|
32
32
|
# For the histogram, we only want to consider items that have both a start and a stop time.
|
33
|
-
histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.
|
33
|
+
histogram_issues = stopped_issues.select { |issue| issue.board.cycletime.started_stopped_times(issue).first }
|
34
34
|
rules_to_issues = group_issues histogram_issues
|
35
35
|
|
36
36
|
data_sets = rules_to_issues.keys.collect do |rules|
|
@@ -24,7 +24,7 @@ class CycletimeScatterplot < ChartBase
|
|
24
24
|
predict that most work of this type will complete in <%= overall_percent_line %> days or
|
25
25
|
less. The other lines reflect the 85% line for that respective type of work.
|
26
26
|
</div>
|
27
|
-
#{
|
27
|
+
#{describe_non_working_days}
|
28
28
|
HTML
|
29
29
|
|
30
30
|
init_configuration_block block do
|
@@ -53,10 +53,7 @@ class CycletimeScatterplot < ChartBase
|
|
53
53
|
def create_datasets completed_issues
|
54
54
|
data_sets = []
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
groups.each_key do |rules|
|
59
|
-
completed_issues_by_type = groups[rules]
|
56
|
+
group_issues(completed_issues).each do |rules, completed_issues_by_type|
|
60
57
|
label = rules.label
|
61
58
|
color = rules.color
|
62
59
|
percent_line = calculate_percent_line completed_issues_by_type
|
@@ -117,7 +114,7 @@ class CycletimeScatterplot < ChartBase
|
|
117
114
|
|
118
115
|
{
|
119
116
|
y: cycle_time,
|
120
|
-
x: chart_format(issue.board.cycletime.
|
117
|
+
x: chart_format(issue.board.cycletime.started_stopped_times(issue).last),
|
121
118
|
title: ["#{issue.key} : #{issue.summary} (#{label_days(cycle_time)})"]
|
122
119
|
}
|
123
120
|
end
|
@@ -4,7 +4,7 @@ require 'jirametrics/daily_wip_chart'
|
|
4
4
|
|
5
5
|
class DailyWipByAgeChart < DailyWipChart
|
6
6
|
def initialize block
|
7
|
-
super
|
7
|
+
super
|
8
8
|
|
9
9
|
add_trend_line line_color: '--aging-work-in-progress-by-age-trend-line-color', group_labels: [
|
10
10
|
'Less than a day',
|
@@ -49,9 +49,7 @@ class DailyWipByAgeChart < DailyWipChart
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def default_grouping_rules issue:, rules:
|
52
|
-
|
53
|
-
started = cycletime.started_time(issue)&.to_date
|
54
|
-
stopped = cycletime.stopped_time(issue)&.to_date
|
52
|
+
started, stopped = issue.board.cycletime.started_stopped_dates(issue)
|
55
53
|
|
56
54
|
rules.issue_hint = "(age: #{label_days (rules.current_date - started + 1).to_i})" if started
|
57
55
|
|
@@ -39,8 +39,8 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def default_grouping_rules issue:, rules:
|
42
|
-
started = issue.board.cycletime.
|
43
|
-
stopped_date =
|
42
|
+
started, stopped = issue.board.cycletime.started_stopped_times(issue)
|
43
|
+
stopped_date = stopped&.to_date
|
44
44
|
|
45
45
|
date = rules.current_date
|
46
46
|
change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
|
@@ -6,7 +6,7 @@ class DailyGroupingRules < GroupingRules
|
|
6
6
|
attr_accessor :current_date, :group_priority, :issue_hint
|
7
7
|
|
8
8
|
def initialize
|
9
|
-
super
|
9
|
+
super
|
10
10
|
@group_priority = 0
|
11
11
|
end
|
12
12
|
end
|
@@ -22,10 +22,10 @@ class DailyWipChart < ChartBase
|
|
22
22
|
|
23
23
|
instance_eval(&block) if block
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
return if @group_by_block
|
26
|
+
|
27
|
+
grouping_rules do |issue, rules|
|
28
|
+
default_grouping_rules issue: issue, rules: rules
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -66,9 +66,7 @@ class DailyWipChart < ChartBase
|
|
66
66
|
hash = {}
|
67
67
|
|
68
68
|
@issues.each do |issue|
|
69
|
-
|
70
|
-
start = cycletime.started_time(issue)&.to_date
|
71
|
-
stop = cycletime.stopped_time(issue)&.to_date
|
69
|
+
start, stop = issue.board.cycletime.started_stopped_dates(issue)
|
72
70
|
next if start.nil? && stop.nil?
|
73
71
|
|
74
72
|
# If it stopped but never started then assume it started at creation so the data points
|
@@ -158,7 +156,7 @@ class DailyWipChart < ChartBase
|
|
158
156
|
|
159
157
|
{
|
160
158
|
type: 'line',
|
161
|
-
label:
|
159
|
+
label: 'Trendline',
|
162
160
|
data: data_points,
|
163
161
|
fill: false,
|
164
162
|
borderWidth: 1,
|
@@ -48,6 +48,7 @@ class DataQualityReport < ChartBase
|
|
48
48
|
scan_for_issues_not_created_in_a_backlog_status entry: entry, backlog_statuses: backlog_statuses
|
49
49
|
scan_for_stopped_before_started entry: entry
|
50
50
|
scan_for_issues_not_started_with_subtasks_that_have entry: entry
|
51
|
+
scan_for_incomplete_subtasks_when_issue_done entry: entry
|
51
52
|
scan_for_discarded_data entry: entry
|
52
53
|
end
|
53
54
|
|
@@ -56,7 +57,25 @@ class DataQualityReport < ChartBase
|
|
56
57
|
entries_with_problems = entries_with_problems()
|
57
58
|
return '' if entries_with_problems.empty?
|
58
59
|
|
59
|
-
|
60
|
+
caller_binding = binding
|
61
|
+
result = +''
|
62
|
+
result << render_top_text(caller_binding)
|
63
|
+
|
64
|
+
result << '<ul class="quality_report">'
|
65
|
+
result << render_problem_type(:discarded_changes)
|
66
|
+
result << render_problem_type(:completed_but_not_started)
|
67
|
+
result << render_problem_type(:status_changes_after_done)
|
68
|
+
result << render_problem_type(:backwards_through_status_categories)
|
69
|
+
result << render_problem_type(:backwords_through_statuses)
|
70
|
+
result << render_problem_type(:status_not_on_board)
|
71
|
+
result << render_problem_type(:created_in_wrong_status)
|
72
|
+
result << render_problem_type(:stopped_before_started)
|
73
|
+
result << render_problem_type(:issue_not_started_but_subtasks_have)
|
74
|
+
result << render_problem_type(:incomplete_subtasks_when_issue_done)
|
75
|
+
result << render_problem_type(:issue_on_multiple_boards)
|
76
|
+
result << '</ul>'
|
77
|
+
|
78
|
+
result
|
60
79
|
end
|
61
80
|
|
62
81
|
def problems_for key
|
@@ -69,6 +88,18 @@ class DataQualityReport < ChartBase
|
|
69
88
|
result
|
70
89
|
end
|
71
90
|
|
91
|
+
def render_problem_type problem_key
|
92
|
+
problems = problems_for problem_key
|
93
|
+
return '' if problems.empty?
|
94
|
+
|
95
|
+
<<-HTML
|
96
|
+
<li>
|
97
|
+
#{__send__ :"render_#{problem_key}", problems}
|
98
|
+
#{collapsible_issues_panel problems}
|
99
|
+
</li>
|
100
|
+
HTML
|
101
|
+
end
|
102
|
+
|
72
103
|
# Return a format that's easier to assert against
|
73
104
|
def testable_entries
|
74
105
|
format = '%Y-%m-%d %H:%M:%S %z'
|
@@ -87,9 +118,7 @@ class DataQualityReport < ChartBase
|
|
87
118
|
|
88
119
|
def initialize_entries
|
89
120
|
@entries = @issues.filter_map do |issue|
|
90
|
-
|
91
|
-
started = cycletime.started_time(issue)
|
92
|
-
stopped = cycletime.stopped_time(issue)
|
121
|
+
started, stopped = issue.board.cycletime.started_stopped_times(issue)
|
93
122
|
next if stopped && stopped < time_range.begin
|
94
123
|
next if started && started > time_range.end
|
95
124
|
|
@@ -223,14 +252,13 @@ class DataQualityReport < ChartBase
|
|
223
252
|
|
224
253
|
started_subtasks = []
|
225
254
|
entry.issue.subtasks.each do |subtask|
|
226
|
-
started_subtasks << subtask if subtask.board.cycletime.
|
255
|
+
started_subtasks << subtask if subtask.board.cycletime.started_stopped_times(subtask).first
|
227
256
|
end
|
228
257
|
|
229
258
|
return if started_subtasks.empty?
|
230
259
|
|
231
260
|
subtask_labels = started_subtasks.collect do |subtask|
|
232
|
-
|
233
|
-
"#{subtask.summary[..50].inspect}"
|
261
|
+
subtask_label(subtask)
|
234
262
|
end
|
235
263
|
entry.report(
|
236
264
|
problem_key: :issue_not_started_but_subtasks_have,
|
@@ -238,6 +266,47 @@ class DataQualityReport < ChartBase
|
|
238
266
|
)
|
239
267
|
end
|
240
268
|
|
269
|
+
def subtask_label subtask
|
270
|
+
"<img src='#{subtask.type_icon_url}' /> #{link_to_issue(subtask)} #{subtask.summary[..50].inspect}"
|
271
|
+
end
|
272
|
+
|
273
|
+
def time_as_english(from_time, to_time)
|
274
|
+
delta = (to_time - from_time).to_i
|
275
|
+
return "#{delta} seconds" if delta < 60
|
276
|
+
|
277
|
+
delta /= 60
|
278
|
+
return "#{delta} minutes" if delta < 60
|
279
|
+
|
280
|
+
delta /= 60
|
281
|
+
return "#{delta} hours" if delta < 24
|
282
|
+
|
283
|
+
delta /= 24
|
284
|
+
"#{delta} days"
|
285
|
+
end
|
286
|
+
|
287
|
+
def scan_for_incomplete_subtasks_when_issue_done entry:
|
288
|
+
return unless entry.stopped
|
289
|
+
|
290
|
+
subtask_labels = entry.issue.subtasks.filter_map do |subtask|
|
291
|
+
subtask_started, subtask_stopped = subtask.board.cycletime.started_stopped_times(subtask)
|
292
|
+
|
293
|
+
if !subtask_started && !subtask_stopped
|
294
|
+
"#{subtask_label subtask} (Not even started)"
|
295
|
+
elsif !subtask_stopped
|
296
|
+
"#{subtask_label subtask} (Still not done)"
|
297
|
+
elsif subtask_stopped > entry.stopped
|
298
|
+
"#{subtask_label subtask} (Closed #{time_as_english entry.stopped, subtask_stopped} later)"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
return if subtask_labels.empty?
|
303
|
+
|
304
|
+
entry.report(
|
305
|
+
problem_key: :incomplete_subtasks_when_issue_done,
|
306
|
+
detail: subtask_labels.join('<br />')
|
307
|
+
)
|
308
|
+
end
|
309
|
+
|
241
310
|
def label_issues number
|
242
311
|
return '1 item' if number == 1
|
243
312
|
|
@@ -278,4 +347,94 @@ class DataQualityReport < ChartBase
|
|
278
347
|
)
|
279
348
|
end
|
280
349
|
end
|
350
|
+
|
351
|
+
def render_discarded_changes problems
|
352
|
+
<<-HTML
|
353
|
+
#{label_issues problems.size} have had information discarded. This configuration is set
|
354
|
+
to "reset the clock" if an item is moved back to the backlog after it's been started. This hides important
|
355
|
+
information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b> HTML
|
356
|
+
HTML
|
357
|
+
end
|
358
|
+
|
359
|
+
def render_completed_but_not_started problems
|
360
|
+
percentage_work_included = ((issues.size - problems.size).to_f / issues.size * 100).to_i
|
361
|
+
html = <<-HTML
|
362
|
+
#{label_issues problems.size} were discarded from all charts using cycletime (scatterplot, histogram, etc)
|
363
|
+
as we couldn't determine when they started.
|
364
|
+
HTML
|
365
|
+
if percentage_work_included < 85
|
366
|
+
html << <<-HTML
|
367
|
+
Consider whether looking at only #{percentage_work_included}% of the total data points is enough
|
368
|
+
to come to any reasonable conclusions. See <a href="https://en.wikipedia.org/wiki/Survivorship_bias">
|
369
|
+
Survivorship Bias</a>.
|
370
|
+
HTML
|
371
|
+
end
|
372
|
+
html
|
373
|
+
end
|
374
|
+
|
375
|
+
def render_status_changes_after_done problems
|
376
|
+
<<-HTML
|
377
|
+
#{label_issues problems.size} had a status change after being identified as done. We should question
|
378
|
+
whether they were really done at that point or if we stopped the clock too early.
|
379
|
+
HTML
|
380
|
+
end
|
381
|
+
|
382
|
+
def render_backwards_through_status_categories problems
|
383
|
+
<<-HTML
|
384
|
+
#{label_issues problems.size} moved backwards across the board, <b>crossing status categories</b>.
|
385
|
+
This will almost certainly have impacted timings as the end times are often taken at status category
|
386
|
+
boundaries. You should assume that any timing measurements for this item are wrong.
|
387
|
+
HTML
|
388
|
+
end
|
389
|
+
|
390
|
+
def render_backwords_through_statuses problems
|
391
|
+
<<-HTML
|
392
|
+
#{label_issues problems.size} moved backwards across the board. Depending where we have set the
|
393
|
+
start and end points, this may give us incorrect timing data. Note that these items did not cross
|
394
|
+
a status category and may not have affected metrics.
|
395
|
+
HTML
|
396
|
+
end
|
397
|
+
|
398
|
+
def render_status_not_on_board problems
|
399
|
+
<<-HTML
|
400
|
+
#{label_issues problems.size} were not visible on the board for some period of time. This may impact
|
401
|
+
timings as the work was likely to have been forgotten if it wasn't visible.
|
402
|
+
HTML
|
403
|
+
end
|
404
|
+
|
405
|
+
def render_created_in_wrong_status problems
|
406
|
+
<<-HTML
|
407
|
+
#{label_issues problems.size} were created in a status not designated as Backlog. This will impact
|
408
|
+
the measurement of start times and will therefore impact whether it's shown as in progress or not.
|
409
|
+
HTML
|
410
|
+
end
|
411
|
+
|
412
|
+
def render_stopped_before_started problems
|
413
|
+
<<-HTML
|
414
|
+
#{label_issues problems.size} were stopped before they were started and this will play havoc with
|
415
|
+
any cycletime or WIP calculations. The most common case for this is when an item gets closed and
|
416
|
+
then moved back into an in-progress status.
|
417
|
+
HTML
|
418
|
+
end
|
419
|
+
|
420
|
+
def render_issue_not_started_but_subtasks_have problems
|
421
|
+
<<-HTML
|
422
|
+
#{label_issues problems.size} still showing 'not started' while sub-tasks underneath them have
|
423
|
+
started. This is almost always a mistake; if we're working on subtasks, the top level item should
|
424
|
+
also have started.
|
425
|
+
HTML
|
426
|
+
end
|
427
|
+
|
428
|
+
def render_incomplete_subtasks_when_issue_done problems
|
429
|
+
<<-HTML
|
430
|
+
#{label_issues problems.size} issues were marked as done while subtasks were still not done.
|
431
|
+
HTML
|
432
|
+
end
|
433
|
+
|
434
|
+
def render_issue_on_multiple_boards problems
|
435
|
+
<<-HTML
|
436
|
+
For #{label_issues problems.size}, we have an issue that shows up on more than one board. This
|
437
|
+
could result in more data points showing up on a chart then there really should be.
|
438
|
+
HTML
|
439
|
+
end
|
281
440
|
end
|
@@ -183,9 +183,8 @@ class DependencyChart < ChartBase
|
|
183
183
|
return stdout.read
|
184
184
|
end
|
185
185
|
rescue # rubocop:disable Style/RescueStandardError
|
186
|
-
message =
|
187
|
-
|
188
|
-
puts message
|
186
|
+
message = 'Unable to generate the dependency chart because graphviz could not be found in the path.'
|
187
|
+
file_system.log message, also_write_to_stderr: true
|
189
188
|
message
|
190
189
|
end
|
191
190
|
|
@@ -229,7 +228,7 @@ class DependencyChart < ChartBase
|
|
229
228
|
elsif is_done
|
230
229
|
line2 << 'Done'
|
231
230
|
else
|
232
|
-
started_at = issue.board.cycletime.
|
231
|
+
started_at = issue.board.cycletime.started_stopped_times(issue).first
|
233
232
|
if started_at.nil?
|
234
233
|
line2 << 'Not started'
|
235
234
|
else
|
@@ -31,7 +31,7 @@ module DiscardChangesBefore
|
|
31
31
|
discard_changes_before_hook issues_cutoff_times
|
32
32
|
|
33
33
|
issues_cutoff_times.each do |issue, cutoff_time|
|
34
|
-
issue.
|
34
|
+
issue.discard_changes_before cutoff_time
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|