lapsoss 0.2.0 → 0.3.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +22 -22
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
  6. data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
  7. data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
  8. data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
  9. data/lib/lapsoss/backtrace_frame.rb +37 -206
  10. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  11. data/lib/lapsoss/backtrace_processor.rb +26 -23
  12. data/lib/lapsoss/client.rb +2 -4
  13. data/lib/lapsoss/configuration.rb +28 -32
  14. data/lib/lapsoss/current.rb +10 -2
  15. data/lib/lapsoss/event.rb +28 -5
  16. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  17. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  18. data/lib/lapsoss/exclusion_filter.rb +0 -273
  19. data/lib/lapsoss/exclusion_presets.rb +249 -0
  20. data/lib/lapsoss/fingerprinter.rb +28 -28
  21. data/lib/lapsoss/http_client.rb +8 -8
  22. data/lib/lapsoss/merged_scope.rb +63 -0
  23. data/lib/lapsoss/middleware/base.rb +15 -0
  24. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  25. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  26. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  27. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  28. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  29. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  30. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  31. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  32. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  33. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  34. data/lib/lapsoss/middleware.rb +0 -339
  35. data/lib/lapsoss/pipeline.rb +0 -68
  36. data/lib/lapsoss/pipeline_builder.rb +69 -0
  37. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  38. data/lib/lapsoss/rails_middleware.rb +78 -0
  39. data/lib/lapsoss/railtie.rb +22 -50
  40. data/lib/lapsoss/registry.rb +18 -5
  41. data/lib/lapsoss/release_providers.rb +110 -0
  42. data/lib/lapsoss/release_tracker.rb +159 -232
  43. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  44. data/lib/lapsoss/sampling/base.rb +11 -0
  45. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  46. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  47. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  48. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  49. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  50. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  51. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  52. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  53. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  54. data/lib/lapsoss/sampling.rb +0 -322
  55. data/lib/lapsoss/scope.rb +12 -48
  56. data/lib/lapsoss/scrubber.rb +7 -7
  57. data/lib/lapsoss/user_context.rb +30 -203
  58. data/lib/lapsoss/user_context_integrations.rb +39 -0
  59. data/lib/lapsoss/user_context_middleware.rb +50 -0
  60. data/lib/lapsoss/user_context_provider.rb +93 -0
  61. data/lib/lapsoss/utils.rb +13 -0
  62. data/lib/lapsoss/validators.rb +15 -15
  63. data/lib/lapsoss/version.rb +1 -1
  64. data/lib/lapsoss.rb +3 -3
  65. metadata +54 -5
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'socket'
3
+ require "json"
4
+ require "socket"
5
5
 
6
6
  module Lapsoss
7
7
  module Adapters
8
8
  class AppsignalAdapter < Base
9
- PUSH_API_URI = 'https://push.appsignal.com'
10
- ERRORS_API_URI = 'https://appsignal-endpoint.net'
11
- JSON_CONTENT_TYPE = 'application/json; charset=UTF-8'
9
+ PUSH_API_URI = "https://push.appsignal.com"
10
+ ERRORS_API_URI = "https://appsignal-endpoint.net"
11
+ JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
12
12
 
13
13
  def initialize(name, settings = {})
14
14
  super
15
- @push_api_key = settings[:push_api_key] || ENV.fetch('APPSIGNAL_PUSH_API_KEY', nil)
16
- @frontend_api_key = settings[:frontend_api_key] || ENV.fetch('APPSIGNAL_FRONTEND_API_KEY', nil)
17
- @app_name = settings[:app_name] || ENV.fetch('APPSIGNAL_APP_NAME', nil)
15
+ @push_api_key = settings[:push_api_key] || ENV.fetch("APPSIGNAL_PUSH_API_KEY", nil)
16
+ @frontend_api_key = settings[:frontend_api_key] || ENV.fetch("APPSIGNAL_FRONTEND_API_KEY", nil)
17
+ @app_name = settings[:app_name] || ENV.fetch("APPSIGNAL_APP_NAME", nil)
18
18
  @environment = Lapsoss.configuration.environment
19
19
 
20
20
  validate_settings!
@@ -30,7 +30,7 @@ module Lapsoss
30
30
  return unless payload
31
31
 
32
32
  path = "/errors?api_key=#{@frontend_api_key}"
33
- headers = { 'Content-Type' => JSON_CONTENT_TYPE }
33
+ headers = { "Content-Type" => JSON_CONTENT_TYPE }
34
34
 
35
35
  begin
36
36
  @errors_client.post(path, body: JSON.generate(payload), headers: headers)
@@ -64,7 +64,7 @@ module Lapsoss
64
64
  def build_exception_payload(event)
65
65
  {
66
66
  timestamp: event.timestamp.to_i,
67
- namespace: event.context[:namespace] || 'backend',
67
+ namespace: event.context[:namespace] || "backend",
68
68
  error: {
69
69
  name: event.exception.class.name,
70
70
  message: event.exception.message,
@@ -86,18 +86,18 @@ module Lapsoss
86
86
  # Log when messages are dropped due to level filtering
87
87
  Lapsoss.configuration.logger&.debug(
88
88
  "[Lapsoss::AppsignalAdapter] Dropping message with level '#{event.level}' - " \
89
- 'AppSignal only supports :error, :fatal, and :critical levels for messages'
89
+ "AppSignal only supports :error, :fatal, and :critical levels for messages"
90
90
  )
91
91
  return nil
92
92
  end
93
93
 
94
94
  {
95
- action: event.context[:action] || 'log_message',
96
- path: event.context[:path] || '/',
95
+ action: event.context[:action] || "log_message",
96
+ path: event.context[:path] || "/",
97
97
  exception: {
98
98
  # AppSignal requires exception format for messages - this isn't a real exception
99
99
  # but rather a way to send structured log messages through their error API
100
- name: 'LogMessage', # Clear indication this is a log message
100
+ name: "LogMessage", # Clear indication this is a log message
101
101
  message: event.message,
102
102
  backtrace: [] # No fake backtrace for log messages
103
103
  },
@@ -110,9 +110,9 @@ module Lapsoss
110
110
 
111
111
  def build_environment_context(event)
112
112
  {
113
- 'hostname' => Socket.gethostname,
114
- 'app_name' => @app_name,
115
- 'environment' => @environment
113
+ "hostname" => Socket.gethostname,
114
+ "app_name" => @app_name,
115
+ "environment" => @environment
116
116
  }.merge(stringify_hash(event.context[:environment] || {}))
117
117
  end
118
118
 
@@ -122,13 +122,13 @@ module Lapsoss
122
122
 
123
123
  def validate_settings!
124
124
  unless @push_api_key || @frontend_api_key
125
- raise ValidationError, 'AppSignal API key is required (either push_api_key or frontend_api_key)'
125
+ raise ValidationError, "AppSignal API key is required (either push_api_key or frontend_api_key)"
126
126
  end
127
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
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
132
132
  end
133
133
  end
134
134
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../http_client'
4
- require_relative '../validators'
5
-
6
3
  module Lapsoss
7
4
  module Adapters
8
5
  class Base
@@ -1,28 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'socket'
3
+ require "json"
5
4
 
6
5
  module Lapsoss
7
6
  module Adapters
8
- # Adapter for Insight Hub (formerly Bugsnag)
9
- # Note: The API endpoints still use bugsnag.com domains for backwards compatibility
10
7
  class InsightHubAdapter < Base
11
- NOTIFY_URI = 'https://notify.bugsnag.com'
12
- SESSION_URI = 'https://sessions.bugsnag.com'
13
- JSON_CONTENT_TYPE = 'application/json'
8
+ API_URI = "https://notify.bugsnag.com"
9
+ JSON_CONTENT_TYPE = "application/json"
14
10
 
15
11
  def initialize(name, settings = {})
16
12
  super
17
- @api_key = settings[:api_key] || ENV['INSIGHT_HUB_API_KEY'] || ENV.fetch('BUGSNAG_API_KEY', nil)
18
- @release_stage = settings[:release_stage] || Lapsoss.configuration.environment
19
- @app_version = settings[:app_version]
20
- @app_type = settings[:app_type] || 'ruby'
13
+ @api_key = settings[:api_key] || ENV.fetch("INSIGHT_HUB_API_KEY", nil)
21
14
 
22
15
  validate_settings!
23
16
 
24
- @notify_client = create_http_client(NOTIFY_URI)
25
- @session_client = create_http_client(SESSION_URI) if settings[:enable_sessions]
17
+ @client = create_http_client(API_URI)
26
18
  @backtrace_processor = BacktraceProcessor.new
27
19
  end
28
20
 
@@ -32,133 +24,113 @@ module Lapsoss
32
24
  payload = build_payload(event)
33
25
  return unless payload
34
26
 
35
- headers = {
36
- 'Content-Type' => JSON_CONTENT_TYPE,
37
- 'Bugsnag-Api-Key' => @api_key,
38
- 'Bugsnag-Payload-Version' => '5.0',
39
- 'Bugsnag-Sent-At' => Time.now.utc.iso8601
40
- }
41
-
42
- begin
43
- @notify_client.post('/', body: JSON.generate(payload), headers: headers)
44
- rescue DeliveryError => e
45
- # Log the error and potentially notify error handler
46
- Lapsoss.configuration.logger&.error("[Lapsoss::InsightHubAdapter] Failed to deliver event: #{e.message}")
47
- Lapsoss.configuration.error_handler&.call(e)
48
-
49
- # Re-raise to let the caller know delivery failed
50
- raise
51
- end
52
- end
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
+ })
53
32
 
54
- def shutdown
55
- @notify_client&.shutdown
56
- @session_client&.shutdown
57
- super
33
+ handle_response(response, event)
34
+ rescue StandardError => e
35
+ handle_delivery_error(e)
58
36
  end
59
37
 
60
38
  def capabilities
61
39
  super.merge(
62
- code_context: true,
63
40
  breadcrumbs: true,
41
+ user_tracking: true,
42
+ custom_context: true,
43
+ release_tracking: true,
64
44
  sessions: true
65
45
  )
66
46
  end
67
47
 
48
+ def validate!
49
+ validate_settings!
50
+ true
51
+ end
52
+
68
53
  private
69
54
 
70
55
  def build_payload(event)
71
56
  {
72
57
  apiKey: @api_key,
73
- payloadVersion: '5.0',
58
+ payloadVersion: "5",
74
59
  notifier: {
75
- name: 'Lapsoss',
60
+ name: "Lapsoss Ruby",
76
61
  version: Lapsoss::VERSION,
77
- url: 'https://github.com/seuros/lapsoss'
62
+ url: "https://github.com/yourusername/lapsoss"
78
63
  },
79
- events: [build_event(event)]
64
+ events: [ build_event(event) ]
80
65
  }
81
66
  end
82
67
 
83
68
  def build_event(event)
84
- base_event = {
85
- app: {
86
- version: @app_version,
87
- releaseStage: @release_stage,
88
- type: @app_type
89
- },
90
- device: {
91
- hostname: Socket.gethostname,
92
- runtimeVersions: {
93
- ruby: RUBY_VERSION
94
- }
95
- },
69
+ {
70
+ app: build_app_data(event),
71
+ device: build_device_data,
72
+ exceptions: build_exceptions(event),
73
+ breadcrumbs: build_breadcrumbs(event),
74
+ request: event.request_context,
75
+ user: build_user_data(event),
76
+ context: event.context[:custom]&.dig(:context) || "production",
96
77
  severity: map_severity(event.level),
97
78
  unhandled: event.context[:unhandled] || false,
98
- severityReason: {
99
- type: event.context[:unhandled] ? 'unhandledException' : 'handledException'
100
- },
101
- user: build_user(event),
102
- context: event.context[:context],
103
- groupingHash: event.context[:fingerprint],
104
- breadcrumbs: build_breadcrumbs(event),
105
- metaData: event.context[:extra] || {}
106
- }
79
+ metaData: event.context[:custom] || {}
80
+ }.compact
81
+ end
107
82
 
108
- case event.type
109
- when :exception
110
- base_event.merge(build_exception_data(event))
111
- when :message
112
- base_event.merge(build_message_data(event))
113
- end
83
+ def build_exceptions(event)
84
+ return [] unless event.type == :exception && event.exception
85
+
86
+ [ {
87
+ errorClass: event.exception_type,
88
+ message: event.message,
89
+ stacktrace: build_stacktrace(event.exception),
90
+ type: "ruby"
91
+ } ]
114
92
  end
115
93
 
116
- def build_exception_data(event)
117
- {
118
- exceptions: [{
119
- errorClass: event.exception.class.name,
120
- message: event.exception.message,
121
- stacktrace: build_stacktrace(event.exception),
122
- type: 'ruby'
123
- }]
124
- }
94
+ def build_stacktrace(exception)
95
+ frames = @backtrace_processor.process_exception(exception, follow_cause: true)
96
+ @backtrace_processor.format_frames(frames, :bugsnag)
125
97
  end
126
98
 
127
- def build_message_data(event)
99
+ def build_app_data(event)
128
100
  {
129
- exceptions: [{
130
- # Insight Hub (Bugsnag) requires exception format for messages - this isn't a real exception
131
- # but rather a way to send structured log messages through their error API
132
- errorClass: 'LogMessage', # Clear indication this is a log message
133
- message: event.message,
134
- stacktrace: [], # No fake backtrace for log messages
135
- type: 'log' # Mark as log type, not ruby exception
136
- }]
137
- }
101
+ id: event.context[:app]&.dig(:id),
102
+ version: event.context[:release]&.dig(:version),
103
+ releaseStage: @environment || "production",
104
+ type: detect_app_type
105
+ }.compact
138
106
  end
139
107
 
140
- def build_stacktrace(exception)
141
- return [] unless exception
142
-
143
- frames = @backtrace_processor.process_exception(exception)
144
- @backtrace_processor.format_frames(frames, :bugsnag)
108
+ def build_device_data
109
+ {
110
+ hostname: Socket.gethostname,
111
+ osName: RUBY_PLATFORM,
112
+ runtimeVersions: {
113
+ ruby: RUBY_VERSION
114
+ }
115
+ }
145
116
  end
146
117
 
147
118
  def build_breadcrumbs(event)
148
119
  breadcrumbs = event.context[:breadcrumbs] || []
120
+
149
121
  breadcrumbs.map do |crumb|
150
122
  {
151
- timestamp: crumb[:timestamp]&.iso8601 || Time.now.utc.iso8601,
152
- name: crumb[:message] || crumb[:name],
153
- type: crumb[:type] || 'manual',
123
+ timestamp: crumb[:timestamp]&.iso8601,
124
+ name: crumb[:message],
125
+ type: crumb[:type] || "manual",
154
126
  metaData: crumb[:data] || {}
155
127
  }
156
128
  end
157
129
  end
158
130
 
159
- def build_user(event)
131
+ def build_user_data(event)
160
132
  user = event.context[:user]
161
- return unless user
133
+ return nil unless user
162
134
 
163
135
  {
164
136
  id: user[:id]&.to_s,
@@ -169,18 +141,50 @@ module Lapsoss
169
141
 
170
142
  def map_severity(level)
171
143
  case level
172
- when :debug, :info then 'info'
173
- when :warning, :warn then 'warning'
174
- when :error, :fatal, :critical then 'error'
175
- else 'error'
144
+ when :debug, :info then "info"
145
+ when :warning then "warning"
146
+ when :error, :fatal then "error"
147
+ else "error"
148
+ end
149
+ end
150
+
151
+ def detect_app_type
152
+ return "rails" if defined?(Rails)
153
+ return "rack" if defined?(Rack)
154
+
155
+ "ruby"
156
+ end
157
+
158
+ def handle_response(response, _event)
159
+ case response.status
160
+ when 200
161
+ true
162
+ when 400
163
+ body = begin
164
+ JSON.parse(response.body)
165
+ rescue
166
+ {}
167
+ end
168
+ raise DeliveryError.new("Bad request: #{body['errors']&.join(', ')}", response: response)
169
+ when 401
170
+ raise DeliveryError.new("Unauthorized: Invalid API key", response: response)
171
+ when 413
172
+ raise DeliveryError.new("Payload too large", response: response)
173
+ when 429
174
+ raise DeliveryError.new("Rate limit exceeded", response: response)
175
+ else
176
+ raise DeliveryError.new("Unexpected response: #{response.status}", response: response)
176
177
  end
177
178
  end
178
179
 
179
180
  def validate_settings!
180
- validate_presence!(@api_key, 'Insight Hub API key')
181
- validate_api_key!(@api_key, 'Insight Hub API key', format: :alphanumeric) if @api_key
182
- validate_environment!(@app_version, 'Insight Hub app version') if @app_version
183
- validate_type!(@release_stage, [String, Symbol], 'Insight Hub release stage') if @release_stage
181
+ validate_presence!(@api_key, "Insight Hub API key")
182
+ validate_api_key!(@api_key, "Insight Hub API key", format: :alphanumeric) if @api_key
183
+ end
184
+
185
+ def handle_delivery_error(error, response = nil)
186
+ message = "Insight Hub delivery failed: #{error.message}"
187
+ raise DeliveryError.new(message, response: response, cause: error)
184
188
  end
185
189
  end
186
190
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logger'
3
+ require "logger"
4
4
 
5
5
  module Lapsoss
6
6
  module Adapters
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'socket'
5
- require 'securerandom'
3
+ require "json"
4
+ require "socket"
5
+ require "securerandom"
6
6
 
7
7
  module Lapsoss
8
8
  module Adapters
9
9
  class RollbarAdapter < Base
10
- API_URI = 'https://api.rollbar.com'
11
- API_VERSION = '1'
12
- JSON_CONTENT_TYPE = 'application/json'
10
+ API_URI = "https://api.rollbar.com"
11
+ API_VERSION = "1"
12
+ JSON_CONTENT_TYPE = "application/json"
13
13
 
14
14
  def initialize(name, settings = {})
15
15
  super
16
- @access_token = settings[:access_token] || ENV.fetch('ROLLBAR_ACCESS_TOKEN', nil)
17
- @environment = settings[:environment] || Lapsoss.configuration.environment || 'development'
16
+ @access_token = settings[:access_token] || ENV.fetch("ROLLBAR_ACCESS_TOKEN", nil)
17
+ @environment = settings[:environment] || Lapsoss.configuration.environment || "development"
18
18
 
19
19
  validate_settings!
20
20
 
@@ -28,62 +28,50 @@ module Lapsoss
28
28
  payload = build_payload(event)
29
29
  return unless payload
30
30
 
31
- path = "/api/#{API_VERSION}/item/"
32
- headers = {
33
- 'Content-Type' => JSON_CONTENT_TYPE,
34
- 'X-Rollbar-Access-Token' => @access_token
35
- }
36
-
37
- begin
38
- @client.post(path, body: JSON.generate(payload), headers: headers)
39
- rescue DeliveryError => e
40
- # Log the error and potentially notify error handler
41
- Lapsoss.configuration.logger&.error("[Lapsoss::RollbarAdapter] Failed to deliver event: #{e.message}")
42
- Lapsoss.configuration.error_handler&.call(e)
43
-
44
- # Re-raise to let the caller know delivery failed
45
- raise
46
- end
47
- end
31
+ response = @client.post("/api/#{API_VERSION}/item/", body: payload.to_json, headers: {
32
+ "Content-Type" => JSON_CONTENT_TYPE,
33
+ "X-Rollbar-Access-Token" => @access_token
34
+ })
48
35
 
49
- def shutdown
50
- @client&.shutdown
51
- super
36
+ handle_response(response, event)
37
+ rescue StandardError => e
38
+ handle_delivery_error(e)
52
39
  end
53
40
 
54
41
  def capabilities
55
42
  super.merge(
56
- code_context: true
43
+ breadcrumbs: true,
44
+ user_tracking: true,
45
+ custom_context: true,
46
+ release_tracking: true
57
47
  )
58
48
  end
59
49
 
50
+ def validate!
51
+ validate_settings!
52
+ true
53
+ end
54
+
60
55
  private
61
56
 
62
57
  def build_payload(event)
63
- data = {
64
- environment: @environment,
65
- body: build_body(event),
66
- level: map_level(event.level),
67
- timestamp: event.timestamp.to_i,
68
- platform: 'ruby',
69
- language: 'ruby',
70
- framework: detect_framework,
71
- context: event.context[:context],
72
- request: event.context[:request],
73
- person: build_person(event),
74
- custom: event.context[:extra] || {},
75
- title: event.message || (event.exception&.message if event.type == :exception),
76
- uuid: SecureRandom.uuid,
77
- notifier: {
78
- name: 'lapsoss',
79
- version: Lapsoss::VERSION
58
+ {
59
+ access_token: @access_token,
60
+ data: {
61
+ environment: @environment,
62
+ body: build_body(event),
63
+ level: map_level(event.level),
64
+ timestamp: event.timestamp.to_i,
65
+ code_version: event.context[:release]&.dig(:commit_sha),
66
+ platform: "ruby",
67
+ language: "ruby",
68
+ framework: detect_framework,
69
+ server: build_server_data,
70
+ person: build_person_data(event),
71
+ request: event.request_context,
72
+ custom: event.context[:custom] || {}
80
73
  }
81
74
  }
82
-
83
- # Only add fingerprint if it exists
84
- data[:fingerprint] = event.context[:fingerprint].to_s if event.context[:fingerprint]
85
-
86
- { data: data }
87
75
  end
88
76
 
89
77
  def build_body(event)
@@ -93,9 +81,9 @@ module Lapsoss
93
81
  trace: {
94
82
  frames: build_backtrace_frames(event.exception),
95
83
  exception: {
96
- class: event.exception.class.name,
97
- message: event.exception.message,
98
- description: event.exception.message
84
+ class: event.exception_type,
85
+ message: event.message,
86
+ description: event.exception.to_s
99
87
  }
100
88
  }
101
89
  }
@@ -117,9 +105,9 @@ module Lapsoss
117
105
  formatted_frames.reverse
118
106
  end
119
107
 
120
- def build_person(event)
108
+ def build_person_data(event)
121
109
  user = event.context[:user]
122
- return unless user
110
+ return nil unless user
123
111
 
124
112
  {
125
113
  id: user[:id]&.to_s,
@@ -128,28 +116,80 @@ module Lapsoss
128
116
  }.compact
129
117
  end
130
118
 
119
+ def build_server_data
120
+ {
121
+ host: Socket.gethostname,
122
+ root: defined?(Rails) ? Rails.root.to_s : Dir.pwd,
123
+ branch: git_branch,
124
+ code_version: git_sha
125
+ }.compact
126
+ end
127
+
131
128
  def map_level(level)
132
129
  case level
133
- when :debug then 'debug'
134
- when :info then 'info'
135
- when :warning, :warn then 'warning'
136
- when :error then 'error'
137
- when :fatal, :critical then 'critical'
138
- else 'error'
130
+ when :debug then "debug"
131
+ when :info then "info"
132
+ when :warning then "warning"
133
+ when :error then "error"
134
+ when :fatal then "critical"
135
+ else "error"
139
136
  end
140
137
  end
141
138
 
142
139
  def detect_framework
143
- return 'rails' if defined?(Rails)
144
- return 'sinatra' if defined?(Sinatra)
140
+ return "rails" if defined?(Rails)
141
+ return "sinatra" if defined?(Sinatra)
142
+
143
+ "ruby"
144
+ end
145
+
146
+ def git_branch
147
+ `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip.presence
148
+ rescue StandardError
149
+ nil
150
+ end
151
+
152
+ def git_sha
153
+ `git rev-parse HEAD 2>/dev/null`.strip.presence
154
+ rescue StandardError
155
+ nil
156
+ end
145
157
 
146
- 'ruby'
158
+ def handle_response(response, event)
159
+ case response.code.to_i
160
+ when 200
161
+ true
162
+ when 400
163
+ handle_client_error(response, event)
164
+ when 401
165
+ raise DeliveryError.new("Unauthorized: Invalid access token", response: response)
166
+ when 429
167
+ raise DeliveryError.new("Rate limit exceeded", response: response)
168
+ else
169
+ raise DeliveryError.new("Unexpected response: #{response.code}", response: response)
170
+ end
171
+ end
172
+
173
+ def handle_client_error(response, _event)
174
+ body = begin
175
+ JSON.parse(response.body)
176
+ rescue
177
+ {}
178
+ end
179
+ error_msg = body["message"] || "Bad request"
180
+
181
+ raise DeliveryError.new("Client error: #{error_msg}", response: response)
147
182
  end
148
183
 
149
184
  def validate_settings!
150
- validate_presence!(@access_token, 'Rollbar access token')
151
- validate_api_key!(@access_token, 'Rollbar access token', format: :alphanumeric) if @access_token
152
- validate_environment!(@environment, 'Rollbar environment') if @environment
185
+ validate_presence!(@access_token, "Rollbar access token")
186
+ validate_api_key!(@access_token, "Rollbar access token", format: :alphanumeric) if @access_token
187
+ validate_environment!(@environment, "Rollbar environment") if @environment
188
+ end
189
+
190
+ def handle_delivery_error(error, response = nil)
191
+ message = "Rollbar delivery failed: #{error.message}"
192
+ raise DeliveryError.new(message, response: response, cause: error)
153
193
  end
154
194
  end
155
195
  end