jira_reporting 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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