jirametrics 2.3 → 2.4pre2
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/blocked_stalled_change.rb +24 -4
- data/lib/jirametrics/change_item.rb +13 -5
- data/lib/jirametrics/columns_config.rb +4 -0
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +3 -16
- data/lib/jirametrics/data_quality_report.rb +4 -1
- data/lib/jirametrics/exporter.rb +7 -3
- data/lib/jirametrics/file_config.rb +7 -1
- data/lib/jirametrics/html/index.erb +1 -0
- data/lib/jirametrics/issue.rb +61 -20
- data/lib/jirametrics/jira_gateway.rb +11 -9
- data/lib/jirametrics/project_config.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd626d730d591c7cc7ec2a9245fa395bc1d8d7fc22d658b6663dce28aa40f351
|
4
|
+
data.tar.gz: b0437b25f7def75b2bd2f0fd889aead1104b65ec53e339f48b8f2e535dc84d96
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c8c7f1ce346a98f989641fe4cb962d4f3fd04eab690b861e296154564f2d145cfa705fbf8d590ec004a4b02f707799fb2c21eef5cddc7e4e00bcc090730bca7
|
7
|
+
data.tar.gz: 596c0b5559ae4a64d46e95f38fb13e0fdec5bc8c315195ea9a45d69abd26841d4d4aa28cee1fe46b769b596810129aaf79e9fb1052ec3827aaa89c092a5687e4
|
@@ -15,12 +15,12 @@ class BlockedStalledChange
|
|
15
15
|
@time = time
|
16
16
|
end
|
17
17
|
|
18
|
-
def blocked? = @flag || blocked_by_status? || @blocking_issue_keys
|
19
|
-
def stalled? = @stalled_days || stalled_by_status?
|
18
|
+
def blocked? = !!(@flag || blocked_by_status? || @blocking_issue_keys)
|
19
|
+
def stalled? = !!(@stalled_days || stalled_by_status?)
|
20
20
|
def active? = !blocked? && !stalled?
|
21
21
|
|
22
|
-
def blocked_by_status? = @status && @status_is_blocking
|
23
|
-
def stalled_by_status? = @status && !@status_is_blocking
|
22
|
+
def blocked_by_status? = !!(@status && @status_is_blocking)
|
23
|
+
def stalled_by_status? = !!(@status && !@status_is_blocking)
|
24
24
|
|
25
25
|
def reasons
|
26
26
|
result = []
|
@@ -35,4 +35,24 @@ class BlockedStalledChange
|
|
35
35
|
end
|
36
36
|
result.join(', ')
|
37
37
|
end
|
38
|
+
|
39
|
+
def as_symbol
|
40
|
+
if blocked?
|
41
|
+
:blocked
|
42
|
+
elsif stalled?
|
43
|
+
:stalled
|
44
|
+
else
|
45
|
+
:active
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
text = +"BlockedStalledChange(time: '#{@time}', "
|
51
|
+
if active?
|
52
|
+
text << 'Active'
|
53
|
+
else
|
54
|
+
text << reasons
|
55
|
+
end
|
56
|
+
text << ')'
|
57
|
+
end
|
38
58
|
end
|
@@ -5,7 +5,6 @@ class ChangeItem
|
|
5
5
|
attr_accessor :value, :old_value, :time
|
6
6
|
|
7
7
|
def initialize raw:, time:, author:, artificial: false
|
8
|
-
# raw will only ever be nil in a test and in that case field and value should be passed in
|
9
8
|
@raw = raw
|
10
9
|
@time = time
|
11
10
|
raise "Time must be an object of type Time in the correct timezone: #{@time}" if @time.is_a? String
|
@@ -36,16 +35,17 @@ class ChangeItem
|
|
36
35
|
def link? = (field == 'Link')
|
37
36
|
|
38
37
|
def to_s
|
39
|
-
message =
|
40
|
-
message
|
41
|
-
message
|
38
|
+
message = +''
|
39
|
+
message << "ChangeItem(field: #{field.inspect}, value: #{value.inspect}, time: #{time_to_s(@time).inspect}"
|
40
|
+
message << ', artificial' if artificial?
|
41
|
+
message << ')'
|
42
42
|
message
|
43
43
|
end
|
44
44
|
|
45
45
|
def inspect = to_s
|
46
46
|
|
47
47
|
def == other
|
48
|
-
field.eql?(other.field) && value.eql?(other.value) && time.
|
48
|
+
field.eql?(other.field) && value.eql?(other.value) && time_to_s(time).eql?(time_to_s(other.time))
|
49
49
|
end
|
50
50
|
|
51
51
|
def current_status_matches *status_names_or_ids
|
@@ -77,4 +77,12 @@ class ChangeItem
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def time_to_s time
|
84
|
+
# MRI and JRuby return different strings for to_s() so we have to explicitly provide a full
|
85
|
+
# format so that tests work under both environments.
|
86
|
+
time.strftime '%Y-%m-%d %H:%M:%S %z'
|
87
|
+
end
|
80
88
|
end
|
@@ -34,6 +34,10 @@ class ColumnsConfig
|
|
34
34
|
@columns << [:string, label, proc]
|
35
35
|
end
|
36
36
|
|
37
|
+
def integer label, proc
|
38
|
+
@columns << [:integer, label, proc]
|
39
|
+
end
|
40
|
+
|
37
41
|
def column_entry_times board_id: nil
|
38
42
|
@file_config.project_config.find_board_by_id(board_id).visible_columns.each do |column|
|
39
43
|
date column.name, first_time_in_status(*column.status_ids)
|
@@ -38,25 +38,12 @@ class DailyWipByBlockedStalledChart < DailyWipChart
|
|
38
38
|
HTML
|
39
39
|
end
|
40
40
|
|
41
|
-
def key_blocked_stalled_change issue:, date:, end_time:
|
42
|
-
stalled_change = nil
|
43
|
-
blocked_change = nil
|
44
|
-
|
45
|
-
issue.blocked_stalled_changes_on_date(date: date, end_time: end_time) do |change|
|
46
|
-
blocked_change = change if change.blocked?
|
47
|
-
stalled_change = change if change.stalled?
|
48
|
-
end
|
49
|
-
|
50
|
-
return blocked_change if blocked_change
|
51
|
-
return stalled_change if stalled_change
|
52
|
-
|
53
|
-
nil
|
54
|
-
end
|
55
|
-
|
56
41
|
def default_grouping_rules issue:, rules:
|
57
42
|
started = issue.board.cycletime.started_time(issue)
|
58
43
|
stopped_date = issue.board.cycletime.stopped_time(issue)&.to_date
|
59
|
-
|
44
|
+
|
45
|
+
date = rules.current_date
|
46
|
+
change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
|
60
47
|
|
61
48
|
stopped_today = stopped_date == rules.current_date
|
62
49
|
|
@@ -71,7 +71,10 @@ class DataQualityReport < ChartBase
|
|
71
71
|
|
72
72
|
# Return a format that's easier to assert against
|
73
73
|
def testable_entries
|
74
|
-
|
74
|
+
format = '%Y-%m-%d %H:%M:%S %z'
|
75
|
+
@entries.collect do |entry|
|
76
|
+
[entry.started&.strftime(format) || '', entry.stopped&.strftime(format) || '', entry.issue]
|
77
|
+
end
|
75
78
|
end
|
76
79
|
|
77
80
|
def entries_with_problems
|
data/lib/jirametrics/exporter.rb
CHANGED
@@ -5,7 +5,7 @@ require 'fileutils'
|
|
5
5
|
class Object
|
6
6
|
def deprecated message:, date:
|
7
7
|
text = +''
|
8
|
-
text << "Deprecated(#{date}):"
|
8
|
+
text << "Deprecated(#{date}): "
|
9
9
|
text << message
|
10
10
|
text << "\n-> Called from #{caller(1..1).first}"
|
11
11
|
warn text
|
@@ -13,7 +13,8 @@ class Object
|
|
13
13
|
end
|
14
14
|
|
15
15
|
class Exporter
|
16
|
-
attr_reader :project_configs
|
16
|
+
attr_reader :project_configs
|
17
|
+
attr_accessor :file_system
|
17
18
|
|
18
19
|
def self.configure &block
|
19
20
|
logfile_name = 'jirametrics.log'
|
@@ -99,7 +100,10 @@ class Exporter
|
|
99
100
|
end
|
100
101
|
|
101
102
|
def jira_config filename = nil
|
102
|
-
|
103
|
+
if filename
|
104
|
+
@jira_config = file_system.load_json(filename)
|
105
|
+
@jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
|
106
|
+
end
|
103
107
|
@jira_config
|
104
108
|
end
|
105
109
|
|
@@ -66,7 +66,9 @@ class FileConfig
|
|
66
66
|
# is that all empty values in the first column should be at the bottom.
|
67
67
|
def sort_output all_lines
|
68
68
|
all_lines.sort do |a, b|
|
69
|
-
if a[0]
|
69
|
+
if a[0] == b[0]
|
70
|
+
a[1..] <=> b[1..]
|
71
|
+
elsif a[0].nil?
|
70
72
|
1
|
71
73
|
elsif b[0].nil?
|
72
74
|
-1
|
@@ -110,6 +112,10 @@ class FileConfig
|
|
110
112
|
object.to_s
|
111
113
|
end
|
112
114
|
|
115
|
+
def to_integer object
|
116
|
+
object.to_i
|
117
|
+
end
|
118
|
+
|
113
119
|
def file_suffix suffix = nil
|
114
120
|
@file_suffix = suffix unless suffix.nil?
|
115
121
|
@file_suffix
|
@@ -1,6 +1,7 @@
|
|
1
1
|
<html>
|
2
2
|
<head>
|
3
3
|
<meta charset="UTF-8">
|
4
|
+
<link rel="icon" type="image/png" href="https://github.com/mikebowler/jirametrics/blob/main/favicon.png?raw=true" />
|
4
5
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
|
5
6
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
6
7
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -186,29 +186,62 @@ class Issue
|
|
186
186
|
end
|
187
187
|
|
188
188
|
def blocked_on_date? date, end_time:
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
189
|
+
(blocked_stalled_by_date date_range: date..date, chart_end_time: end_time)[date].blocked?
|
190
|
+
end
|
191
|
+
|
192
|
+
# For any day in the day range...
|
193
|
+
# If the issue was blocked at any point in this day, the whole day is blocked.
|
194
|
+
# If the issue was active at any point in this day, the whole day is active
|
195
|
+
# If the day was stalled for the entire day then it's stalled
|
196
|
+
# If there was no activity at all on this day then the last change from the previous day carries over
|
197
|
+
def blocked_stalled_by_date date_range:, chart_end_time:, settings: nil
|
198
|
+
results = {}
|
199
|
+
current_date = nil
|
200
|
+
blocked_stalled_changes = blocked_stalled_changes(end_time: chart_end_time, settings: settings)
|
201
|
+
blocked_stalled_changes.each do |change|
|
202
|
+
current_date = change.time.to_date
|
203
|
+
|
204
|
+
winning_change, _last_change = results[current_date]
|
205
|
+
if winning_change.nil? ||
|
206
|
+
change.blocked? ||
|
207
|
+
(change.active? && (winning_change.active? || winning_change.stalled?)) ||
|
208
|
+
(change.stalled? && winning_change.stalled?)
|
209
|
+
|
210
|
+
winning_change = change
|
211
|
+
end
|
194
212
|
|
195
|
-
|
196
|
-
|
197
|
-
this_day_start_time = date.to_time
|
213
|
+
results[current_date] = [winning_change, change]
|
214
|
+
end
|
198
215
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
216
|
+
last_populated_date = nil
|
217
|
+
(results.keys.min..results.keys.max).each do |date|
|
218
|
+
if results.key? date
|
219
|
+
last_populated_date = date
|
220
|
+
else
|
221
|
+
_winner, last = results[last_populated_date]
|
222
|
+
results[date] = [last, last]
|
205
223
|
end
|
206
|
-
|
207
|
-
yield change if previous_change_time < next_day_start_time && change.time >= this_day_start_time
|
208
224
|
end
|
225
|
+
results = results.transform_values(&:first)
|
226
|
+
|
227
|
+
# The requested date range may span outside the actual changes we find in the changelog
|
228
|
+
date_of_first_change = blocked_stalled_changes[0].time.to_date
|
229
|
+
date_of_last_change = blocked_stalled_changes[-1].time.to_date
|
230
|
+
date_range.each do |date|
|
231
|
+
results[date] = blocked_stalled_changes[0] if date < date_of_first_change
|
232
|
+
results[date] = blocked_stalled_changes[-1] if date > date_of_last_change
|
233
|
+
end
|
234
|
+
|
235
|
+
# To make the code simpler, we've been accumulating data for every date. Now remove anything
|
236
|
+
# that isn't in the requested date_range
|
237
|
+
results.select! { |date, _value| date_range.include? date }
|
238
|
+
|
239
|
+
results
|
209
240
|
end
|
210
241
|
|
211
|
-
def blocked_stalled_changes end_time:, settings:
|
242
|
+
def blocked_stalled_changes end_time:, settings: nil
|
243
|
+
settings ||= @board.project_config.settings
|
244
|
+
|
212
245
|
blocked_statuses = settings['blocked_statuses']
|
213
246
|
stalled_statuses = settings['stalled_statuses']
|
214
247
|
unless blocked_statuses.is_a?(Array) && stalled_statuses.is_a?(Array)
|
@@ -222,7 +255,7 @@ class Issue
|
|
222
255
|
blocking_issue_keys = []
|
223
256
|
|
224
257
|
result = []
|
225
|
-
previous_was_active =
|
258
|
+
previous_was_active = false # Must start as false so that the creation will insert an :active
|
226
259
|
previous_change_time = created
|
227
260
|
|
228
261
|
blocking_status = nil
|
@@ -231,6 +264,7 @@ class Issue
|
|
231
264
|
# This mock change is to force the writing of one last entry at the end of the time range.
|
232
265
|
# By doing this, we're able to eliminate a lot of duplicated code in charts.
|
233
266
|
mock_change = ChangeItem.new time: end_time, author: '', artificial: true, raw: { 'field' => '' }
|
267
|
+
|
234
268
|
(changes + [mock_change]).each do |change|
|
235
269
|
previous_was_active = false if check_for_stalled(
|
236
270
|
change_time: change.time,
|
@@ -292,6 +326,7 @@ class Issue
|
|
292
326
|
stalled_days: result[-1].stalled_days
|
293
327
|
)
|
294
328
|
end
|
329
|
+
|
295
330
|
result
|
296
331
|
end
|
297
332
|
|
@@ -301,6 +336,9 @@ class Issue
|
|
301
336
|
# The most common case will be nothing to split so quick escape.
|
302
337
|
return false if (change_time - previous_change_time).to_i < stalled_threshold_seconds
|
303
338
|
|
339
|
+
# If the last identified change was blocked then it doesn't matter now long we've waited, we're still blocked.
|
340
|
+
return false if blocking_stalled_changes[-1]&.blocked?
|
341
|
+
|
304
342
|
list = [previous_change_time..change_time]
|
305
343
|
all_subtask_activity_times.each do |time|
|
306
344
|
matching_range = list.find { |range| time >= range.begin && time <= range.end }
|
@@ -457,7 +495,7 @@ class Issue
|
|
457
495
|
value = change.value
|
458
496
|
old_value = change.old_value
|
459
497
|
|
460
|
-
message = " [change] #{change.time} [#{change.field}] "
|
498
|
+
message = " [change] #{change.time.strftime '%Y-%m-%d %H:%M:%S %z'} [#{change.field}] "
|
461
499
|
message << "#{compact_text(old_value).inspect} -> " unless old_value.nil? || old_value.empty?
|
462
500
|
message << compact_text(value).inspect
|
463
501
|
message << " (#{change.author})"
|
@@ -511,7 +549,10 @@ class Issue
|
|
511
549
|
# It's common that a resolved will happen at the same time as a status change.
|
512
550
|
# Put them in a defined order so tests can be deterministic.
|
513
551
|
compare = a.time <=> b.time
|
514
|
-
|
552
|
+
if compare.zero?
|
553
|
+
compare = 1 if a.resolution?
|
554
|
+
compare = -1 if b.resolution?
|
555
|
+
end
|
515
556
|
compare
|
516
557
|
end
|
517
558
|
end
|
@@ -48,15 +48,17 @@ class JiraGateway
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def make_curl_command url:
|
51
|
-
command = '
|
52
|
-
command
|
53
|
-
command
|
54
|
-
command
|
55
|
-
command
|
56
|
-
command
|
57
|
-
command
|
58
|
-
command
|
59
|
-
command
|
51
|
+
command = +''
|
52
|
+
command << 'curl'
|
53
|
+
command << ' -L' # follow redirects
|
54
|
+
command << ' -s' # silent
|
55
|
+
command << ' -k' if @ignore_ssl_errors # insecure
|
56
|
+
command << " --cookie #{@cookies.inspect}" unless @cookies.empty?
|
57
|
+
command << " --user #{@jira_email}:#{@jira_api_token}" if @jira_api_token
|
58
|
+
command << " -H \"Authorization: Bearer #{@jira_personal_access_token}\"" if @jira_personal_access_token
|
59
|
+
command << ' --request GET'
|
60
|
+
command << ' --header "Accept: application/json"'
|
61
|
+
command << " --url \"#{url}\""
|
60
62
|
command
|
61
63
|
end
|
62
64
|
end
|
@@ -249,7 +249,7 @@ class ProjectConfig
|
|
249
249
|
|
250
250
|
def to_time string, end_of_day: false
|
251
251
|
time = end_of_day ? '23:59:59' : '00:00:00'
|
252
|
-
string = "#{string}T#{time}#{
|
252
|
+
string = "#{string}T#{time}#{exporter.timezone_offset}" if string.match?(/^\d{4}-\d{2}-\d{2}$/)
|
253
253
|
Time.parse string
|
254
254
|
end
|
255
255
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jirametrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.4pre2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Bowler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: random-word
|
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
153
|
- !ruby/object:Gem::Version
|
154
154
|
version: '0'
|
155
155
|
requirements: []
|
156
|
-
rubygems_version: 3.5.
|
156
|
+
rubygems_version: 3.5.15
|
157
157
|
signing_key:
|
158
158
|
specification_version: 4
|
159
159
|
summary: Extract Jira metrics
|