jirametrics 2.4 → 2.11
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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +9 -4
- data/lib/jirametrics/aging_work_bar_chart.rb +13 -11
- data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
- data/lib/jirametrics/aging_work_table.rb +54 -7
- data/lib/jirametrics/blocked_stalled_change.rb +1 -1
- data/lib/jirametrics/board.rb +44 -15
- data/lib/jirametrics/board_config.rb +7 -3
- data/lib/jirametrics/board_movement_calculator.rb +147 -0
- data/lib/jirametrics/change_item.rb +19 -6
- data/lib/jirametrics/chart_base.rb +63 -27
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cycletime_config.rb +59 -8
- data/lib/jirametrics/cycletime_histogram.rb +68 -3
- 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 +219 -41
- data/lib/jirametrics/dependency_chart.rb +37 -10
- data/lib/jirametrics/download_config.rb +12 -0
- data/lib/jirametrics/downloader.rb +68 -50
- data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
- data/lib/jirametrics/examples/aggregated_project.rb +7 -21
- data/lib/jirametrics/examples/standard_project.rb +18 -34
- data/lib/jirametrics/expedited_chart.rb +8 -9
- data/lib/jirametrics/exporter.rb +28 -11
- data/lib/jirametrics/file_config.rb +23 -6
- data/lib/jirametrics/file_system.rb +39 -3
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
- data/lib/jirametrics/groupable_issue_chart.rb +1 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
- data/lib/jirametrics/html/aging_work_table.erb +6 -4
- data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
- data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
- data/lib/jirametrics/html/expedited_chart.erb +1 -10
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/index.css +28 -5
- data/lib/jirametrics/html/index.erb +8 -4
- data/lib/jirametrics/html/sprint_burndown.erb +1 -10
- data/lib/jirametrics/html/throughput_chart.erb +1 -10
- data/lib/jirametrics/html_report_config.rb +33 -23
- data/lib/jirametrics/issue.rb +232 -47
- data/lib/jirametrics/jira_gateway.rb +16 -3
- data/lib/jirametrics/project_config.rb +245 -134
- data/lib/jirametrics/rules.rb +2 -2
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/settings.json +5 -2
- data/lib/jirametrics/sprint_burndown.rb +3 -3
- data/lib/jirametrics/status.rb +84 -19
- data/lib/jirametrics/status_collection.rb +77 -39
- data/lib/jirametrics/throughput_chart.rb +1 -1
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +22 -6
- metadata +10 -13
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/html/data_quality_report.erb +0 -126
data/lib/jirametrics/status.rb
CHANGED
|
@@ -3,30 +3,65 @@
|
|
|
3
3
|
require 'jirametrics/value_equality'
|
|
4
4
|
|
|
5
5
|
class Status
|
|
6
|
-
|
|
7
|
-
attr_reader :id, :category_name, :category_id, :project_id
|
|
6
|
+
attr_reader :id, :project_id, :category
|
|
8
7
|
attr_accessor :name
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
class Category
|
|
10
|
+
attr_reader :id, :name, :key
|
|
11
|
+
|
|
12
|
+
def initialize id:, name:, key:
|
|
13
|
+
@id = id
|
|
14
|
+
@name = name
|
|
15
|
+
@key = key
|
|
16
|
+
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
def to_s
|
|
19
|
+
"#{name.inspect}:#{id.inspect}"
|
|
20
|
+
end
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
def <=> other
|
|
23
|
+
id <=> other.id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def == other
|
|
27
|
+
id == other.id
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def eql?(other) = id.eql?(other.id)
|
|
31
|
+
def hash = id.hash
|
|
32
|
+
|
|
33
|
+
def new? = (@key == 'new')
|
|
34
|
+
def indeterminate? = (@key == 'indeterminate')
|
|
35
|
+
def done? = (@key == 'done')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.from_raw raw
|
|
39
|
+
raise "raw cannot be nil" if raw.nil?
|
|
22
40
|
|
|
23
41
|
category_config = raw['statusCategory']
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
raise "statusCategory can't be nil in #{category_config.inspect}" if category_config.nil?
|
|
43
|
+
|
|
44
|
+
Status.new(
|
|
45
|
+
name: raw['name'],
|
|
46
|
+
id: raw['id'].to_i,
|
|
47
|
+
category_name: category_config['name'],
|
|
48
|
+
category_id: category_config['id'].to_i,
|
|
49
|
+
category_key: category_config['key'],
|
|
50
|
+
project_id: raw['scope']&.[]('project')&.[]('id'),
|
|
51
|
+
artificial: false
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize name:, id:, category_name:, category_id:, category_key:, project_id: nil, artificial: true
|
|
56
|
+
# These checks are needed because nils used to be possible and now they aren't.
|
|
57
|
+
raise 'id cannot be nil' if id.nil?
|
|
58
|
+
raise 'category_id cannot be nil' if category_id.nil?
|
|
26
59
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@
|
|
60
|
+
@name = name
|
|
61
|
+
@id = id
|
|
62
|
+
@category = Category.new id: category_id, name: category_name, key: category_key
|
|
63
|
+
@project_id = project_id
|
|
64
|
+
@artificial = artificial
|
|
30
65
|
end
|
|
31
66
|
|
|
32
67
|
def project_scoped?
|
|
@@ -38,8 +73,38 @@ class Status
|
|
|
38
73
|
end
|
|
39
74
|
|
|
40
75
|
def to_s
|
|
41
|
-
"
|
|
42
|
-
|
|
76
|
+
"#{name.inspect}:#{id.inspect}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def artificial?
|
|
80
|
+
@artificial
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def == other
|
|
84
|
+
return false unless other.is_a? Status
|
|
85
|
+
|
|
86
|
+
@id == other.id && @name == other.name && @category.id == other.category.id && @category.name == other.category.name
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def eql?(other)
|
|
90
|
+
self == other
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def <=> other
|
|
94
|
+
result = @name.casecmp(other.name)
|
|
95
|
+
result = @id <=> other.id if result.zero?
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def inspect
|
|
100
|
+
result = []
|
|
101
|
+
result << "Status(name: #{@name.inspect}"
|
|
102
|
+
result << "id: #{@id.inspect}"
|
|
103
|
+
result << "project_id: #{@project_id}" if @project_id
|
|
104
|
+
category = self.category
|
|
105
|
+
result << "category: {name:#{category.name.inspect}, id: #{category.id.inspect}, key: #{category.key.inspect}}"
|
|
106
|
+
result << 'artificial' if artificial?
|
|
107
|
+
result.join(', ') << ')'
|
|
43
108
|
end
|
|
44
109
|
|
|
45
110
|
def value_equality_ignored_variables
|
|
@@ -4,68 +4,106 @@ class StatusNotFoundError < StandardError
|
|
|
4
4
|
end
|
|
5
5
|
|
|
6
6
|
class StatusCollection
|
|
7
|
+
attr_reader :historical_status_mappings
|
|
8
|
+
|
|
7
9
|
def initialize
|
|
8
10
|
@list = []
|
|
11
|
+
@historical_status_mappings = {} # 'name:id' => category
|
|
9
12
|
end
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
# Return the status matching this id or nil if it can't be found.
|
|
15
|
+
def find_by_id id
|
|
16
|
+
@list.find { |status| status.id == id }
|
|
17
|
+
end
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
including.any? { |s| s.name == status.name }
|
|
18
|
-
keep = false if excluding.any? { |s| s.name == status.name }
|
|
19
|
+
def find_all_by_name identifier
|
|
20
|
+
name, id = parse_name_id identifier
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
if id
|
|
23
|
+
status = find_by_id id
|
|
24
|
+
return [] if status.nil?
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
names_or_ids = [names_or_ids] unless names_or_ids.is_a? Array
|
|
29
|
-
|
|
30
|
-
names_or_ids.each do |name_or_id|
|
|
31
|
-
status = @list.find { |s| s.name == name_or_id || s.id == name_or_id }
|
|
32
|
-
if status.nil?
|
|
33
|
-
if block_given?
|
|
34
|
-
yield name_or_id
|
|
35
|
-
next
|
|
36
|
-
else
|
|
37
|
-
all_status_names = @list.collect { |s| "#{s.name.inspect}:#{s.id.inspect}" }.uniq.sort.join(', ')
|
|
38
|
-
raise StatusNotFoundError, "Status not found: \"#{name_or_id}\". Possible statuses are: #{all_status_names}"
|
|
39
|
-
end
|
|
26
|
+
if name && status.name != name
|
|
27
|
+
raise "Specified status ID of #{id} does not match specified name #{name.inspect}. " \
|
|
28
|
+
"You might have meant one of these: #{self}."
|
|
40
29
|
end
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
[status]
|
|
31
|
+
else
|
|
32
|
+
@list.select { |status| status.name == name }
|
|
43
33
|
end
|
|
44
|
-
result
|
|
45
34
|
end
|
|
46
35
|
|
|
47
|
-
def
|
|
48
|
-
|
|
36
|
+
def find_all_categories
|
|
37
|
+
@list
|
|
38
|
+
.collect(&:category)
|
|
39
|
+
.uniq
|
|
40
|
+
.sort_by(&:id)
|
|
49
41
|
end
|
|
50
42
|
|
|
51
|
-
def
|
|
52
|
-
|
|
43
|
+
def parse_name_id name
|
|
44
|
+
# Names could arrive in one of the following formats: "Done:3", "3", "Done"
|
|
45
|
+
if name =~ /^(.*):(\d+)$/
|
|
46
|
+
[$1, $2.to_i]
|
|
47
|
+
elsif name.match?(/^\d+$/)
|
|
48
|
+
[nil, name.to_i]
|
|
49
|
+
else
|
|
50
|
+
[name, nil]
|
|
51
|
+
end
|
|
53
52
|
end
|
|
54
53
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
def find_all_categories_by_name identifier
|
|
55
|
+
key = nil
|
|
56
|
+
id = nil
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if identifier.is_a? Symbol
|
|
59
|
+
key = identifier.to_s
|
|
60
|
+
else
|
|
61
|
+
name, id = parse_name_id identifier
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
find_all_categories.select { |c| c.id == id || c.name == name || c.key == key }
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
def find(&block)= @list.find(&block)
|
|
64
67
|
def collect(&block) = @list.collect(&block)
|
|
68
|
+
def find(&block) = @list.find(&block)
|
|
65
69
|
def each(&block) = @list.each(&block)
|
|
66
70
|
def select(&block) = @list.select(&block)
|
|
67
71
|
def <<(arg) = @list << arg
|
|
68
72
|
def empty? = @list.empty?
|
|
69
73
|
def clear = @list.clear
|
|
70
74
|
def delete(object) = @list.delete(object)
|
|
75
|
+
|
|
76
|
+
def to_s
|
|
77
|
+
"[#{@list.sort.join(', ')}]"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def inspect
|
|
81
|
+
"StatusCollection#{self}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def fabricate_status_for id:, name:
|
|
85
|
+
category = @historical_status_mappings["#{name.inspect}:#{id.inspect}"]
|
|
86
|
+
category = in_progress_category if category.nil?
|
|
87
|
+
|
|
88
|
+
status = Status.new(
|
|
89
|
+
name: name,
|
|
90
|
+
id: id,
|
|
91
|
+
category_name: category.name,
|
|
92
|
+
category_id: category.id,
|
|
93
|
+
category_key: category.key,
|
|
94
|
+
artificial: true
|
|
95
|
+
)
|
|
96
|
+
@list << status
|
|
97
|
+
status
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Return the in-progress category or raise an error if we can't find one.
|
|
103
|
+
def in_progress_category
|
|
104
|
+
first_in_progress_status = find { |s| s.category.indeterminate? }
|
|
105
|
+
raise "Can't find even one in-progress status in #{self}" unless first_in_progress_status
|
|
106
|
+
|
|
107
|
+
first_in_progress_status.category
|
|
108
|
+
end
|
|
71
109
|
end
|
|
@@ -82,7 +82,7 @@ class ThroughputChart < ChartBase
|
|
|
82
82
|
def throughput_dataset periods:, completed_issues:
|
|
83
83
|
periods.collect do |period|
|
|
84
84
|
closed_issues = completed_issues.filter_map do |issue|
|
|
85
|
-
stop_date = issue.board.cycletime.
|
|
85
|
+
stop_date = issue.board.cycletime.started_stopped_dates(issue).last
|
|
86
86
|
[stop_date, issue] if stop_date && period.include?(stop_date)
|
|
87
87
|
end
|
|
88
88
|
|
|
@@ -9,9 +9,9 @@ module ValueEquality
|
|
|
9
9
|
names = object.instance_variables
|
|
10
10
|
if object.respond_to? :value_equality_ignored_variables
|
|
11
11
|
ignored_variables = object.value_equality_ignored_variables
|
|
12
|
-
names.reject! { |n| ignored_variables.include? n }
|
|
12
|
+
names.reject! { |n| ignored_variables.include? n.to_sym }
|
|
13
13
|
end
|
|
14
|
-
names.map { |variable| instance_variable_get variable }
|
|
14
|
+
names.map { |variable| object.instance_variable_get variable }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
code.call(self) == code.call(other)
|
data/lib/jirametrics.rb
CHANGED
|
@@ -3,9 +3,20 @@
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
|
|
5
5
|
class JiraMetrics < Thor
|
|
6
|
+
def self.exit_on_failure?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
map %w[--version -v] => :__print_version
|
|
11
|
+
|
|
12
|
+
desc '--version, -v', 'print the version'
|
|
13
|
+
def __print_version
|
|
14
|
+
puts Gem.loaded_specs['jirametrics'].version
|
|
15
|
+
end
|
|
16
|
+
|
|
6
17
|
option :config
|
|
7
18
|
option :name
|
|
8
|
-
desc 'export
|
|
19
|
+
desc 'export', "Export data into either reports or CSV's as per the configuration"
|
|
9
20
|
def export
|
|
10
21
|
load_config options[:config]
|
|
11
22
|
Exporter.instance.export(name_filter: options[:name] || '*')
|
|
@@ -13,7 +24,7 @@ class JiraMetrics < Thor
|
|
|
13
24
|
|
|
14
25
|
option :config
|
|
15
26
|
option :name
|
|
16
|
-
desc 'download
|
|
27
|
+
desc 'download', 'Download data from Jira'
|
|
17
28
|
def download
|
|
18
29
|
load_config options[:config]
|
|
19
30
|
Exporter.instance.download(name_filter: options[:name] || '*')
|
|
@@ -21,7 +32,7 @@ class JiraMetrics < Thor
|
|
|
21
32
|
|
|
22
33
|
option :config
|
|
23
34
|
option :name
|
|
24
|
-
desc '
|
|
35
|
+
desc 'go', 'Same as running download, followed by export'
|
|
25
36
|
def go
|
|
26
37
|
load_config options[:config]
|
|
27
38
|
Exporter.instance.download(name_filter: options[:name] || '*')
|
|
@@ -30,6 +41,13 @@ class JiraMetrics < Thor
|
|
|
30
41
|
Exporter.instance.export(name_filter: options[:name] || '*')
|
|
31
42
|
end
|
|
32
43
|
|
|
44
|
+
option :config
|
|
45
|
+
desc 'info', 'Dump information about one issue'
|
|
46
|
+
def info keys
|
|
47
|
+
load_config options[:config]
|
|
48
|
+
Exporter.instance.info(keys, name_filter: options[:name] || '*')
|
|
49
|
+
end
|
|
50
|
+
|
|
33
51
|
private
|
|
34
52
|
|
|
35
53
|
def load_config config_file
|
|
@@ -50,7 +68,6 @@ class JiraMetrics < Thor
|
|
|
50
68
|
require 'jirametrics/grouping_rules'
|
|
51
69
|
require 'jirametrics/daily_wip_chart'
|
|
52
70
|
require 'jirametrics/groupable_issue_chart'
|
|
53
|
-
require 'jirametrics/discard_changes_before'
|
|
54
71
|
require 'jirametrics/css_variable'
|
|
55
72
|
|
|
56
73
|
require 'jirametrics/aggregate_config'
|
|
@@ -69,6 +86,7 @@ class JiraMetrics < Thor
|
|
|
69
86
|
require 'jirametrics/daily_wip_by_parent_chart'
|
|
70
87
|
require 'jirametrics/aging_work_in_progress_chart'
|
|
71
88
|
require 'jirametrics/cycletime_scatterplot'
|
|
89
|
+
require 'jirametrics/flow_efficiency_scatterplot'
|
|
72
90
|
require 'jirametrics/sprint_issue_change_data'
|
|
73
91
|
require 'jirametrics/cycletime_histogram'
|
|
74
92
|
require 'jirametrics/daily_wip_by_blocked_stalled_chart'
|
|
@@ -97,6 +115,4 @@ class JiraMetrics < Thor
|
|
|
97
115
|
require 'jirametrics/board'
|
|
98
116
|
load config_file
|
|
99
117
|
end
|
|
100
|
-
|
|
101
|
-
# Dir.foreach('lib/jirametrics') {|file| puts "require 'jirametrics/#{$1}'" if file =~ /^(.+)\.rb$/}
|
|
102
118
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jirametrics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '2.
|
|
4
|
+
version: '2.11'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bowler
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2025-03-11 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: random-word
|
|
@@ -52,8 +51,7 @@ dependencies:
|
|
|
52
51
|
- - "~>"
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: 1.2.2
|
|
55
|
-
description:
|
|
56
|
-
CSV files
|
|
54
|
+
description: Extract metrics from Jira and export to either a report or to CSV files
|
|
57
55
|
email: mbowler@gargoylesoftware.com
|
|
58
56
|
executables:
|
|
59
57
|
- jirametrics
|
|
@@ -71,6 +69,7 @@ files:
|
|
|
71
69
|
- lib/jirametrics/board.rb
|
|
72
70
|
- lib/jirametrics/board_column.rb
|
|
73
71
|
- lib/jirametrics/board_config.rb
|
|
72
|
+
- lib/jirametrics/board_movement_calculator.rb
|
|
74
73
|
- lib/jirametrics/change_item.rb
|
|
75
74
|
- lib/jirametrics/chart_base.rb
|
|
76
75
|
- lib/jirametrics/columns_config.rb
|
|
@@ -84,7 +83,6 @@ files:
|
|
|
84
83
|
- lib/jirametrics/daily_wip_chart.rb
|
|
85
84
|
- lib/jirametrics/data_quality_report.rb
|
|
86
85
|
- lib/jirametrics/dependency_chart.rb
|
|
87
|
-
- lib/jirametrics/discard_changes_before.rb
|
|
88
86
|
- lib/jirametrics/download_config.rb
|
|
89
87
|
- lib/jirametrics/downloader.rb
|
|
90
88
|
- lib/jirametrics/estimate_accuracy_chart.rb
|
|
@@ -95,6 +93,7 @@ files:
|
|
|
95
93
|
- lib/jirametrics/file_config.rb
|
|
96
94
|
- lib/jirametrics/file_system.rb
|
|
97
95
|
- lib/jirametrics/fix_version.rb
|
|
96
|
+
- lib/jirametrics/flow_efficiency_scatterplot.rb
|
|
98
97
|
- lib/jirametrics/groupable_issue_chart.rb
|
|
99
98
|
- lib/jirametrics/grouping_rules.rb
|
|
100
99
|
- lib/jirametrics/hierarchy_table.rb
|
|
@@ -105,9 +104,9 @@ files:
|
|
|
105
104
|
- lib/jirametrics/html/cycletime_histogram.erb
|
|
106
105
|
- lib/jirametrics/html/cycletime_scatterplot.erb
|
|
107
106
|
- lib/jirametrics/html/daily_wip_chart.erb
|
|
108
|
-
- lib/jirametrics/html/data_quality_report.erb
|
|
109
107
|
- lib/jirametrics/html/estimate_accuracy_chart.erb
|
|
110
108
|
- lib/jirametrics/html/expedited_chart.erb
|
|
109
|
+
- lib/jirametrics/html/flow_efficiency_scatterplot.erb
|
|
111
110
|
- lib/jirametrics/html/hierarchy_table.erb
|
|
112
111
|
- lib/jirametrics/html/index.css
|
|
113
112
|
- lib/jirametrics/html/index.erb
|
|
@@ -130,15 +129,14 @@ files:
|
|
|
130
129
|
- lib/jirametrics/tree_organizer.rb
|
|
131
130
|
- lib/jirametrics/trend_line_calculator.rb
|
|
132
131
|
- lib/jirametrics/value_equality.rb
|
|
133
|
-
homepage: https://
|
|
132
|
+
homepage: https://jirametrics.org
|
|
134
133
|
licenses:
|
|
135
134
|
- Apache-2.0
|
|
136
135
|
metadata:
|
|
137
136
|
rubygems_mfa_required: 'true'
|
|
138
137
|
bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
|
|
139
|
-
changelog_uri: https://
|
|
140
|
-
documentation_uri: https://
|
|
141
|
-
post_install_message:
|
|
138
|
+
changelog_uri: https://jirametrics.org/changes
|
|
139
|
+
documentation_uri: https://jirametrics.org
|
|
142
140
|
rdoc_options: []
|
|
143
141
|
require_paths:
|
|
144
142
|
- lib
|
|
@@ -153,8 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
153
151
|
- !ruby/object:Gem::Version
|
|
154
152
|
version: '0'
|
|
155
153
|
requirements: []
|
|
156
|
-
rubygems_version: 3.
|
|
157
|
-
signing_key:
|
|
154
|
+
rubygems_version: 3.6.2
|
|
158
155
|
specification_version: 4
|
|
159
156
|
summary: Extract Jira metrics
|
|
160
157
|
test_files: []
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module DiscardChangesBefore
|
|
4
|
-
def discard_changes_before status_becomes: nil, &block
|
|
5
|
-
if status_becomes
|
|
6
|
-
status_becomes = [status_becomes] unless status_becomes.is_a? Array
|
|
7
|
-
|
|
8
|
-
block = lambda do |issue|
|
|
9
|
-
trigger_statuses = status_becomes.collect do |status_name|
|
|
10
|
-
if status_name == :backlog
|
|
11
|
-
issue.board.backlog_statuses.collect(&:name)
|
|
12
|
-
else
|
|
13
|
-
status_name
|
|
14
|
-
end
|
|
15
|
-
end.flatten
|
|
16
|
-
|
|
17
|
-
time = nil
|
|
18
|
-
issue.changes.each do |change|
|
|
19
|
-
time = change.time if change.status? && trigger_statuses.include?(change.value) && change.artificial? == false
|
|
20
|
-
end
|
|
21
|
-
time
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
issues_cutoff_times = []
|
|
26
|
-
issues.each do |issue|
|
|
27
|
-
cutoff_time = block.call(issue)
|
|
28
|
-
issues_cutoff_times << [issue, cutoff_time] if cutoff_time
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
discard_changes_before_hook issues_cutoff_times
|
|
32
|
-
|
|
33
|
-
issues_cutoff_times.each do |issue, cutoff_time|
|
|
34
|
-
issue.changes.reject! { |change| change.status? && change.time <= cutoff_time && change.artificial? == false }
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
<%
|
|
2
|
-
problems = problems_for :discarded_changes
|
|
3
|
-
unless problems.empty?
|
|
4
|
-
%>
|
|
5
|
-
<p>
|
|
6
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> have had information discarded. This configuration is set
|
|
7
|
-
to "reset the clock" if an item is moved back to the backlog after it's been started. This hides important
|
|
8
|
-
information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b>
|
|
9
|
-
<%= collapsible_issues_panel problems %>
|
|
10
|
-
</p>
|
|
11
|
-
<%
|
|
12
|
-
end
|
|
13
|
-
%>
|
|
14
|
-
|
|
15
|
-
<%
|
|
16
|
-
problems = problems_for :completed_but_not_started
|
|
17
|
-
unless problems.empty?
|
|
18
|
-
percentage_work_included = ((issues.size - problems.size).to_f / issues.size * 100).to_i
|
|
19
|
-
%>
|
|
20
|
-
<p>
|
|
21
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were discarded from all charts using cycletime (scatterplot, histogram, etc) as we couldn't determine when they started.
|
|
22
|
-
<% if percentage_work_included < 85 %>
|
|
23
|
-
Consider whether looking at only <%= percentage_work_included %>% of the total data points is enough to come to any reasonable conclusions. See <a href="https://en.wikipedia.org/wiki/Survivorship_bias">Survivorship Bias</a>.
|
|
24
|
-
<% end %>
|
|
25
|
-
<%= collapsible_issues_panel problems %>
|
|
26
|
-
</p>
|
|
27
|
-
<%
|
|
28
|
-
end
|
|
29
|
-
%>
|
|
30
|
-
|
|
31
|
-
<%
|
|
32
|
-
problems = problems_for :status_changes_after_done
|
|
33
|
-
unless problems.empty?
|
|
34
|
-
%>
|
|
35
|
-
<p>
|
|
36
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> had a status change after being identified as done. We should question whether they were really done at that point or if we stopped the clock too early.
|
|
37
|
-
<%= collapsible_issues_panel problems %>
|
|
38
|
-
</p>
|
|
39
|
-
<%
|
|
40
|
-
end
|
|
41
|
-
%>
|
|
42
|
-
|
|
43
|
-
<%
|
|
44
|
-
problems = problems_for :backwards_through_status_categories
|
|
45
|
-
unless problems.empty?
|
|
46
|
-
%>
|
|
47
|
-
<p>
|
|
48
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board, <b>crossing status categories</b>. This will almost certainly have impacted timings as the end times are often taken at status category boundaries. You should assume that any timing measurements for this item are wrong.
|
|
49
|
-
<%= collapsible_issues_panel problems %>
|
|
50
|
-
</p>
|
|
51
|
-
<%
|
|
52
|
-
end
|
|
53
|
-
%>
|
|
54
|
-
|
|
55
|
-
<%
|
|
56
|
-
problems = problems_for :backwords_through_statuses
|
|
57
|
-
unless problems.empty?
|
|
58
|
-
%>
|
|
59
|
-
<p>
|
|
60
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board. Depending where we have set the start and end points, this may give us incorrect timing data. Note that these items did not cross a status category and may not have affected metrics.
|
|
61
|
-
<%= collapsible_issues_panel problems %>
|
|
62
|
-
</p>
|
|
63
|
-
<%
|
|
64
|
-
end
|
|
65
|
-
%>
|
|
66
|
-
|
|
67
|
-
<%
|
|
68
|
-
problems = problems_for :status_not_on_board
|
|
69
|
-
unless problems.empty?
|
|
70
|
-
%>
|
|
71
|
-
<p>
|
|
72
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were not visible on the board for some period of time. This may impact timings as the work was likely to have been forgotten if it wasn't visible.
|
|
73
|
-
<%= collapsible_issues_panel problems %>
|
|
74
|
-
</p>
|
|
75
|
-
<%
|
|
76
|
-
end
|
|
77
|
-
%>
|
|
78
|
-
|
|
79
|
-
<%
|
|
80
|
-
problems = problems_for :created_in_wrong_status
|
|
81
|
-
unless problems.empty?
|
|
82
|
-
%>
|
|
83
|
-
<p>
|
|
84
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were created in a status not designated as Backlog. This will impact the measurement of start times and will therefore impact whether it's shown as in progress or not.
|
|
85
|
-
<%= collapsible_issues_panel problems %>
|
|
86
|
-
</p>
|
|
87
|
-
<%
|
|
88
|
-
end
|
|
89
|
-
%>
|
|
90
|
-
|
|
91
|
-
<%
|
|
92
|
-
problems = problems_for :stopped_before_started
|
|
93
|
-
unless problems.empty?
|
|
94
|
-
%>
|
|
95
|
-
<p>
|
|
96
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were stopped before they were started and this will play havoc with any cycletime or WIP calculations. The most common case for this is when an item gets closed and then moved back into an in-progress status.
|
|
97
|
-
<%= collapsible_issues_panel problems %>
|
|
98
|
-
</p>
|
|
99
|
-
<%
|
|
100
|
-
end
|
|
101
|
-
%>
|
|
102
|
-
|
|
103
|
-
<%
|
|
104
|
-
problems = problems_for :issue_not_started_but_subtasks_have
|
|
105
|
-
unless problems.empty?
|
|
106
|
-
%>
|
|
107
|
-
<p>
|
|
108
|
-
<span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> still showing 'not started' while sub-tasks underneath them have started. This is almost always a mistake; if we're working on subtasks, the top level
|
|
109
|
-
item should also have started.
|
|
110
|
-
<%= collapsible_issues_panel problems %>
|
|
111
|
-
</p>
|
|
112
|
-
<%
|
|
113
|
-
end
|
|
114
|
-
%>
|
|
115
|
-
|
|
116
|
-
<%
|
|
117
|
-
problems = problems_for :issue_on_multiple_boards
|
|
118
|
-
unless problems.empty?
|
|
119
|
-
%>
|
|
120
|
-
<p>
|
|
121
|
-
<span class="quality_note_bullet">⮕</span> For <%= label_issues problems.size %>, we have an issue that shows up on more than one board. This could result in more data points showing up on a chart then there really should be.
|
|
122
|
-
<%= collapsible_issues_panel problems, :hide_board_column %>
|
|
123
|
-
</p>
|
|
124
|
-
<%
|
|
125
|
-
end
|
|
126
|
-
%>
|