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.
@@ -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
+