lapsoss 0.3.1 → 0.4.2

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -7
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +18 -12
  4. data/lib/lapsoss/adapters/base.rb +19 -0
  5. data/lib/lapsoss/adapters/concerns/envelope_builder.rb +127 -0
  6. data/lib/lapsoss/adapters/concerns/http_delivery.rb +130 -0
  7. data/lib/lapsoss/adapters/concerns/level_mapping.rb +65 -0
  8. data/lib/lapsoss/adapters/insight_hub_adapter.rb +21 -21
  9. data/lib/lapsoss/adapters/rollbar_adapter.rb +64 -122
  10. data/lib/lapsoss/adapters/sentry_adapter.rb +77 -143
  11. data/lib/lapsoss/backtrace_processor.rb +1 -1
  12. data/lib/lapsoss/breadcrumb.rb +59 -0
  13. data/lib/lapsoss/client.rb +3 -5
  14. data/lib/lapsoss/configuration.rb +26 -31
  15. data/lib/lapsoss/event.rb +90 -96
  16. data/lib/lapsoss/fingerprinter.rb +57 -49
  17. data/lib/lapsoss/merged_scope.rb +1 -6
  18. data/lib/lapsoss/middleware/release_tracker.rb +11 -98
  19. data/lib/lapsoss/pipeline_builder.rb +2 -2
  20. data/lib/lapsoss/rails_error_subscriber.rb +3 -4
  21. data/lib/lapsoss/rails_middleware.rb +2 -2
  22. data/lib/lapsoss/railtie.rb +13 -2
  23. data/lib/lapsoss/registry.rb +7 -7
  24. data/lib/lapsoss/router.rb +1 -3
  25. data/lib/lapsoss/scope.rb +1 -6
  26. data/lib/lapsoss/scrubber.rb +15 -148
  27. data/lib/lapsoss/validators.rb +63 -92
  28. data/lib/lapsoss/version.rb +1 -1
  29. metadata +8 -24
  30. data/CHANGELOG.md +0 -5
  31. data/lib/lapsoss/exclusion_configuration.rb +0 -30
  32. data/lib/lapsoss/exclusion_presets.rb +0 -249
  33. data/lib/lapsoss/middleware/sample_filter.rb +0 -23
  34. data/lib/lapsoss/middleware/sampling_middleware.rb +0 -18
  35. data/lib/lapsoss/middleware/user_context_enhancer.rb +0 -46
  36. data/lib/lapsoss/release_providers.rb +0 -110
  37. data/lib/lapsoss/sampling/adaptive_sampler.rb +0 -46
  38. data/lib/lapsoss/sampling/composite_sampler.rb +0 -26
  39. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +0 -30
  40. data/lib/lapsoss/sampling/exception_type_sampler.rb +0 -44
  41. data/lib/lapsoss/sampling/health_based_sampler.rb +0 -19
  42. data/lib/lapsoss/sampling/sampling_factory.rb +0 -69
  43. data/lib/lapsoss/sampling/time_based_sampler.rb +0 -44
  44. data/lib/lapsoss/sampling/user_based_sampler.rb +0 -42
  45. data/lib/lapsoss/user_context.rb +0 -175
  46. data/lib/lapsoss/user_context_integrations.rb +0 -39
  47. data/lib/lapsoss/user_context_middleware.rb +0 -50
  48. data/lib/lapsoss/user_context_provider.rb +0 -93
  49. data/lib/lapsoss/utils.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d64055e7377f1d53c9c8ec93302e04c279a33cbad65686257e34d420ea00600b
4
- data.tar.gz: e2e80f0d005f741607cc2b163cf51be796e9bfbdf36957f030249cab6a127269
3
+ metadata.gz: 93ff41a78372ad8d5080a5b7d87c2783a1f5834020df285e2db5eb28f0a54d61
4
+ data.tar.gz: 1cbceab257813d8e7659d9a31abd2cfba0a33a6f5585776ed426c984c32a61e4
5
5
  SHA512:
6
- metadata.gz: 9af0fd3cbcd82dac58b2d38fa51aa648957e21f5e9b2e24c41237d93d3645ab56c27c7a3137768faad3026b980e8e985348cea906412171602530519ae357504
7
- data.tar.gz: 8b53db58c0309885de19c514c5af321e772f48b54ebfd1c011d290078eff853efb57228bced817dccb34d66d58e4cc9916b611b00cf99e0941740200dbe34471
6
+ metadata.gz: da64a5b1ec01779137189432e8dfba438e3768a1af44f71de44f3827c6ef3f2eb27b49d1b6d88479a23c51218e328f4068a86b4e210a2f73b869d2ff7167bb30
7
+ data.tar.gz: 126e54769b4f4ad8dad6617c27b5a80aeba5ae073879b462c8c6e01346fbc6a78029aca8589b028b6eccf9900fd1cc24e8072d119098190dbb86ac0b88521eba
data/README.md CHANGED
@@ -31,6 +31,11 @@ Lapsoss.configure do |config|
31
31
  end
32
32
  ```
33
33
 
34
+ ## Requirements
35
+
36
+ - Ruby 3.3+
37
+ - Rails 7.2+
38
+
34
39
  ## Installation
35
40
 
36
41
  ```ruby
@@ -191,23 +196,82 @@ Lapsoss.configure do |config|
191
196
  # Data scrubbing (uses Rails filter_parameters automatically)
192
197
  config.scrub_fields = %w[password credit_card ssn] # Or leave nil to use Rails defaults
193
198
 
194
- # Error filtering
199
+ # Performance
200
+ config.async = true # Send errors in background
201
+
202
+ # Sampling (see docs/sampling_strategies.md for advanced examples)
203
+ config.sample_rate = Rails.env.production? ? 0.25 : 1.0
204
+
205
+ # Transport settings
206
+ config.transport_timeout = 10 # seconds
207
+ config.transport_max_retries = 3
208
+ end
209
+ ```
210
+
211
+ ### Filtering Errors
212
+
213
+ You decide what errors to track. Lapsoss doesn't make assumptions:
214
+
215
+ ```ruby
216
+ Lapsoss.configure do |config|
217
+ # Use the before_send callback for simple filtering
195
218
  config.before_send = lambda do |event|
196
219
  # Return nil to prevent sending
197
220
  return nil if event.exception.is_a?(ActiveRecord::RecordNotFound)
198
221
  event
199
222
  end
223
+
224
+ # Or use the exclusion filter for more complex rules
225
+ config.exclusion_filter = Lapsoss::ExclusionFilter.new(
226
+ # Exclude specific exception types
227
+ excluded_exceptions: [
228
+ "ActionController::RoutingError", # Your choice
229
+ "ActiveRecord::RecordNotFound" # Your decision
230
+ ],
231
+
232
+ # Exclude by pattern matching
233
+ excluded_patterns: [
234
+ /timeout/i, # If timeouts are expected in your app
235
+ /user not found/i # If these are normal in your workflow
236
+ ],
237
+
238
+ # Exclude specific error messages
239
+ excluded_messages: [
240
+ "No route matches",
241
+ "Invalid authenticity token"
242
+ ]
243
+ )
244
+
245
+ # Add custom exclusion logic
246
+ config.exclusion_filter.add_exclusion(:custom, lambda do |event|
247
+ # Your business logic here
248
+ event.context[:request]&.dig(:user_agent)&.match?(/bot/i)
249
+ end)
250
+ end
251
+ ```
200
252
 
201
- # Sampling
202
- config.sample_rate = Rails.env.production? ? 0.25 : 1.0
253
+ #### Common Patterns (Your Choice)
203
254
 
204
- # Performance
205
- config.async = true # Send errors in background
206
- config.transport_timeout = 10 # seconds
207
- config.transport_max_retries = 3
255
+ ```ruby
256
+ # Development/Test exclusions
257
+ if Rails.env.development?
258
+ config.exclusion_filter.add_exclusion(:exception, "RSpec::Expectations::ExpectationNotMetError")
259
+ config.exclusion_filter.add_exclusion(:exception, "Minitest::Assertion")
208
260
  end
261
+
262
+ # User input errors (if you don't want to track them)
263
+ config.exclusion_filter.add_exclusion(:exception, "ActiveRecord::RecordInvalid")
264
+ config.exclusion_filter.add_exclusion(:exception, "ActionController::ParameterMissing")
265
+
266
+ # Bot traffic (if you want to exclude it)
267
+ config.exclusion_filter.add_exclusion(:custom, lambda do |event|
268
+ request = event.context[:request]
269
+ request && request[:user_agent]&.match?(/googlebot|bingbot/i)
270
+ end)
209
271
  ```
210
272
 
273
+ Your app, your rules. Lapsoss just provides the mechanism.
274
+
211
275
  ### Data Protection
212
276
 
213
277
  Lapsoss automatically integrates with Rails' parameter filtering:
@@ -8,7 +8,6 @@ module Lapsoss
8
8
  class AppsignalAdapter < Base
9
9
  PUSH_API_URI = "https://push.appsignal.com"
10
10
  ERRORS_API_URI = "https://appsignal-endpoint.net"
11
- JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
12
11
 
13
12
  def initialize(name, settings = {})
14
13
  super
@@ -17,7 +16,20 @@ module Lapsoss
17
16
  @app_name = settings[:app_name] || ENV.fetch("APPSIGNAL_APP_NAME", nil)
18
17
  @environment = Lapsoss.configuration.environment
19
18
 
20
- validate_settings!
19
+ # Just log if keys look unusual but don't fail
20
+ if @push_api_key.present?
21
+ validate_api_key!(@push_api_key, "AppSignal push API key", format: :uuid)
22
+ end
23
+
24
+ if @frontend_api_key.present?
25
+ validate_api_key!(@frontend_api_key, "AppSignal frontend API key", format: :uuid)
26
+ end
27
+
28
+ if @push_api_key.blank? && @frontend_api_key.blank?
29
+ Lapsoss.configuration.logger&.warn "[Lapsoss::AppsignalAdapter] No API keys provided, adapter disabled"
30
+ @enabled = false
31
+ return
32
+ end
21
33
 
22
34
  @push_client = create_http_client(PUSH_API_URI) if @push_api_key
23
35
  @errors_client = create_http_client(ERRORS_API_URI) if @frontend_api_key
@@ -30,10 +42,10 @@ module Lapsoss
30
42
  return unless payload
31
43
 
32
44
  path = "/errors?api_key=#{@frontend_api_key}"
33
- headers = { "Content-Type" => JSON_CONTENT_TYPE }
45
+ headers = default_headers(content_type: json_content_type)
34
46
 
35
47
  begin
36
- @errors_client.post(path, body: JSON.generate(payload), headers: headers)
48
+ @errors_client.post(path, body: ActiveSupport::JSON.encode(payload), headers: headers)
37
49
  rescue DeliveryError => e
38
50
  # Log the error and potentially notify error handler
39
51
  Lapsoss.configuration.logger&.error("[Lapsoss::AppsignalAdapter] Failed to deliver event: #{e.message}")
@@ -120,15 +132,9 @@ module Lapsoss
120
132
  (hash || {}).transform_keys(&:to_s).transform_values(&:to_s)
121
133
  end
122
134
 
135
+ # No longer need strict validation
123
136
  def validate_settings!
124
- unless @push_api_key || @frontend_api_key
125
- raise ValidationError, "AppSignal API key is required (either push_api_key or frontend_api_key)"
126
- end
127
-
128
- validate_api_key!(@push_api_key, "AppSignal push API key", format: :uuid) if @push_api_key
129
- validate_api_key!(@frontend_api_key, "AppSignal frontend API key", format: :uuid) if @frontend_api_key
130
- validate_presence!(@app_name, "AppSignal app name") if @app_name
131
- validate_environment!(@environment, "AppSignal environment") if @environment
137
+ # Validation moved to initialize with logging
132
138
  end
133
139
  end
134
140
  end
@@ -5,6 +5,9 @@ module Lapsoss
5
5
  class Base
6
6
  include Validators
7
7
 
8
+ USER_AGENT = "lapsoss/#{Lapsoss::VERSION}".freeze
9
+ JSON_CONTENT_TYPE = "application/json; charset=UTF-8".freeze
10
+
8
11
  attr_reader :name, :settings
9
12
 
10
13
  def initialize(name, settings = {})
@@ -80,6 +83,22 @@ module Lapsoss
80
83
 
81
84
  HttpClient.new(uri, transport_config)
82
85
  end
86
+
87
+ def user_agent
88
+ USER_AGENT
89
+ end
90
+
91
+ def json_content_type
92
+ JSON_CONTENT_TYPE
93
+ end
94
+
95
+ def default_headers(content_type: nil, gzip: false, extra: {})
96
+ headers = { "User-Agent" => user_agent }
97
+ headers["Content-Type"] = content_type if content_type
98
+ headers["Content-Encoding"] = "gzip" if gzip
99
+ headers.merge!(extra) if extra && !extra.empty?
100
+ headers
101
+ end
83
102
  end
84
103
  end
85
104
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/object/blank"
5
+ require "active_support/json"
6
+ require "active_support/core_ext/numeric/bytes"
7
+ require "active_support/gzip"
8
+ require "securerandom"
9
+
10
+ module Lapsoss
11
+ module Adapters
12
+ module Concerns
13
+ module EnvelopeBuilder
14
+ extend ActiveSupport::Concern
15
+
16
+ GZIP_THRESHOLD = 30.kilobytes
17
+
18
+ included do
19
+ class_attribute :envelope_format, default: :json
20
+ class_attribute :compress_threshold, default: GZIP_THRESHOLD
21
+ end
22
+
23
+ # Build envelope with common structure
24
+ def build_envelope_wrapper(event)
25
+ envelope = {
26
+ id: event.fingerprint.presence || SecureRandom.uuid,
27
+ timestamp: format_timestamp(event.timestamp),
28
+ environment: event.environment.presence || "production",
29
+ level: map_level(event.level),
30
+ platform: "ruby",
31
+ sdk: sdk_info
32
+ }
33
+
34
+ # Add event-specific data
35
+ envelope.merge!(build_event_data(event))
36
+
37
+ # Add context data
38
+ envelope.merge!(
39
+ tags: event.tags.presence,
40
+ user: event.user_context.presence,
41
+ extra: event.extra.presence,
42
+ breadcrumbs: format_breadcrumbs(event.breadcrumbs)
43
+ ).compact_blank
44
+ end
45
+
46
+ # Format timestamp using AS helpers
47
+ def format_timestamp(time)
48
+ time = Time.current if time.blank?
49
+ time.in_time_zone("UTC").iso8601
50
+ end
51
+
52
+ # Format breadcrumbs consistently
53
+ def format_breadcrumbs(breadcrumbs)
54
+ return nil if breadcrumbs.blank?
55
+
56
+ breadcrumbs.map do |crumb|
57
+ {
58
+ timestamp: format_timestamp(crumb[:timestamp]),
59
+ type: crumb[:type].presence || "default",
60
+ message: crumb[:message],
61
+ data: crumb.except(:timestamp, :type, :message).presence
62
+ }.compact_blank
63
+ end
64
+ end
65
+
66
+ # SDK info for all adapters
67
+ def sdk_info
68
+ {
69
+ name: "lapsoss",
70
+ version: Lapsoss::VERSION,
71
+ packages: [ {
72
+ name: "lapsoss-ruby",
73
+ version: Lapsoss::VERSION
74
+ } ]
75
+ }
76
+ end
77
+
78
+ # Serialize and optionally compress
79
+ def serialize_payload(data, compress: :auto)
80
+ json = ActiveSupport::JSON.encode(data)
81
+
82
+ should_compress = case compress
83
+ when :auto then json.bytesize >= compress_threshold
84
+ when true then true
85
+ else false
86
+ end
87
+
88
+ if should_compress
89
+ [ ActiveSupport::Gzip.compress(json), true ]
90
+ else
91
+ [ json, false ]
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # Override in adapter for specific event data
98
+ def build_event_data(event)
99
+ case event.type
100
+ in :exception
101
+ build_exception_data(event)
102
+ in :message
103
+ build_message_data(event)
104
+ else
105
+ {}
106
+ end
107
+ end
108
+
109
+ def build_exception_data(event)
110
+ {
111
+ exception: {
112
+ type: event.exception_type,
113
+ message: event.exception_message,
114
+ backtrace: event.backtrace_frames&.map(&:to_h)
115
+ }.compact_blank
116
+ }
117
+ end
118
+
119
+ def build_message_data(event)
120
+ {
121
+ message: event.message
122
+ }
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/module/attribute_accessors"
5
+ require "active_support/notifications"
6
+ require "active_support/core_ext/numeric/bytes"
7
+
8
+ module Lapsoss
9
+ module Adapters
10
+ module Concerns
11
+ module HttpDelivery
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ class_attribute :api_endpoint, instance_writer: false
16
+ class_attribute :api_path, default: "/", instance_writer: false
17
+
18
+ # Memoized git info using AS
19
+ mattr_accessor :git_info_cache, default: {}
20
+ end
21
+
22
+ # Unified HTTP delivery with instrumentation
23
+ def deliver(event)
24
+ return unless enabled?
25
+
26
+ payload = build_payload(event)
27
+ return if payload.blank?
28
+
29
+ body, compressed = serialize_payload(payload)
30
+ headers = build_delivery_headers(compressed: compressed)
31
+
32
+ ActiveSupport::Notifications.instrument("deliver.lapsoss",
33
+ adapter: self.class.name,
34
+ event_type: event.type,
35
+ compressed: compressed,
36
+ size: body.bytesize
37
+ ) do
38
+ response = http_client.post(api_path, body: body, headers: headers)
39
+ handle_response(response)
40
+ end
41
+ rescue => error
42
+ handle_delivery_error(error)
43
+ end
44
+
45
+ # Common headers for all adapters
46
+ def build_delivery_headers(compressed: false, content_type: "application/json")
47
+ {
48
+ "User-Agent" => user_agent,
49
+ "Content-Type" => content_type,
50
+ "Content-Encoding" => ("gzip" if compressed),
51
+ "X-Lapsoss-Version" => Lapsoss::VERSION
52
+ }.merge(adapter_specific_headers).compact_blank
53
+ end
54
+
55
+ # Override for adapter-specific headers
56
+ def adapter_specific_headers
57
+ {}
58
+ end
59
+
60
+ # Git info with AS memoization
61
+ def git_branch
62
+ self.class.git_info_cache[:branch] ||= begin
63
+ `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip.presence
64
+ rescue
65
+ nil
66
+ end
67
+ end
68
+
69
+ def git_sha
70
+ self.class.git_info_cache[:sha] ||= begin
71
+ `git rev-parse HEAD 2>/dev/null`.strip.presence
72
+ rescue
73
+ nil
74
+ end
75
+ end
76
+
77
+ # Common response handling
78
+ def handle_response(response)
79
+ code = response.respond_to?(:status) ? response.status.to_i : response.code.to_i
80
+ case code
81
+ when 200..299
82
+ ActiveSupport::Notifications.instrument("success.lapsoss",
83
+ adapter: self.class.name,
84
+ response_code: code
85
+ )
86
+ true
87
+ when 429
88
+ raise DeliveryError.new("Rate limit exceeded", response: response)
89
+ when 401, 403
90
+ raise DeliveryError.new("Authentication failed", response: response)
91
+ when 400..499
92
+ handle_client_error(response)
93
+ else
94
+ raise DeliveryError.new("Server error: #{code}", response: response)
95
+ end
96
+ end
97
+
98
+ def handle_client_error(response)
99
+ body = ActiveSupport::JSON.decode(response.body) rescue {}
100
+ message = body["message"].presence || body["error"].presence || "Bad request"
101
+ raise DeliveryError.new("Client error: #{message}", response: response)
102
+ end
103
+
104
+ def handle_delivery_error(error)
105
+ ActiveSupport::Notifications.instrument("error.lapsoss",
106
+ adapter: self.class.name,
107
+ error: error.class.name,
108
+ message: error.message
109
+ )
110
+
111
+ Lapsoss.configuration.logger&.error("[#{self.class.name}] Delivery failed: #{error.message}")
112
+ Lapsoss.configuration.error_handler&.call(error)
113
+
114
+ raise error if error.is_a?(DeliveryError)
115
+ raise DeliveryError.new("Delivery failed: #{error.message}", cause: error)
116
+ end
117
+
118
+ private
119
+
120
+ def http_client
121
+ @http_client ||= create_http_client(api_endpoint)
122
+ end
123
+
124
+ def user_agent
125
+ "Lapsoss/#{Lapsoss::VERSION} Ruby/#{RUBY_VERSION} Rails/#{Rails.version if defined?(Rails)}"
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+
6
+ module Lapsoss
7
+ module Adapters
8
+ module Concerns
9
+ module LevelMapping
10
+ extend ActiveSupport::Concern
11
+
12
+ # Single source of truth for level mappings
13
+ LEVEL_MAPPINGS = {
14
+ sentry: {
15
+ debug: "debug",
16
+ info: "info",
17
+ warn: "warning",
18
+ warning: "warning",
19
+ error: "error",
20
+ fatal: "fatal"
21
+ }.with_indifferent_access,
22
+
23
+ rollbar: {
24
+ debug: "debug",
25
+ info: "info",
26
+ warning: "warning",
27
+ error: "error",
28
+ fatal: "critical"
29
+ }.with_indifferent_access,
30
+
31
+ bugsnag: {
32
+ debug: "info",
33
+ info: "info",
34
+ warning: "warning",
35
+ error: "error",
36
+ fatal: "error"
37
+ }.with_indifferent_access,
38
+
39
+ appsignal: {
40
+ debug: "debug",
41
+ info: "info",
42
+ warning: "warning",
43
+ error: "error",
44
+ fatal: "error",
45
+ critical: "error"
46
+ }.with_indifferent_access
47
+ }.freeze
48
+
49
+ included do
50
+ # Define which mapping this adapter uses
51
+ class_attribute :level_mapping_type, default: :sentry
52
+ end
53
+
54
+ # Map level using the adapter's configured mapping
55
+ def map_level(level)
56
+ mapping = LEVEL_MAPPINGS[self.class.level_mapping_type]
57
+ mapping[level] || mapping[:info]
58
+ end
59
+
60
+ # Map severity (alias for bugsnag compatibility)
61
+ alias_method :map_severity, :map_level
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require "active_support/json"
4
4
 
5
5
  module Lapsoss
6
6
  module Adapters
7
7
  class InsightHubAdapter < Base
8
8
  API_URI = "https://notify.bugsnag.com"
9
- JSON_CONTENT_TYPE = "application/json"
10
9
 
11
10
  def initialize(name, settings = {})
12
11
  super
13
12
  @api_key = settings[:api_key] || ENV.fetch("INSIGHT_HUB_API_KEY", nil)
14
13
 
15
- validate_settings!
14
+ if @api_key.blank?
15
+ Lapsoss.configuration.logger&.warn "[Lapsoss::InsightHubAdapter] No API key provided, adapter disabled"
16
+ @enabled = false
17
+ return
18
+ else
19
+ validate_api_key!(@api_key, "Insight Hub API key", format: :alphanumeric)
20
+ end
16
21
 
17
22
  @client = create_http_client(API_URI)
18
23
  @backtrace_processor = BacktraceProcessor.new
@@ -24,11 +29,15 @@ module Lapsoss
24
29
  payload = build_payload(event)
25
30
  return unless payload
26
31
 
27
- response = @client.post("/", body: payload.to_json, headers: {
28
- "Content-Type" => JSON_CONTENT_TYPE,
29
- "Bugsnag-Api-Key" => @api_key,
30
- "Bugsnag-Payload-Version" => "5"
31
- })
32
+ headers = default_headers(
33
+ content_type: json_content_type,
34
+ extra: {
35
+ "Bugsnag-Api-Key" => @api_key,
36
+ "Bugsnag-Payload-Version" => "5"
37
+ }
38
+ )
39
+
40
+ response = @client.post("/", body: ActiveSupport::JSON.encode(payload), headers: headers)
32
41
 
33
42
  handle_response(response, event)
34
43
  rescue StandardError => e
@@ -116,16 +125,7 @@ module Lapsoss
116
125
  end
117
126
 
118
127
  def build_breadcrumbs(event)
119
- breadcrumbs = event.context[:breadcrumbs] || []
120
-
121
- breadcrumbs.map do |crumb|
122
- {
123
- timestamp: crumb[:timestamp]&.iso8601,
124
- name: crumb[:message],
125
- type: crumb[:type] || "manual",
126
- metaData: crumb[:data] || {}
127
- }
128
- end
128
+ Breadcrumb.for_insight_hub(event.context[:breadcrumbs] || [])
129
129
  end
130
130
 
131
131
  def build_user_data(event)
@@ -161,7 +161,7 @@ module Lapsoss
161
161
  true
162
162
  when 400
163
163
  body = begin
164
- JSON.parse(response.body)
164
+ ActiveSupport::JSON.decode(response.body)
165
165
  rescue
166
166
  {}
167
167
  end
@@ -177,9 +177,9 @@ module Lapsoss
177
177
  end
178
178
  end
179
179
 
180
+ # No longer need strict validation
180
181
  def validate_settings!
181
- validate_presence!(@api_key, "Insight Hub API key")
182
- validate_api_key!(@api_key, "Insight Hub API key", format: :alphanumeric) if @api_key
182
+ # Validation moved to initialize with logging
183
183
  end
184
184
 
185
185
  def handle_delivery_error(error, response = nil)