jirametrics 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/bin/jirametrics +4 -0
  3. data/lib/jirametrics/aggregate_config.rb +89 -0
  4. data/lib/jirametrics/aging_work_bar_chart.rb +235 -0
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +148 -0
  6. data/lib/jirametrics/aging_work_table.rb +149 -0
  7. data/lib/jirametrics/anonymizer.rb +186 -0
  8. data/lib/jirametrics/blocked_stalled_change.rb +43 -0
  9. data/lib/jirametrics/board.rb +85 -0
  10. data/lib/jirametrics/board_column.rb +14 -0
  11. data/lib/jirametrics/board_config.rb +31 -0
  12. data/lib/jirametrics/change_item.rb +80 -0
  13. data/lib/jirametrics/chart_base.rb +239 -0
  14. data/lib/jirametrics/columns_config.rb +42 -0
  15. data/lib/jirametrics/cycletime_config.rb +69 -0
  16. data/lib/jirametrics/cycletime_histogram.rb +74 -0
  17. data/lib/jirametrics/cycletime_scatterplot.rb +128 -0
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +88 -0
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +77 -0
  20. data/lib/jirametrics/daily_wip_chart.rb +123 -0
  21. data/lib/jirametrics/data_quality_report.rb +278 -0
  22. data/lib/jirametrics/dependency_chart.rb +217 -0
  23. data/lib/jirametrics/discard_changes_before.rb +37 -0
  24. data/lib/jirametrics/download_config.rb +41 -0
  25. data/lib/jirametrics/downloader.rb +337 -0
  26. data/lib/jirametrics/examples/aggregated_project.rb +36 -0
  27. data/lib/jirametrics/examples/standard_project.rb +111 -0
  28. data/lib/jirametrics/expedited_chart.rb +169 -0
  29. data/lib/jirametrics/experimental/generator.rb +209 -0
  30. data/lib/jirametrics/experimental/info.rb +77 -0
  31. data/lib/jirametrics/exporter.rb +127 -0
  32. data/lib/jirametrics/file_config.rb +119 -0
  33. data/lib/jirametrics/fix_version.rb +21 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +44 -0
  35. data/lib/jirametrics/grouping_rules.rb +13 -0
  36. data/lib/jirametrics/hierarchy_table.rb +31 -0
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +72 -0
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +52 -0
  39. data/lib/jirametrics/html/aging_work_table.erb +60 -0
  40. data/lib/jirametrics/html/collapsible_issues_panel.erb +32 -0
  41. data/lib/jirametrics/html/cycletime_histogram.erb +41 -0
  42. data/lib/jirametrics/html/cycletime_scatterplot.erb +103 -0
  43. data/lib/jirametrics/html/daily_wip_chart.erb +63 -0
  44. data/lib/jirametrics/html/data_quality_report.erb +126 -0
  45. data/lib/jirametrics/html/expedited_chart.erb +67 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +29 -0
  47. data/lib/jirametrics/html/index.erb +66 -0
  48. data/lib/jirametrics/html/sprint_burndown.erb +116 -0
  49. data/lib/jirametrics/html/story_point_accuracy_chart.erb +57 -0
  50. data/lib/jirametrics/html/throughput_chart.erb +65 -0
  51. data/lib/jirametrics/html_report_config.rb +217 -0
  52. data/lib/jirametrics/issue.rb +521 -0
  53. data/lib/jirametrics/issue_link.rb +60 -0
  54. data/lib/jirametrics/json_file_loader.rb +9 -0
  55. data/lib/jirametrics/project_config.rb +442 -0
  56. data/lib/jirametrics/rules.rb +34 -0
  57. data/lib/jirametrics/self_or_issue_dispatcher.rb +15 -0
  58. data/lib/jirametrics/sprint.rb +43 -0
  59. data/lib/jirametrics/sprint_burndown.rb +335 -0
  60. data/lib/jirametrics/sprint_issue_change_data.rb +31 -0
  61. data/lib/jirametrics/status.rb +26 -0
  62. data/lib/jirametrics/status_collection.rb +67 -0
  63. data/lib/jirametrics/story_point_accuracy_chart.rb +139 -0
  64. data/lib/jirametrics/throughput_chart.rb +91 -0
  65. data/lib/jirametrics/tree_organizer.rb +96 -0
  66. data/lib/jirametrics/trend_line_calculator.rb +74 -0
  67. data/lib/jirametrics.rb +85 -0
  68. metadata +167 -0
@@ -0,0 +1,66 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="UTF-8">
4
+ <script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
5
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/1.2.2/chartjs-plugin-annotation.min.js" integrity="sha512-HycvvBSFvDEVyJ0tjE2rPmymkt6XqsP/Zo96XgLRjXwn6SecQqsn+6V/7KYev66OshZZ9+f9AttCGmYqmzytiw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
8
+ <script type="text/javascript">
9
+ function expand_collapse(link_id, issues_id) {
10
+ link_text = document.getElementById(link_id).textContent
11
+ if( link_text == 'Show details') {
12
+ document.getElementById(link_id).textContent = 'Hide details'
13
+ document.getElementById(issues_id).style.display = 'block'
14
+ }
15
+ else {
16
+ document.getElementById(link_id).textContent = 'Show details'
17
+ document.getElementById(issues_id).style.display = 'none'
18
+ }
19
+ }
20
+ </script>
21
+ <style>
22
+ h1 {
23
+ border: 1px solid black;
24
+ background: lightgray;
25
+ padding-left: 0.2em;
26
+ }
27
+ dl, dd, dt {
28
+ padding: 0;
29
+ margin: 0;
30
+ }
31
+ dd {
32
+ margin-bottom: 0.4em;
33
+ }
34
+ span.highlight {
35
+ background: #FDD5B1;
36
+ }
37
+ a.issue_key {
38
+ white-space: nowrap;
39
+ }
40
+ table.standard thead tr th {
41
+ border-bottom: 1px solid gray;
42
+ position: sticky;
43
+ top: 0;
44
+ background: white;
45
+ }
46
+ table.standard tbody tr td {
47
+ padding-left: 0.5em;
48
+ padding-right: 0.5em;
49
+ vertical-align: top;
50
+ }
51
+ table.standard tbody tr:nth-child(odd){
52
+ background-color: #eee;
53
+ }
54
+ .quality_note_bullet {
55
+ color: red;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div>
61
+ Page generated <%= (timezone_offset.nil? ? DateTime.now : DateTime.now.new_offset(timezone_offset)).strftime('%Y-%b-%d at %I:%M:%S%P') %>
62
+ </div>
63
+ <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
64
+ <%= "\n" + @sections.collect { |text, type| text if type == :body }.compact.join("\n\n") %>
65
+ </body>
66
+ </html>
@@ -0,0 +1,116 @@
1
+ <h2>Burndown by <%= y_axis_title %></h2>
2
+
3
+ <div>
4
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
5
+ </div>
6
+ <script>
7
+ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
8
+ type: 'scatter',
9
+ data: {
10
+ datasets: <%= JSON.generate(data_sets) %>
11
+ },
12
+ options: {
13
+ title: {
14
+ display: true,
15
+ text: "Sprint Burndown"
16
+ },
17
+ responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
18
+ scales: {
19
+ x: {
20
+ type: "time",
21
+ time: {
22
+ format: 'YYYY-MM-DD'
23
+ },
24
+ scaleLabel: {
25
+ display: true,
26
+ labelString: 'Date'
27
+ },
28
+ min: "<%= date_range.begin.to_s %>",
29
+ max: "<%= (date_range.end + 1).to_s %>"
30
+
31
+ },
32
+ y: {
33
+ scaleLabel: {
34
+ display: true,
35
+ labelString: 'Items remaining'
36
+ },
37
+ title: {
38
+ display: true,
39
+ text: "<%= y_axis_title %>"
40
+ },
41
+ min: 0.0
42
+ }
43
+ },
44
+ plugins: {
45
+ tooltip: {
46
+ callbacks: {
47
+ label: function(context) {
48
+ return context.dataset.data[context.dataIndex].title
49
+ }
50
+ }
51
+ },
52
+ annotation: {
53
+ annotations: {
54
+ <% holidays().each_with_index do |range, index| %>
55
+ holiday<%= index %>: {
56
+ drawTime: 'beforeDraw',
57
+ type: 'box',
58
+ xMin: '<%= range.begin %>T00:00:00',
59
+ xMax: '<%= range.end %>T23:59:59',
60
+ backgroundColor: '#F0F0F0',
61
+ borderColor: '#F0F0F0'
62
+ },
63
+ <% end %>
64
+ }
65
+ }
66
+ }
67
+ }
68
+ });
69
+ </script>
70
+
71
+ <%
72
+ link_id = next_id
73
+ issues_id = next_id
74
+ %>
75
+ [<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
76
+ <div id="<%= issues_id %>" style="display: none;">
77
+ <table class='standard' style="margin-left: 1em;">
78
+ <thead>
79
+ <th>Sprint</th>
80
+ <th>State</th>
81
+ <th>Started</th>
82
+ <th>Completed</th>
83
+ <th>Added</th>
84
+ <th>Removed</th>
85
+ <th>Remaining</th>
86
+ <th>Note</th>
87
+ </thead>
88
+ <tbody>
89
+ <% @summary_stats.keys.sort_by(&:start_time).each do |sprint| %>
90
+ <tr>
91
+ <td><%= sprint.name %></td>
92
+ <td><%= sprint.raw['state'] %></td>
93
+ <% stats = @summary_stats[sprint] %>
94
+ <td><%= stats.started %></td>
95
+ <td><%= stats.completed %></td>
96
+ <td><%= stats.added %></td>
97
+ <td><%= stats.removed %></td>
98
+ <td><%= stats.remaining %></td>
99
+ <td>
100
+ <% if stats.points_values_changed %>
101
+ Points values changed mid-sprint. Numbers may not add up
102
+ <% end %>
103
+ </td>
104
+ </tr>
105
+ <% end %>
106
+ </tbody>
107
+ </table>
108
+
109
+ <p>Legend:
110
+ <ul>
111
+ <% legend.each do |line| %>
112
+ <li><%= line %></li>
113
+ <% end %>
114
+ </ul>
115
+ </p>
116
+ </div>
@@ -0,0 +1,57 @@
1
+ <div>
2
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
3
+ </div>
4
+ <script>
5
+ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
6
+ type: 'bubble',
7
+ data: {
8
+ datasets: <%= JSON.generate(data_sets) %>
9
+ },
10
+ options: {
11
+ title: {
12
+ display: true,
13
+ text: "Sprint Burndown"
14
+ },
15
+ responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
16
+ scales: {
17
+ x: {
18
+ type: "linear",
19
+ scaleLabel: {
20
+ display: true,
21
+ labelString: 'Date'
22
+ },
23
+ title: {
24
+ display: true,
25
+ text: "Cycletime (days)"
26
+ },
27
+ min: 0
28
+
29
+ },
30
+ y: {
31
+ type: "<%= @y_axis_type %>",
32
+ <% if @y_axis_sort_order %>
33
+ labels: ["","<%= @y_axis_sort_order.reverse.join('","')%>",""],
34
+ <% end %>
35
+ scaleLabel: {
36
+ display: true,
37
+ labelString: 'Items remaining'
38
+ },
39
+ title: {
40
+ display: true,
41
+ text: "<%= @y_axis_label %>"
42
+ },
43
+ min: 0.0
44
+ }
45
+ },
46
+ plugins: {
47
+ tooltip: {
48
+ callbacks: {
49
+ label: function(context) {
50
+ return context.dataset.data[context.dataIndex].title
51
+ }
52
+ }
53
+ },
54
+ }
55
+ }
56
+ });
57
+ </script>
@@ -0,0 +1,65 @@
1
+
2
+ <div>
3
+ <canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
4
+ </div>
5
+ <script>
6
+ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
7
+ type: 'scatter',
8
+ data: {
9
+ datasets: <%= JSON.generate(data_sets) %>
10
+ },
11
+ options: {
12
+ title: {
13
+ display: true,
14
+ text: "Throughput Chart"
15
+ },
16
+ responsive: <%= canvas_responsive? %>, // If responsive is true then it fills the screen
17
+ scales: {
18
+ x: {
19
+ type: "time",
20
+ time: {
21
+ format: 'YYYY-MM-DD'
22
+ },
23
+ scaleLabel: {
24
+ display: true,
25
+ labelString: 'Date Completed'
26
+ }
27
+ },
28
+ y: {
29
+ scaleLabel: {
30
+ display: true,
31
+ },
32
+ title: {
33
+ display: true,
34
+ text: 'Count of items'
35
+ },
36
+ min: 0
37
+ },
38
+ },
39
+ plugins: {
40
+ tooltip: {
41
+ callbacks: {
42
+ label: function(context) {
43
+ return context.dataset.data[context.dataIndex].title
44
+ }
45
+ }
46
+ },
47
+ annotation: {
48
+ annotations: {
49
+ <% holidays.each_with_index do |range, index| %>
50
+ holiday<%= index %>: {
51
+ drawTime: 'beforeDraw',
52
+ type: 'box',
53
+ xMin: '<%= range.begin %>T00:00:00',
54
+ xMax: '<%= range.end %>T23:59:59',
55
+ backgroundColor: '#F0F0F0',
56
+ borderColor: '#F0F0F0'
57
+ },
58
+ <% end %>
59
+ }
60
+ }
61
+ }
62
+ }
63
+ });
64
+ </script>
65
+
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'jirametrics/self_or_issue_dispatcher'
5
+
6
+ class HtmlReportConfig
7
+ include SelfOrIssueDispatcher
8
+ include DiscardChangesBefore
9
+
10
+ attr_reader :file_config, :sections
11
+
12
+ def initialize file_config:, block:
13
+ @file_config = file_config
14
+ @block = block
15
+ # @cycletimes = []
16
+ @sections = []
17
+ end
18
+
19
+ def cycletime label = nil, &block
20
+ # TODO: This is about to become deprecated
21
+
22
+ @file_config.project_config.all_boards.each do |_id, board|
23
+ raise 'Multiple cycletimes not supported yet' if board.cycletime
24
+
25
+ board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
26
+ end
27
+ end
28
+
29
+ def run
30
+ instance_eval(&@block)
31
+
32
+ # The quality report has to be generated last because otherwise cycletime won't have been
33
+ # set. Then we have to rotate it to the first position so it's at the top of the report.
34
+ execute_chart DataQualityReport.new(@original_issue_times || {})
35
+ @sections.rotate!(-1)
36
+
37
+ File.open @file_config.output_filename, 'w' do |file|
38
+ html_directory = "#{Pathname.new(File.realpath(__FILE__)).dirname}/html"
39
+ erb = ERB.new File.read("#{html_directory}/index.erb")
40
+ file.puts erb.result(binding)
41
+ end
42
+ end
43
+
44
+ def board_id id = nil
45
+ @board_id = id unless id.nil?
46
+ @board_id
47
+ end
48
+
49
+ def timezone_offset
50
+ @file_config.project_config.exporter.timezone_offset
51
+ end
52
+
53
+ def aging_work_in_progress_chart board_id: nil, &block
54
+ if board_id.nil?
55
+ ids = issues.collect { |i| i.board.id }.uniq.sort
56
+ else
57
+ ids = [board_id]
58
+ end
59
+
60
+ ids.each do |id|
61
+ execute_chart(AgingWorkInProgressChart.new(block)) do |chart|
62
+ chart.board_id = id
63
+ end
64
+ end
65
+ end
66
+
67
+ def aging_work_bar_chart &block
68
+ execute_chart AgingWorkBarChart.new(block)
69
+ end
70
+
71
+ def aging_work_table priority_name = nil, &block
72
+ if priority_name
73
+ deprecated message: 'priority name should no longer be passed into the chart. Specify it in the ' \
74
+ 'board declaration. See https://github.com/mikebowler/jira-export/wiki/Deprecated',
75
+ date: '2022-12-26'
76
+ end
77
+ execute_chart AgingWorkTable.new(block)
78
+ end
79
+
80
+ def cycletime_scatterplot &block
81
+ execute_chart CycletimeScatterplot.new block
82
+ end
83
+
84
+ def total_wip_over_time_chart &block
85
+ puts 'Deprecated(total_wip_over_time_chart). Use daily_wip_by_age_chart instead.'
86
+ execute_chart DailyWipByAgeChart.new block
87
+ end
88
+
89
+ def daily_wip_chart &block
90
+ execute_chart DailyWipChart.new(block)
91
+ end
92
+
93
+ def daily_wip_by_age_chart &block
94
+ execute_chart DailyWipByAgeChart.new block
95
+ end
96
+
97
+ def daily_wip_by_type &block
98
+ execute_chart DailyWipChart.new block
99
+ end
100
+
101
+ def daily_wip_by_blocked_stalled_chart
102
+ execute_chart DailyWipByBlockedStalledChart.new
103
+ end
104
+
105
+ def throughput_chart &block
106
+ execute_chart ThroughputChart.new(block)
107
+ end
108
+
109
+ def blocked_stalled_chart
110
+ puts 'Deprecated(blocked_stalled_chart). Use daily_wip_by_blocked_stalled_chart instead.'
111
+ execute_chart DailyWipByBlockedStalledChart.new
112
+ end
113
+
114
+ def expedited_chart priority_name = nil
115
+ if priority_name
116
+ deprecated message: 'priority name should no longer be passed into the chart. Specify it in the ' \
117
+ 'board declaration. See https://github.com/mikebowler/jira-export/wiki/Deprecated',
118
+ date: '2022-12-26'
119
+ end
120
+ execute_chart ExpeditedChart.new
121
+ end
122
+
123
+ def cycletime_histogram &block
124
+ execute_chart CycletimeHistogram.new block
125
+ end
126
+
127
+ def random_color
128
+ "\##{Random.bytes(3).unpack1('H*')}"
129
+ end
130
+
131
+ def html string, type: :body
132
+ raise "Unexpected type: #{type}" unless [:body, :header].include? type
133
+
134
+ @sections << [string, type]
135
+ end
136
+
137
+ def sprint_burndown options = :points_and_counts
138
+ execute_chart SprintBurndown.new do |chart|
139
+ chart.options = options
140
+ end
141
+ end
142
+
143
+ def story_point_accuracy_chart &block
144
+ execute_chart StoryPointAccuracyChart.new block
145
+ end
146
+
147
+ def hierarchy_table &block
148
+ execute_chart HierarchyTable.new block
149
+ end
150
+
151
+ def discard_changes_before_hook issues_cutoff_times
152
+ # raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
153
+
154
+ @original_issue_times = {}
155
+ issues_cutoff_times.each do |issue, cutoff_time|
156
+ started = issue.board.cycletime.started_time(issue)
157
+ if started && started <= cutoff_time
158
+ # We only need to log this if data was discarded
159
+ @original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
160
+ end
161
+ end
162
+ end
163
+
164
+ def discarded_changes_report
165
+ puts 'Deprecated(discarded_changes_report) No need to specify this anymore as this information is ' \
166
+ 'now included in the data quality checks.'
167
+ end
168
+
169
+ def dependency_chart &block
170
+ execute_chart DependencyChart.new block
171
+ end
172
+
173
+ def execute_chart chart, &after_init_block
174
+ project_config = @file_config.project_config
175
+
176
+ chart.issues = issues
177
+ chart.time_range = project_config.time_range
178
+ chart.timezone_offset = timezone_offset
179
+ chart.settings = project_config.settings
180
+
181
+ chart.all_boards = project_config.all_boards
182
+ chart.board_id = find_board_id if chart.respond_to? :board_id=
183
+ chart.holiday_dates = project_config.exporter.holiday_dates
184
+
185
+ time_range = @file_config.project_config.time_range
186
+ chart.date_range = time_range.begin.to_date..time_range.end.to_date
187
+ chart.aggregated_project = project_config.aggregated_project?
188
+
189
+ after_init_block&.call chart
190
+
191
+ html chart.run
192
+ end
193
+
194
+ def find_board_id
195
+ @board_id || @file_config.project_config.guess_board_id
196
+ end
197
+
198
+ def issues
199
+ @file_config.issues
200
+ end
201
+
202
+ def find_board id
203
+ @file_config.project_config.all_boards[id]
204
+ end
205
+
206
+ def project_name
207
+ @file_config.project_config.name
208
+ end
209
+
210
+ def boards
211
+ @file_config.project_config.board_configs.collect(&:id).collect { |id| find_board id }
212
+ end
213
+
214
+ def find_project_by_name name
215
+ @file_config.project_config.exporter.project_configs.find { |p| p.name == name }
216
+ end
217
+ end