jirametrics 2.7 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +4 -4
  3. data/lib/jirametrics/aging_work_bar_chart.rb +7 -5
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
  5. data/lib/jirametrics/aging_work_table.rb +50 -2
  6. data/lib/jirametrics/board.rb +33 -5
  7. data/lib/jirametrics/board_config.rb +6 -2
  8. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  9. data/lib/jirametrics/change_item.rb +19 -6
  10. data/lib/jirametrics/chart_base.rb +59 -21
  11. data/lib/jirametrics/css_variable.rb +1 -1
  12. data/lib/jirametrics/cycletime_config.rb +37 -5
  13. data/lib/jirametrics/cycletime_histogram.rb +67 -2
  14. data/lib/jirametrics/data_quality_report.rb +174 -35
  15. data/lib/jirametrics/download_config.rb +2 -2
  16. data/lib/jirametrics/downloader.rb +44 -25
  17. data/lib/jirametrics/examples/aggregated_project.rb +2 -5
  18. data/lib/jirametrics/examples/standard_project.rb +4 -6
  19. data/lib/jirametrics/expedited_chart.rb +7 -7
  20. data/lib/jirametrics/exporter.rb +10 -20
  21. data/lib/jirametrics/file_config.rb +23 -6
  22. data/lib/jirametrics/file_system.rb +39 -4
  23. data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -4
  24. data/lib/jirametrics/groupable_issue_chart.rb +1 -3
  25. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  26. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  27. data/lib/jirametrics/html/aging_work_table.erb +6 -4
  28. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  29. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  30. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  31. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  32. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  33. data/lib/jirametrics/html/index.css +28 -5
  34. data/lib/jirametrics/html/index.erb +8 -4
  35. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  36. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  37. data/lib/jirametrics/html_report_config.rb +32 -23
  38. data/lib/jirametrics/issue.rb +104 -44
  39. data/lib/jirametrics/jira_gateway.rb +16 -3
  40. data/lib/jirametrics/project_config.rb +223 -120
  41. data/lib/jirametrics/sprint_burndown.rb +1 -1
  42. data/lib/jirametrics/status.rb +81 -26
  43. data/lib/jirametrics/status_collection.rb +74 -40
  44. data/lib/jirametrics/throughput_chart.rb +1 -1
  45. data/lib/jirametrics/value_equality.rb +2 -2
  46. data/lib/jirametrics.rb +7 -1
  47. metadata +8 -13
  48. data/lib/jirametrics/discard_changes_before.rb +0 -37
  49. data/lib/jirametrics/html/data_quality_report.erb +0 -138
@@ -4,64 +4,68 @@ 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
- def filter_status_names category_name:, including: nil, excluding: nil
12
- including = expand_statuses including
13
- excluding = expand_statuses excluding
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
- @list.filter_map do |status|
16
- keep = status.category_name == category_name ||
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
- status.name if keep
21
- end
22
- end
22
+ if id
23
+ status = find_by_id id
24
+ return [] if status.nil?
23
25
 
24
- def expand_statuses names_or_ids
25
- result = []
26
- return result if names_or_ids.nil?
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
- result << status
30
+ [status]
31
+ else
32
+ @list.select { |status| status.name == name }
43
33
  end
44
- result
45
34
  end
46
35
 
47
- def todo including: nil, excluding: nil
48
- filter_status_names category_name: 'To Do', including: including, excluding: excluding
36
+ def find_all_categories
37
+ @list
38
+ .collect(&:category)
39
+ .uniq
40
+ .sort_by(&:id)
49
41
  end
50
42
 
51
- def in_progress including: nil, excluding: nil
52
- filter_status_names category_name: 'In Progress', including: including, excluding: excluding
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 done including: nil, excluding: nil
56
- filter_status_names category_name: 'Done', including: including, excluding: excluding
57
- end
54
+ def find_all_categories_by_name identifier
55
+ key = nil
56
+ id = nil
57
+
58
+ if identifier.is_a? Symbol
59
+ key = identifier.to_s
60
+ else
61
+ name, id = parse_name_id identifier
62
+ end
58
63
 
59
- def find_by_name name
60
- find { |status| status.name == name }
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
@@ -69,7 +73,37 @@ class StatusCollection
69
73
  def clear = @list.clear
70
74
  def delete(object) = @list.delete(object)
71
75
 
76
+ def to_s
77
+ "[#{@list.sort.join(', ')}]"
78
+ end
79
+
72
80
  def inspect
73
- "StatusCollection(#{@list.collect(&:inspect).join(', ')})"
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
74
108
  end
75
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.started_stopped_times(issue).last&.to_date
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
@@ -7,6 +7,13 @@ class JiraMetrics < Thor
7
7
  true
8
8
  end
9
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
+
10
17
  option :config
11
18
  option :name
12
19
  desc 'export', "Export data into either reports or CSV's as per the configuration"
@@ -61,7 +68,6 @@ class JiraMetrics < Thor
61
68
  require 'jirametrics/grouping_rules'
62
69
  require 'jirametrics/daily_wip_chart'
63
70
  require 'jirametrics/groupable_issue_chart'
64
- require 'jirametrics/discard_changes_before'
65
71
  require 'jirametrics/css_variable'
66
72
 
67
73
  require 'jirametrics/aggregate_config'
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.7'
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: 2024-10-27 00:00:00.000000000 Z
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: Tool to extract metrics from Jira and export to either a report or to
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
@@ -106,7 +104,6 @@ files:
106
104
  - lib/jirametrics/html/cycletime_histogram.erb
107
105
  - lib/jirametrics/html/cycletime_scatterplot.erb
108
106
  - lib/jirametrics/html/daily_wip_chart.erb
109
- - lib/jirametrics/html/data_quality_report.erb
110
107
  - lib/jirametrics/html/estimate_accuracy_chart.erb
111
108
  - lib/jirametrics/html/expedited_chart.erb
112
109
  - lib/jirametrics/html/flow_efficiency_scatterplot.erb
@@ -132,15 +129,14 @@ files:
132
129
  - lib/jirametrics/tree_organizer.rb
133
130
  - lib/jirametrics/trend_line_calculator.rb
134
131
  - lib/jirametrics/value_equality.rb
135
- homepage: https://github.com/mikebowler/jirametrics
132
+ homepage: https://jirametrics.org
136
133
  licenses:
137
134
  - Apache-2.0
138
135
  metadata:
139
136
  rubygems_mfa_required: 'true'
140
137
  bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
141
- changelog_uri: https://github.com/mikebowler/jirametrics/wiki/Changes
142
- documentation_uri: https://github.com/mikebowler/jirametrics/wiki
143
- post_install_message:
138
+ changelog_uri: https://jirametrics.org/changes
139
+ documentation_uri: https://jirametrics.org
144
140
  rdoc_options: []
145
141
  require_paths:
146
142
  - lib
@@ -155,8 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
151
  - !ruby/object:Gem::Version
156
152
  version: '0'
157
153
  requirements: []
158
- rubygems_version: 3.5.21
159
- signing_key:
154
+ rubygems_version: 3.6.2
160
155
  specification_version: 4
161
156
  summary: Extract Jira metrics
162
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.discard_changes_before cutoff_time
35
- end
36
- end
37
- end
@@ -1,138 +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 :incomplete_subtasks_when_issue_done
118
- unless problems.empty?
119
- %>
120
- <p>
121
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> issues were marked as done while subtasks were still not done.
122
- <%= collapsible_issues_panel problems %>
123
- </p>
124
- <%
125
- end
126
- %>
127
-
128
- <%
129
- problems = problems_for :issue_on_multiple_boards
130
- unless problems.empty?
131
- %>
132
- <p>
133
- <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.
134
- <%= collapsible_issues_panel problems, :hide_board_column %>
135
- </p>
136
- <%
137
- end
138
- %>