zentool 0.8.0

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,117 @@
1
+ # Graph class for pull_articles.rb
2
+
3
+ class Graph
4
+ def initialize(articles, sections, categories, basic_auth)
5
+ @articles, @sections, @categories, @basic_auth = articles, sections, categories, basic_auth
6
+ end
7
+
8
+ def generate
9
+ id_title_map = Graph.create_id_title_map(@articles)
10
+ @article_link_map = Graph.article_link_map(@articles, @categories, @sections, id_title_map, @basic_auth)
11
+ graph_settings
12
+ graph_nodes(id_title_map)
13
+ graph_edges(id_title_map)
14
+ @g.output(png: 'article_relationships.png')
15
+ end
16
+
17
+ def self.wrap(s, width = 15)
18
+ s.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
19
+ end
20
+
21
+ def self.create_id_title_map(articles)
22
+ x = {}
23
+ articles.each do |article|
24
+ x[article['id'].to_i] = article['title']
25
+ end
26
+ x
27
+ end
28
+
29
+ def self.extract_links(string)
30
+ [URI.extract(string, /http(s)?/)].flatten
31
+ end
32
+
33
+ def self.extract_IDs(string)
34
+ string.split(//).map { |x| x[/\d+/] }.compact.join('').to_i
35
+ end
36
+
37
+ def self.validate_link(article, link, basic_auth)
38
+ response = HTTParty.get(link, basic_auth)
39
+ unless response.code == 200
40
+ puts
41
+ puts 'Broken Link Found'
42
+ puts '-----------------'
43
+ puts
44
+ puts 'From page: ' + article['title']
45
+ puts 'At URL: ' + article['html_url']
46
+ puts
47
+ puts "Message: Error #{response.code}: #{response.message}"
48
+ puts 'Broken link: ' + link
49
+ return false
50
+ end
51
+ return true
52
+ end
53
+
54
+ def self.article_link_map(articles, categories, sections, id_title_map, basic_auth)
55
+ puts 'Constructing article link map...'
56
+ article_link_map = {}
57
+ articles.each do |article|
58
+ unless (categories[sections[article['section_id']]['category_id']]['name'] == 'Announcements') || (article['body'].class != String)
59
+ referenced_links = Graph.extract_links(article['body'])
60
+ referenced_articles = []
61
+ unless referenced_links.empty?
62
+ referenced_links.each do |link|
63
+ if validate_link(article, link, basic_auth)
64
+ id = Graph.extract_IDs(link)
65
+ title = id_title_map[id]
66
+ unless (id.class == NilClass) || (title.class == NilClass) || (id.to_s.size != 9)
67
+ referenced_articles << Graph.wrap("#{title}\n#{id}")
68
+ end
69
+ else
70
+
71
+ end
72
+ end
73
+ article_link_map[article['id']] = referenced_articles
74
+ end
75
+ end
76
+ end
77
+ article_link_map
78
+ end
79
+
80
+ def graph_settings
81
+ $LOAD_PATH.unshift('../lib')
82
+ @g = GraphViz.new('G')
83
+
84
+ @g.node[:color] = '#222222'
85
+ @g.node[:style] = 'filled'
86
+ @g.node[:shape] = 'box'
87
+ @g.node[:penwidth] = '1'
88
+ @g.node[:fontname] = 'Helvetica'
89
+ @g.node[:fillcolor] = '#eeeeee'
90
+ @g.node[:fontcolor] = '#333333'
91
+ @g.node[:margin] = '0.05'
92
+ @g.node[:fontsize] = '12'
93
+ @g.edge[:color] = '#666666'
94
+ @g.edge[:weight] = '1'
95
+ @g.edge[:fontsize] = '10'
96
+ @g.edge[:fontcolor] = '#444444'
97
+ @g.edge[:fontname] = 'Helvetica'
98
+ @g.edge[:dir] = 'forward'
99
+ @g.edge[:arrowsize] = '1'
100
+ @g.edge[:arrowhead] = 'vee'
101
+ end
102
+
103
+ def graph_nodes(id_title_map)
104
+ nodes = []
105
+ @article_link_map.each do |id, _referenced_articles|
106
+ nodes << Graph.wrap("#{id_title_map[id]}\n#{id}")
107
+ end
108
+ @g.add_nodes(nodes)
109
+ end
110
+
111
+ def graph_edges(id_title_map)
112
+ @article_link_map.each do |id, referenced_articles|
113
+ @g.add_edges(Graph.wrap("#{id_title_map[id]}\n#{id}"), referenced_articles.map(&:to_s))
114
+ end
115
+ end
116
+
117
+ end
@@ -0,0 +1,172 @@
1
+ require 'csv'
2
+ require 'date'
3
+
4
+ # Represents an aggregate of metrics from multiple tickets
5
+ class Metrics
6
+
7
+ MINUTES_IN_DAY = 1440
8
+ MINUTES_IN_HOUR = 60
9
+ PLOT_WIDTH = 200
10
+ CURRENT_DATE = Date.today
11
+ attr_accessor :tickets, :avg_user_priority, :avg_development_priority,
12
+ :unsolved_tickets_by_age_log_scale, :solved_tickets_by_age_log_scale
13
+
14
+ def initialize(tickets)
15
+ @tickets = tickets
16
+ log_scale = {'0' => 0, '1' => 0, '2' => 0, '3' => 0, '4' => 0,
17
+ '5' => 0, '6-10' => 0, '11-20' => 0, '21-50' => 0, '51-100' => 0, '101+' => 0}
18
+ log_scale2 = log_scale.clone
19
+ @unsolved_tickets_by_age_log_scale = unsolved_age(log_scale)
20
+ @solved_tickets_by_age_log_scale = solved_age(log_scale2)
21
+ @avg_user_priority = avg_priority('user_priority')
22
+ @avg_development_priority = avg_priority('development_priority')
23
+ end
24
+
25
+ def plot_as_log(tickets_by_log_scale, age)
26
+ if age <= 5
27
+ tickets_by_log_scale[age.to_s] += 1
28
+ elsif age <= 10
29
+ tickets_by_log_scale['6-10'] += 1
30
+ elsif age <= 20
31
+ tickets_by_log_scale['11-20'] += 1
32
+ elsif age <= 50
33
+ tickets_by_log_scale['21-50'] += 1
34
+ elsif age <= 100
35
+ tickets_by_log_scale['51-100'] += 1
36
+ else
37
+ tickets_by_log_scale['101+'] += 1
38
+ end
39
+ tickets_by_log_scale
40
+ end
41
+
42
+ # Creates plot data for number of solved tickets by age
43
+ def unsolved_age(log_scale)
44
+ out_scale = Hash.new
45
+ @tickets.each do |ticket|
46
+ metrics = ticket.metrics
47
+ # Rounds the age of the ticket down to the nearest day
48
+ unless metrics['full_resolution_time_in_minutes']
49
+ if metrics['initially_assigned_at']
50
+ start_date = Date.parse metrics['initially_assigned_at']
51
+ age = (CURRENT_DATE - start_date).to_i
52
+ log_scale = plot_as_log(log_scale, age)
53
+ end
54
+ end
55
+ end
56
+ log_scale
57
+ end
58
+
59
+ # Creates plot data for number of solved tickets by age
60
+ def solved_age(log_scale)
61
+ out_scale = Hash.new
62
+ @tickets.each do |ticket|
63
+ metrics = ticket.metrics
64
+ # Rounds the age of the ticket down to the nearest day
65
+ if metrics['full_resolution_time_in_minutes']
66
+ age = metrics['full_resolution_time_in_minutes'] / MINUTES_IN_DAY
67
+ log_scale = plot_as_log(log_scale, age)
68
+ end
69
+ end
70
+ log_scale
71
+ end
72
+
73
+ # Creates plot data for average first reply time by priority
74
+ def avg_priority(priority_type)
75
+ # Hash with key => user_priority, and the value => array of reply times from tickets with that priority
76
+ tickets_by_priority = Hash.new {|h,k| h[k] = []}
77
+
78
+ # Hash with key => user_priority, and the value => average reply time of that priority
79
+ avg_priority = Hash.new(0)
80
+
81
+ tickets.each do |ticket|
82
+
83
+ priority = ticket.info[priority_type]
84
+ reply_time = ticket.metrics['reply_time_in_minutes']
85
+
86
+ if priority == nil
87
+ priority = 'None'
88
+ end
89
+
90
+ if reply_time
91
+ tickets_by_priority[priority] << reply_time
92
+ end
93
+ end
94
+
95
+ tickets_by_priority.each do |key, value|
96
+ if key == nil
97
+ key = 'None'
98
+ end
99
+ avg = value.inject(:+).to_f / value.length
100
+ avg_priority[key] = avg / MINUTES_IN_HOUR
101
+ end
102
+ avg_priority
103
+ end
104
+
105
+ # draws command line graph based on ticket metrics
106
+ def graph
107
+ puts
108
+ puts 'Age of Solved Tickets'
109
+ puts '_____________________'
110
+ puts 'Days Ticket-Count'
111
+ @solved_tickets_by_age_log_scale.keys.each do |age|
112
+ printf "%-10s %5d %s \n" % [age, @solved_tickets_by_age_log_scale[age],
113
+ '#' * (Math.log10(@solved_tickets_by_age_log_scale[age]+1)*5)]
114
+ end
115
+
116
+ puts
117
+ puts 'Age of Unsolved Tickets'
118
+ puts '_______________________'
119
+ puts 'Days Ticket-Count'
120
+ @unsolved_tickets_by_age_log_scale.keys.each do |age|
121
+ printf "%-10s %5d %s \n" % [age, @unsolved_tickets_by_age_log_scale[age],
122
+ '#' * (Math.log10(@unsolved_tickets_by_age_log_scale[age]+1)*5)]
123
+ end
124
+
125
+ puts
126
+ puts 'Average First Reply Time by Development Priority'
127
+ puts '___________________________________________'
128
+ puts 'Priority Reply-Time-Hours'
129
+ @avg_development_priority.keys.sort.each do |development_priority|
130
+ printf "%-10s %5d %s \n" % [development_priority, @avg_development_priority[development_priority],
131
+ '#' * (@avg_development_priority[development_priority] / PLOT_WIDTH)]
132
+ end
133
+
134
+ puts
135
+ puts 'Average First Reply Time by User Priority'
136
+ puts '___________________________________________'
137
+ puts 'Priority Reply-Time-Hours'
138
+ @avg_user_priority.keys.sort.each do |user_priority|
139
+ printf "%-10s %5d %s \n" % [user_priority, @avg_user_priority[user_priority],
140
+ '#' * (@avg_user_priority[user_priority] / PLOT_WIDTH)]
141
+ end
142
+ end
143
+
144
+ def save
145
+ CSV.open('solved_tickets_by_age.csv', 'wb') do |csv|
146
+ csv << ['Days', 'Ticket-Count']
147
+ end
148
+ @solved_tickets_by_age_log_scale.keys.each do |age|
149
+ CSV.open('solved_tickets_by_age.csv', 'a') do |csv|
150
+ csv << [age, @solved_tickets_by_age_log_scale[age]]
151
+ end
152
+ end
153
+
154
+ CSV.open('unsolved_tickets_by_age.csv', 'wb') do |csv|
155
+ csv << ['Days', 'Ticket-Count']
156
+ end
157
+ @unsolved_tickets_by_age_log_scale.keys.each do |age|
158
+ CSV.open('unsolved_tickets_by_age.csv', 'a') do |csv|
159
+ csv << [age, @unsolved_tickets_by_age_log_scale[age]]
160
+ end
161
+ end
162
+
163
+ CSV.open('avg_reply_time_priority.csv', 'wb') do |csv|
164
+ csv << ['Priority', 'Average-Reply-Time']
165
+ end
166
+ @avg_development_priority.keys.sort.each do |age|
167
+ CSV.open('avg_reply_time_priority.csv', 'a') do |csv|
168
+ csv << [age, @avg_development_priority[age]]
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,10 @@
1
+ # Represents a single ticket, including its data and metrics
2
+
3
+ class Ticket
4
+ attr_accessor :metrics, :info
5
+
6
+ def initialize(info, metrics)
7
+ @info = info
8
+ @metrics = metrics
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Zentool
2
+ VERSION = '0.8.0'.freeze
3
+ end
@@ -0,0 +1,84 @@
1
+ # Interface with zendesk to access articles
2
+
3
+ class ZendeskArticle
4
+ def initialize(username, password, domain)
5
+ @root_uri = "https://#{domain}.zendesk.com/api/v2/help_center/en-us/"
6
+ @articles_uri = @root_uri + 'articles.json'
7
+ @sections_uri = @root_uri + 'sections.json'
8
+ @categories_uri = @root_uri + 'categories.json'
9
+ @username, @password = username, password
10
+ check_auth
11
+ end
12
+
13
+ def articles
14
+ @articles ||= begin
15
+ progressbar = ProgressBar.create(title: "#{raw_articles['count']} Articles", starting_at: 1, format: '%a |%b>>%i| %p%% %t', total: raw_articles['page_count'])
16
+ articles = raw_articles['articles']
17
+
18
+ (raw_articles['page_count'] - 1).times do |page|
19
+ progressbar.increment
20
+ articles += HTTParty.get("#{@articles_uri}?page=#{page + 2}", basic_auth)['articles']
21
+ end
22
+ end
23
+ articles
24
+ end
25
+
26
+ def sections
27
+ @sections ||= begin
28
+ progressbar = ProgressBar.create(title: "#{raw_sections['count']} Sections", starting_at: 1, format: '%a |%b>>%i| %p%% %t', total: raw_sections['page_count'])
29
+ sections = raw_sections['sections']
30
+
31
+ (raw_sections['page_count'] - 1).times do |page|
32
+ progressbar.increment
33
+ sections += HTTParty.get("#{@sections_uri}?page=#{page + 2}", basic_auth)['sections']
34
+ end
35
+ end
36
+ sections
37
+ end
38
+
39
+ def categories
40
+ @categories ||= begin
41
+ progressbar = ProgressBar.create(title: "#{raw_categories['count']} Categories", starting_at: 1, format: '%a |%b>>%i| %p%% %t', total: raw_categories['page_count'])
42
+ categories = raw_categories['categories']
43
+
44
+ (raw_categories['page_count'] - 1).times do |page|
45
+ progressbar.increment
46
+ categories += HTTParty.get("#{@categories_uri}?page=#{page + 2}", basic_auth)['categories']
47
+ end
48
+ end
49
+ categories
50
+ end
51
+
52
+ def raw_articles
53
+ @raw_articles ||= HTTParty.get(@articles_uri, basic_auth)
54
+ end
55
+
56
+ def raw_sections
57
+ @raw_sections ||= HTTParty.get(@sections_uri, basic_auth)
58
+ end
59
+
60
+ def raw_categories
61
+ @raw_sections ||= HTTParty.get(@categories_uri, basic_auth)
62
+ end
63
+
64
+ def export_columns
65
+ %w(id category section title word_count draft promoted outdated html_url created_at updated_at)
66
+ end
67
+
68
+ def basic_auth
69
+ {
70
+ basic_auth: {
71
+ username: @username,
72
+ password: @password
73
+ }
74
+ }
75
+ end
76
+
77
+ def check_auth
78
+ response = HTTParty.get(@sections_uri, basic_auth)
79
+ unless response.code == 200
80
+ puts "Error #{response.code}: #{response.message}"
81
+ abort
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,172 @@
1
+ require 'nokogiri'
2
+ require 'active_support/all'
3
+ require 'httparty'
4
+ require 'ruby-progressbar'
5
+ require 'csv'
6
+ require 'pry'
7
+ require 'optparse'
8
+ require_relative 'metrics'
9
+ require_relative 'ticket'
10
+
11
+ class ZendeskTicket
12
+
13
+ attr_accessor :root_uri, :start_time, :tickets_uri, :domain, :username, :password
14
+
15
+ def initialize(username, password, domain)
16
+ @root_uri = "https://#{domain}.zendesk.com/api/v2/"
17
+ @start_time = Time.new('2016-01-01').to_i
18
+ @tickets_uri = @root_uri + "incremental/tickets.json?start_time=#{@start_time}"
19
+ @username, @password, @domain = username, password, domain
20
+ end
21
+
22
+ def run
23
+
24
+ puts 'Checking authentication...'
25
+ check_auth
26
+ puts 'Authentication successful!'
27
+ puts
28
+ puts 'Envision Zendesk Tickets'
29
+ puts '------------------------'
30
+ puts '-> Retrieving Tickets...'
31
+
32
+ tickets_in = self.download_tickets
33
+ tickets = self.retrieve_fields(tickets_in)
34
+ metrics = Metrics.new(tickets)
35
+ metrics.graph
36
+ metrics.save
37
+ end
38
+
39
+ def download_tickets
40
+ @tickets ||= begin
41
+
42
+ first_page = HTTParty.get(@tickets_uri, basic_auth)
43
+ # puts first_page
44
+ tickets = first_page['tickets']
45
+ next_url = first_page['next_page']
46
+ count = first_page['count']
47
+ puts " Got: #{count}"
48
+
49
+ while count == 1000
50
+ next_page = HTTParty.get(next_url, basic_auth)
51
+ tickets += next_page['tickets']
52
+ next_url = next_page['next_page']
53
+ count = next_page['count']
54
+ puts " Got: #{count}"
55
+ end
56
+ end
57
+ tickets
58
+ end
59
+
60
+ def export_columns
61
+ %w(id type subject status user_priority development_priority company project
62
+ platform function satisfaction_rating created_at updated_at)
63
+ end
64
+
65
+ def metric_columns
66
+ %w(initially_assigned_at solved_at full_resolution_time_in_minutes
67
+ requester_wait_time_in_minutes reply_time_in_minutes)
68
+ end
69
+
70
+ def basic_auth
71
+ {
72
+ basic_auth: {
73
+ username: @username,
74
+ password: @password
75
+ }
76
+ }
77
+ end
78
+
79
+ def check_auth
80
+ response = HTTParty.get(@tickets_uri, basic_auth)
81
+ unless response.code == 200
82
+ puts "Error #{response.code}: #{response.message}"
83
+ abort
84
+ end
85
+ end
86
+
87
+ def retrieve_fields(tickets_in)
88
+
89
+ puts ' Total tickets = ' + tickets_in.count.to_s
90
+ puts
91
+
92
+ CSV.open('all_tickets.csv', 'wb') do |csv|
93
+ csv << self.export_columns + self.metric_columns
94
+ end
95
+
96
+ tickets = Array.new
97
+
98
+ print "Enter number of tickets (max of #{tickets_in.count.to_s}): "
99
+ number_of_tickets = gets.chomp.to_i
100
+ puts
101
+
102
+ progressbar = ProgressBar.create(title: "#{number_of_tickets} Tickets",
103
+ starting_at: 0, format: '%a |%b>>%i| %p%% %t', total: number_of_tickets)
104
+
105
+ tickets_in.first(number_of_tickets).each do |ticket|
106
+ CSV.open('all_tickets.csv', 'a') do |csv|
107
+ row = []
108
+ info = Hash.new
109
+ metrics_info = Hash.new
110
+ self.export_columns.each do |column|
111
+ case column
112
+ when 'type'
113
+ info['type'] = ticket['custom_fields'][0]['value']
114
+ row << info['type']
115
+ when 'user_priority'
116
+ info['user_priority'] = ticket['custom_fields'][1]['value']
117
+ row << info['user_priority']
118
+ when 'development_priority'
119
+ value = ticket['custom_fields'][2]['value']
120
+ if value
121
+ info['development_priority'] = "d#{value[-1]}" if value[-1].to_i > 0
122
+ else
123
+ info['development_priority'] = value
124
+ end
125
+ row << info['development_priority']
126
+ when 'company'
127
+ info['company'] = ticket['custom_fields'][3]['value']
128
+ row << info['company']
129
+ when 'project'
130
+ info['project'] = ticket['custom_fields'][4]['value']
131
+ row << info['project']
132
+ when 'platform'
133
+ info['platform'] = ticket['custom_fields'][5]['value']
134
+ row << info['platform']
135
+ when 'function'
136
+ info['function'] = ticket['custom_fields'][6]['value']
137
+ row << info['function']
138
+ when 'satisfaction_rating'
139
+ info['satisfaction_rating'] = ticket['satisfaction_rating']['score']
140
+ row << info['satisfaction_rating']
141
+ else
142
+ info[column] = ticket[column]
143
+ row << info[column]
144
+ end
145
+ end
146
+
147
+ begin
148
+ metrics = HTTParty.get("#{@root_uri}tickets/#{ticket['id']}/metrics.json", self.basic_auth)
149
+ self.metric_columns.each do |column|
150
+ if metrics['ticket_metric']
151
+ case column
152
+ when 'solved_at', 'initially_assigned_at'
153
+ metrics_info[column] = metrics['ticket_metric'][column]
154
+ else
155
+ metrics_info[column] = metrics['ticket_metric'][column]['business']
156
+ end
157
+ row << metrics_info[column]
158
+ end
159
+ end
160
+ rescue
161
+ retry
162
+ end
163
+
164
+ this = Ticket.new(info, metrics_info)
165
+ tickets << this
166
+ csv << row
167
+ progressbar.increment
168
+ end
169
+ end
170
+ tickets
171
+ end
172
+ end