bugwatch-ruby 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d0be9627dc1693332f0a201853dbca22da0e1f5a1e98ea7253ef5749c94f245
4
- data.tar.gz: d896401132b2ba5eb708fac049711184a12a0cc0001831121e9a11bccee16ebc
3
+ metadata.gz: 21032d38b9ffee6bf1dbef68e9ce7c39b56c28b0eb2369d76bdf684b4ed25df3
4
+ data.tar.gz: 052521dd770495c7bd271313788d71e9b1d0fd8dede69f1d2fa86603bd25eccc
5
5
  SHA512:
6
- metadata.gz: c1212d03ecfe11f84acb0f6ef8a1dda3e97b3896bbd878fba89bd1f933b6275afabd2d51a7bd49a934cd25652e8641a37d070c9a034d3d72be410152c4a0aefd
7
- data.tar.gz: 79c7558fc9c17f8540e51638a0958f520faaef012cf524d0433c87d801dcc3bf4d4abdcad740d8f77d481fbe54fd4b8906c37b5395364bf26cb16e212e8f3f67
6
+ metadata.gz: 9895be8ecb3c5db57f8996b1d702f75da6658d8370a470b72b88b6b12dc5da8690461208991cd14980bb5a996b70a19854c6747e4f460b9a868a75b3da0aaae8
7
+ data.tar.gz: a51dd3a8af6502533474624b1c1352661e0a5ce047b25d5f30dd693810cce7fd7532af83b7071a6987760a6bd61cce485fc4b6700c8f2b5f9d3d5366ef9a1ee2
data/README.md CHANGED
@@ -62,6 +62,125 @@ rescue => e
62
62
  end
63
63
  ```
64
64
 
65
+ ## Deploy tracking
66
+
67
+ Track deploys so BugWatch can correlate new errors with releases.
68
+
69
+ ```ruby
70
+ Bugwatch.track_deploy(
71
+ version: "abc1234",
72
+ environment: "production", # defaults to config.release_stage
73
+ description: "Deployed abc1234",
74
+ deployed_by: "ci"
75
+ )
76
+ ```
77
+
78
+ ### Heroku
79
+
80
+ On Heroku, `SOURCE_VERSION` (the git SHA) is available during the **build** phase but not during the **release** phase. The recommended approach is to bake the SHA into a `REVISION` file at build time, then read it at release time.
81
+
82
+ **1. Create a Rake task** in `lib/tasks/bugwatch.rake`:
83
+
84
+ ```ruby
85
+ namespace :bugwatch do
86
+ task write_revision: :environment do
87
+ revision = ENV["SOURCE_VERSION"] || `git rev-parse HEAD 2>/dev/null`.strip
88
+ if revision.present?
89
+ File.write(Rails.root.join("REVISION"), revision)
90
+ puts "Bugwatch: wrote REVISION #{revision[0..6]}"
91
+ end
92
+ end
93
+
94
+ task track_deploy: :environment do
95
+ revision_file = Rails.root.join("REVISION")
96
+ version = ENV["SOURCE_VERSION"] \
97
+ || (File.read(revision_file).strip if File.exist?(revision_file)).presence \
98
+ || Time.now.utc.strftime("%Y%m%d%H%M%S")
99
+
100
+ thread = Bugwatch.track_deploy(
101
+ version: version,
102
+ description: "Deployed #{version[0..6]}",
103
+ deployed_by: ENV["BUGWATCH_DEPLOYED_BY"] || "heroku"
104
+ )
105
+ thread&.join(5)
106
+ puts "Bugwatch: tracked deploy #{version[0..6]}"
107
+ end
108
+ end
109
+
110
+ # Bake the SHA into the slug during assets:precompile
111
+ if Rake::Task.task_defined?("assets:precompile")
112
+ Rake::Task["assets:precompile"].enhance(["bugwatch:write_revision"])
113
+ end
114
+ ```
115
+
116
+ **2. Add the deploy task to your `Procfile`** release phase:
117
+
118
+ ```
119
+ release: bundle exec rails db:migrate && bundle exec rails bugwatch:track_deploy
120
+ ```
121
+
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
+
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
+
65
184
  ## How it works
66
185
 
67
186
  1. `Bugwatch::Middleware` wraps your entire Rack stack.
@@ -0,0 +1,26 @@
1
+ module Bugwatch
2
+ class ActiveJobHandler
3
+ def call(job, exception)
4
+ return if Bugwatch.configuration.ignore?(exception)
5
+ return unless Bugwatch.configuration.notify_for_release_stage?
6
+
7
+ payload = ErrorBuilder.new(exception).build
8
+ payload[:context] = {
9
+ job_class: job.class.name,
10
+ job_id: job.job_id,
11
+ queue_name: job.queue_name,
12
+ arguments: safe_arguments(job.arguments)
13
+ }
14
+
15
+ Notification.new(payload).deliver
16
+ end
17
+
18
+ private
19
+
20
+ def safe_arguments(args)
21
+ args.map { |arg| arg.is_a?(GlobalID::Identification) ? arg.to_global_id.to_s : arg }
22
+ rescue StandardError
23
+ ["[unserializable]"]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module Bugwatch
2
+ class ErrorSubscriber
3
+ def report(error, handled:, severity:, context: {}, source: nil)
4
+ return if Bugwatch.configuration.ignore?(error)
5
+ return unless Bugwatch.configuration.notify_for_release_stage?
6
+
7
+ payload = ErrorBuilder.new(error).build
8
+ payload[:context] = context.merge(
9
+ handled: handled,
10
+ severity: severity,
11
+ source: source
12
+ ).compact
13
+
14
+ Notification.new(payload).deliver
15
+ end
16
+ end
17
+ 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
@@ -9,6 +9,27 @@ module Bugwatch
9
9
  include Bugwatch::ControllerMethods
10
10
  end
11
11
  end
12
+
13
+ initializer "bugwatch.active_job" do
14
+ ActiveSupport.on_load(:active_job) do
15
+ rescue_from(Exception) do |exception|
16
+ Bugwatch::ActiveJobHandler.new.call(self, exception)
17
+ raise exception
18
+ end
19
+ end
20
+ end
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.error_subscriber" do
29
+ if defined?(Rails.error) && Rails.error.respond_to?(:subscribe)
30
+ Rails.error.subscribe(Bugwatch::ErrorSubscriber.new)
31
+ end
32
+ end
12
33
  end
13
34
 
14
35
  module ControllerMethods
@@ -1,3 +1,3 @@
1
1
  module Bugwatch
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/bugwatch.rb CHANGED
@@ -11,7 +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/feedback_sender"
15
+ require_relative "bugwatch/feedback_helper"
14
16
  require_relative "bugwatch/middleware"
17
+ require_relative "bugwatch/active_job_handler"
18
+ require_relative "bugwatch/error_subscriber"
15
19
  require_relative "bugwatch/railtie" if defined?(Rails::Railtie)
16
20
 
17
21
  module Bugwatch
@@ -41,6 +45,15 @@ module Bugwatch
41
45
  Notification.new(payload).deliver
42
46
  end
43
47
 
48
+ def send_feedback(message, email: nil, name: nil, url: nil, issue_id: nil, metadata: {})
49
+ return if message.to_s.strip.empty?
50
+
51
+ FeedbackSender.new(
52
+ message: message, email: email, name: name,
53
+ url: url, issue_id: issue_id, metadata: metadata
54
+ ).deliver
55
+ end
56
+
44
57
  def set_user(id: nil, email: nil, name: nil, **custom)
45
58
  UserContext.set(id: id, email: email, name: name, **custom)
46
59
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bugwatch-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BugWatch
@@ -48,10 +48,14 @@ files:
48
48
  - README.md
49
49
  - lib/bugwatch-ruby.rb
50
50
  - lib/bugwatch.rb
51
+ - lib/bugwatch/active_job_handler.rb
51
52
  - lib/bugwatch/backtrace_cleaner.rb
52
53
  - lib/bugwatch/breadcrumb_collector.rb
53
54
  - lib/bugwatch/configuration.rb
54
55
  - lib/bugwatch/error_builder.rb
56
+ - lib/bugwatch/error_subscriber.rb
57
+ - lib/bugwatch/feedback_helper.rb
58
+ - lib/bugwatch/feedback_sender.rb
55
59
  - lib/bugwatch/middleware.rb
56
60
  - lib/bugwatch/notification.rb
57
61
  - lib/bugwatch/railtie.rb