jira_reporting 0.0.1

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.
data/bin/sla_warning ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ PROJECT_ROOT = "#{File.dirname(__FILE__)}/../"
4
+
5
+ $:.unshift "#{File.dirname(__FILE__)}/../lib"
6
+ require 'jira_reporting'
7
+ require 'active_support'
8
+ require 'yaml'
9
+
10
+ auth = YAML.load_file(File.join(PROJECT_ROOT,'auth.yml'))
11
+ jira_username = auth["jira"]["username"]
12
+ jira_password = auth["jira"]["password"]
13
+ JiraReporting.connect! jira_username, jira_password
14
+
15
+ # Open TS issues
16
+ query = %Q{
17
+ project = "Tech Support"
18
+ AND status not in (Resolved, "Not Accepted", "Can't Reproduce",
19
+ "Requester Review", Merged, MERGED_ST, "On Production")
20
+ }
21
+
22
+ ts_issues = JiraIssue.find(query)
23
+
24
+ ts_issues.each do |issue|
25
+ issue.set_sla_due_time! if issue.sla_due_time.nil?
26
+ issue.set_sla_triaged_at! if issue.sla_triaged_at.nil?
27
+ issue.set_over_triage_sla! if issue.over_triage_sla.nil?
28
+ issue.set_sla_closed_at! if issue.sla_closed_at.nil?
29
+ issue.set_sla_due_warning! if issue.sla_due_warning.nil?
30
+ issue.set_over_sla! if issue.over_sla.nil?
31
+ # issue.set_total_time_over_sla! if issue.total_time_over_sla.nil?
32
+
33
+ puts "#{issue.priority} #{issue.key} #{issue.summary} has been updated."
34
+ end
35
+
36
+ puts 'SLA due time and overdue status update complete.'
data/bin/time_in_dev ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift "#{File.dirname(__FILE__)}/../lib"
4
+ require 'jira_reporting'
5
+ require 'pry-byebug'
6
+ require 'active_support'
7
+ require 'yaml'
8
+ require 'code_climate'
9
+ require 'auto_hash'
10
+ require 'pull_request'
11
+
12
+ auth = YAML.load_file('auth.yml')
13
+
14
+ jira_username = auth["jira"]["username"]
15
+ jira_password = auth["jira"]["password"]
16
+ JiraReporting.connect! jira_username, jira_password
17
+
18
+ # This takes a while
19
+ issues = JiraIssue.find(%Q{
20
+ project in (BLINQ.com, OptiTurn, "Tech Support", Tech)
21
+ and (status was Merged DURING ("2014-01-01", "2015-01-01")
22
+ OR status was Resolved DURING ("2014-01-01", "2015-01-01"))
23
+ })
24
+
25
+ type_times = Hash.new(0)
26
+ type_counts = Hash.new(0)
27
+
28
+ issues.each do |issue|
29
+ puts "#{issue.key} #{issue.type}"
30
+ type_times[issue.type] += (issue.status_ages["In Progress"] || 0)
31
+ type_counts[issue.type] += 1
32
+ puts type_times
33
+ puts type_counts
34
+ end
35
+
36
+ type_counts.keys.each { |type|
37
+ puts [type, type_counts[type], type_times[type]].join(",")
38
+ }
39
+
40
+ File.open("result.csv", "w") do |csv|
41
+ issues.each do |issue|
42
+ puts([
43
+ issue.key,
44
+ issue.type,
45
+ issue.status_ages["In Progress"] || 0,
46
+ ].join(","))
47
+ csv.puts([
48
+ issue.key,
49
+ issue.type,
50
+ issue.status_ages["In Progress"] || 0,
51
+ ].join(","))
52
+ end
53
+ end
54
+
55
+ # (1..100).each { issue = issues.sample; puts issue.type ; puts issue.status_ages; type_times[issue.type] += (issue.status_ages["In Progress"] || 0); type_times["count"] += 1 }
56
+
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jira_reporting/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jira_reporting"
8
+ spec.version = JiraReporting::VERSION
9
+ spec.authors = ["Joshua Szmajda"]
10
+ spec.email = ["josh@optoro.com"]
11
+ spec.summary = %q{Misc tools for gathering data}
12
+ spec.description = %q{Misc tools for gathering data}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "byebug"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "pry-byebug"
27
+ spec.add_development_dependency "minitest"
28
+ spec.add_dependency "jiralicious"
29
+ spec.add_dependency "thor"
30
+ spec.add_dependency "dot_notation"
31
+ spec.add_dependency "business_time"
32
+ spec.add_dependency "holidays"
33
+ spec.add_dependency "octokit"
34
+ spec.add_dependency "rubypython"
35
+ spec.add_dependency "andand"
36
+ spec.add_dependency "os"
37
+ end
data/lib/auto_hash.rb ADDED
@@ -0,0 +1,21 @@
1
+
2
+ class AutoHash < Hash
3
+ def initialize(*args)
4
+ super()
5
+ @update, @update_index = args[0][:update], args[0][:update_key] unless args.empty?
6
+ end
7
+
8
+ def [](k)
9
+ if self.has_key?k
10
+ super(k)
11
+ else
12
+ AutoHash.new(:update => self, :update_key => k)
13
+ end
14
+ end
15
+
16
+ def []=(k, v)
17
+ @update[@update_index] = self if @update and @update_index
18
+ super
19
+ end
20
+ end
21
+
@@ -0,0 +1,35 @@
1
+
2
+ require 'httparty'
3
+
4
+ # API: https://codeclimate.com/docs/api
5
+
6
+ class CodeClimate
7
+ include HTTParty
8
+ format :json
9
+ base_uri 'https://codeclimate.com/api'
10
+
11
+ def initialize(api_token)
12
+ self.class.default_params api_token: api_token
13
+ end
14
+
15
+ # Expose some HTTParty class things
16
+ def get(*args)
17
+ self.class.get(*args)
18
+ end
19
+
20
+ def repo_list
21
+ get('/repos')
22
+ end
23
+
24
+ def repo_id(repo_name)
25
+ repo_list.find { |repo| repo["url"].include?(repo_name) }["id"]
26
+ end
27
+
28
+ def repo_details(repo_id)
29
+ get("/repos/#{repo_id}")
30
+ end
31
+
32
+ def repo_details_by_name(repo_name)
33
+ get("/repos/#{repo_id(repo_name)}")
34
+ end
35
+ end
data/lib/jira_issue.rb ADDED
@@ -0,0 +1,288 @@
1
+ require 'dot_notation'
2
+
3
+ class JiraIssue
4
+ include SLATracker
5
+ include TriageTracker
6
+ # The FIVE_P_DATE variable exists for tickets created before
7
+ # 2015-03-06 15:55:16 -0500. Not used for any tickets after that.
8
+ # Tickets created after above date use differnt SLA measurements.
9
+ FIVE_P_DATE = Time.at(142_567_531_6).freeze
10
+ STORY_POINTS_FIELD = 'customfield_10007'.freeze
11
+ TRIAGER_FIELD = 'customfield_14100'.freeze
12
+ SLA_TRIAGED_AT_FIELD = 'customfield_14400'.freeze
13
+ OVER_TRIAGE_SLA_FIELD = 'customfield_14200'.freeze
14
+ OVER_TRIAGE_SLA_FIELD_YES = '12500'.freeze
15
+ OVER_TRIAGE_SLA_FIELD_NO = '12501'.freeze
16
+ API_OVER_TRIAGE_SLA_FIELD_YES = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/12500", "value"=>"Yes", "id"=>"12500"}.freeze
17
+ API_OVER_TRIAGE_SLA_FIELD_NO = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/12501", "value"=>"No", "id"=>"12501"}.freeze
18
+ SLA_DUE_TIME_FIELD = 'customfield_12900'.freeze
19
+ SLA_DUE_WARNING_FIELD = 'customfield_13401'.freeze
20
+ SLA_DUE_WARNING_FIELD_SET = '12100'.freeze
21
+ SLA_CLOSED_AT_FIELD = 'customfield_14000'.freeze
22
+ OVER_SLA_FIELD = 'customfield_12901'.freeze
23
+ OVER_SLA_FIELD_YES = '11901'.freeze
24
+ OVER_SLA_FIELD_NO = '11900'.freeze
25
+ TOTAL_TIME_OVER_SLA_FIELD = 'customfield_14201'.freeze
26
+ API_OVER_SLA_FIELD_YES = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/11901", "value"=>"Yes", "id"=>"11901"}.freeze
27
+ API_OVER_SLA_FIELD_NO = {"self"=>"https://optoro.atlassian.net/rest/api/2/customFieldOption/11900", "value"=>"No", "id"=>"11900"}.freeze
28
+ AFFECTED_USER_GROUP_FIELD = 'customfield_13602'.freeze
29
+
30
+ attr_accessor :key, :created_at, :type, :priority_name, :status, :orig,
31
+ :url, :project, :story_points, :raw_sprints, :description,
32
+ :summary, :labels, :assigned_team, :changelog, :sla_due_time,
33
+ :sla_due_warning, :over_sla,:affected_user_group,
34
+ :cant_reproduce, :in_progress, :not_accepted, :requester_denied,
35
+ :requester_review, :resolved, :merged, :merged_st,
36
+ :on_production, :deploy, :completed, :reverify, :pr_review,
37
+ :triaged, :sla_triaged_at, :over_triage_sla, :triager,
38
+ :verified, :sla_closed_at, :total_time_over_sla
39
+
40
+ def initialize(issue)
41
+ issue.extend(DotNotation)
42
+ self.key = issue.dot 'key'
43
+ self.created_at = Time.parse(issue.dot('fields.created'))
44
+ self.type = issue.dot 'fields.issuetype.name'
45
+ self.priority_name = issue.dot 'fields.priority.name'
46
+ self.status = issue.dot('fields.status.name').downcase
47
+ self.project = issue.dot 'fields.project.key'
48
+ self.description = issue.dot 'fields.description'
49
+ self.summary = issue.dot 'fields.summary'
50
+ self.labels = issue.dot 'fields.labels'
51
+ self.assigned_team = issue.dot 'fields.customfield_12601.value'
52
+ self.story_points = issue.dot "fields.#{STORY_POINTS_FIELD}"
53
+ self.orig = issue
54
+ self.url = "https://optoro.atlassian.net/browse/#{key}"
55
+ self.raw_sprints = issue.dot 'fields.customfield_10007'
56
+ self.sla_triaged_at = Time.parse(issue.dot("fields.#{SLA_TRIAGED_AT_FIELD}")) if issue.dot("fields.#{SLA_TRIAGED_AT_FIELD}").present?
57
+ self.over_triage_sla = issue.dot "fields.#{OVER_TRIAGE_SLA_FIELD}"
58
+ self.triager = issue.dot "fields.#{TRIAGER_FIELD}.name"
59
+ self.sla_due_time = Time.parse(issue.dot("fields.#{SLA_DUE_TIME_FIELD}")) if issue.dot("fields.#{SLA_DUE_TIME_FIELD}").present?
60
+ self.sla_due_warning = issue.dot "fields.#{SLA_DUE_WARNING_FIELD}"
61
+ self.sla_closed_at = Time.parse(issue.dot("fields.#{SLA_CLOSED_AT_FIELD}")) if issue.dot("fields.#{SLA_CLOSED_AT_FIELD}").present?
62
+ self.over_sla = issue.dot "fields.#{OVER_SLA_FIELD}"
63
+ self.total_time_over_sla = issue.dot "fields.#{TOTAL_TIME_OVER_SLA_FIELD}"
64
+ self.affected_user_group = issue.dot "fields.#{AFFECTED_USER_GROUP_FIELD}"
65
+
66
+ inv = self.class.custom_time_fields.invert
67
+ inv.each do |name, key|
68
+ x = name.gsub(/'/,'').gsub(/ +/, '_').downcase.gsub(/support_xt_/,'')
69
+ v = issue.dot "fields.#{key}"
70
+ v = Time.parse(v) if v
71
+ x = "pr_review" if x == "review" # rename
72
+ send("#{x}=", v)
73
+ end
74
+ end
75
+
76
+ def jiralicious_issue
77
+ Jiralicious::Issue.find(key)
78
+ end
79
+
80
+ def self.find(query, offset = 0)
81
+ ask = 100
82
+ result = Jiralicious.search(query, start_at: offset, max_results: ask)
83
+ issues = result.issues_raw
84
+ issues.map! { |i| new(i) }
85
+ if issues.count == ask && result.num_results > offset + ask
86
+ issues += find(query, offset + ask)
87
+ end
88
+ issues
89
+ rescue Jiralicious::JqlError => e
90
+ $stderr.puts "Error: #{e}"
91
+ []
92
+ end
93
+
94
+ def self.custom_time_fields
95
+ Hash[[
96
+ [10812, "Support xt Can't Reproduce"],
97
+ [10808, 'Support xt In Progress'],
98
+ [10809, 'Support xt Not Accepted'],
99
+ [10817, 'Support xt Requester Denied'],
100
+ [10814, 'Support xt Requester Review'],
101
+ [13101, 'Support xt Merged'],
102
+ [13102, 'Support xt On Production'],
103
+ [10816, 'Support xt Resolved'],
104
+ [10815, 'Support xt Reverify'],
105
+ [10813, 'Support xt Review'],
106
+ [10810, 'Support xt Triaged'],
107
+ [10811, 'Support xt Verified']
108
+ ].map{|k,v| ["customfield_#{k}", v]}]
109
+ end
110
+
111
+ def open?
112
+ !closed?
113
+ # ["unverified", "triaged", "verified",
114
+ # "in progress", "pr_review", "pr review"].include? status
115
+ end
116
+
117
+ def closed?
118
+ [ "requester review",
119
+ "merged",
120
+ "merged_st",
121
+ "on production",
122
+ "resolved",
123
+ "not accepted",
124
+ "can't reproduce",
125
+ "deploy",
126
+ "completed"
127
+ ].include? status
128
+ end
129
+
130
+ def closed_at
131
+ [ requester_review,
132
+ merged,
133
+ merged_st,
134
+ on_production,
135
+ resolved,
136
+ not_accepted,
137
+ cant_reproduce,
138
+ deploy,
139
+ completed
140
+ ].compact.min
141
+ end
142
+
143
+ def bug?
144
+ type == "Bug / Outage"
145
+ end
146
+
147
+ def operational?
148
+ type == "Operational Request"
149
+ end
150
+
151
+ def feature?
152
+ type == "Feature Enhancement"
153
+ end
154
+
155
+ def research?
156
+ type == "Research"
157
+ end
158
+
159
+ def affected_user_groups
160
+ affected_user_group.map {|user_group| user_group["value"]} unless affected_user_group.nil?
161
+ end
162
+
163
+ def affects_warehouse_users?
164
+ return false if affected_user_groups.nil?
165
+ affected_user_groups.include?("WH Users")
166
+ end
167
+
168
+ def affects_customers?
169
+ return false if affected_user_groups.nil?
170
+ affected_user_groups.include?("Customers")
171
+ end
172
+
173
+ def affects_client_corporate_users?
174
+ return false if affected_user_groups.nil?
175
+ affected_user_groups.include?("Client Corporate Users")
176
+ end
177
+
178
+ def affects_internal_optoro_users?
179
+ return false if affected_user_groups.nil?
180
+ affected_user_groups.include?("Internal Optoro Users")
181
+ end
182
+
183
+ def priority
184
+ case priority_name
185
+ when /P1/
186
+ :p1
187
+ when /P2/
188
+ :p2
189
+ when /P3/
190
+ :p3
191
+ when /P4/
192
+ :p4
193
+ else
194
+ :p5
195
+ end
196
+ end
197
+
198
+ def time_to_qa
199
+ if triaged && (verified || cant_reproduce || in_progress)
200
+ if verified
201
+ verified - triaged
202
+ elsif cant_reproduce
203
+ cant_reproduce - triaged
204
+ else
205
+ in_progress - triaged
206
+ end
207
+ elsif triaged
208
+ Time.now - triaged
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+ def sprints
215
+ if raw_sprints
216
+ raw_sprints.map {|s| Hash[s.match(/\[(.*)\]/)[1].split(',').map{|f|f.split('=')}]}
217
+ else
218
+ []
219
+ end
220
+ end
221
+
222
+ def changelog
223
+ return @changelog if @changelog
224
+ changelog = Jiralicious::Issue.fetch(key: key, body_to_params: true, body: {expand: 'changelog'})
225
+ @changelog = changelog["changelog"]["histories"]
226
+ end
227
+
228
+ def changelog_simple
229
+ changelog.map { |c|
230
+ c["items"].map { |i|
231
+ i.slice("field", "fromString","toString")
232
+ .merge({
233
+ "created" => c["created"],
234
+ "author_out" => c["author"]["name"] || ""
235
+ })
236
+ }
237
+ }.flatten
238
+ end
239
+
240
+ def status_changes
241
+ status_changes = changelog_simple.select{|c| c["field"] == "status"}
242
+ start = created_at
243
+ author = orig["fields"]["reporter"].andand["name"] || ""
244
+ status_changes.each { |c|
245
+ c["age_hours"] = (Time.parse(c["created"]).to_i - start.to_i)/60.0/60
246
+ c["started"] = start
247
+ start = Time.parse(c["created"])
248
+ cur_author = author
249
+ author = c["author_out"]
250
+ c["author_in"] = cur_author
251
+ }
252
+ statuses = status_changes.map { |s|
253
+ {
254
+ status: s["fromString"],
255
+ start: s["started"],
256
+ end: Time.parse(s["created"]),
257
+ author_in: s["author_in"],
258
+ author_out: s["author_out"],
259
+ age_hours: s["age_hours"]
260
+ }
261
+ }
262
+ statuses << {
263
+ status: status,
264
+ start: start,
265
+ author_in: author
266
+ }
267
+
268
+ end
269
+
270
+ def status_ages
271
+ ages = {}
272
+ status_changes.each{|c| ages[c[:status]] = ages.fetch(c[:status], 0) + c[:age_hours].to_r.round(2).to_f}
273
+ ages
274
+ end
275
+
276
+ def status_duration_during(status, start_date, end_date)
277
+ ages = {}
278
+ status_changes.each{|c| ages[c["fromString"]] = ages.fetch(c["fromString"],0) + c["age_hours"]}
279
+ status_changes.each{|c| ages[c[:status]] = ages.fetch(c[:status], 0) + c[:age_hours].to_r.round(2).to_f}
280
+ last_status = status_changes.last
281
+ if last_status
282
+ ages[last_status["toString"]] =
283
+ ages.fetch(last_status["toString"], 0) +
284
+ ((Time.now.to_f - Time.parse(last_status["created"]).to_f)/60.0/60)
285
+ end
286
+ ages
287
+ end
288
+ end