bugwatch-ruby 0.7.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d6a47f1c5176ec17f87160b5ab4a273e6ef68e76dee98f6e60ee5da474a61fd
4
- data.tar.gz: 713ab0b06839f57c473a98c8f3e1cd46f2f43b2c9ccfef2b18d7ba0e3abb1076
3
+ metadata.gz: c2096369bb2864dc1713e2998bc6438302122dc3237194c4f12349094d4b046e
4
+ data.tar.gz: 472e7299734b4f451401a3d828c3465ac1071fb1cd8a1bfba49edebd66db06dc
5
5
  SHA512:
6
- metadata.gz: cb255c2fcfe9562b06320de183bc1ee141f394eacb327eb6ad71739a662aaeb435f5da3248404a7074b45e6a397612d86fe8d243918284196cb7e3892c193f8a
7
- data.tar.gz: 9ce11bcc466b319298cb788f51cdb1c89f00f6b3126189d02c9a7838eeb6f087291bb0864bc5115734849a1af966f2995385a627a057befbf815c63354e122ee
6
+ metadata.gz: 013ae4e4faa1dba3cc77ccc7dc9a81646025aaeb30cd9b835e512ee0d47bc3cfd58f44c47f9516e2272ec352e912470163d8dee0d394e490001f37d58c6df1c2
7
+ data.tar.gz: 38f4a61e9b51617b4095b3ee6d8320453ec4de615c6a76aa8c13303a2ef9a22cd8c4cde377b261ef447040a9ede948e4e4b462d15ce91019b34ca8ecaef46aaa
@@ -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,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
@@ -5,7 +5,9 @@ module Bugwatch
5
5
  end
6
6
 
7
7
  def call(env)
8
- return @app.call(env) if Bugwatch.skip_tracking?
8
+ if Bugwatch.skip_tracking? || skip_path?(env)
9
+ return @app.call(env)
10
+ end
9
11
 
10
12
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
13
  BreadcrumbCollector.clear
@@ -15,6 +17,9 @@ module Bugwatch
15
17
  collecting = config.enable_db_tracking && (rand < config.db_sample_rate)
16
18
  DbTracker.start_request(collecting: collecting)
17
19
 
20
+ collecting_http = config.enable_http_tracking && (rand < config.http_sample_rate)
21
+ HttpTracker.start_request(collecting: collecting_http)
22
+
18
23
  begin
19
24
  status, headers, body = @app.call(env)
20
25
  record_transaction(env, status, start)
@@ -32,11 +37,17 @@ module Bugwatch
32
37
  raise
33
38
  ensure
34
39
  DbTracker.clear
40
+ HttpTracker.clear
35
41
  end
36
42
  end
37
43
 
38
44
  private
39
45
 
46
+ def skip_path?(env)
47
+ path = env["PATH_INFO"].to_s
48
+ Bugwatch.configuration.ignore_request_paths.any? { |pattern| path.match?(pattern) }
49
+ end
50
+
40
51
  def record_transaction(env, status, start)
41
52
  config = Bugwatch.configuration
42
53
  req = Rack::Request.new(env)
@@ -67,6 +78,16 @@ module Bugwatch
67
78
  queries: db_result[:queries]
68
79
  })
69
80
  end
81
+
82
+ http_result = HttpTracker.finish_request
83
+ if http_result && http_result[:calls].any?
84
+ Bugwatch.http_call_buffer.push({
85
+ transaction_name: payload[:name],
86
+ environment: config.release_stage,
87
+ occurred_at: payload[:occurred_at],
88
+ calls: http_result[:calls]
89
+ })
90
+ end
70
91
  rescue StandardError
71
92
  # Never let tracking break the app
72
93
  end
@@ -31,6 +31,10 @@ 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)
@@ -1,3 +1,3 @@
1
1
  module Bugwatch
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
data/lib/bugwatch.rb CHANGED
@@ -14,6 +14,9 @@ require_relative "bugwatch/transaction_buffer"
14
14
  require_relative "bugwatch/db_tracker"
15
15
  require_relative "bugwatch/db_query_sender"
16
16
  require_relative "bugwatch/db_query_buffer"
17
+ require_relative "bugwatch/http_tracker"
18
+ require_relative "bugwatch/http_call_sender"
19
+ require_relative "bugwatch/http_call_buffer"
17
20
  require_relative "bugwatch/feedback_sender"
18
21
  require_relative "bugwatch/feedback_helper"
19
22
  require_relative "bugwatch/middleware"
@@ -138,10 +141,15 @@ module Bugwatch
138
141
  def db_query_buffer
139
142
  @db_query_buffer ||= DbQueryBuffer.new(config: configuration)
140
143
  end
144
+
145
+ def http_call_buffer
146
+ @http_call_buffer ||= HttpCallBuffer.new(config: configuration)
147
+ end
141
148
  end
142
149
  end
143
150
 
144
151
  at_exit do
145
152
  Bugwatch.transaction_buffer.shutdown rescue nil
146
- Bugwatch.db_query_buffer.shutdown rescue nil
153
+ Bugwatch.db_query_buffer.shutdown rescue nil
154
+ Bugwatch.http_call_buffer.shutdown rescue nil
147
155
  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.0
4
+ version: 0.8.0
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-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -59,6 +59,9 @@ files:
59
59
  - lib/bugwatch/error_subscriber.rb
60
60
  - lib/bugwatch/feedback_helper.rb
61
61
  - lib/bugwatch/feedback_sender.rb
62
+ - lib/bugwatch/http_call_buffer.rb
63
+ - lib/bugwatch/http_call_sender.rb
64
+ - lib/bugwatch/http_tracker.rb
62
65
  - lib/bugwatch/middleware.rb
63
66
  - lib/bugwatch/notification.rb
64
67
  - lib/bugwatch/railtie.rb