jira_reporting 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ require "jira_reporting/version"
2
+ require "jira_reporting/connection"
3
+ require "jira_reporting/sla_report"
4
+ require "jira_reporting/sla_tracker"
5
+ require "jira_reporting/triage_tracker"
6
+ require 'jira_issue'
7
+ require 'jiralicious'
8
+ require 'business_time'
9
+ require 'holidays'
10
+
11
+ PROJECT_ROOT = "#{File.dirname(__FILE__)}/../"
12
+
13
+ module JiraReporting
14
+ def self.connect!(user, pass)
15
+ Connection.instance.connect!(user, pass)
16
+ end
17
+ end
18
+
19
+ BusinessTime::Config.beginning_of_workday = "8:00 am"
20
+ BusinessTime::Config.end_of_workday = "6:00 pm"
21
+ # init business time
22
+ Holidays.load_custom(File.join(PROJECT_ROOT,'optoro_holidays.yaml'))
23
+ Holidays.between(Time.now, 2.years.from_now, :optorolandia, :observed).map do |holiday|
24
+ BusinessTime::Config.holidays << holiday[:date]
25
+ # Implement long weekends if they apply to the region, eg:
26
+ # BusinessTime::Config.holidays << holiday[:date].next_week if !holiday[:date].weekday?
27
+ end
@@ -0,0 +1,16 @@
1
+ require 'singleton'
2
+
3
+ module JiraReporting
4
+ class Connection
5
+ include Singleton
6
+ def connect!(user, pass)
7
+ Jiralicious.configure do |c|
8
+ c.username = user
9
+ c.password = pass
10
+ c.uri = "https://optoro.atlassian.net"
11
+ c.api_version = 'latest'
12
+ c.auth_type = :basic
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,103 @@
1
+ module JiraReporting
2
+ class SLAReport
3
+
4
+ def self.show_report
5
+ self.new.show_report
6
+ end
7
+
8
+ def initialize
9
+ @conn = Connection.instance
10
+ @debug = true
11
+ end
12
+
13
+ attr_accessor :conn
14
+
15
+ def log(str)
16
+ puts "#{Time.now} #{str}" if @debug
17
+ end
18
+
19
+ def show_report
20
+
21
+ log 'querying'
22
+ #query = %q{project = "Tech Support" AND ("Support xt Can't Reproduce" > startOfDay(-7d) OR "Support xt Not Accepted" > startOfDay(-7d) OR "Support xt Resolved" > startOfDay(-7d)) AND status not in ("Not Accepted", "Can't Reproduce")}
23
+ query = %q{
24
+ project = "Tech Support"
25
+ AND status not in ("Not Accepted", "Can't Reproduce")}
26
+ #issues = Jiralicious.search(query).issues_raw
27
+ issues = find_all(query)
28
+
29
+ #log 'unmapping custom fields'
30
+ #unmap_custom_fields(issues.first)
31
+ rept = issues.map{|i| ReportIssue.new(i, custom_fields) }
32
+ prios = rept.group_by(&:priority)
33
+
34
+ prio_report("P1", prios[:p1])
35
+ prio_report("P2", prios[:p2])
36
+ prio_report("P3", prios[:p3])
37
+ prio_report("P4", prios[:p4])
38
+ end
39
+
40
+ def find_all(query, offset = 0)
41
+ ask = 100
42
+ result = Jiralicious.search(query, start_at: offset, max_results: ask)
43
+ issues = result.issues_raw
44
+ if issues.count == ask && result.num_results > issues.count
45
+ issues += find_all(query, offset + ask)
46
+ end
47
+ # puts "found #{issues.count}"
48
+ issues
49
+ end
50
+
51
+ def prio_report(name, group)
52
+ puts "\n#{name}:"
53
+
54
+ cur_week = Time.now.to_date.cweek
55
+ week_groups = group.group_by{|t| t.created_at.to_date.cweek }
56
+ wgs = week_groups.to_a.select{|week, set| week >= (cur_week - 27) }
57
+ wgs.sort!{|a,b| a[0] <=> b[0] }
58
+
59
+ puts "Week: #{wgs.map{|w,s| "%12i" % w }.join(" | ")}"
60
+
61
+ o = []
62
+ wgs.each do |week, set|
63
+ sla_rate = set.select{|t| t.sla_diff <= 0 }.length.to_f / set.length
64
+ o << "%11.2f%" % (sla_rate*100)
65
+ end
66
+ puts "SLA Hit Rate: #{o.join " | "}"
67
+
68
+ o = []
69
+ wgs.each do |week, set|
70
+ over_under = set.map(&:sla_diff).sum
71
+ o << "%12i" % over_under
72
+ end
73
+ puts "Over Under: #{o.join " | "}"
74
+ end
75
+
76
+ def custom_fields
77
+ @custom_fields ||= Hash[[
78
+ [10812, "Support xt Can't Reproduce"],
79
+ [10808, 'Support xt In Progress'],
80
+ [10809, 'Support xt Not Accepted'],
81
+ [10817, 'Support xt Requester Denied'],
82
+ [10814, 'Support xt Requester Review'],
83
+ [10816, 'Support xt Resolved'],
84
+ [10815, 'Support xt Reverify'],
85
+ [10813, 'Support xt Review'],
86
+ [10810, 'Support xt Triaged'],
87
+ [10811, 'Support xt Verified'],
88
+ ].map{|k,v| ["customfield_#{k}", v]}]
89
+ end
90
+
91
+ #def unmap_custom_fields(sample_issue)
92
+ # sample_issue['fields'].keys.select {|f| f =~ /^customfield_/ }.each do |f|
93
+ # begin
94
+ # id = f.sub(/customfield_/,'')
95
+ # field = Jiralicious::CustomFieldOption.find(id)
96
+ # custom_fields[f] = field['value']
97
+ # rescue Jiralicious::IssueNotFound
98
+ # custom_fields[f] = 'Unknown'
99
+ # end
100
+ # end
101
+ #end
102
+ end
103
+ end
@@ -0,0 +1,183 @@
1
+ class JiraIssue
2
+ module SLATracker
3
+ def set_sla_due_time!(due_time = nil)
4
+ due_time ||= sla_target
5
+ return unless due_time
6
+ jira_issue = jiralicious_issue
7
+ jira_issue.fields.set(SLA_DUE_TIME_FIELD, due_time.iso8601)
8
+ jira_issue.save!
9
+ end
10
+
11
+ def set_over_sla!(over = nil)
12
+ over ||= over_sla?
13
+ return if over.nil? || over == false
14
+ jira_issue = jiralicious_issue
15
+ jira_issue.fields.set_id(OVER_SLA_FIELD, OVER_SLA_FIELD_YES)
16
+ jira_issue.save!
17
+ end
18
+
19
+ def set_sla_due_warning!(almost_over_sla = nil)
20
+ almost_over_sla ||= approaching_sla_due_time?
21
+ return if almost_over_sla.nil? || almost_over_sla == false
22
+ jira_issue = jiralicious_issue
23
+ jira_issue.fields.set_id(SLA_DUE_WARNING_FIELD, SLA_DUE_WARNING_FIELD_SET)
24
+ jira_issue.save!
25
+ end
26
+
27
+ def set_sla_closed_at!(sla_closed_at = nil)
28
+ sla_closed_at ||= closed_at
29
+ return if sla_closed_at.nil? || open?
30
+ jira_issue = jiralicious_issue
31
+ jira_issue.fields.set(SLA_CLOSED_AT_FIELD, sla_closed_at.iso8601)
32
+ jira_issue.save!
33
+ end
34
+
35
+ def set_total_time_over_sla!(time_over_sla = nil)
36
+ time_over_sla ||= sum_total_time_over_sla
37
+ return unless over_sla == API_OVER_SLA_FIELD_YES || closed? || time_over_sla.present?
38
+ jira_issue = jiralicious_issue
39
+ jira_issue.fields.set(TOTAL_TIME_OVER_SLA_FIELD, time_over_sla)
40
+ jira_issue.save!
41
+ end
42
+
43
+ def sla_base
44
+ created_at
45
+ end
46
+
47
+ def sla_target
48
+ case priority
49
+ when :p1
50
+ sla_base + 2.hours
51
+ when :p2
52
+ sla_base + 24.hours
53
+ when :p3
54
+ if sla_base <= FIVE_P_DATE
55
+ 5.business_days.after(sla_base)
56
+ else
57
+ 3.business_days.after(sla_base)
58
+ end
59
+ when :p4
60
+ if sla_base <= FIVE_P_DATE
61
+ nil
62
+ else
63
+ 5.business_days.after(sla_base).change(:hour => BusinessTime::Config.end_of_workday.hour)
64
+ end
65
+ else
66
+ nil
67
+ end
68
+ end
69
+
70
+ def sla_time_ratio(from = Time.now)
71
+ return 0 if priority == :p5
72
+ from = closed_at if closed?
73
+ available_time = sla_target - sla_base
74
+ used_time = from - sla_base
75
+ used_time.to_f / available_time
76
+ end
77
+
78
+ def sla_total_available_time
79
+ sla_due_time - sla_base
80
+ end
81
+
82
+ def sla_remaining_time
83
+ sla_due_time - Time.now
84
+ end
85
+
86
+ def sla_warning_time
87
+ sla_total_available_time*0.20
88
+ end
89
+
90
+ def approaching_sla_due_time?
91
+ return false if sla_due_time.nil? || closed?
92
+ sla_remaining_time < sla_warning_time
93
+ end
94
+
95
+ def over_sla?
96
+ case
97
+ when over_sla == API_OVER_SLA_FIELD_NO
98
+ false
99
+ when sla_due_time.nil?
100
+ false
101
+ when closed? && sla_closed_at > sla_due_time
102
+ true
103
+ when open? && Time.now > sla_due_time
104
+ true
105
+ end
106
+ end
107
+
108
+ def sum_total_time_over_sla # => difference in hours
109
+ if over_sla == API_OVER_SLA_FIELD_YES
110
+ ((sla_closed_at - sla_due_time) / 60) / 60
111
+ else
112
+ 0
113
+ end
114
+ end
115
+
116
+ def sla_diff(now = Time.now)
117
+ t = sla_target
118
+ b = resolution_base
119
+ if t && b
120
+ b - t
121
+ elsif t
122
+ now - t
123
+ else
124
+ 0
125
+ end
126
+ end
127
+
128
+ def time_to_start_work
129
+ if verified
130
+ if in_progress
131
+ in_progress - verified
132
+ else
133
+ Time.now - verified
134
+ end
135
+ elsif triaged && in_progress
136
+ in_progress - triaged
137
+ elsif in_progress
138
+ in_progress - created
139
+ else
140
+ nil
141
+ end
142
+ end
143
+
144
+ def time_to_finish_work
145
+ if in_progress
146
+ if pr_review
147
+ pr_review - in_progress
148
+ elsif requester_review
149
+ requester_review - in_progress
150
+ end
151
+ else
152
+ nil
153
+ #elsif resolved
154
+ # # wtf?
155
+ # resolved - created_at
156
+ end
157
+ end
158
+
159
+ def resolution_base
160
+ if requester_review
161
+ requester_review
162
+ elsif resolved
163
+ resolved
164
+ elsif cant_reproduce
165
+ cant_reproduce
166
+ elsif not_accepted
167
+ not_accepted
168
+ else
169
+ # not resolved
170
+ nil
171
+ end
172
+ end
173
+
174
+ def total_time_to_resolution
175
+ b = resolution_base
176
+ if b
177
+ resolution_base - created_at
178
+ else
179
+ nil
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,69 @@
1
+ class JiraIssue
2
+ module TriageTracker
3
+ def set_sla_triaged_at!(sla_triaged = nil)
4
+ sla_triaged ||= triaged
5
+ return if sla_triaged.nil?
6
+ jira_issue = jiralicious_issue
7
+ jira_issue.fields.set(SLA_TRIAGED_AT_FIELD, sla_triaged.iso8601)
8
+ jira_issue.save!
9
+ end
10
+
11
+ def set_over_triage_sla!(over_triage = nil)
12
+ over_triage ||= over_triage_sla?
13
+ return if over_triage.nil? || over_triage == false
14
+ jira_issue = jiralicious_issue
15
+ jira_issue.fields.set_id(OVER_TRIAGE_SLA_FIELD, OVER_TRIAGE_SLA_FIELD_YES)
16
+ jira_issue.save!
17
+ end
18
+
19
+ def time_to_triage
20
+ if triaged
21
+ triaged - sla_base
22
+ else
23
+ nil
24
+ end
25
+ end
26
+
27
+ def triage_sla_target
28
+ case priority
29
+ when :p1
30
+ sla_base + 0.5.hours
31
+ when :p2
32
+ sla_base + 1.hours
33
+ when :p3
34
+ 2.business_hours.after(sla_base)
35
+ when :p4
36
+ 1.business_day.after(sla_base)
37
+ else
38
+ nil
39
+ end
40
+ end
41
+
42
+ def triage_sla_total_available_time
43
+ triage_sla_target - sla_base
44
+ end
45
+
46
+ def triage_sla_remaining_time
47
+ triage_sla_target - Time.now
48
+ end
49
+
50
+ def over_triage_sla?
51
+ case
52
+ when over_triage_sla == API_OVER_TRIAGE_SLA_FIELD_NO
53
+ false
54
+ when triage_sla_target.nil?
55
+ false
56
+ when triaged.nil? && Time.now > triage_sla_target
57
+ true
58
+ when triaged.nil? && Time.now < triage_sla_target
59
+ false
60
+ when sla_triaged_at.nil? && triaged < triage_sla_target
61
+ false
62
+ when sla_triaged_at.nil? && triaged > triage_sla_target
63
+ true
64
+ when triaged && sla_triaged_at > triage_sla_target
65
+ true
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ module JiraReporting
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,104 @@
1
+ require 'octokit'
2
+ require 'csv'
3
+ require 'io/console'
4
+ require 'pry-byebug'
5
+ require 'andand'
6
+ require 'jira_reporting'
7
+
8
+ class PullRequest
9
+
10
+ attr_accessor :pr_number, :jira, :title, :created, :requester, :cl, :ci,
11
+ :code_reviewed, :needs_testing, :project, :issue, :issue_current_sprint,
12
+ :issue_status, :pr_raw
13
+
14
+ def initialize(params)
15
+ params.each do |key, value|
16
+ instance_variable_set("@#{key}", value)
17
+ end
18
+ end
19
+
20
+ def to_csv
21
+ [:pr_number, :jira, :title, :created, :requester, :cl, :ci, :code_reviewed, :needs_testing, :projec, :issue_current_sprint, :issue_status]
22
+ .map{|col| instance_variable_get("@#{col}")}
23
+ .to_csv
24
+ end
25
+
26
+ def self.client
27
+ @@client
28
+ end
29
+
30
+ @@client = nil
31
+ def self.connect(token = nil)
32
+ return @@client if @@client
33
+ client = nil
34
+ if token
35
+ client = Octokit::Client.new(access_token: token)
36
+ else
37
+ print "Github Login: "
38
+ login = gets.chomp
39
+ print "Github Password: "
40
+ passwd = STDIN.noecho(&:gets).chomp
41
+ puts "\nBuilding report..."
42
+ client = Octokit::Client.new(login: login, password: passwd)
43
+ end
44
+ client.auto_paginate = true
45
+ @@client = client
46
+ end
47
+
48
+ def self.get_prs(project, options = {})
49
+
50
+ pulls = client.pulls("optoro/#{project}", options)
51
+
52
+ prs = []
53
+
54
+ pulls.each do |pull|
55
+ pr = PullRequest.new(
56
+ pr_number: pull.number,
57
+ title: pull.title,
58
+ created: pull.created_at,
59
+ requester: pull.user.login,
60
+ ci: pull.rels[:statuses].get.data.first.andand.state,
61
+ pr_raw: pull,
62
+ )
63
+
64
+ if pr.title =~ /([A-Z]{2,}-[0-9]+)/
65
+ pr.jira = $1
66
+ end
67
+
68
+ labels = pr.labels
69
+ if labels.member? "CL - Low"
70
+ pr.cl = "Low"
71
+ end
72
+ if labels.member? "CL - Medium"
73
+ pr.cl = "Medium"
74
+ end
75
+ if labels.member? "CL - High"
76
+ pr.cl = "High"
77
+ end
78
+ if labels.member? "CL - System Impacting"
79
+ pr.cl = "System"
80
+ end
81
+ pr.needs_testing = labels.member?("Needs External Testing") ? "Yes" : "No"
82
+ pr.code_reviewed = labels.member?("Review Passed") ? "Yes" : "No"
83
+ pr.project = labels.member?("Part of Project") ? "Yes" : "No"
84
+
85
+ if pr.jira
86
+ issue = JiraIssue.find("issueKey = #{pr.jira}").first
87
+ if issue
88
+ pr.issue = issue
89
+ pr.issue_current_sprint = !!issue.sprints.detect{|s|s["state"] == "ACTIVE"}
90
+ pr.issue_status = issue.status
91
+ end
92
+ end
93
+
94
+ prs << pr
95
+ end
96
+ prs
97
+ end
98
+
99
+ def labels
100
+ pr_raw.rels[:issue].get.data.labels.map(&:name).to_set
101
+ end
102
+
103
+ end
104
+