bugwatch-ruby 0.4.0 → 0.6.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 +60 -0
- data/lib/bugwatch/configuration.rb +9 -1
- data/lib/bugwatch/db_query_buffer.rb +61 -0
- data/lib/bugwatch/db_query_sender.rb +66 -0
- data/lib/bugwatch/db_tracker.rb +94 -0
- data/lib/bugwatch/feedback_helper.rb +86 -0
- data/lib/bugwatch/feedback_sender.rb +58 -0
- data/lib/bugwatch/middleware.rb +18 -0
- data/lib/bugwatch/railtie.rb +12 -0
- data/lib/bugwatch/version.rb +1 -1
- data/lib/bugwatch.rb +22 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4cb56c42da0cab5b2e855deca671231a744b1624b5fcbd6dbef365f30bc9d08
|
|
4
|
+
data.tar.gz: 92889ce7b6cec458ee2c75c15759756acbaa32981051704c9a3534a42e58fe1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 479c753810d0c158b339281f7f7c5c083134bfe4321cfe7739a28d24d14fc3c82fb26e17330de1bc92f13e41e66e2726c4fe0b42b2d5110301c3410e566fc564
|
|
7
|
+
data.tar.gz: 6b2a55bf2cdd3a01fd82bd96a51c75f50fbbc48a373b8102c7c6a301ab748c75f7434084402d56cbf3b2009a838648205fa95c4706eb07655a423ef290f35d49
|
data/README.md
CHANGED
|
@@ -121,6 +121,66 @@ release: bundle exec rails db:migrate && bundle exec rails bugwatch:track_deploy
|
|
|
121
121
|
|
|
122
122
|
The build will write the commit SHA to `REVISION`, and the release phase will read it back and report the deploy to BugWatch.
|
|
123
123
|
|
|
124
|
+
## User Feedback Widget
|
|
125
|
+
|
|
126
|
+
Drop a feedback form into any view so users can report issues directly from your app. The form submits to BugWatch's feedback API — no controller code needed.
|
|
127
|
+
|
|
128
|
+
```erb
|
|
129
|
+
<%%= bugwatch_feedback_widget %>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The helper renders a plain, unstyled HTML form with CSS classes you can target:
|
|
133
|
+
|
|
134
|
+
| Class | Element |
|
|
135
|
+
|-------|---------|
|
|
136
|
+
| `.bugwatch-feedback-form` | The `<form>` wrapper |
|
|
137
|
+
| `.bugwatch-feedback-field` | Each field's `<div>` |
|
|
138
|
+
| `.bugwatch-feedback-label` | `<label>` elements |
|
|
139
|
+
| `.bugwatch-feedback-input` | Text/email `<input>` |
|
|
140
|
+
| `.bugwatch-feedback-textarea` | The message `<textarea>` |
|
|
141
|
+
| `.bugwatch-feedback-submit` | Submit `<button>` |
|
|
142
|
+
| `.bugwatch-feedback-success` | Success message (hidden by default) |
|
|
143
|
+
| `.bugwatch-feedback-error` | Error message (hidden by default) |
|
|
144
|
+
|
|
145
|
+
### Options
|
|
146
|
+
|
|
147
|
+
```erb
|
|
148
|
+
<%%= bugwatch_feedback_widget(
|
|
149
|
+
user_email: current_user&.email,
|
|
150
|
+
user_name: current_user&.name,
|
|
151
|
+
placeholder: "Describe the issue...",
|
|
152
|
+
submit_text: "Report Issue",
|
|
153
|
+
success_message: "We got it — thanks!",
|
|
154
|
+
issue_id: @issue_id, # optional: link to a specific BugWatch issue
|
|
155
|
+
metadata: { page: "checkout" }
|
|
156
|
+
) %>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Styling example (Tailwind)
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
.bugwatch-feedback-form { @apply space-y-4 max-w-md; }
|
|
163
|
+
.bugwatch-feedback-label { @apply block text-sm font-medium text-gray-700; }
|
|
164
|
+
.bugwatch-feedback-input,
|
|
165
|
+
.bugwatch-feedback-textarea { @apply w-full border rounded-lg px-3 py-2 text-sm; }
|
|
166
|
+
.bugwatch-feedback-submit { @apply bg-blue-600 text-white px-4 py-2 rounded-lg text-sm; }
|
|
167
|
+
.bugwatch-feedback-success { @apply text-green-600 text-sm; }
|
|
168
|
+
.bugwatch-feedback-error { @apply text-red-600 text-sm; }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Server-side feedback
|
|
172
|
+
|
|
173
|
+
You can also send feedback from Ruby (e.g. from a controller that handles your own form):
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
Bugwatch.send_feedback(
|
|
177
|
+
params[:message],
|
|
178
|
+
email: current_user.email,
|
|
179
|
+
name: current_user.name,
|
|
180
|
+
url: request.original_url
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
124
184
|
## How it works
|
|
125
185
|
|
|
126
186
|
1. `Bugwatch::Middleware` wraps your entire Rack stack.
|
|
@@ -9,7 +9,11 @@ module Bugwatch
|
|
|
9
9
|
:logger,
|
|
10
10
|
:enable_performance_tracking,
|
|
11
11
|
:sample_rate,
|
|
12
|
-
:ignore_request_paths
|
|
12
|
+
:ignore_request_paths,
|
|
13
|
+
:enable_db_tracking,
|
|
14
|
+
:db_sample_rate,
|
|
15
|
+
:db_query_threshold_ms,
|
|
16
|
+
:max_queries_per_request
|
|
13
17
|
|
|
14
18
|
def initialize
|
|
15
19
|
@endpoint = nil
|
|
@@ -20,6 +24,10 @@ module Bugwatch
|
|
|
20
24
|
@enable_performance_tracking = true
|
|
21
25
|
@sample_rate = 1.0
|
|
22
26
|
@ignore_request_paths = []
|
|
27
|
+
@enable_db_tracking = true
|
|
28
|
+
@db_sample_rate = 1.0
|
|
29
|
+
@db_query_threshold_ms = 0.0
|
|
30
|
+
@max_queries_per_request = 200
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
def notify_for_release_stage?
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Bugwatch
|
|
2
|
+
class DbQueryBuffer
|
|
3
|
+
BATCH_SIZE = 50
|
|
4
|
+
FLUSH_INTERVAL = 15 # seconds
|
|
5
|
+
|
|
6
|
+
def initialize(config: Bugwatch.configuration)
|
|
7
|
+
@config = config
|
|
8
|
+
@sender = DbQuerySender.new(config: config)
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@buffer = []
|
|
11
|
+
start_flusher
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def push(payload)
|
|
15
|
+
should_flush = false
|
|
16
|
+
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
@buffer << payload
|
|
19
|
+
should_flush = @buffer.size >= BATCH_SIZE
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
flush if should_flush
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def flush
|
|
26
|
+
batch = @mutex.synchronize do
|
|
27
|
+
items = @buffer
|
|
28
|
+
@buffer = []
|
|
29
|
+
items
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@sender.send_batch(batch) unless batch.empty?
|
|
33
|
+
rescue StandardError
|
|
34
|
+
# Never let flushing break the app
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def shutdown
|
|
38
|
+
stop_flusher
|
|
39
|
+
flush
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def start_flusher
|
|
45
|
+
@flusher = Thread.new do
|
|
46
|
+
loop do
|
|
47
|
+
sleep FLUSH_INTERVAL
|
|
48
|
+
flush
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError
|
|
51
|
+
# Silently handle thread errors
|
|
52
|
+
end
|
|
53
|
+
@flusher.abort_on_exception = false
|
|
54
|
+
@flusher.daemon = true if @flusher.respond_to?(:daemon=)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stop_flusher
|
|
58
|
+
@flusher&.kill
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bugwatch
|
|
6
|
+
class DbQuerySender
|
|
7
|
+
TIMEOUT = 3
|
|
8
|
+
|
|
9
|
+
def initialize(config: Bugwatch.configuration)
|
|
10
|
+
@config = config
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def send_batch(grouped_payloads)
|
|
14
|
+
return if grouped_payloads.empty?
|
|
15
|
+
return unless @config.api_key
|
|
16
|
+
return unless @config.endpoint
|
|
17
|
+
|
|
18
|
+
Thread.new do
|
|
19
|
+
post_batch(grouped_payloads)
|
|
20
|
+
rescue StandardError
|
|
21
|
+
# Fire-and-forget: swallow all errors
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def post_batch(grouped_payloads)
|
|
28
|
+
records = grouped_payloads.flat_map do |group|
|
|
29
|
+
group[:queries].map do |q|
|
|
30
|
+
{
|
|
31
|
+
transaction_name: group[:transaction_name],
|
|
32
|
+
environment: group[:environment],
|
|
33
|
+
occurred_at: group[:occurred_at],
|
|
34
|
+
sql: q[:sql],
|
|
35
|
+
raw_sql: q[:raw_sql],
|
|
36
|
+
duration_ms: q[:duration_ms],
|
|
37
|
+
name: q[:name],
|
|
38
|
+
operation: q[:operation],
|
|
39
|
+
caller_location: q[:caller_location]
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return if records.empty?
|
|
45
|
+
|
|
46
|
+
uri = URI.parse("#{@config.endpoint.chomp("/")}/api/v1/db_queries/batch")
|
|
47
|
+
|
|
48
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
49
|
+
http.use_ssl = uri.scheme == "https"
|
|
50
|
+
http.open_timeout = TIMEOUT
|
|
51
|
+
http.read_timeout = TIMEOUT
|
|
52
|
+
http.write_timeout = TIMEOUT
|
|
53
|
+
|
|
54
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
55
|
+
request["Content-Type"] = "application/json"
|
|
56
|
+
request["X-Api-Key"] = @config.api_key
|
|
57
|
+
request["X-BugWatch-Ruby"] = Bugwatch::VERSION
|
|
58
|
+
|
|
59
|
+
request.body = JSON.generate({ queries: records })
|
|
60
|
+
|
|
61
|
+
http.request(request)
|
|
62
|
+
rescue StandardError
|
|
63
|
+
# Silently discard network errors
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require "active_support/notifications"
|
|
2
|
+
|
|
3
|
+
module Bugwatch
|
|
4
|
+
module DbTracker
|
|
5
|
+
THREAD_KEY = :bugwatch_db_tracker
|
|
6
|
+
IGNORED_NAMES = %w[SCHEMA EXPLAIN].freeze
|
|
7
|
+
IGNORED_SQL_PATTERNS = /\A\s*(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE SAVEPOINT)/i
|
|
8
|
+
|
|
9
|
+
CALLER_FILTER = %r{/(active_record|bugwatch|ruby/gems)/}
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def subscribe!
|
|
14
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
15
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
16
|
+
handle_event(event)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def start_request(collecting:)
|
|
21
|
+
Thread.current[THREAD_KEY] = {
|
|
22
|
+
queries: [],
|
|
23
|
+
total_db_ms: 0.0,
|
|
24
|
+
collecting: collecting
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def finish_request
|
|
29
|
+
state = Thread.current[THREAD_KEY]
|
|
30
|
+
Thread.current[THREAD_KEY] = nil
|
|
31
|
+
state
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear
|
|
35
|
+
Thread.current[THREAD_KEY] = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_event(event)
|
|
39
|
+
state = Thread.current[THREAD_KEY]
|
|
40
|
+
return unless state
|
|
41
|
+
|
|
42
|
+
name = event.payload[:name].to_s
|
|
43
|
+
sql = event.payload[:sql].to_s
|
|
44
|
+
return if IGNORED_NAMES.include?(name)
|
|
45
|
+
return if sql.match?(IGNORED_SQL_PATTERNS)
|
|
46
|
+
|
|
47
|
+
duration_ms = event.duration
|
|
48
|
+
|
|
49
|
+
state[:total_db_ms] += duration_ms
|
|
50
|
+
|
|
51
|
+
return unless state[:collecting]
|
|
52
|
+
|
|
53
|
+
config = Bugwatch.configuration
|
|
54
|
+
return if duration_ms < config.db_query_threshold_ms
|
|
55
|
+
return if state[:queries].size >= config.max_queries_per_request
|
|
56
|
+
|
|
57
|
+
state[:queries] << {
|
|
58
|
+
sql: sanitize_sql(sql),
|
|
59
|
+
raw_sql: sql,
|
|
60
|
+
duration_ms: duration_ms.round(2),
|
|
61
|
+
name: name.presence,
|
|
62
|
+
operation: extract_operation(sql),
|
|
63
|
+
caller_location: extract_caller
|
|
64
|
+
}
|
|
65
|
+
rescue StandardError
|
|
66
|
+
# Never let tracking break the app
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def sanitize_sql(sql)
|
|
70
|
+
s = sql.dup
|
|
71
|
+
s.gsub!(/'(?:[^'\\]|\\.)*'/, "?")
|
|
72
|
+
s.gsub!(/"(?:[^"\\]|\\.)*"/, "?") unless s.include?(".")
|
|
73
|
+
s.gsub!(/\b\d+(\.\d+)?\b/, "?")
|
|
74
|
+
s.gsub!(/\b(TRUE|FALSE|NULL)\b/i, "?")
|
|
75
|
+
s.gsub!(/IN\s*\(\s*\?(?:\s*,\s*\?)*\s*\)/i, "IN (?)")
|
|
76
|
+
s
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def extract_operation(sql)
|
|
80
|
+
sql.strip.split(/\s/, 2).first&.upcase
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def extract_caller
|
|
84
|
+
caller_locations(4, 30)&.each do |loc|
|
|
85
|
+
path = loc.path.to_s
|
|
86
|
+
next if path.match?(CALLER_FILTER)
|
|
87
|
+
return "#{loc.path}:#{loc.lineno}"
|
|
88
|
+
end
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private_class_method :handle_event, :sanitize_sql, :extract_operation, :extract_caller
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Bugwatch
|
|
2
|
+
module FeedbackHelper
|
|
3
|
+
# Renders a minimal, unstyled feedback widget form.
|
|
4
|
+
#
|
|
5
|
+
# The form submits via fetch to the BugWatch feedback API.
|
|
6
|
+
# All markup uses "bugwatch-feedback-*" CSS classes so you
|
|
7
|
+
# can style it however you want without collisions.
|
|
8
|
+
#
|
|
9
|
+
# Options:
|
|
10
|
+
# :user_email - Pre-fill email (e.g. current_user.email)
|
|
11
|
+
# :user_name - Pre-fill name (e.g. current_user.name)
|
|
12
|
+
# :issue_id - Link feedback to a specific BugWatch issue
|
|
13
|
+
# :metadata - Hash of extra data to attach
|
|
14
|
+
# :placeholder - Textarea placeholder text
|
|
15
|
+
# :submit_text - Submit button label (default: "Send Feedback")
|
|
16
|
+
# :success_message - Message shown after successful submit
|
|
17
|
+
#
|
|
18
|
+
def bugwatch_feedback_widget(user_email: nil, user_name: nil, issue_id: nil, metadata: {}, placeholder: "What happened? Tell us about the issue...", submit_text: "Send Feedback", success_message: "Thanks for your feedback!")
|
|
19
|
+
config = Bugwatch.configuration
|
|
20
|
+
endpoint = "#{config.endpoint&.chomp("/")}/api/v1/feedback"
|
|
21
|
+
api_key = config.api_key
|
|
22
|
+
widget_id = "bugwatch-feedback-#{SecureRandom.hex(4)}"
|
|
23
|
+
|
|
24
|
+
html = <<~HTML
|
|
25
|
+
<form id="#{widget_id}" class="bugwatch-feedback-form" data-bugwatch-feedback>
|
|
26
|
+
<div class="bugwatch-feedback-field">
|
|
27
|
+
<label for="#{widget_id}-name" class="bugwatch-feedback-label">Name</label>
|
|
28
|
+
<input type="text" id="#{widget_id}-name" name="name" value="#{ERB::Util.html_escape(user_name.to_s)}" class="bugwatch-feedback-input" />
|
|
29
|
+
</div>
|
|
30
|
+
<div class="bugwatch-feedback-field">
|
|
31
|
+
<label for="#{widget_id}-email" class="bugwatch-feedback-label">Email</label>
|
|
32
|
+
<input type="email" id="#{widget_id}-email" name="email" value="#{ERB::Util.html_escape(user_email.to_s)}" class="bugwatch-feedback-input" />
|
|
33
|
+
</div>
|
|
34
|
+
<div class="bugwatch-feedback-field">
|
|
35
|
+
<label for="#{widget_id}-message" class="bugwatch-feedback-label">Message</label>
|
|
36
|
+
<textarea id="#{widget_id}-message" name="message" required class="bugwatch-feedback-textarea" placeholder="#{ERB::Util.html_escape(placeholder)}"></textarea>
|
|
37
|
+
</div>
|
|
38
|
+
<button type="submit" class="bugwatch-feedback-submit">#{ERB::Util.html_escape(submit_text)}</button>
|
|
39
|
+
<div class="bugwatch-feedback-success" style="display:none">#{ERB::Util.html_escape(success_message)}</div>
|
|
40
|
+
<div class="bugwatch-feedback-error" style="display:none"></div>
|
|
41
|
+
</form>
|
|
42
|
+
<script>
|
|
43
|
+
(function() {
|
|
44
|
+
var form = document.getElementById("#{widget_id}");
|
|
45
|
+
form.addEventListener("submit", function(e) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
var btn = form.querySelector(".bugwatch-feedback-submit");
|
|
48
|
+
var success = form.querySelector(".bugwatch-feedback-success");
|
|
49
|
+
var error = form.querySelector(".bugwatch-feedback-error");
|
|
50
|
+
success.style.display = "none";
|
|
51
|
+
error.style.display = "none";
|
|
52
|
+
btn.disabled = true;
|
|
53
|
+
|
|
54
|
+
var body = {
|
|
55
|
+
api_key: #{api_key.to_json},
|
|
56
|
+
message: form.querySelector("[name=message]").value,
|
|
57
|
+
name: form.querySelector("[name=name]").value || undefined,
|
|
58
|
+
email: form.querySelector("[name=email]").value || undefined,
|
|
59
|
+
url: window.location.href
|
|
60
|
+
};
|
|
61
|
+
#{issue_id ? "body.issue_id = #{issue_id.to_json};" : ""}
|
|
62
|
+
#{metadata.any? ? "body.metadata = #{metadata.to_json};" : ""}
|
|
63
|
+
|
|
64
|
+
fetch(#{endpoint.to_json}, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify(body)
|
|
68
|
+
}).then(function(r) {
|
|
69
|
+
if (!r.ok) throw new Error("Request failed");
|
|
70
|
+
form.querySelector("[name=message]").value = "";
|
|
71
|
+
success.style.display = "";
|
|
72
|
+
btn.disabled = false;
|
|
73
|
+
}).catch(function(err) {
|
|
74
|
+
error.textContent = "Something went wrong. Please try again.";
|
|
75
|
+
error.style.display = "";
|
|
76
|
+
btn.disabled = false;
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
})();
|
|
80
|
+
</script>
|
|
81
|
+
HTML
|
|
82
|
+
|
|
83
|
+
html.html_safe
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bugwatch
|
|
6
|
+
class FeedbackSender
|
|
7
|
+
TIMEOUT = 3
|
|
8
|
+
|
|
9
|
+
def initialize(message:, email: nil, name: nil, url: nil, issue_id: nil, metadata: {}, config: Bugwatch.configuration)
|
|
10
|
+
@message = message
|
|
11
|
+
@email = email
|
|
12
|
+
@name = name
|
|
13
|
+
@url = url
|
|
14
|
+
@issue_id = issue_id
|
|
15
|
+
@metadata = metadata
|
|
16
|
+
@config = config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def deliver
|
|
20
|
+
return unless @config.api_key
|
|
21
|
+
return unless @config.endpoint
|
|
22
|
+
|
|
23
|
+
Thread.new do
|
|
24
|
+
post_feedback
|
|
25
|
+
rescue StandardError
|
|
26
|
+
# Fire-and-forget: never affect the host app
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def post_feedback
|
|
33
|
+
uri = URI.parse("#{@config.endpoint.chomp("/")}/api/v1/feedback")
|
|
34
|
+
|
|
35
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
36
|
+
http.use_ssl = uri.scheme == "https"
|
|
37
|
+
http.open_timeout = TIMEOUT
|
|
38
|
+
http.read_timeout = TIMEOUT
|
|
39
|
+
http.write_timeout = TIMEOUT
|
|
40
|
+
|
|
41
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
42
|
+
request["Content-Type"] = "application/json"
|
|
43
|
+
request["X-Api-Key"] = @config.api_key
|
|
44
|
+
request["X-BugWatch-Ruby"] = Bugwatch::VERSION
|
|
45
|
+
|
|
46
|
+
payload = { message: @message }
|
|
47
|
+
payload[:email] = @email if @email
|
|
48
|
+
payload[:name] = @name if @name
|
|
49
|
+
payload[:url] = @url if @url
|
|
50
|
+
payload[:issue_id] = @issue_id if @issue_id
|
|
51
|
+
payload[:metadata] = @metadata if @metadata.any?
|
|
52
|
+
|
|
53
|
+
request.body = JSON.generate(payload)
|
|
54
|
+
|
|
55
|
+
http.request(request)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/bugwatch/middleware.rb
CHANGED
|
@@ -9,6 +9,10 @@ module Bugwatch
|
|
|
9
9
|
BreadcrumbCollector.clear
|
|
10
10
|
UserContext.clear
|
|
11
11
|
|
|
12
|
+
config = Bugwatch.configuration
|
|
13
|
+
collecting = config.enable_db_tracking && (rand < config.db_sample_rate)
|
|
14
|
+
DbTracker.start_request(collecting: collecting)
|
|
15
|
+
|
|
12
16
|
begin
|
|
13
17
|
status, headers, body = @app.call(env)
|
|
14
18
|
record_transaction(env, status, start)
|
|
@@ -24,6 +28,8 @@ module Bugwatch
|
|
|
24
28
|
|
|
25
29
|
record_transaction(env, 500, start)
|
|
26
30
|
raise
|
|
31
|
+
ensure
|
|
32
|
+
DbTracker.clear
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
|
|
@@ -35,6 +41,7 @@ module Bugwatch
|
|
|
35
41
|
return unless config.track_request?(req.path)
|
|
36
42
|
|
|
37
43
|
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
44
|
+
db_result = DbTracker.finish_request
|
|
38
45
|
|
|
39
46
|
payload = {
|
|
40
47
|
name: "#{req.request_method} #{req.path}",
|
|
@@ -46,7 +53,18 @@ module Bugwatch
|
|
|
46
53
|
occurred_at: Time.now.utc.iso8601
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
payload[:db_duration_ms] = db_result[:total_db_ms].round(1) if db_result
|
|
57
|
+
|
|
49
58
|
Bugwatch.transaction_buffer.push(payload)
|
|
59
|
+
|
|
60
|
+
if db_result && db_result[:queries].any?
|
|
61
|
+
Bugwatch.db_query_buffer.push({
|
|
62
|
+
transaction_name: payload[:name],
|
|
63
|
+
environment: config.release_stage,
|
|
64
|
+
occurred_at: payload[:occurred_at],
|
|
65
|
+
queries: db_result[:queries]
|
|
66
|
+
})
|
|
67
|
+
end
|
|
50
68
|
rescue StandardError
|
|
51
69
|
# Never let tracking break the app
|
|
52
70
|
end
|
data/lib/bugwatch/railtie.rb
CHANGED
|
@@ -19,6 +19,18 @@ module Bugwatch
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
initializer "bugwatch.view_helpers" do
|
|
23
|
+
ActiveSupport.on_load(:action_view) do
|
|
24
|
+
include Bugwatch::FeedbackHelper
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
initializer "bugwatch.db_tracking" do
|
|
29
|
+
ActiveSupport.on_load(:active_record) do
|
|
30
|
+
Bugwatch::DbTracker.subscribe! if Bugwatch.configuration.enable_db_tracking
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
22
34
|
initializer "bugwatch.error_subscriber" do
|
|
23
35
|
if defined?(Rails.error) && Rails.error.respond_to?(:subscribe)
|
|
24
36
|
Rails.error.subscribe(Bugwatch::ErrorSubscriber.new)
|
data/lib/bugwatch/version.rb
CHANGED
data/lib/bugwatch.rb
CHANGED
|
@@ -11,6 +11,11 @@ require_relative "bugwatch/report_builder"
|
|
|
11
11
|
require_relative "bugwatch/notification"
|
|
12
12
|
require_relative "bugwatch/transaction_sender"
|
|
13
13
|
require_relative "bugwatch/transaction_buffer"
|
|
14
|
+
require_relative "bugwatch/db_tracker"
|
|
15
|
+
require_relative "bugwatch/db_query_sender"
|
|
16
|
+
require_relative "bugwatch/db_query_buffer"
|
|
17
|
+
require_relative "bugwatch/feedback_sender"
|
|
18
|
+
require_relative "bugwatch/feedback_helper"
|
|
14
19
|
require_relative "bugwatch/middleware"
|
|
15
20
|
require_relative "bugwatch/active_job_handler"
|
|
16
21
|
require_relative "bugwatch/error_subscriber"
|
|
@@ -43,6 +48,15 @@ module Bugwatch
|
|
|
43
48
|
Notification.new(payload).deliver
|
|
44
49
|
end
|
|
45
50
|
|
|
51
|
+
def send_feedback(message, email: nil, name: nil, url: nil, issue_id: nil, metadata: {})
|
|
52
|
+
return if message.to_s.strip.empty?
|
|
53
|
+
|
|
54
|
+
FeedbackSender.new(
|
|
55
|
+
message: message, email: email, name: name,
|
|
56
|
+
url: url, issue_id: issue_id, metadata: metadata
|
|
57
|
+
).deliver
|
|
58
|
+
end
|
|
59
|
+
|
|
46
60
|
def set_user(id: nil, email: nil, name: nil, **custom)
|
|
47
61
|
UserContext.set(id: id, email: email, name: name, **custom)
|
|
48
62
|
end
|
|
@@ -109,7 +123,14 @@ module Bugwatch
|
|
|
109
123
|
def transaction_buffer
|
|
110
124
|
@transaction_buffer ||= TransactionBuffer.new(config: configuration)
|
|
111
125
|
end
|
|
126
|
+
|
|
127
|
+
def db_query_buffer
|
|
128
|
+
@db_query_buffer ||= DbQueryBuffer.new(config: configuration)
|
|
129
|
+
end
|
|
112
130
|
end
|
|
113
131
|
end
|
|
114
132
|
|
|
115
|
-
at_exit
|
|
133
|
+
at_exit do
|
|
134
|
+
Bugwatch.transaction_buffer.shutdown rescue nil
|
|
135
|
+
Bugwatch.db_query_buffer.shutdown rescue nil
|
|
136
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bugwatch-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BugWatch
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-14 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: railties
|
|
@@ -52,8 +52,13 @@ files:
|
|
|
52
52
|
- lib/bugwatch/backtrace_cleaner.rb
|
|
53
53
|
- lib/bugwatch/breadcrumb_collector.rb
|
|
54
54
|
- lib/bugwatch/configuration.rb
|
|
55
|
+
- lib/bugwatch/db_query_buffer.rb
|
|
56
|
+
- lib/bugwatch/db_query_sender.rb
|
|
57
|
+
- lib/bugwatch/db_tracker.rb
|
|
55
58
|
- lib/bugwatch/error_builder.rb
|
|
56
59
|
- lib/bugwatch/error_subscriber.rb
|
|
60
|
+
- lib/bugwatch/feedback_helper.rb
|
|
61
|
+
- lib/bugwatch/feedback_sender.rb
|
|
57
62
|
- lib/bugwatch/middleware.rb
|
|
58
63
|
- lib/bugwatch/notification.rb
|
|
59
64
|
- lib/bugwatch/railtie.rb
|