bugwatch-ruby 0.7.1 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1cf7953dfa5c81c889aa0e51cc8ed4211b06c566620c48248e1171580cde9077
4
- data.tar.gz: c8eacd0b880a71d0502ef6622d4a30257932de7ed72d3665d36c92db5db47635
3
+ metadata.gz: f7845d9912f5a403dec979a3457ab7e121a12fb3de870b978d864d8eafb71755
4
+ data.tar.gz: cc43de7851a77523328ca61f5e8b0cd0d4aaa0f9867b3229aa9e34fb04e45a1a
5
5
  SHA512:
6
- metadata.gz: 8c65e8c7831523385112c3bb24148c118d0c0124f30dea1022f2c5a39d05f775b53c088dfecc007ada12af0c0c41657bfb0376b638153a837981b08923e0ce03
7
- data.tar.gz: 9d4d10c1ce02671998a4085550d59f11e2dcefb15a3b9b1177d16081cd33a52b24a3316bc1295b318ea0ba9c02bc35347487c6ccbe071760898fb5e62a2670e6
6
+ metadata.gz: 1d5479d1aa4ea0b1060c06328835f8c11b808389d83656a70121ab78e8ff30887caf828433edc27541d39e91ae76e43a568fa08bbf30e014a9c12246d5b88fb3
7
+ data.tar.gz: 3eb19a7f384614714730f68b07243d98064c4b90cca61b90a92b84a10a817cceabc1a520cf61ffdfcdc806a7deb64f0d7dc6157a7f9e0a9b0724ac809f243afb
data/README.md CHANGED
@@ -257,8 +257,8 @@ Sensitive params (`password`, `token`, `secret`, `key`, `auth`, `credit`, `card`
257
257
  ## Publishing
258
258
 
259
259
  ```bash
260
- cd /home/max/bugwatch-ruby
260
+ cd gems/bugwatch-ruby
261
261
  gem build bugwatch-ruby.gemspec
262
262
  gem signin
263
- gem push bugwatch-ruby-0.1.0.gem
263
+ gem push bugwatch-ruby-0.8.0.gem
264
264
  ```
@@ -3,6 +3,7 @@ module Bugwatch
3
3
  def call(job, exception)
4
4
  return if Bugwatch.configuration.ignore?(exception)
5
5
  return unless Bugwatch.configuration.notify_for_release_stage?
6
+ return if ReportedExceptions.reported?(exception)
6
7
 
7
8
  payload = ErrorBuilder.new(exception).build
8
9
  payload[:context] = {
@@ -13,6 +14,7 @@ module Bugwatch
13
14
  }
14
15
 
15
16
  Notification.new(payload).deliver
17
+ ReportedExceptions.mark(exception)
16
18
  end
17
19
 
18
20
  private
@@ -13,7 +13,10 @@ module Bugwatch
13
13
  :enable_db_tracking,
14
14
  :db_sample_rate,
15
15
  :db_query_threshold_ms,
16
- :max_queries_per_request
16
+ :max_queries_per_request,
17
+ :enable_http_tracking,
18
+ :http_sample_rate,
19
+ :max_http_calls_per_request
17
20
 
18
21
  def initialize
19
22
  @endpoint = nil
@@ -28,6 +31,9 @@ module Bugwatch
28
31
  @db_sample_rate = 1.0
29
32
  @db_query_threshold_ms = 0.0
30
33
  @max_queries_per_request = 200
34
+ @enable_http_tracking = true
35
+ @http_sample_rate = 1.0
36
+ @max_http_calls_per_request = 100
31
37
  end
32
38
 
33
39
  def notify_for_release_stage?
@@ -0,0 +1,23 @@
1
+ module Bugwatch
2
+ module ControllerRescueHook
3
+ def rescue_with_handler(exception, **kwargs)
4
+ handler = super
5
+
6
+ if handler && exception.is_a?(Exception) && !ReportedExceptions.reported?(exception)
7
+ begin
8
+ context = {
9
+ handled: true,
10
+ controller: self.class.name,
11
+ action: (action_name if respond_to?(:action_name))
12
+ }.compact
13
+
14
+ Bugwatch.notify(exception, context: context)
15
+ rescue StandardError
16
+ # Never let reporting break the host's rescue flow
17
+ end
18
+ end
19
+
20
+ handler
21
+ end
22
+ end
23
+ end
@@ -3,6 +3,7 @@ module Bugwatch
3
3
  def report(error, handled:, severity:, context: {}, source: nil)
4
4
  return if Bugwatch.configuration.ignore?(error)
5
5
  return unless Bugwatch.configuration.notify_for_release_stage?
6
+ return if ReportedExceptions.reported?(error)
6
7
 
7
8
  payload = ErrorBuilder.new(error).build
8
9
  payload[:context] = context.merge(
@@ -12,6 +13,7 @@ module Bugwatch
12
13
  ).compact
13
14
 
14
15
  Notification.new(payload).deliver
16
+ ReportedExceptions.mark(error)
15
17
  end
16
18
  end
17
19
  end
@@ -0,0 +1,61 @@
1
+ module Bugwatch
2
+ class HttpCallBuffer
3
+ BATCH_SIZE = 50
4
+ FLUSH_INTERVAL = 15 # seconds
5
+
6
+ def initialize(config: Bugwatch.configuration)
7
+ @config = config
8
+ @sender = HttpCallSender.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,68 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Bugwatch
6
+ class HttpCallSender
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
+ Bugwatch.without_tracking { 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[:calls].map do |c|
30
+ {
31
+ transaction_name: group[:transaction_name],
32
+ environment: group[:environment],
33
+ occurred_at: group[:occurred_at],
34
+ host: c[:host],
35
+ port: c[:port],
36
+ method: c[:method],
37
+ path: c[:path],
38
+ status_code: c[:status_code],
39
+ duration_ms: c[:duration_ms],
40
+ library: c[:library],
41
+ caller_location: c[:caller_location]
42
+ }
43
+ end
44
+ end
45
+
46
+ return if records.empty?
47
+
48
+ uri = URI.parse("#{@config.endpoint.chomp("/")}/api/v1/http_calls/batch")
49
+
50
+ http = Net::HTTP.new(uri.host, uri.port)
51
+ http.use_ssl = uri.scheme == "https"
52
+ http.open_timeout = TIMEOUT
53
+ http.read_timeout = TIMEOUT
54
+ http.write_timeout = TIMEOUT
55
+
56
+ request = Net::HTTP::Post.new(uri.path)
57
+ request["Content-Type"] = "application/json"
58
+ request["X-Api-Key"] = @config.api_key
59
+ request["X-BugWatch-Ruby"] = Bugwatch::VERSION
60
+
61
+ request.body = JSON.generate({ http_calls: records })
62
+
63
+ http.request(request)
64
+ rescue StandardError
65
+ # Silently discard network errors
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,101 @@
1
+ require "net/http"
2
+
3
+ module Bugwatch
4
+ module HttpTracker
5
+ THREAD_KEY = :bugwatch_http_tracker
6
+
7
+ CALLER_FILTER = %r{/(bugwatch|ruby/gems)/}
8
+
9
+ module NetHttpPatch
10
+ def request(req, body = nil, &block)
11
+ return super if Thread.current[:bugwatch_skip_tracking]
12
+ return super unless HttpTracker.collecting?
13
+
14
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
+ response = super
16
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(2)
17
+
18
+ HttpTracker.handle_call(self, req, response, duration_ms)
19
+ response
20
+ rescue Exception # rubocop:disable Lint/RescueException
21
+ raise
22
+ end
23
+ end
24
+
25
+ module_function
26
+
27
+ def subscribe!
28
+ return if Net::HTTP.ancestors.include?(NetHttpPatch)
29
+
30
+ Net::HTTP.prepend(NetHttpPatch)
31
+ end
32
+
33
+ def start_request(collecting:)
34
+ Thread.current[THREAD_KEY] = {
35
+ calls: [],
36
+ collecting: collecting
37
+ }
38
+ end
39
+
40
+ def finish_request
41
+ state = Thread.current[THREAD_KEY]
42
+ Thread.current[THREAD_KEY] = nil
43
+ state
44
+ end
45
+
46
+ def clear
47
+ Thread.current[THREAD_KEY] = nil
48
+ end
49
+
50
+ def collecting?
51
+ state = Thread.current[THREAD_KEY]
52
+ state && state[:collecting]
53
+ end
54
+
55
+ def handle_call(http, req, response, duration_ms)
56
+ state = Thread.current[THREAD_KEY]
57
+ return unless state
58
+ return unless state[:collecting]
59
+
60
+ config = Bugwatch.configuration
61
+ return if state[:calls].size >= config.max_http_calls_per_request
62
+
63
+ state[:calls] << {
64
+ host: http.address,
65
+ port: http.port,
66
+ method: req.method,
67
+ path: extract_path(req.path),
68
+ status_code: response.code.to_i,
69
+ duration_ms: duration_ms,
70
+ library: detect_library,
71
+ caller_location: extract_caller
72
+ }
73
+ rescue StandardError
74
+ # Never let tracking break the app
75
+ end
76
+
77
+ def extract_path(full_path)
78
+ full_path.to_s.split("?").first
79
+ end
80
+
81
+ def detect_library
82
+ caller_locations(4, 20)&.each do |loc|
83
+ path = loc.path.to_s
84
+ return "faraday" if path.include?("faraday")
85
+ return "httparty" if path.include?("httparty")
86
+ end
87
+ "net_http"
88
+ end
89
+
90
+ def extract_caller
91
+ caller_locations(4, 30)&.each do |loc|
92
+ path = loc.path.to_s
93
+ next if path.match?(CALLER_FILTER)
94
+ return "#{loc.path}:#{loc.lineno}"
95
+ end
96
+ nil
97
+ end
98
+
99
+ private_class_method :handle_call, :extract_path, :detect_library, :extract_caller
100
+ end
101
+ end
@@ -12,11 +12,15 @@ module Bugwatch
12
12
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
13
13
  BreadcrumbCollector.clear
14
14
  UserContext.clear
15
+ ReportedExceptions.clear
15
16
 
16
17
  config = Bugwatch.configuration
17
18
  collecting = config.enable_db_tracking && (rand < config.db_sample_rate)
18
19
  DbTracker.start_request(collecting: collecting)
19
20
 
21
+ collecting_http = config.enable_http_tracking && (rand < config.http_sample_rate)
22
+ HttpTracker.start_request(collecting: collecting_http)
23
+
20
24
  begin
21
25
  status, headers, body = @app.call(env)
22
26
  record_transaction(env, status, start)
@@ -24,16 +28,22 @@ module Bugwatch
24
28
  rescue Exception => e # rubocop:disable Lint/RescueException
25
29
  duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
26
30
 
27
- unless Bugwatch.configuration.ignore?(e)
28
- payload = ErrorBuilder.new(e, env).build
29
- payload[:duration_ms] = duration_ms
30
- Notification.new(payload).deliver
31
+ begin
32
+ if !Bugwatch.configuration.ignore?(e) && !ReportedExceptions.reported?(e)
33
+ payload = ErrorBuilder.new(e, env).build
34
+ payload[:duration_ms] = duration_ms
35
+ Notification.new(payload).deliver
36
+ ReportedExceptions.mark(e)
37
+ end
38
+ rescue StandardError
39
+ # Never let reporting failures drop the original exception
31
40
  end
32
41
 
33
42
  record_transaction(env, 500, start)
34
43
  raise
35
44
  ensure
36
45
  DbTracker.clear
46
+ HttpTracker.clear
37
47
  end
38
48
  end
39
49
 
@@ -74,6 +84,16 @@ module Bugwatch
74
84
  queries: db_result[:queries]
75
85
  })
76
86
  end
87
+
88
+ http_result = HttpTracker.finish_request
89
+ if http_result && http_result[:calls].any?
90
+ Bugwatch.http_call_buffer.push({
91
+ transaction_name: payload[:name],
92
+ environment: config.release_stage,
93
+ occurred_at: payload[:occurred_at],
94
+ calls: http_result[:calls]
95
+ })
96
+ end
77
97
  rescue StandardError
78
98
  # Never let tracking break the app
79
99
  end
@@ -31,11 +31,21 @@ module Bugwatch
31
31
  end
32
32
  end
33
33
 
34
+ initializer "bugwatch.http_tracking" do
35
+ Bugwatch::HttpTracker.subscribe! if Bugwatch.configuration.enable_http_tracking
36
+ end
37
+
34
38
  initializer "bugwatch.error_subscriber" do
35
39
  if defined?(Rails.error) && Rails.error.respond_to?(:subscribe)
36
40
  Rails.error.subscribe(Bugwatch::ErrorSubscriber.new)
37
41
  end
38
42
  end
43
+
44
+ initializer "bugwatch.controller_rescue_hook" do
45
+ ActiveSupport.on_load(:action_controller) do
46
+ prepend Bugwatch::ControllerRescueHook
47
+ end
48
+ end
39
49
  end
40
50
 
41
51
  module ControllerMethods
@@ -0,0 +1,19 @@
1
+ module Bugwatch
2
+ module ReportedExceptions
3
+ THREAD_KEY = :bugwatch_reported_exceptions
4
+
5
+ def self.mark(exception)
6
+ return unless exception
7
+ (Thread.current[THREAD_KEY] ||= []) << exception.object_id
8
+ end
9
+
10
+ def self.reported?(exception)
11
+ return false unless exception
12
+ (Thread.current[THREAD_KEY] || []).include?(exception.object_id)
13
+ end
14
+
15
+ def self.clear
16
+ Thread.current[THREAD_KEY] = nil
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module Bugwatch
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.1"
3
3
  end
data/lib/bugwatch.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "bugwatch/configuration"
6
6
  require_relative "bugwatch/user_context"
7
7
  require_relative "bugwatch/breadcrumb_collector"
8
8
  require_relative "bugwatch/backtrace_cleaner"
9
+ require_relative "bugwatch/reported_exceptions"
9
10
  require_relative "bugwatch/error_builder"
10
11
  require_relative "bugwatch/report_builder"
11
12
  require_relative "bugwatch/notification"
@@ -14,11 +15,15 @@ require_relative "bugwatch/transaction_buffer"
14
15
  require_relative "bugwatch/db_tracker"
15
16
  require_relative "bugwatch/db_query_sender"
16
17
  require_relative "bugwatch/db_query_buffer"
18
+ require_relative "bugwatch/http_tracker"
19
+ require_relative "bugwatch/http_call_sender"
20
+ require_relative "bugwatch/http_call_buffer"
17
21
  require_relative "bugwatch/feedback_sender"
18
22
  require_relative "bugwatch/feedback_helper"
19
23
  require_relative "bugwatch/middleware"
20
24
  require_relative "bugwatch/active_job_handler"
21
25
  require_relative "bugwatch/error_subscriber"
26
+ require_relative "bugwatch/controller_rescue_hook"
22
27
  require_relative "bugwatch/railtie" if defined?(Rails::Railtie)
23
28
 
24
29
  module Bugwatch
@@ -34,10 +39,12 @@ module Bugwatch
34
39
  def notify(exception, context: {})
35
40
  return if configuration.ignore?(exception)
36
41
  return unless configuration.notify_for_release_stage?
42
+ return if ReportedExceptions.reported?(exception)
37
43
 
38
44
  payload = ErrorBuilder.new(exception).build
39
45
  payload.merge!(context)
40
46
  Notification.new(payload).deliver
47
+ ReportedExceptions.mark(exception)
41
48
  end
42
49
 
43
50
  def report(title, category: "other", severity: "warning", tags: {})
@@ -138,10 +145,15 @@ module Bugwatch
138
145
  def db_query_buffer
139
146
  @db_query_buffer ||= DbQueryBuffer.new(config: configuration)
140
147
  end
148
+
149
+ def http_call_buffer
150
+ @http_call_buffer ||= HttpCallBuffer.new(config: configuration)
151
+ end
141
152
  end
142
153
  end
143
154
 
144
155
  at_exit do
145
156
  Bugwatch.transaction_buffer.shutdown rescue nil
146
- Bugwatch.db_query_buffer.shutdown rescue nil
157
+ Bugwatch.db_query_buffer.shutdown rescue nil
158
+ Bugwatch.http_call_buffer.shutdown rescue nil
147
159
  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.7.1
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - BugWatch
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-14 00:00:00.000000000 Z
10
+ date: 2026-04-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -52,6 +52,7 @@ files:
52
52
  - lib/bugwatch/backtrace_cleaner.rb
53
53
  - lib/bugwatch/breadcrumb_collector.rb
54
54
  - lib/bugwatch/configuration.rb
55
+ - lib/bugwatch/controller_rescue_hook.rb
55
56
  - lib/bugwatch/db_query_buffer.rb
56
57
  - lib/bugwatch/db_query_sender.rb
57
58
  - lib/bugwatch/db_tracker.rb
@@ -59,10 +60,14 @@ files:
59
60
  - lib/bugwatch/error_subscriber.rb
60
61
  - lib/bugwatch/feedback_helper.rb
61
62
  - lib/bugwatch/feedback_sender.rb
63
+ - lib/bugwatch/http_call_buffer.rb
64
+ - lib/bugwatch/http_call_sender.rb
65
+ - lib/bugwatch/http_tracker.rb
62
66
  - lib/bugwatch/middleware.rb
63
67
  - lib/bugwatch/notification.rb
64
68
  - lib/bugwatch/railtie.rb
65
69
  - lib/bugwatch/report_builder.rb
70
+ - lib/bugwatch/reported_exceptions.rb
66
71
  - lib/bugwatch/transaction_buffer.rb
67
72
  - lib/bugwatch/transaction_sender.rb
68
73
  - lib/bugwatch/user_context.rb