snitch_reporting 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +23 -3
- data/app/assets/javascripts/snitch_reporting/snitch_report.js +60 -0
- data/app/assets/stylesheets/snitch_reporting/_variables.scss +5 -0
- data/app/assets/stylesheets/snitch_reporting/components.scss +120 -0
- data/app/assets/stylesheets/snitch_reporting/containers.scss +6 -0
- data/app/assets/stylesheets/snitch_reporting/default.scss +18 -0
- data/app/assets/stylesheets/snitch_reporting/forms.scss +36 -0
- data/app/assets/stylesheets/snitch_reporting/navigation.scss +37 -0
- data/app/assets/stylesheets/snitch_reporting/snitch_report.scss +1 -0
- data/app/assets/stylesheets/snitch_reporting/tables.scss +127 -0
- data/app/controllers/snitch_reporting/application_controller.rb +4 -0
- data/app/controllers/snitch_reporting/snitch_reports_controller.rb +167 -0
- data/app/helpers/snitch_reporting/params_helper.rb +38 -0
- data/app/helpers/snitch_reporting/snitch_report_helper.rb +2 -0
- data/app/models/snitch_reporting/service/json_wrapper.rb +5 -0
- data/app/models/snitch_reporting/snitch_comment.rb +7 -0
- data/app/models/snitch_reporting/snitch_history.rb +7 -0
- data/app/models/snitch_reporting/snitch_occurrence.rb +183 -0
- data/app/models/snitch_reporting/snitch_report.rb +301 -0
- data/app/models/snitch_reporting/snitch_tracker.rb +17 -0
- data/app/views/snitch_reporting/snitch_reports/_filters.html.erb +16 -0
- data/app/views/snitch_reporting/snitch_reports/_navigation.html.erb +0 -0
- data/app/views/snitch_reporting/snitch_reports/edit.html.erb +19 -0
- data/app/views/snitch_reporting/snitch_reports/index.html.erb +75 -0
- data/app/views/snitch_reporting/snitch_reports/show.html.erb +189 -0
- data/config/routes.rb +4 -1
- data/lib/generators/snitch_reporting/install/install_generator.rb +24 -0
- data/lib/generators/snitch_reporting/install/templates/install_snitch_reporting.rb +62 -0
- data/lib/snitch_reporting/engine.rb +3 -0
- data/lib/snitch_reporting/rack.rb +29 -0
- data/lib/snitch_reporting/version.rb +1 -1
- data/lib/snitch_reporting.rb +3 -1
- metadata +58 -3
@@ -0,0 +1,17 @@
|
|
1
|
+
# belongs_to :snitch_report
|
2
|
+
# date :date
|
3
|
+
# bigint :count
|
4
|
+
|
5
|
+
class SnitchReporting::SnitchTracker < ApplicationRecord
|
6
|
+
belongs_to :report, class_name: "SnitchReporting::SnitchReport"
|
7
|
+
|
8
|
+
def self.tracker_for_date(date=nil)
|
9
|
+
date ||= Date.today
|
10
|
+
|
11
|
+
find_or_create_by(date: date)
|
12
|
+
end
|
13
|
+
|
14
|
+
# def self.count_for_date_range(start, end)
|
15
|
+
# where(date: start..end).sum(:count)
|
16
|
+
# end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<div class="tabs-container">
|
2
|
+
<% all_path = current_params.merge(param => :all) %>
|
3
|
+
<% current_tabs = (current_params[param] || []).map(&:to_sym) %>
|
4
|
+
<%= link_to "All", all_path, class: "tab white-label-text #{'selected' if current_tabs.none?}" %>
|
5
|
+
<% available_tabs.each do |tab_label| %>
|
6
|
+
<% new_tabs = current_tabs.include?(tab_label) ? (current_tabs - [tab_label]) : (current_tabs + [tab_label]) %>
|
7
|
+
<%
|
8
|
+
new_path = if new_tabs.sort == available_tabs.sort
|
9
|
+
all_path
|
10
|
+
else
|
11
|
+
{param => new_tabs}.reverse_merge(all_path)
|
12
|
+
end
|
13
|
+
%>
|
14
|
+
<%= link_to tab_label.to_s.titleize, new_path, class: "tab white-label-text #{'selected' if current_tabs.include?(tab_label)}" %>
|
15
|
+
<% end %>
|
16
|
+
</div>
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<div class="skinny-container">
|
2
|
+
<%= form_for @bug_report do |f| %>
|
3
|
+
<%= hidden_field_tag :redirect_to_report, true %>
|
4
|
+
|
5
|
+
<div class="form-field">
|
6
|
+
<%= f.label :title %>
|
7
|
+
<%= f.text_area :title %>
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<div class="form-field">
|
11
|
+
<%= f.label :custom_details, "Notes" %>
|
12
|
+
<%= f.text_area :custom_details %>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<div class="form-field">
|
16
|
+
<%= f.submit "Submit", class: "orca-btn" %>
|
17
|
+
</div>
|
18
|
+
<% end %>
|
19
|
+
</div>
|
@@ -0,0 +1,75 @@
|
|
1
|
+
<div class="snitch-reporting">
|
2
|
+
<!-- <div class="snitch-nav">
|
3
|
+
<div class="nav-tab">
|
4
|
+
<input type="search" name="search" value="<%= params[:search] %>" placeholder="Search">
|
5
|
+
</div>
|
6
|
+
<a href="<%# %>">Status</a>
|
7
|
+
<a href="<%# %>">Assignee</a>
|
8
|
+
<a href="<%# %>">Occurred</a>
|
9
|
+
<a href="<%# %>">Ignored</a>
|
10
|
+
</div> -->
|
11
|
+
<div class="filters">
|
12
|
+
<% @filter_sets.each do |filter_name, filter_set| %>
|
13
|
+
<div class="snitch-table filter-table">
|
14
|
+
<div class="snitch-tr">
|
15
|
+
<div class="snitch-th"><%= filter_name.to_s.titleize %></div>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<% selected_filter_value = @filters[filter_name] %>
|
19
|
+
<% default_filter_value = filter_set[:default] %>
|
20
|
+
<% filter_set[:values].each do |filter_value| %>
|
21
|
+
<div class="snitch-tr">
|
22
|
+
<% url_value = filter_value == default_filter_value ? @filters[:set_filters].except(filter_name) : @filters[:set_filters].merge(filter_name => filter_value) %>
|
23
|
+
<%= link_to filter_value.to_s.titleize, url_value, class: "snitch-td link-cell #{'selected' if selected_filter_value == filter_value}" %>
|
24
|
+
</div>
|
25
|
+
<% end %>
|
26
|
+
</div>
|
27
|
+
<% end %>
|
28
|
+
</div>
|
29
|
+
|
30
|
+
<div class="snitch-index">
|
31
|
+
<div class="snitch-title-section">
|
32
|
+
<h1><%= Rails.application.class.parent.name %></h1>
|
33
|
+
<!-- Sort -->
|
34
|
+
</div>
|
35
|
+
<div class="snitch-center">
|
36
|
+
<%= paginate @reports %>
|
37
|
+
<br>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<div class="snitch-errors">
|
41
|
+
<div class="snitch-table bordered padded">
|
42
|
+
<div class="snitch-thead">
|
43
|
+
<div class="snitch-tr">
|
44
|
+
<div class="snitch-th">Error</div>
|
45
|
+
<div class="snitch-th" style="width: 150px;">Last</div>
|
46
|
+
<div class="snitch-th" style="width: 1px;">Times</div>
|
47
|
+
<div class="snitch-th" style="width: 1px;">Assigned</div>
|
48
|
+
<div class="snitch-th" style="width: 1px;">Status</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
<div class="snitch-tbody">
|
52
|
+
<% @reports.each do |report| %>
|
53
|
+
<div class="snitch-tr">
|
54
|
+
<%= link_to report, class: "snitch-td link-cell" do %>
|
55
|
+
<div class="report-title-wrapper">
|
56
|
+
<span class="report-title"><%= report.error %></span>
|
57
|
+
<% if report.klass.present? && report.action.present? %>
|
58
|
+
in <span class="report-location"><%= report.klass %>#<%= report.action %></span>
|
59
|
+
<% end %>
|
60
|
+
</div>
|
61
|
+
<small class="report-message"><%= truncate(report.message, length: 100) %></small>
|
62
|
+
<% end %>
|
63
|
+
<div class="snitch-td"><%= report.last_occurrence_at %></div>
|
64
|
+
<div class="snitch-td"><%= number_with_delimiter(report.occurrence_count) %></div>
|
65
|
+
<div class="snitch-td"><%= report.assigned_to.try(:name).presence || "-" %></div>
|
66
|
+
<div class="snitch-td">
|
67
|
+
<%= content_tag :input, "", type: :checkbox, name: :resolved, class: "snitch-resolution-switch", checked: report.resolved?, data: { mark_resolution_url: snitch_report_url(report) } %>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
<% end %>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
</div>
|
@@ -0,0 +1,189 @@
|
|
1
|
+
<div class="snitch-reporting">
|
2
|
+
<div class="snitch-nav">
|
3
|
+
<a href="#summary">Summary</a>
|
4
|
+
<a href="#comments">Comments</a>
|
5
|
+
<a href="#backtrace">Backtrace</a>
|
6
|
+
<a href="#context">Context</a>
|
7
|
+
<a href="#params">Params</a>
|
8
|
+
<a href="#environment">Environment</a>
|
9
|
+
<a href="#history">History</a>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div class="snitch-breadcrumbs">
|
13
|
+
<%= link_to snitch_reports_path, class: "snitch-btn primary" do %>
|
14
|
+
←
|
15
|
+
<%= Rails.application.class.parent.name %>
|
16
|
+
<% end %>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<% if @report.ignored? %>
|
20
|
+
<div class="snitch-banner">
|
21
|
+
<div>Report is ignored- you will not be notified for future occurrences of this error.</div>
|
22
|
+
</div>
|
23
|
+
<% end %>
|
24
|
+
|
25
|
+
<div class="snitch-title-section">
|
26
|
+
<div class="snitch-center">
|
27
|
+
<%= link_to "« First", snitch_report_path(@report, occurrence: @paged_ids[:first]) if @paged_ids[:first].present? %>
|
28
|
+
<%= link_to "‹ Previous", snitch_report_path(@report, occurrence: @paged_ids[:prev]) if @paged_ids[:prev].present? %>
|
29
|
+
<%= link_to "Next ›", snitch_report_path(@report, occurrence: @paged_ids[:next]) if @paged_ids[:next].present? %>
|
30
|
+
<%= link_to "Last »", snitch_report_path(@report, occurrence: @paged_ids[:last]) if @paged_ids[:last].present? %>
|
31
|
+
</div>
|
32
|
+
<!-- <%= '-' if @paged_ids.present? %>
|
33
|
+
<todo>(a few seconds ago)</todo> -->
|
34
|
+
<div class="flex-row">
|
35
|
+
<div class="flex-cell">
|
36
|
+
<h2><%= @report.error %></h2>
|
37
|
+
<% if @report.klass.present? %>
|
38
|
+
in <%= @report.klass %>#<%= @report.action %>
|
39
|
+
<% end %>
|
40
|
+
This error has occurred <%= number_with_delimiter(@report.occurrence_count) %> <%= "time".pluralize(@report.occurrence_count) %> since <%= @report.first_occurrence_at.strftime("%m/%d/%Y @ %I:%M%P %:z") %>
|
41
|
+
</div>
|
42
|
+
<div class="flex-cell">
|
43
|
+
<%= content_tag :input, "", type: :checkbox, name: :resolved, class: "snitch-resolution-switch", checked: @report.resolved?, data: { mark_resolution_url: snitch_report_url(@report) } %>
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div id="summary" class="snitch-section">
|
49
|
+
<div class="snitch-table padded">
|
50
|
+
<div class="snitch-tr">
|
51
|
+
<div class="snitch-td">Error</div>
|
52
|
+
<div class="snitch-td">
|
53
|
+
<div class="scrollable">
|
54
|
+
<%= @report.error %>: <br>
|
55
|
+
<code><%= @report.message %></code>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
<div class="snitch-tr">
|
60
|
+
<div class="snitch-td">Occurred</div>
|
61
|
+
<div class="snitch-td"><%= @occurrence.created_at %></div>
|
62
|
+
</div>
|
63
|
+
<div class="snitch-tr">
|
64
|
+
<div class="snitch-td">Backtrace</div>
|
65
|
+
<div class="snitch-td">
|
66
|
+
<%= (@occurrence.filtered_backtrace.first || @occurrence.backtrace.first).split(":in").first %>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
<div class="snitch-tr">
|
70
|
+
<div class="snitch-td">URL</div>
|
71
|
+
<div class="snitch-td">
|
72
|
+
<% if @occurrence.http_method %>
|
73
|
+
[<%= @occurrence.http_method %>]
|
74
|
+
<% end %>
|
75
|
+
<%= @occurrence.url %>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
<div class="snitch-tr">
|
79
|
+
<div class="snitch-td">User Agent</div>
|
80
|
+
<div class="snitch-td">
|
81
|
+
<%= @occurrence.user_agent %>
|
82
|
+
</div>
|
83
|
+
</div>
|
84
|
+
<!-- <div class="snitch-tr">
|
85
|
+
<div class="snitch-td">Timeline</div>
|
86
|
+
<div class="snitch-td">
|
87
|
+
<todo>
|
88
|
+
X: Date
|
89
|
+
Y: Occurrence Count (Hover for date/count)
|
90
|
+
[Hour|Day|Week|Month]
|
91
|
+
</todo>
|
92
|
+
</div>
|
93
|
+
</div> -->
|
94
|
+
<!-- <div class="snitch-tr">
|
95
|
+
<div class="snitch-td">Tags <todo>(edit)</todo></div>
|
96
|
+
<div class="snitch-td"><%= @report.tags %></div>
|
97
|
+
</div> -->
|
98
|
+
</div>
|
99
|
+
</div>
|
100
|
+
|
101
|
+
<div class="btn-row">
|
102
|
+
<!-- <a class="snitch-btn danger" href="">Delete</a> -->
|
103
|
+
<% if @report.ignored? %>
|
104
|
+
<%= link_to "Mark as Unignored", snitch_report_path(@report, snitch_report: { ignored: false }), method: :patch, class: "snitch-btn" %>
|
105
|
+
<% else %>
|
106
|
+
<%= link_to "Mark as Ignored", snitch_report_path(@report, snitch_report: { ignored: true }), method: :patch, class: "snitch-btn" %>
|
107
|
+
<% end %>
|
108
|
+
<!-- <a class="snitch-btn" href="">Assign</a> -->
|
109
|
+
<!-- <a class="snitch-btn" href="">URL</a> -->
|
110
|
+
<!-- <a class="snitch-btn" href="">Search StackOverflow</a> -->
|
111
|
+
<!-- <a class="snitch-btn" href="">Create GH Issue</a> -->
|
112
|
+
</div>
|
113
|
+
|
114
|
+
<!-- <div id="comments" class="snitch-section">
|
115
|
+
<h3>Comments</h3>
|
116
|
+
<todo>
|
117
|
+
Text area - Markdown/editor
|
118
|
+
- list of comments
|
119
|
+
</todo>
|
120
|
+
</div> -->
|
121
|
+
|
122
|
+
<div id="params" class="snitch-section">
|
123
|
+
<h3>Params</h3>
|
124
|
+
<div class="line-trace">
|
125
|
+
<code><%= JSON.pretty_generate(@occurrence.params) %></code>
|
126
|
+
</div>
|
127
|
+
</div>
|
128
|
+
|
129
|
+
<div id="backtrace" class="snitch-section">
|
130
|
+
<h3>Application Backtrace</h3>
|
131
|
+
<% @occurrence.backtrace_data.each do |row_data| %>
|
132
|
+
<% row_data = row_data.with_indifferent_access %>
|
133
|
+
<% file_path = row_data[:file_path] %>
|
134
|
+
<% line_number = row_data[:line_number].to_i %>
|
135
|
+
|
136
|
+
<div class="line-trace">
|
137
|
+
<div class="trace-details">
|
138
|
+
<span class="trace-file"><%= file_path.split("/").last(2).join("/") %></span>
|
139
|
+
<span>→</span>
|
140
|
+
<span class="trace-line"><%= line_number %></span>
|
141
|
+
<div class="trace-full"><%= file_path %></div>
|
142
|
+
</div>
|
143
|
+
|
144
|
+
<code><% row_data[:snippet].each do |line_num, line_code| %><span class="<%= 'current-line' if line_num.to_s.to_i == line_number %>"><%= line_num %> <%= line_code %></span><br><% end %></code>
|
145
|
+
</div>
|
146
|
+
<% end %>
|
147
|
+
</div>
|
148
|
+
|
149
|
+
<div id="backtrace" class="snitch-section">
|
150
|
+
<h3>Full Backtrace</h3>
|
151
|
+
<div class="line-trace cap-height">
|
152
|
+
<code><%= @occurrence.backtrace.join("\n") %></code>
|
153
|
+
</div>
|
154
|
+
</div>
|
155
|
+
|
156
|
+
<div id="context" class="snitch-section">
|
157
|
+
<h3>Context</h3>
|
158
|
+
<div class="line-trace">
|
159
|
+
<code><%= JSON.pretty_generate @occurrence.context %></code>
|
160
|
+
</div>
|
161
|
+
</div>
|
162
|
+
|
163
|
+
<div id="environment" class="snitch-section">
|
164
|
+
<h3>Environment</h3>
|
165
|
+
<div class="line-trace">
|
166
|
+
<code><%= JSON.pretty_generate @occurrence.headers %></code>
|
167
|
+
</div>
|
168
|
+
</div>
|
169
|
+
|
170
|
+
<!-- <div id="history" class="snitch-section">
|
171
|
+
<h3>History</h3>
|
172
|
+
<div class="snitch-table">
|
173
|
+
<div class="snitch-thead">
|
174
|
+
<div class="snitch-tr">
|
175
|
+
<div class="snitch-th">Action</div>
|
176
|
+
<div class="snitch-th">User</div>
|
177
|
+
<div class="snitch-th">Timestamp</div>
|
178
|
+
</div>
|
179
|
+
</div>
|
180
|
+
<div class="snitch-tbody">
|
181
|
+
<% @report.histories.each do |history| %>
|
182
|
+
<div class="snitch-td"><%= history.text %></div>
|
183
|
+
<div class="snitch-td"><%= history.user %></div>
|
184
|
+
<div class="snitch-td"><%= history.created_at %></div>
|
185
|
+
<% end %>
|
186
|
+
</div>
|
187
|
+
</div>
|
188
|
+
</div> -->
|
189
|
+
</div>
|
data/config/routes.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "rails/generators/migration"
|
2
|
+
|
3
|
+
module SnitchReporting
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
source_root File.expand_path("../templates", __FILE__)
|
8
|
+
desc "add the migrations"
|
9
|
+
|
10
|
+
def self.next_migration_number(path)
|
11
|
+
unless @prev_migration_nr
|
12
|
+
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
13
|
+
else
|
14
|
+
@prev_migration_nr += 1
|
15
|
+
end
|
16
|
+
@prev_migration_nr.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_migrations
|
20
|
+
migration_template "install_snitch_reporting.rb", "db/migrate/install_snitch_reporting.rb"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class InstallSnitchReporting < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
create_table :snitch_reporting_snitch_reports do |t|
|
4
|
+
t.text :error
|
5
|
+
t.text :message
|
6
|
+
t.integer :log_level
|
7
|
+
t.string :klass
|
8
|
+
t.string :action
|
9
|
+
t.text :tags
|
10
|
+
|
11
|
+
t.datetime :first_occurrence_at
|
12
|
+
t.datetime :last_occurrence_at
|
13
|
+
t.bigint :occurrence_count
|
14
|
+
|
15
|
+
t.belongs_to :assigned_to
|
16
|
+
t.datetime :resolved_at
|
17
|
+
t.belongs_to :resolved_by
|
18
|
+
t.datetime :ignored_at
|
19
|
+
t.belongs_to :ignored_by
|
20
|
+
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table :snitch_reporting_snitch_occurrences do |t|
|
25
|
+
t.belongs_to :report
|
26
|
+
t.string :http_method
|
27
|
+
t.string :url
|
28
|
+
t.text :user_agent
|
29
|
+
t.text :backtrace
|
30
|
+
t.text :backtrace_data
|
31
|
+
t.text :context
|
32
|
+
t.text :params
|
33
|
+
t.text :headers
|
34
|
+
|
35
|
+
t.timestamps
|
36
|
+
end
|
37
|
+
|
38
|
+
create_table :snitch_reporting_snitch_comments do |t|
|
39
|
+
t.belongs_to :report
|
40
|
+
t.belongs_to :author
|
41
|
+
t.text :body
|
42
|
+
|
43
|
+
t.timestamps
|
44
|
+
end
|
45
|
+
|
46
|
+
create_table :snitch_reporting_snitch_histories do |t|
|
47
|
+
t.belongs_to :report
|
48
|
+
t.belongs_to :user
|
49
|
+
t.text :text
|
50
|
+
|
51
|
+
t.timestamps
|
52
|
+
end
|
53
|
+
|
54
|
+
create_table :snitch_reporting_snitch_trackers do |t|
|
55
|
+
t.belongs_to :report
|
56
|
+
t.date :date
|
57
|
+
t.bigint :count
|
58
|
+
|
59
|
+
t.timestamps
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class SnitchReporting::Rack
|
2
|
+
class SnitchException < RuntimeError; end
|
3
|
+
attr_accessor :notify_callback
|
4
|
+
|
5
|
+
def initialize(app, notify_callback=nil)
|
6
|
+
@app = app
|
7
|
+
@notify_callback = notify_callback
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
response = @app.call(env)
|
12
|
+
_, headers, = response
|
13
|
+
|
14
|
+
if headers["X-Cascade"] == "pass"
|
15
|
+
msg = "This exception means that the preceding Rack middleware set the 'X-Cascade' header to 'pass' -- in " \
|
16
|
+
"Rails, this often means that the route was not found (404 error)."
|
17
|
+
raise SnitchException, msg
|
18
|
+
end
|
19
|
+
|
20
|
+
response
|
21
|
+
rescue Exception => exception
|
22
|
+
occurrence = ::SnitchReporting::SnitchReport.fatal(exception, env: env)
|
23
|
+
notify_callback.call(occurrence) if occurrence.notify?
|
24
|
+
|
25
|
+
raise exception unless exception.is_a?(SnitchException)
|
26
|
+
|
27
|
+
response
|
28
|
+
end
|
29
|
+
end
|