jirametrics 2.0 → 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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +19 -26
  3. data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
  5. data/lib/jirametrics/aging_work_table.rb +78 -43
  6. data/lib/jirametrics/anonymizer.rb +6 -5
  7. data/lib/jirametrics/blocked_stalled_change.rb +24 -4
  8. data/lib/jirametrics/board.rb +44 -15
  9. data/lib/jirametrics/board_config.rb +8 -4
  10. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  11. data/lib/jirametrics/change_item.rb +31 -10
  12. data/lib/jirametrics/chart_base.rb +102 -61
  13. data/lib/jirametrics/columns_config.rb +4 -0
  14. data/lib/jirametrics/css_variable.rb +33 -0
  15. data/lib/jirametrics/cycletime_config.rb +59 -8
  16. data/lib/jirametrics/cycletime_histogram.rb +69 -4
  17. data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
  20. data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
  21. data/lib/jirametrics/daily_wip_chart.rb +61 -14
  22. data/lib/jirametrics/data_quality_report.rb +222 -41
  23. data/lib/jirametrics/dependency_chart.rb +54 -23
  24. data/lib/jirametrics/download_config.rb +12 -0
  25. data/lib/jirametrics/downloader.rb +76 -57
  26. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +48 -33
  27. data/lib/jirametrics/examples/aggregated_project.rb +22 -39
  28. data/lib/jirametrics/examples/standard_project.rb +25 -49
  29. data/lib/jirametrics/expedited_chart.rb +28 -25
  30. data/lib/jirametrics/exporter.rb +59 -32
  31. data/lib/jirametrics/file_config.rb +34 -13
  32. data/lib/jirametrics/file_system.rb +48 -3
  33. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +2 -6
  35. data/lib/jirametrics/grouping_rules.rb +7 -1
  36. data/lib/jirametrics/hierarchy_table.rb +4 -4
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
  39. data/lib/jirametrics/html/aging_work_table.erb +19 -25
  40. data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
  41. data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
  42. data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
  43. data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
  44. data/lib/jirametrics/html/expedited_chart.erb +10 -13
  45. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +2 -2
  47. data/lib/jirametrics/html/index.css +209 -0
  48. data/lib/jirametrics/html/index.erb +16 -39
  49. data/lib/jirametrics/html/sprint_burndown.erb +10 -14
  50. data/lib/jirametrics/html/throughput_chart.erb +10 -13
  51. data/lib/jirametrics/html_report_config.rb +108 -86
  52. data/lib/jirametrics/issue.rb +357 -96
  53. data/lib/jirametrics/jira_gateway.rb +29 -11
  54. data/lib/jirametrics/project_config.rb +256 -144
  55. data/lib/jirametrics/rules.rb +2 -2
  56. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  57. data/lib/jirametrics/settings.json +10 -0
  58. data/lib/jirametrics/sprint_burndown.rb +24 -7
  59. data/lib/jirametrics/status.rb +84 -19
  60. data/lib/jirametrics/status_collection.rb +80 -39
  61. data/lib/jirametrics/throughput_chart.rb +12 -4
  62. data/lib/jirametrics/value_equality.rb +2 -2
  63. data/lib/jirametrics.rb +25 -7
  64. metadata +16 -17
  65. data/lib/jirametrics/discard_changes_before.rb +0 -37
  66. data/lib/jirametrics/experimental/generator.rb +0 -210
  67. data/lib/jirametrics/experimental/info.rb +0 -77
  68. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -1,210 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'random-word'
4
- require 'require_all'
5
- require_all 'lib'
6
-
7
- def to_time date
8
- Time.new date.year, date.month, date.day, rand(0..23), rand(0..59), rand(0..59)
9
- end
10
-
11
- class FakeIssue
12
- @@issue_number = 1
13
- attr_reader :effort, :raw, :worker
14
-
15
- def initialize date:, type:, worker:
16
- @raw = {
17
- key: "FAKE-#{@@issue_number += 1}",
18
- changelog: {
19
- histories: []
20
- },
21
- fields: {
22
- created: to_time(date),
23
- updated: to_time(date),
24
- creator: {
25
- displayName: 'George Jetson'
26
- },
27
- issuetype: {
28
- name: type
29
- },
30
- status: {
31
- name: 'To Do',
32
- id: 1,
33
- statusCategory: {
34
- id: 2,
35
- name: 'To Do'
36
- }
37
- },
38
- priority: {
39
- name: ''
40
- },
41
- summary: RandomWord.phrases.next.gsub(_, ' '),
42
- issuelinks: [],
43
- fixVersions: []
44
- }
45
- }
46
-
47
- @workers = [worker]
48
- @effort = case type
49
- when 'Story'
50
- [1, 2, 3, 3, 3, 3, 4, 4, 4, 5, 6].sample
51
- else
52
- [1, 2, 3].sample
53
- end
54
- unblock
55
- @done = false
56
- @last_status = 'To Do'
57
- @last_status_id = 1
58
- change_status new_status: 'In Progress', new_status_id: 3, date: date
59
- end
60
-
61
- def blocked? = @blocked
62
- def block = @blocked = true
63
- def unblock = @blocked = false
64
-
65
- def key = @raw[:key]
66
-
67
- def do_work date:, effort:
68
- raise 'Already done' if done?
69
-
70
- @effort -= effort
71
- return unless done?
72
-
73
- change_status new_status: 'Done', new_status_id: 5, date: date
74
- # fix_change_timestamps
75
- end
76
-
77
- def fix_change_timestamps
78
- # since the timestamps have random hours, it's possible for them to be issued out of order. Sort them now
79
- changes = @raw[:changelog][:histories]
80
- times = [@raw[:fields][:created]] + changes.collect { |change| change[:created] }
81
- times.sort!
82
-
83
- @raw[:fields][:created] = times.shift
84
- @raw[:fields][:updated] = times[-1]
85
- changes.each do |change|
86
- change[:created] = times.shift
87
- end
88
- end
89
-
90
- def done? = @effort <= 0
91
-
92
- def change_status date:, new_status:, new_status_id:
93
- @raw[:changelog][:histories] << {
94
- author: {
95
- emailAddress: 'george@jetson.com',
96
- displayName: 'George Jetson'
97
- },
98
- created: to_time(date),
99
- items: [
100
- {
101
- field: 'status',
102
- fieldtype: 'jira',
103
- fieldId: 'status',
104
- from: @last_status_id,
105
- fromString: @last_status,
106
- to: new_status_id,
107
- toString: new_status
108
- }
109
- ]
110
- }
111
-
112
- @last_status = new_status
113
- @last_status_id = new_status_id
114
- end
115
- end
116
-
117
- class Worker
118
- attr_accessor :issue
119
- end
120
-
121
- class Generator
122
- def initialize
123
- @random = Random.new
124
- @file_prefix = 'fake'
125
- @target_path = 'target/'
126
-
127
- # @probability_work_will_be_pushed = 20
128
- @probability_unblocked_work_becomes_blocked = 20
129
- @probability_blocked_work_becomes_unblocked = 20
130
- @date_range = (Date.today - 500)..Date.today
131
- @issues = []
132
- @workers = []
133
- 5.times { @workers << Worker.new }
134
- end
135
-
136
- def run
137
- remove_old_files
138
- @date_range.each_with_index do |date, day|
139
- yield date, day if block_given?
140
- process_date(date, day) if (1..5).cover? date.wday # Weekday
141
- end
142
-
143
- @issues.each do |issue|
144
- issue.fix_change_timestamps
145
- File.open "target/fake_issues/#{issue.key}.json", 'w' do |file|
146
- file.puts JSON.pretty_generate(issue.raw)
147
- end
148
- end
149
-
150
- File.write 'target/fake_meta.json', JSON.pretty_generate({
151
- time_start: (@date_range.end - 90).to_time,
152
- time_end: @date_range.end.to_time,
153
- 'no-download': true
154
- })
155
- puts "Created #{@issues.size} fake issues"
156
- end
157
-
158
- def remove_old_files
159
- path = "#{@target_path}#{@file_prefix}_issues"
160
- Dir.foreach path do |file|
161
- next unless file.match?(/-\d+\.json$/)
162
-
163
- filename = "#{path}/#{file}"
164
- File.unlink filename
165
- end
166
- end
167
-
168
- def lucky? probability
169
- @random.rand(1..100) <= probability
170
- end
171
-
172
- def next_issue_for worker:, date:, type:
173
- # First look for something I already started
174
- issue = @issues.find { |i| i.worker == worker && !i.done? && !i.blocked? }
175
-
176
- # Then look for something that someone else started
177
- issue = @issues.find { |i| i.worker != worker && !i.done? && !i.blocked? } if issue.nil? && lucky?(40)
178
-
179
- # Then start new work
180
- issue = FakeIssue.new(date: date, type: type, worker: worker) if issue.nil?
181
-
182
- issue
183
- end
184
-
185
- def process_date date, _simulation_day
186
- @issues.each do |issue|
187
- if issue.blocked?
188
- issue.unblock if lucky? @probability_blocked_work_becomes_unblocked
189
- elsif lucky? @probability_unblocked_work_becomes_blocked
190
- issue.block
191
- end
192
- end
193
-
194
- possible_capacities = [0, 1, 1, 1, 2]
195
- @workers.each do |worker|
196
- worker_capacity = possible_capacities.sample
197
- if worker.issue.nil? || worker.issue.done?
198
- type = lucky?(89) ? 'Story' : 'Bug'
199
- worker.issue = next_issue_for worker: worker, date: date, type: type
200
- @issues << worker.issue
201
- end
202
-
203
- worker.issue = next_issue_for worker: worker, date: date, type: type if worker.issue.blocked?
204
- worker.issue.do_work date: date, effort: worker_capacity
205
- worker.issue = nil if worker.issue.done?
206
- end
207
- end
208
- end
209
-
210
- Generator.new.run if __FILE__ == $PROGRAM_NAME
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'require_all'
4
- require_all 'lib'
5
-
6
- class InfoDumper
7
- def initialize
8
- @target_dir = 'target/'
9
- end
10
-
11
- def run key
12
- find_file_prefixes.each do |prefix|
13
- path = "#{@target_dir}#{prefix}_issues/#{key}.json"
14
- path = "#{@target_dir}#{prefix}_issues"
15
- Dir.foreach path do |file|
16
- if file.match?(/^#{key}.+\.json$/)
17
- issue = Issue.new raw: JSON.parse(File.read(File.join(path, file))), board: nil
18
- dump issue
19
- end
20
- end
21
- end
22
- end
23
-
24
- def find_file_prefixes
25
- prefixes = []
26
- Dir.foreach @target_dir do |file|
27
- prefixes << $1 if file =~ /^(.+)_issues$/
28
- end
29
- prefixes
30
- end
31
-
32
- def dump issue
33
- puts "#{issue.key} (#{issue.type}): #{compact_text issue.summary, 200}"
34
-
35
- assignee = issue.raw['fields']['assignee']
36
- puts " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>" unless assignee.nil?
37
-
38
- issue.raw['fields']['issuelinks'].each do |link|
39
- puts " [link] #{link['type']['outward']} #{link['outwardIssue']['key']}" if link['outwardIssue']
40
- puts " [link] #{link['type']['inward']} #{link['inwardIssue']['key']}" if link['inwardIssue']
41
- end
42
- issue.changes.each do |change|
43
- value = change.value
44
- old_value = change.old_value
45
-
46
- # Description fields get pretty verbose so reduce the clutter
47
- if change.field == 'description' || change.field == 'summary'
48
- value = compact_text value
49
- old_value = compact_text old_value
50
- end
51
-
52
- author = change.author
53
- author = "(#{author})" if author
54
- message = " [change] #{change.time} [#{change.field}] "
55
- message << "#{compact_text(old_value).inspect} -> " unless old_value.nil? || old_value.empty?
56
- message << compact_text(value).inspect
57
- message << " #{author}" if author
58
- message << ' <<artificial entry>>' if change.artificial?
59
- puts message
60
- end
61
- puts ''
62
- end
63
-
64
- def compact_text text, max = 60
65
- return nil if text.nil?
66
-
67
- text = text.gsub(/\s+/, ' ').strip
68
- text = "#{text[0..max]}..." if text.length > max
69
- text
70
- end
71
- end
72
-
73
- if __FILE__ == $PROGRAM_NAME
74
- ARGV.each do |key|
75
- InfoDumper.new.run key
76
- end
77
- 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
- %>