jirametrics 2.31 → 3.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +42 -29
  3. data/lib/jirametrics/aging_work_bar_chart.rb +52 -40
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +65 -60
  5. data/lib/jirametrics/aging_work_table.rb +36 -33
  6. data/lib/jirametrics/anonymizer.rb +31 -86
  7. data/lib/jirametrics/atlassian_document_format.rb +11 -4
  8. data/lib/jirametrics/blocked_stalled_by_date_builder.rb +64 -0
  9. data/lib/jirametrics/blocked_stalled_change.rb +5 -1
  10. data/lib/jirametrics/blocked_stalled_change_stream_builder.rb +194 -0
  11. data/lib/jirametrics/board.rb +23 -9
  12. data/lib/jirametrics/board_config.rb +0 -7
  13. data/lib/jirametrics/board_movement_calculator.rb +24 -21
  14. data/lib/jirametrics/cfd_data_builder.rb +46 -44
  15. data/lib/jirametrics/change_item.rb +19 -7
  16. data/lib/jirametrics/chart_base.rb +50 -64
  17. data/lib/jirametrics/cumulative_flow_diagram.rb +59 -46
  18. data/lib/jirametrics/cycle_time_config.rb +35 -46
  19. data/lib/jirametrics/cycletime_histogram.rb +7 -0
  20. data/lib/jirametrics/cycletime_scatterplot.rb +7 -0
  21. data/lib/jirametrics/daily_view.rb +83 -61
  22. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +24 -9
  23. data/lib/jirametrics/daily_wip_chart.rb +72 -45
  24. data/lib/jirametrics/data_quality_report.rb +61 -63
  25. data/lib/jirametrics/dependency_chart.rb +40 -31
  26. data/lib/jirametrics/download_config.rb +0 -3
  27. data/lib/jirametrics/downloader.rb +8 -7
  28. data/lib/jirametrics/downloader_for_cloud.rb +108 -72
  29. data/lib/jirametrics/estimate_accuracy_chart.rb +13 -13
  30. data/lib/jirametrics/examples/standard_project.rb +2 -2
  31. data/lib/jirametrics/expedited_chart.rb +47 -37
  32. data/lib/jirametrics/exporter.rb +19 -10
  33. data/lib/jirametrics/file_config.rb +19 -12
  34. data/lib/jirametrics/file_system.rb +16 -10
  35. data/lib/jirametrics/flow_efficiency_calculator.rb +62 -0
  36. data/lib/jirametrics/flow_efficiency_scatterplot.rb +4 -2
  37. data/lib/jirametrics/github_gateway.rb +23 -9
  38. data/lib/jirametrics/groupable_issue_chart.rb +3 -0
  39. data/lib/jirametrics/html/index.css +6 -0
  40. data/lib/jirametrics/html_report_config.rb +9 -29
  41. data/lib/jirametrics/issue.rb +290 -389
  42. data/lib/jirametrics/issue_collection.rb +1 -0
  43. data/lib/jirametrics/issue_printer.rb +60 -23
  44. data/lib/jirametrics/jira_gateway.rb +29 -18
  45. data/lib/jirametrics/mcp_server.rb +225 -209
  46. data/lib/jirametrics/project_config.rb +158 -134
  47. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +1 -20
  48. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +11 -33
  49. data/lib/jirametrics/rules.rb +1 -0
  50. data/lib/jirametrics/self_or_issue_dispatcher.rb +1 -3
  51. data/lib/jirametrics/sprint_burndown.rb +142 -242
  52. data/lib/jirametrics/sprint_count_measure.rb +42 -0
  53. data/lib/jirametrics/sprint_issue_change_data.rb +1 -0
  54. data/lib/jirametrics/sprint_points_measure.rb +62 -0
  55. data/lib/jirametrics/sprint_summary_stats.rb +16 -0
  56. data/lib/jirametrics/status_collection.rb +2 -2
  57. data/lib/jirametrics/stitcher.rb +21 -16
  58. data/lib/jirametrics/throughput_chart.rb +38 -24
  59. data/lib/jirametrics/time_based_chart.rb +65 -0
  60. data/lib/jirametrics/time_based_histogram.rb +30 -23
  61. data/lib/jirametrics/time_based_scatterplot.rb +11 -2
  62. data/lib/jirametrics/trend_line_calculator.rb +2 -2
  63. data/lib/jirametrics/wip_by_column_chart.rb +73 -32
  64. data/lib/jirametrics.rb +6 -2
  65. metadata +13 -20
@@ -7,8 +7,10 @@ class ThroughputChart < ChartBase
7
7
 
8
8
  attr_accessor :possible_statuses
9
9
 
10
- def initialize block
10
+ def initialize block, throughput_samples: nil, not_started_count: nil
11
11
  super()
12
+ @throughput_samples = throughput_samples
13
+ @not_started_count = not_started_count
12
14
 
13
15
  header_text 'Throughput Chart'
14
16
  description_text <<-TEXT
@@ -120,30 +122,42 @@ class ThroughputChart < ChartBase
120
122
  def throughput_dataset periods:, completed_issues:, label_hint: nil
121
123
  custom_mode = @issue_periods&.values&.any?
122
124
  periods.collect do |period|
123
- closed_issues = completed_issues.filter_map do |issue|
124
- stop_date = issue.started_stopped_dates.last
125
- next unless stop_date
126
-
127
- if custom_mode
128
- [stop_date, issue] if @issue_periods[issue] == period.end
129
- elsif period.include?(stop_date)
130
- [stop_date, issue]
131
- end
132
- end
125
+ closed_issues = closed_issues_in_period(period, completed_issues, custom_mode)
126
+ period_dataset(period: period, closed_issues: closed_issues, label_hint: label_hint)
127
+ end
128
+ end
129
+
130
+ def closed_issues_in_period period, completed_issues, custom_mode
131
+ completed_issues.filter_map do |issue|
132
+ stop_date = issue.started_stopped_dates.last
133
+ next unless stop_date
133
134
 
134
- date_label = "on #{period.end}"
135
- date_label = "between #{period.begin} and #{period.end}" unless period.begin == period.end
136
-
137
- with_label_hint = label_hint ? " with #{label_hint}" : ''
138
- {
139
- y: closed_issues.size,
140
- x: "#{period.end}T23:59:59",
141
- title: ["#{closed_issues.size} items closed#{with_label_hint} #{date_label}"] +
142
- closed_issues.collect do |_stop_date, issue|
143
- hint = @issue_hints&.fetch(issue, nil)
144
- "#{issue.key} : #{issue.summary}#{" #{hint}" if hint}"
145
- end
146
- }
135
+ if custom_mode
136
+ issue if @issue_periods[issue] == period.end
137
+ elsif period.include?(stop_date)
138
+ issue
139
+ end
147
140
  end
148
141
  end
142
+
143
+ def period_dataset period:, closed_issues:, label_hint:
144
+ label_hint_text = label_hint ? " with #{label_hint}" : ''
145
+ header = "#{closed_issues.size} items closed#{label_hint_text} #{period_label(period)}"
146
+ {
147
+ y: closed_issues.size,
148
+ x: "#{period.end}T23:59:59",
149
+ title: [header] + closed_issues.collect { |issue| issue_title_line(issue) }
150
+ }
151
+ end
152
+
153
+ def period_label period
154
+ return "on #{period.end}" if period.begin == period.end
155
+
156
+ "between #{period.begin} and #{period.end}"
157
+ end
158
+
159
+ def issue_title_line issue
160
+ hint = @issue_hints&.fetch(issue, nil)
161
+ "#{issue.key} : #{issue.summary}#{" #{hint}" if hint}"
162
+ end
149
163
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/chart_base'
4
+
5
+ # Common base for every cycle-time chart. It owns the cycletime *unit system* --
6
+ # which unit the value axis is expressed in and how a raw value is labelled --
7
+ # because that concern is core to any chart that plots a cycle time, not a
8
+ # cross-cutting add-on. The scatter/histo split lives one level down (in
9
+ # TimeBasedScatterplot / TimeBasedHistogram, which supply the value_axis_title=
10
+ # hook so this class doesn't need to know which axis carries the value); the
11
+ # PR-vs-issue split lives one level below that.
12
+ class TimeBasedChart < ChartBase
13
+ # The cycletime units we support, mapped to how each reads in an axis title.
14
+ # :days counts calendar days; :hours24 counts elapsed 24-hour clock periods
15
+ # (the two differ for anything that crosses midnight).
16
+ VALUE_AXIS_LABELS = {
17
+ minutes: 'minutes',
18
+ hours: 'hours',
19
+ days: 'days',
20
+ hours24: '24-hour periods'
21
+ }.freeze
22
+
23
+ def initialize
24
+ super
25
+
26
+ @cycletime_unit = :days
27
+ end
28
+
29
+ def cycletime_unit unit
30
+ axis_label = VALUE_AXIS_LABELS[unit]
31
+ unless axis_label
32
+ raise ArgumentError,
33
+ "cycletime_unit must be one of #{VALUE_AXIS_LABELS.keys.map(&:inspect).join(', ')}, got #{unit.inspect}"
34
+ end
35
+
36
+ @cycletime_unit = unit
37
+ self.value_axis_title = "Cycle time in #{axis_label}"
38
+ end
39
+
40
+ def label_cycletime value
41
+ case @cycletime_unit
42
+ when :minutes then label_minutes(value)
43
+ when :hours then label_hours(value)
44
+ when :days then label_days(value)
45
+ when :hours24 then "#{value}x 24h periods"
46
+ end
47
+ end
48
+
49
+ # Converts the span between two Times into the selected cycletime unit.
50
+ #
51
+ # :days is deliberately different from the other units: it counts calendar days
52
+ # inclusively in the configured timezone (opened and closed on the same day is
53
+ # 1 day, crossing one midnight is 2), matching the working-days cycletime engine.
54
+ # The elapsed units (:hours, :minutes) divide the wall-clock span and round up --
55
+ # a partial unit still counts as one, so a 20-minute PR is 1 hour, never 0.
56
+ def duration_in_unit from, to
57
+ if @cycletime_unit == :days
58
+ tz = timezone_offset || '+00:00'
59
+ (to.getlocal(tz).to_date - from.getlocal(tz).to_date).to_i + 1
60
+ else
61
+ seconds_per_unit = { minutes: 60, hours: 3600, hours24: 86_400 }[@cycletime_unit]
62
+ ((to - from) / seconds_per_unit).ceil
63
+ end
64
+ end
65
+ end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jirametrics/groupable_issue_chart'
4
+ require 'jirametrics/time_based_chart'
4
5
 
5
- class TimeBasedHistogram < ChartBase
6
+ class TimeBasedHistogram < TimeBasedChart
6
7
  include GroupableIssueChart
7
8
 
8
9
  attr_reader :show_stats
@@ -14,6 +15,11 @@ class TimeBasedHistogram < ChartBase
14
15
  @show_stats = true
15
16
  end
16
17
 
18
+ # On a histogram the cycle time is plotted along the x-axis (count is on the y-axis).
19
+ def value_axis_title= title
20
+ @x_axis_title = title
21
+ end
22
+
17
23
  def percentiles percs = nil
18
24
  @percentiles = percs unless percs.nil?
19
25
  @percentiles
@@ -68,42 +74,43 @@ class TimeBasedHistogram < ChartBase
68
74
  return {} if histogram_data.empty?
69
75
 
70
76
  total_values = histogram_data.values.sum
77
+ min, max = histogram_data.keys.minmax
78
+ {
79
+ average: average_for(histogram_data, total_values),
80
+ mode: modes_for(histogram_data),
81
+ min: min,
82
+ max: max,
83
+ percentiles: percentiles_for(histogram_data, percentiles, total_values)
84
+ }
85
+ end
71
86
 
72
- # Calculate the average
73
- weighted_sum = histogram_data.reduce(0) { |sum, (value, frequency)| sum + (value * frequency) }
74
- average = total_values.zero? ? 0 : weighted_sum.to_f / total_values
87
+ def average_for histogram_data, total_values
88
+ return 0 if total_values.zero?
75
89
 
76
- # Find the mode (or modes!) and the spread of the distribution
77
- sorted_histogram = histogram_data.sort_by { |_value, frequency| frequency }
78
- max_freq = sorted_histogram[-1][1]
79
- mode = sorted_histogram.select { |_v, f| f == max_freq }
90
+ weighted_sum = histogram_data.reduce(0) { |sum, (value, frequency)| sum + (value * frequency) }
91
+ weighted_sum.to_f / total_values
92
+ end
80
93
 
81
- minmax = histogram_data.keys.minmax
94
+ # Every value that ties for the highest frequency, so a flat or multi-modal distribution returns them all.
95
+ def modes_for histogram_data
96
+ sorted_by_frequency = histogram_data.sort_by { |_value, frequency| frequency }
97
+ max_frequency = sorted_by_frequency[-1][1]
98
+ sorted_by_frequency.select { |_value, frequency| frequency == max_frequency }.collect(&:first).sort
99
+ end
82
100
 
83
- # Calculate percentiles
101
+ def percentiles_for histogram_data, percentiles, total_values
84
102
  sorted_values = histogram_data.keys.sort
85
103
  cumulative_counts = {}
86
104
  cumulative_sum = 0
87
-
88
105
  sorted_values.each do |value|
89
106
  cumulative_sum += histogram_data[value]
90
107
  cumulative_counts[value] = cumulative_sum
91
108
  end
92
109
 
93
- percentile_results = {}
94
- percentiles.each do |percentile|
110
+ percentiles.to_h do |percentile|
95
111
  rank = (percentile / 100.0) * total_values
96
- percentile_value = sorted_values.find { |value| cumulative_counts[value] >= rank }
97
- percentile_results[percentile] = percentile_value
112
+ [percentile, sorted_values.find { |value| cumulative_counts[value] >= rank }]
98
113
  end
99
-
100
- {
101
- average: average,
102
- mode: mode.collect(&:first).sort,
103
- min: minmax[0],
104
- max: minmax[1],
105
- percentiles: percentile_results
106
- }
107
114
  end
108
115
 
109
116
  def sort_items items
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jirametrics/groupable_issue_chart'
4
+ require 'jirametrics/time_based_chart'
4
5
 
5
- class TimeBasedScatterplot < ChartBase
6
+ class TimeBasedScatterplot < TimeBasedChart
6
7
  include GroupableIssueChart
7
8
 
8
9
  def initialize
@@ -12,13 +13,21 @@ class TimeBasedScatterplot < ChartBase
12
13
  @highest_y_value = 0
13
14
  end
14
15
 
16
+ # On a scatterplot the cycle time is plotted up the y-axis.
17
+ def value_axis_title= title
18
+ @y_axis_title = title
19
+ end
20
+
15
21
  def run
16
22
  items = all_items
17
23
  data_sets = create_datasets items
18
24
  overall_percent_line = calculate_percent_line(items)
19
25
  @percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
20
26
 
21
- return "<h1 class='foldable'>#{@header_text}</h1><div>No data matched the selected criteria. Nothing to show.</div>" if data_sets.empty?
27
+ if data_sets.empty?
28
+ return "<h1 class='foldable'>#{@header_text}</h1>" \
29
+ '<div>No data matched the selected criteria. Nothing to show.</div>'
30
+ end
22
31
 
23
32
  wrap_and_render(binding, __FILE__)
24
33
  end
@@ -24,11 +24,11 @@ class TrendLineCalculator
24
24
  def horizontal? = @slope.zero?
25
25
  def vertical? = @slope.nan? || @slope.infinite?
26
26
 
27
- def calc_y x: # rubocop:disable Naming/MethodParameterName
27
+ def calc_y x:
28
28
  ((x * @slope) + @offset).to_i
29
29
  end
30
30
 
31
- def line_crosses_at y: # rubocop:disable Naming/MethodParameterName
31
+ def line_crosses_at y:
32
32
  raise "line will never cross #{y}. Trend is perfectly horizontal" if horizontal?
33
33
 
34
34
  ((y.to_f - @offset) / @slope).to_i
@@ -7,7 +7,8 @@ class WipByColumnChart < ChartBase
7
7
 
8
8
  ColumnStats = Struct.new(:name, :min_wip_limit, :max_wip_limit, :wip_history, keyword_init: true)
9
9
 
10
- def initialize block
10
+ # Long only because of the inline description_text heredoc and one-time setup; splitting wouldn't help.
11
+ def initialize block # rubocop:disable Metrics/MethodLength
11
12
  super()
12
13
  header_text 'WIP by column'
13
14
  description_text <<-HTML
@@ -64,10 +65,10 @@ class WipByColumnChart < ChartBase
64
65
  total = stat.wip_history.sum { |_wip, seconds| seconds }.to_f
65
66
  next [] if total.zero?
66
67
 
67
- stat.wip_history.collect { |wip, seconds| { 'wip' => wip, 'pct' => format_pct(seconds, total) } }
68
+ wip_percentages(stat.wip_history, total)
68
69
  end
69
- @max_wip = stats.flat_map { |s| s.wip_history.collect { |wip, _| wip } }.max || 0
70
- @wip_limits = stats.collect { |s| { 'min' => s.min_wip_limit, 'max' => s.max_wip_limit } }
70
+ @max_wip = max_wip_level(stats)
71
+ @wip_limits = wip_limits(stats)
71
72
  @recommendations = @show_recommendations ? compute_recommendations(stats) : Array.new(stats.size)
72
73
 
73
74
  trim_zero_end_columns
@@ -144,6 +145,22 @@ class WipByColumnChart < ChartBase
144
145
  end
145
146
  end
146
147
 
148
+ # Turn a column's WIP history into render rows: each WIP level with the percentage of the column's
149
+ # total time spent there.
150
+ def wip_percentages wip_history, total
151
+ wip_history.collect { |wip, seconds| { 'wip' => wip, 'pct' => format_pct(seconds, total) } }
152
+ end
153
+
154
+ # The highest WIP level reached in any column, or 0 when there's no data.
155
+ def max_wip_level stats
156
+ stats.flat_map { |stat| stat.wip_history.collect { |wip, _seconds| wip } }.max || 0
157
+ end
158
+
159
+ # The configured min/max WIP limits for each column, in column order.
160
+ def wip_limits stats
161
+ stats.collect { |stat| { 'min' => stat.min_wip_limit, 'max' => stat.max_wip_limit } }
162
+ end
163
+
147
164
  def format_pct seconds, total
148
165
  raw = seconds / total * 100.0
149
166
  (1..10).each do |decimals|
@@ -173,8 +190,7 @@ class WipByColumnChart < ChartBase
173
190
  next
174
191
  end
175
192
 
176
- last_change = issue.status_changes.reverse.find { |c| c.time <= time_range.begin }
177
- hash[issue] = last_change ? status_to_column[last_change.value_id] : nil
193
+ hash[issue] = column_as_of(issue, time_range.begin, status_to_column)
178
194
  end
179
195
  end
180
196
 
@@ -184,30 +200,46 @@ class WipByColumnChart < ChartBase
184
200
  started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
185
201
  next unless started_time
186
202
 
187
- # Issue starts within the window: add an explicit event to enter WIP in its current column
188
- if started_time > time_range.begin && started_time <= time_range.end
189
- last_change = issue.status_changes.reverse.find { |c| c.time <= started_time }
190
- events << [started_time, issue, last_change ? status_to_column[last_change.value_id] : nil]
203
+ if within_window?(started_time)
204
+ # add an explicit event to enter WIP in its current column
205
+ events << [started_time, issue, column_as_of(issue, started_time, status_to_column)]
191
206
  end
192
207
 
193
- # Status changes while the issue is actively in WIP and within the window
194
208
  issue.status_changes.each do |change|
195
- next unless change.time > time_range.begin
196
- next if change.time > time_range.end
197
- next unless change.time >= started_time
198
- next if stopped_time && change.time >= stopped_time
209
+ next unless in_wip_and_within_window?(change, started_time, stopped_time)
199
210
 
200
211
  events << [change.time, issue, status_to_column[change.value_id]]
201
212
  end
202
213
 
203
- # Issue stops within the window: add an explicit event to exit WIP
204
- if stopped_time && stopped_time > time_range.begin && stopped_time <= time_range.end
205
- events << [stopped_time, issue, nil]
206
- end
214
+ events << [stopped_time, issue, nil] if within_window?(stopped_time)
207
215
  end
208
216
  events.sort_by!(&:first)
209
217
  end
210
218
 
219
+ # True when a (present) time falls inside the chart window: after it opens, up to and including
220
+ # when it closes.
221
+ def within_window? time
222
+ time && time > time_range.begin && time <= time_range.end
223
+ end
224
+
225
+ # A status change counts if it happened inside the window and while the issue was actively in WIP:
226
+ # after the window opens, up to and including when it closes, at or after the issue started, and
227
+ # strictly before it stopped.
228
+ def in_wip_and_within_window? change, started_time, stopped_time
229
+ return false unless within_window?(change.time)
230
+ return false unless change.time >= started_time
231
+ return false if stopped_time && change.time >= stopped_time
232
+
233
+ true
234
+ end
235
+
236
+ # The column the issue was displayed in as of the given time: the column of its most recent
237
+ # status change at or before that time, or nil if it hadn't changed status yet.
238
+ def column_as_of issue, time, status_to_column
239
+ last_change = issue.status_changes.reverse.find { |change| change.time <= time }
240
+ last_change ? status_to_column[last_change.value_id] : nil
241
+ end
242
+
211
243
  def compute_wip_seconds columns, current_column, events
212
244
  wip_counts = Array.new(columns.size, 0)
213
245
  current_column.each_value { |col| wip_counts[col] += 1 unless col.nil? }
@@ -216,21 +248,30 @@ class WipByColumnChart < ChartBase
216
248
  prev_time = time_range.begin
217
249
 
218
250
  events.each do |time, issue, new_col|
219
- elapsed = (time - prev_time).to_i
220
- if elapsed.positive?
221
- wip_counts.each_with_index { |wip, idx| column_wip_seconds[idx][wip] += elapsed }
222
- prev_time = time
223
- end
224
-
225
- old_col = current_column[issue]
226
- wip_counts[old_col] -= 1 unless old_col.nil?
227
- wip_counts[new_col] += 1 unless new_col.nil?
228
- current_column[issue] = new_col
251
+ prev_time = accumulate_wip_seconds(column_wip_seconds, wip_counts, prev_time, time)
252
+ apply_event(wip_counts, current_column, issue, new_col)
229
253
  end
230
-
231
- elapsed = (time_range.end - prev_time).to_i
232
- wip_counts.each_with_index { |wip, idx| column_wip_seconds[idx][wip] += elapsed } if elapsed.positive?
254
+ accumulate_wip_seconds(column_wip_seconds, wip_counts, prev_time, time_range.end)
233
255
 
234
256
  column_wip_seconds
235
257
  end
258
+
259
+ # Attribute the seconds between prev_time and time to each column's current WIP level, then return
260
+ # the new prev_time. A zero-length gap changes nothing and leaves prev_time where it was.
261
+ def accumulate_wip_seconds column_wip_seconds, wip_counts, prev_time, time
262
+ elapsed = (time - prev_time).to_i
263
+ return prev_time unless elapsed.positive?
264
+
265
+ wip_counts.each_with_index { |wip, idx| column_wip_seconds[idx][wip] += elapsed }
266
+ time
267
+ end
268
+
269
+ # Move the issue out of its old column and into new_col, keeping wip_counts and current_column in
270
+ # sync. A nil column means the issue isn't on the board — ie entering or leaving WIP.
271
+ def apply_event wip_counts, current_column, issue, new_col
272
+ old_col = current_column[issue]
273
+ wip_counts[old_col] -= 1 unless old_col.nil?
274
+ wip_counts[new_col] += 1 unless new_col.nil?
275
+ current_column[issue] = new_col
276
+ end
236
277
  end
data/lib/jirametrics.rb CHANGED
@@ -108,13 +108,17 @@ class JiraMetrics < Thor
108
108
  file_system ||= Exporter.instance.file_system
109
109
  return if file_system.logfile == $stdout
110
110
 
111
- file_system.logfile.puts "#{exception.class}: #{exception.message}"
112
- exception.backtrace&.each { |line| file_system.logfile.puts "\t#{line}" }
111
+ write_exception_to_logfile file_system.logfile, exception
113
112
  rescue StandardError
114
113
  # Exporter may not be initialized, or the logfile may already be closed
115
114
  end
116
115
  end
117
116
 
117
+ def self.write_exception_to_logfile logfile, exception
118
+ logfile.puts "#{exception.class}: #{exception.message}"
119
+ exception.backtrace&.each { |line| logfile.puts "\t#{line}" }
120
+ end
121
+
118
122
  at_exit { JiraMetrics.log_uncaught_exception $ERROR_INFO }
119
123
 
120
124
  no_commands do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.31'
4
+ version: '3.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: mutant-rspec
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :development
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: mcp
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -69,16 +55,16 @@ dependencies:
69
55
  name: thor
70
56
  requirement: !ruby/object:Gem::Requirement
71
57
  requirements:
72
- - - "~>"
58
+ - - ">="
73
59
  - !ruby/object:Gem::Version
74
- version: 1.2.2
60
+ version: '0'
75
61
  type: :runtime
76
62
  prerelease: false
77
63
  version_requirements: !ruby/object:Gem::Requirement
78
64
  requirements:
79
- - - "~>"
65
+ - - ">="
80
66
  - !ruby/object:Gem::Version
81
- version: 1.2.2
67
+ version: '0'
82
68
  description: Extract metrics from Jira and export to either a report or to CSV files
83
69
  email: mbowler@gargoylesoftware.com
84
70
  executables:
@@ -97,7 +83,9 @@ files:
97
83
  - lib/jirametrics/anonymizer.rb
98
84
  - lib/jirametrics/atlassian_document_format.rb
99
85
  - lib/jirametrics/bar_chart_range.rb
86
+ - lib/jirametrics/blocked_stalled_by_date_builder.rb
100
87
  - lib/jirametrics/blocked_stalled_change.rb
88
+ - lib/jirametrics/blocked_stalled_change_stream_builder.rb
101
89
  - lib/jirametrics/board.rb
102
90
  - lib/jirametrics/board_column.rb
103
91
  - lib/jirametrics/board_config.rb
@@ -132,6 +120,7 @@ files:
132
120
  - lib/jirametrics/file_config.rb
133
121
  - lib/jirametrics/file_system.rb
134
122
  - lib/jirametrics/fix_version.rb
123
+ - lib/jirametrics/flow_efficiency_calculator.rb
135
124
  - lib/jirametrics/flow_efficiency_scatterplot.rb
136
125
  - lib/jirametrics/github_gateway.rb
137
126
  - lib/jirametrics/groupable_issue_chart.rb
@@ -175,12 +164,16 @@ files:
175
164
  - lib/jirametrics/settings.json
176
165
  - lib/jirametrics/sprint.rb
177
166
  - lib/jirametrics/sprint_burndown.rb
167
+ - lib/jirametrics/sprint_count_measure.rb
178
168
  - lib/jirametrics/sprint_issue_change_data.rb
169
+ - lib/jirametrics/sprint_points_measure.rb
170
+ - lib/jirametrics/sprint_summary_stats.rb
179
171
  - lib/jirametrics/status.rb
180
172
  - lib/jirametrics/status_collection.rb
181
173
  - lib/jirametrics/stitcher.rb
182
174
  - lib/jirametrics/throughput_by_completed_resolution_chart.rb
183
175
  - lib/jirametrics/throughput_chart.rb
176
+ - lib/jirametrics/time_based_chart.rb
184
177
  - lib/jirametrics/time_based_histogram.rb
185
178
  - lib/jirametrics/time_based_scatterplot.rb
186
179
  - lib/jirametrics/tree_organizer.rb
@@ -203,7 +196,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
203
196
  requirements:
204
197
  - - ">="
205
198
  - !ruby/object:Gem::Version
206
- version: 3.0.0
199
+ version: 3.4.0
207
200
  required_rubygems_version: !ruby/object:Gem::Requirement
208
201
  requirements:
209
202
  - - ">="