jirametrics 2.20.1 → 2.23

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +10 -2
  3. data/lib/jirametrics/aging_work_bar_chart.rb +185 -132
  4. data/lib/jirametrics/aging_work_table.rb +1 -1
  5. data/lib/jirametrics/anonymizer.rb +74 -1
  6. data/lib/jirametrics/atlassian_document_format.rb +104 -93
  7. data/lib/jirametrics/bar_chart_range.rb +17 -0
  8. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  9. data/lib/jirametrics/board.rb +21 -3
  10. data/lib/jirametrics/board_config.rb +2 -1
  11. data/lib/jirametrics/change_item.rb +13 -5
  12. data/lib/jirametrics/chart_base.rb +111 -1
  13. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +4 -6
  14. data/lib/jirametrics/cycletime_histogram.rb +15 -103
  15. data/lib/jirametrics/cycletime_scatterplot.rb +15 -85
  16. data/lib/jirametrics/daily_wip_by_age_chart.rb +3 -4
  17. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +13 -3
  18. data/lib/jirametrics/daily_wip_chart.rb +28 -8
  19. data/lib/jirametrics/data_quality_report.rb +2 -0
  20. data/lib/jirametrics/download_config.rb +15 -0
  21. data/lib/jirametrics/downloader.rb +76 -5
  22. data/lib/jirametrics/downloader_for_cloud.rb +39 -0
  23. data/lib/jirametrics/downloader_for_data_center.rb +2 -1
  24. data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
  25. data/lib/jirametrics/examples/standard_project.rb +15 -5
  26. data/lib/jirametrics/expedited_chart.rb +2 -0
  27. data/lib/jirametrics/exporter.rb +7 -3
  28. data/lib/jirametrics/file_system.rb +4 -0
  29. data/lib/jirametrics/fix_version.rb +13 -0
  30. data/lib/jirametrics/flow_efficiency_scatterplot.rb +2 -0
  31. data/lib/jirametrics/github_gateway.rb +99 -0
  32. data/lib/jirametrics/groupable_issue_chart.rb +9 -1
  33. data/lib/jirametrics/grouping_rules.rb +1 -1
  34. data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
  35. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +2 -0
  36. data/lib/jirametrics/html/aging_work_table.erb +2 -0
  37. data/lib/jirametrics/html/daily_wip_chart.erb +7 -4
  38. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  39. data/lib/jirametrics/html/expedited_chart.erb +6 -14
  40. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
  41. data/lib/jirametrics/html/index.css +17 -0
  42. data/lib/jirametrics/html/index.erb +1 -1
  43. data/lib/jirametrics/html/index.js +24 -0
  44. data/lib/jirametrics/html/sprint_burndown.erb +12 -12
  45. data/lib/jirametrics/html/throughput_chart.erb +7 -10
  46. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
  47. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +8 -9
  48. data/lib/jirametrics/html_generator.rb +31 -0
  49. data/lib/jirametrics/html_report_config.rb +6 -24
  50. data/lib/jirametrics/issue.rb +133 -77
  51. data/lib/jirametrics/issue_printer.rb +97 -0
  52. data/lib/jirametrics/project_config.rb +44 -7
  53. data/lib/jirametrics/pull_request.rb +30 -0
  54. data/lib/jirametrics/pull_request_review.rb +13 -0
  55. data/lib/jirametrics/raw_javascript.rb +17 -0
  56. data/lib/jirametrics/settings.json +3 -1
  57. data/lib/jirametrics/sprint.rb +12 -0
  58. data/lib/jirametrics/sprint_burndown.rb +8 -2
  59. data/lib/jirametrics/stitcher.rb +76 -0
  60. data/lib/jirametrics/throughput_chart.rb +7 -1
  61. data/lib/jirametrics/time_based_histogram.rb +139 -0
  62. data/lib/jirametrics/time_based_scatterplot.rb +100 -0
  63. data/lib/jirametrics.rb +8 -1
  64. metadata +15 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbe1101b082615d38939850c0adc688aedefe02a74537503bcd390cdf11d0d4e
4
- data.tar.gz: eeffbda7c7ba8280273e0d749ede1ed3c1caa33d06a1f50a2c47b2331035d5af
3
+ metadata.gz: 2145950e91bf010e3c2790151b32f859fc479a871002f2057e2070ef23a6e1ae
4
+ data.tar.gz: 00bff5cffee6fc49862ae15fdc78f142982337a03abb4ea33735b07a2f0f3426
5
5
  SHA512:
6
- metadata.gz: b73533c90e457c2c5f7a7f2d1759ccd44d218ebe984052fcb581d8ab88225abf43b5612923217b4fa7467ed11e82ba5b4fe2cc656a324f269b71c2497bf67659
7
- data.tar.gz: 57cbc54fe6c739d0c85f68cc0efcfbc6005975af0b40174ed9ee35790a83dac9b5e524601b770dffc6a3fefbd10f0d577d19b840560b3acfb63b0b542728d5fc
6
+ metadata.gz: d16248d2502890619c3da712ebb7f0b6aa864994013a599d29e6795f687ccbd782d9e7f33f253f7f5db81a3c78f1be0898bf27cd4e8a981137f0a3ca55da8c8f
7
+ data.tar.gz: ee3e5d4fdd783a025c9666a6080f587f82cd70f0fb6aeeda9a12a862c454baab64fb635bdf2727b821760e75822638378c87b18e59b68fa8903b3bd87a05f9f8
@@ -65,8 +65,16 @@ class AggregateConfig
65
65
 
66
66
  if issues.nil?
67
67
  file_system.warning "No issues found for #{project_name}"
68
- else
69
- @project_config.add_issues issues
68
+ return
69
+ end
70
+
71
+ @project_config.add_issues issues
72
+
73
+ # Bring fix versions over
74
+ project.fix_versions.each do |fix_version|
75
+ unless @project_config.fix_versions.find { |fv| fv.id == fix_version.id }
76
+ @project_config.fix_versions << fix_version
77
+ end
70
78
  end
71
79
  end
72
80
 
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jirametrics/chart_base'
4
+ require 'jirametrics/bar_chart_range'
4
5
 
5
6
  class AgingWorkBarChart < ChartBase
6
7
  def initialize block
7
8
  super()
8
9
 
10
+ @age_cutoff = nil
9
11
  header_text 'Aging Work Bar Chart'
10
12
  description_text <<-HTML
11
13
  <p>
@@ -13,16 +15,16 @@ class AgingWorkBarChart < ChartBase
13
15
  newest at the bottom.
14
16
  </p>
15
17
  <p>
16
- There are potentially three bars for each issue, although a bar may be missing if the issue has no
17
- information relevant to that. Hovering over any of the bars will provide more details.
18
+ There are three bars for each issue, and hovering over any of the bars will provide more details.
18
19
  <ol>
19
- <li>The top bar tells you what status the issue is in at any time. The colour indicates the
20
+ <li>Status: The status the issue was in at any time. The colour indicates the
20
21
  status category, which will be one of #{color_block '--status-category-todo-color'} To Do,
21
22
  #{color_block '--status-category-inprogress-color'} In Progress,
22
23
  or #{color_block '--status-category-done-color'} Done</li>
23
- <li>The middle bar indicates #{color_block '--blocked-color'} blocked
24
+ <li>Activity: This bar indicates #{color_block '--blocked-color'} blocked
24
25
  or #{color_block '--stalled-color'} stalled.</li>
25
- <li>The bottom bar indicated #{color_block '--expedited-color'} expedited.</li>
26
+ <li>Priority: This shows the priority over time. If one of these priorities is considered expedited
27
+ then it will be drawn with diagonal lines.</li>
26
28
  </ol>
27
29
  </p>
28
30
  #{describe_non_working_days}
@@ -36,6 +38,7 @@ class AgingWorkBarChart < ChartBase
36
38
 
37
39
  def run
38
40
  aging_issues = select_aging_issues issues: @issues
41
+ adjust_time_date_ranges_to_start_from_earliest_issue_start(aging_issues)
39
42
 
40
43
  today = date_range.end
41
44
  sort_by_age! issues: aging_issues, today: today
@@ -58,134 +61,134 @@ class AgingWorkBarChart < ChartBase
58
61
  wrap_and_render(binding, __FILE__)
59
62
  end
60
63
 
64
+ def adjust_time_date_ranges_to_start_from_earliest_issue_start aging_issues
65
+ earliest_start_time = aging_issues.collect do |issue|
66
+ issue.board.cycletime.started_stopped_times(issue).first
67
+ end.min
68
+ return if earliest_start_time.nil? || earliest_start_time >= @time_range.begin
69
+
70
+ @time_range = earliest_start_time..@time_range.end
71
+ @date_range = @time_range.begin.to_date..@time_range.end.to_date
72
+ end
73
+
61
74
  def data_sets_for_one_issue issue:, today:
62
75
  cycletime = issue.board.cycletime
63
- issue_start_time, _stopped_time = cycletime.started_stopped_times(issue)
64
- issue_start_date = issue_start_time.to_date
65
- issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
66
- [
67
- status_data_sets(issue: issue, label: issue_label, today: today),
68
- blocked_data_sets(
69
- issue: issue,
70
- issue_label: issue_label,
71
- stack: 'blocked',
72
- issue_start_time: issue_start_time
73
- ),
74
- data_set_by_block(
75
- issue: issue,
76
- issue_label: issue_label,
77
- title_label: 'Expedited',
78
- stack: 'expedited',
79
- color: CssVariable['--expedited-color'],
80
- start_date: issue_start_date
81
- ) { |day| issue.expedited_on_date?(day) }
76
+ issue_start_time = cycletime.started_stopped_times(issue).first
77
+ end_of_today = Time.parse("#{today}T23:59:59#{@timezone_offset}")
78
+
79
+ bar_data = [
80
+ ['status', collect_status_ranges(issue: issue, now: end_of_today)],
81
+ ['blocked', collect_blocked_stalled_ranges(issue: issue, issue_start_time: issue_start_time)],
82
+ ['priority', collect_priority_ranges(issue: issue)]
82
83
  ]
84
+ bar_data << ['sprints', collect_sprint_ranges(issue: issue)] if current_board.scrum?
85
+
86
+ bar_data.each { |entry| clip_ranges_to_start_time(ranges: entry.last, issue_start_time: issue_start_time) }
87
+
88
+ issue_label = "[#{label_days cycletime.age(issue, today: today)}] #{issue.key}: #{issue.summary}"[0..60]
89
+ bar_data.collect do |stack, ranges|
90
+ bar_chart_range_to_data_set y_value: issue_label, ranges: ranges, stack: stack, issue_start_time: issue_start_time
91
+ end
83
92
  end
84
93
 
85
94
  def sort_by_age! issues:, today:
86
95
  issues.sort! do |a, b|
87
- a.board.cycletime.age(b, today: today) <=> b.board.cycletime.age(a, today: today)
96
+ b.board.cycletime.age(b, today: today) <=> a.board.cycletime.age(a, today: today)
88
97
  end
89
98
  end
90
99
 
91
100
  def select_aging_issues issues:
92
101
  issues.select do |issue|
93
102
  started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
94
- started_time && stopped_time.nil?
103
+ next false unless started_time && stopped_time.nil?
104
+
105
+ age = (date_range.end - started_time.to_date).to_i + 1
106
+ !(@age_cutoff && @age_cutoff >= age)
95
107
  end
96
108
  end
97
109
 
98
110
  def grow_chart_height_if_too_many_issues aging_issue_count:
99
- px_per_bar = 8
111
+ px_per_bar = 10
100
112
  bars_per_issue = 3
113
+ bars_per_issue += 1 if current_board.scrum?
114
+
101
115
  preferred_height = aging_issue_count * px_per_bar * bars_per_issue
102
116
  @canvas_height = preferred_height if @canvas_height.nil? || @canvas_height < preferred_height
103
117
  end
104
118
 
105
- def status_data_sets issue:, label:, today:
106
- cycletime = issue.board.cycletime
119
+ def clip_ranges_to_start_time ranges:, issue_start_time:
120
+ return if issue_start_time.nil?
107
121
 
108
- issue_started_time, _issue_stopped_time = cycletime.started_stopped_times(issue)
122
+ ranges.each { |range| range.start = issue_start_time if range.start < issue_start_time }
123
+ ranges.reject! { |range| range.start >= range.stop }
124
+ end
109
125
 
126
+ def collect_status_ranges issue:, now:
127
+ ranges = []
128
+ issue_started_time = issue.board.cycletime.started_stopped_times(issue).first
110
129
  previous_start = nil
111
130
  previous_status = nil
112
-
113
- data_sets = []
114
- issue.changes.each do |change|
115
- next unless change.status?
116
-
117
- status = issue.find_or_create_status id: change.value_id, name: change.value
118
-
119
- unless previous_start.nil? || previous_start < issue_started_time
120
- hash = {
121
- type: 'bar',
122
- data: [{
123
- x: [chart_format(previous_start), chart_format(change.time)],
124
- y: label,
125
- title: "#{issue.type} : #{change.value}"
126
- }],
127
- backgroundColor: status_category_color(status),
128
- borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
129
- borderWidth: {
130
- top: 0,
131
- right: 1,
132
- bottom: 0,
133
- left: 0
134
- },
135
- stacked: true,
136
- stack: 'status'
137
- }
138
- data_sets << hash if date_range.include?(change.time.to_date)
131
+ issue.status_changes.each do |change|
132
+ new_status = issue.find_or_create_status id: change.value_id, name: change.value
133
+ if previous_start.nil?
134
+ previous_start = change.time
135
+ previous_status = new_status
136
+ next
139
137
  end
140
138
 
139
+ previous_start = issue_started_time if issue_started_time > previous_start
140
+
141
+ ranges << BarChartRange.new(
142
+ start: previous_start,
143
+ stop: change.time,
144
+ color: status_category_color(previous_status),
145
+ title: previous_status.to_s
146
+ )
141
147
  previous_start = change.time
142
- previous_status = status
148
+ previous_status = new_status
143
149
  end
144
150
 
145
- if previous_start
146
- data_sets << {
151
+ ranges << BarChartRange.new(
152
+ start: previous_start,
153
+ stop: now,
154
+ color: status_category_color(previous_status),
155
+ title: previous_status.to_s
156
+ )
157
+ ranges
158
+ end
159
+
160
+ def bar_chart_range_to_data_set y_value:, ranges:, stack:, issue_start_time:
161
+ ranges.filter_map do |bar_chart_range|
162
+ next if bar_chart_range.stop < issue_start_time
163
+
164
+ background_color = bar_chart_range.color
165
+ if bar_chart_range.highlight
166
+ background_color = RawJavascript.new("createDiagonalPattern(#{background_color.to_json})")
167
+ end
168
+
169
+ {
147
170
  type: 'bar',
148
171
  data: [{
149
- x: [chart_format(previous_start), chart_format("#{today}T00:00:00#{@timezone_offset}")],
150
- y: label,
151
- title: "#{issue.type} : #{previous_status.name}"
172
+ x: [chart_format([bar_chart_range.start, issue_start_time].max), chart_format(bar_chart_range.stop)],
173
+ y: y_value,
174
+ title: bar_chart_range.title
152
175
  }],
153
- backgroundColor: status_category_color(previous_status),
176
+ backgroundColor: background_color,
177
+ borderColor: CssVariable['--aging-work-bar-chart-separator-color'],
178
+ borderWidth: {
179
+ top: 0,
180
+ right: 1,
181
+ bottom: 0,
182
+ left: 0
183
+ },
154
184
  stacked: true,
155
- stack: 'status'
185
+ stack: stack
156
186
  }
157
187
  end
158
-
159
- data_sets
160
188
  end
161
189
 
162
- def one_block_change_data_set starting_change:, ending_time:, issue_label:, stack:, issue_start_time:
163
- if settings['blocked_color']
164
- file_system.deprecated message: 'blocked color should be set via css now', date: '2024-05-03'
165
- end
166
- if settings['stalled_color']
167
- file_system.deprecated message: 'stalled color should be set via css now', date: '2024-05-03'
168
- end
169
-
170
- color = settings['blocked_color'] || '--blocked-color'
171
- color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
172
- {
173
- backgroundColor: CssVariable[color],
174
- data: [
175
- {
176
- title: starting_change.reasons,
177
- x: [chart_format([issue_start_time, starting_change.time].max), chart_format(ending_time)],
178
- y: issue_label
179
- }
180
- ],
181
- stack: stack,
182
- stacked: true,
183
- type: 'bar'
184
- }
185
- end
186
-
187
- def blocked_data_sets issue:, issue_label:, issue_start_time:, stack:
188
- data_sets = []
190
+ def collect_blocked_stalled_ranges issue:, issue_start_time:
191
+ results = []
189
192
  starting_change = nil
190
193
 
191
194
  issue.blocked_stalled_changes(end_time: time_range.end).each do |change|
@@ -195,58 +198,104 @@ class AgingWorkBarChart < ChartBase
195
198
  end
196
199
 
197
200
  if change.time >= issue_start_time
198
- data_sets << one_block_change_data_set(
199
- starting_change: starting_change, ending_time: change.time,
200
- issue_label: issue_label, stack: stack, issue_start_time: issue_start_time
201
+ color = settings['blocked_color'] || '--blocked-color'
202
+ color = settings['stalled_color'] || '--stalled-color' if starting_change.stalled?
203
+
204
+ results << BarChartRange.new(
205
+ start: starting_change.time, stop: change.time, color: CssVariable[color], title: starting_change.reasons
201
206
  )
202
207
  end
203
208
 
204
209
  starting_change = change
205
210
  end
211
+ results
212
+ end
213
+
214
+ def collect_priority_ranges issue:
215
+ expedited_priority_names = settings['expedited_priority_names']
216
+
217
+ previous_change = nil
218
+ results = []
219
+
220
+ issue.changes.each do |change|
221
+ next unless change.priority?
206
222
 
207
- data_sets
223
+ if previous_change.nil?
224
+ previous_change = change
225
+ next
226
+ end
227
+
228
+ results << create_range_for_priority(
229
+ previous_change: previous_change, stop_time: change.time,
230
+ expedited_priority_names: expedited_priority_names
231
+ )
232
+ previous_change = change
233
+ end
234
+
235
+ results << create_range_for_priority(
236
+ previous_change: previous_change, stop_time: time_range.end,
237
+ expedited_priority_names: expedited_priority_names
238
+ )
239
+ results
208
240
  end
209
241
 
210
- def data_set_by_block(
211
- issue:, issue_label:, title_label:, stack:, color:, start_date:, end_date: date_range.end
212
- )
213
- started = nil
214
- ended = nil
215
- data = []
216
-
217
- (start_date..end_date).each do |day|
218
- if yield(day)
219
- started = day if started.nil?
220
- ended = day
221
- elsif ended
222
- data << {
223
- x: [chart_format(started), chart_format(ended)],
224
- y: issue_label,
225
- title: "#{issue.type} : #{title_label} #{label_days (ended - started).to_i + 1}"
226
- }
227
-
228
- started = nil
229
- ended = nil
242
+ def collect_sprint_ranges issue:
243
+ results = []
244
+ open_sprints = {}
245
+
246
+ issue.changes.each do |change|
247
+ next unless change.sprint?
248
+
249
+ removed_sprint_ids = change.old_value_id - change.value_id
250
+ added_sprint_ids = change.value_id - change.old_value_id
251
+
252
+ removed_sprint_ids.each do |id|
253
+ data = open_sprints.delete(id)
254
+ next unless data
255
+
256
+ completed = data[:sprint].completed_time
257
+ stop = completed ? [change.time, completed].min : change.time
258
+ results << BarChartRange.new(
259
+ start: data[:start_time], stop: stop,
260
+ color: CssVariable['--sprint-color'], title: data[:sprint].name
261
+ )
262
+ end
263
+
264
+ added_sprint_ids.each do |id|
265
+ sprint = issue.board.sprints.find { |s| s.id == id }
266
+ next unless sprint
267
+ next if sprint.future?
268
+
269
+ start_time = [sprint.start_time, change.time].max
270
+ open_sprints[id] = { start_time: start_time, sprint: sprint }
230
271
  end
231
272
  end
232
273
 
233
- if started
234
- data << {
235
- x: [chart_format(started), chart_format(ended)],
236
- y: issue_label,
237
- title: "#{issue.type} : #{title_label} #{label_days (end_date - started).to_i + 1}"
238
- }
274
+ open_sprints.each_value do |data|
275
+ next if data[:sprint].future?
276
+
277
+ stop = data[:sprint].completed_time || time_range.end
278
+ results << BarChartRange.new(
279
+ start: data[:start_time], stop: stop,
280
+ color: CssVariable['--sprint-color'], title: data[:sprint].name
281
+ )
239
282
  end
240
283
 
241
- return [] if data.empty?
284
+ results
285
+ end
242
286
 
243
- {
244
- type: 'bar',
245
- data: data,
246
- backgroundColor: color,
247
- stacked: true,
248
- stack: stack
249
- }
287
+ def create_range_for_priority previous_change:, stop_time:, expedited_priority_names:
288
+ expedited = expedited_priority_names.include?(previous_change.value)
289
+ title = "Priority: #{previous_change.value}"
290
+ title << ' (expedited)' if expedited
291
+
292
+ BarChartRange.new(
293
+ start: previous_change.time,
294
+ stop: stop_time,
295
+ color: CssVariable["--priority-color-#{previous_change.value.downcase.gsub(/\s/, '')}"],
296
+ title: title,
297
+ highlight: expedited
298
+ )
250
299
  end
251
300
 
252
301
  def calculate_percent_line percentage: 85
@@ -255,4 +304,8 @@ class AgingWorkBarChart < ChartBase
255
304
 
256
305
  days[days.length * percentage / 100]
257
306
  end
307
+
308
+ def age_cutoff days
309
+ @age_cutoff = days
310
+ end
258
311
  end
@@ -174,6 +174,6 @@ class AgingWorkTable < ChartBase
174
174
  end
175
175
 
176
176
  def priority_text issue
177
- "<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' />"
177
+ "<img src='#{issue.priority_url}' title='Priority: #{issue.priority_name}' style='max-width: 1em;'/>"
178
178
  end
179
179
  end
@@ -21,6 +21,10 @@ class Anonymizer < ChartBase
21
21
  anonymize_column_names
22
22
  # anonymize_issue_statuses
23
23
  anonymize_board_names
24
+ anonymize_labels_and_components
25
+ anonymize_sprints
26
+ anonymize_fix_versions
27
+ anonymize_server_url
24
28
  shift_all_dates unless @date_adjustment.zero?
25
29
  @file_system.log 'Anonymize done'
26
30
  end
@@ -38,13 +42,25 @@ class Anonymizer < ChartBase
38
42
 
39
43
  def anonymize_issue_keys_and_titles issues: @issues
40
44
  counter = 0
45
+ seen_author_raws = {}
41
46
  issues.each do |issue|
42
47
  new_key = "ANON-#{counter += 1}"
43
48
 
44
49
  issue.raw['key'] = new_key
45
50
  issue.raw['fields']['summary'] = random_phrase
51
+ issue.raw['fields']['description'] = nil
46
52
  issue.raw['fields']['assignee']['displayName'] = random_name unless issue.raw['fields']['assignee'].nil?
47
53
 
54
+ anonymize_author_raw(issue.raw['fields']['creator'], seen_author_raws)
55
+
56
+ issue.changes.each do |change|
57
+ anonymize_author_raw(change.author_raw, seen_author_raws)
58
+ if change.comment? || change.description?
59
+ change.value = nil
60
+ change.old_value = nil
61
+ end
62
+ end
63
+
48
64
  issue.issue_links.each do |link|
49
65
  other_issue = link.other_issue
50
66
  next if other_issue.key.match?(/^ANON-\d+$/) # Already anonymized?
@@ -55,6 +71,49 @@ class Anonymizer < ChartBase
55
71
  end
56
72
  end
57
73
 
74
+ def anonymize_labels_and_components
75
+ @issues.each do |issue|
76
+ issue.raw['fields']['labels'] = []
77
+ issue.raw['fields']['components'] = []
78
+ end
79
+ end
80
+
81
+ def anonymize_sprints
82
+ sprint_counter = 0
83
+ sprint_name_map = {}
84
+ @all_boards.each_value do |board|
85
+ board.sprints.each do |sprint|
86
+ name = sprint.raw['name']
87
+ unless sprint_name_map[name]
88
+ sprint_counter += 1
89
+ sprint_name_map[name] = "Sprint-#{sprint_counter}"
90
+ end
91
+ sprint.raw['name'] = sprint_name_map[name]
92
+ end
93
+ end
94
+ end
95
+
96
+ def anonymize_fix_versions
97
+ version_counter = 0
98
+ version_name_map = {}
99
+ @issues.each do |issue|
100
+ issue.raw['fields']['fixVersions']&.each do |fix_version|
101
+ name = fix_version['name']
102
+ unless version_name_map[name]
103
+ version_counter += 1
104
+ version_name_map[name] = "Version-#{version_counter}"
105
+ end
106
+ fix_version['name'] = version_name_map[name]
107
+ end
108
+ end
109
+ end
110
+
111
+ def anonymize_server_url
112
+ @all_boards.each_value do |board|
113
+ board.raw['self'] = board.raw['self']&.sub(/^https?:\/\/[^\/]+/, 'https://anon.example.com')
114
+ end
115
+ end
116
+
58
117
  def anonymize_column_names
59
118
  @all_boards.each_key do |board_id|
60
119
  @file_system.log "Anonymizing column names for board #{board_id}"
@@ -143,7 +202,7 @@ class Anonymizer < ChartBase
143
202
  end
144
203
 
145
204
  range = @project_config.time_range
146
- @project_config.time_range = (range.begin + date_adjustment)..(range.end + date_adjustment)
205
+ @project_config.time_range = (range.begin + adjustment_in_seconds)..(range.end + adjustment_in_seconds)
147
206
  end
148
207
 
149
208
  def random_name
@@ -186,4 +245,18 @@ class Anonymizer < ChartBase
186
245
  board.raw['name'] = "#{random_phrase} board"
187
246
  end
188
247
  end
248
+
249
+ private
250
+
251
+ def anonymize_author_raw author_raw, seen
252
+ return unless author_raw
253
+ return if seen[author_raw.object_id]
254
+
255
+ seen[author_raw.object_id] = true
256
+ name = random_name
257
+ author_raw['displayName'] = name
258
+ author_raw['name'] = name
259
+ author_raw.delete('emailAddress')
260
+ author_raw.delete('avatarUrls')
261
+ end
189
262
  end