lapsoss 0.1.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +7 -8
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/bugsnag_adapter.rb +12 -0
  6. data/lib/lapsoss/adapters/insight_hub_adapter.rb +102 -101
  7. data/lib/lapsoss/adapters/logger_adapter.rb +7 -7
  8. data/lib/lapsoss/adapters/rollbar_adapter.rb +93 -54
  9. data/lib/lapsoss/adapters/sentry_adapter.rb +11 -17
  10. data/lib/lapsoss/backtrace_frame.rb +35 -214
  11. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  12. data/lib/lapsoss/backtrace_processor.rb +37 -37
  13. data/lib/lapsoss/client.rb +2 -6
  14. data/lib/lapsoss/configuration.rb +25 -22
  15. data/lib/lapsoss/current.rb +9 -1
  16. data/lib/lapsoss/event.rb +30 -6
  17. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  18. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  19. data/lib/lapsoss/exclusion_filter.rb +156 -0
  20. data/lib/lapsoss/{exclusions.rb → exclusion_presets.rb} +1 -181
  21. data/lib/lapsoss/fingerprinter.rb +9 -13
  22. data/lib/lapsoss/http_client.rb +42 -8
  23. data/lib/lapsoss/merged_scope.rb +63 -0
  24. data/lib/lapsoss/middleware/base.rb +15 -0
  25. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  26. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  27. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  28. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  29. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  30. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  31. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  32. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  33. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  34. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  35. data/lib/lapsoss/middleware.rb +0 -347
  36. data/lib/lapsoss/pipeline.rb +1 -73
  37. data/lib/lapsoss/pipeline_builder.rb +69 -0
  38. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  39. data/lib/lapsoss/rails_middleware.rb +78 -0
  40. data/lib/lapsoss/railtie.rb +22 -50
  41. data/lib/lapsoss/registry.rb +34 -20
  42. data/lib/lapsoss/release_providers.rb +110 -0
  43. data/lib/lapsoss/release_tracker.rb +112 -207
  44. data/lib/lapsoss/router.rb +3 -5
  45. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  46. data/lib/lapsoss/sampling/base.rb +11 -0
  47. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  48. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  49. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  50. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  51. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  52. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  53. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  54. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  55. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  56. data/lib/lapsoss/sampling.rb +0 -326
  57. data/lib/lapsoss/scope.rb +17 -57
  58. data/lib/lapsoss/scrubber.rb +16 -18
  59. data/lib/lapsoss/user_context.rb +18 -198
  60. data/lib/lapsoss/user_context_integrations.rb +39 -0
  61. data/lib/lapsoss/user_context_middleware.rb +50 -0
  62. data/lib/lapsoss/user_context_provider.rb +93 -0
  63. data/lib/lapsoss/utils.rb +13 -0
  64. data/lib/lapsoss/validators.rb +14 -27
  65. data/lib/lapsoss/version.rb +1 -1
  66. data/lib/lapsoss.rb +12 -25
  67. metadata +106 -21
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "json"
4
4
  require "socket"
5
- require_relative "../http_client"
6
5
 
7
6
  module Lapsoss
8
7
  module Adapters
@@ -12,10 +11,10 @@ module Lapsoss
12
11
  JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
13
12
 
14
13
  def initialize(name, settings = {})
15
- super(name, settings)
16
- @push_api_key = settings[:push_api_key] || ENV["APPSIGNAL_PUSH_API_KEY"]
17
- @frontend_api_key = settings[:frontend_api_key] || ENV["APPSIGNAL_FRONTEND_API_KEY"]
18
- @app_name = settings[:app_name] || ENV["APPSIGNAL_APP_NAME"]
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)
19
18
  @environment = Lapsoss.configuration.environment
20
19
 
21
20
  validate_settings!
@@ -83,7 +82,7 @@ module Lapsoss
83
82
  # Instead of creating fake exceptions, we'll structure the message properly
84
83
  # but clearly indicate it's a log message, not an exception
85
84
 
86
- unless [:error, :fatal, :critical].include?(event.level)
85
+ unless %i[error fatal critical].include?(event.level)
87
86
  # Log when messages are dropped due to level filtering
88
87
  Lapsoss.configuration.logger&.debug(
89
88
  "[Lapsoss::AppsignalAdapter] Dropping message with level '#{event.level}' - " \
@@ -98,9 +97,9 @@ module Lapsoss
98
97
  exception: {
99
98
  # AppSignal requires exception format for messages - this isn't a real exception
100
99
  # but rather a way to send structured log messages through their error API
101
- name: "LogMessage", # Clear indication this is a log message
100
+ name: "LogMessage", # Clear indication this is a log message
102
101
  message: event.message,
103
- backtrace: [] # No fake backtrace for log messages
102
+ backtrace: [] # No fake backtrace for log messages
104
103
  },
105
104
  tags: stringify_hash(event.context[:tags]),
106
105
  params: stringify_hash(event.context[:params]),
@@ -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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Adapters
5
+ # Bugsnag adapter - backwards compatibility with InsightHub adapter
6
+ # This allows users to configure with :bugsnag type but uses InsightHub implementation
7
+ # The InsightHub adapter already checks for BUGSNAG_API_KEY environment variable
8
+ class BugsnagAdapter < InsightHubAdapter
9
+ # Inherits all functionality from InsightHubAdapter
10
+ end
11
+ end
12
+ end
@@ -1,30 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "socket"
5
- require_relative "../http_client"
6
- require_relative "../backtrace_processor"
7
4
 
8
5
  module Lapsoss
9
6
  module Adapters
10
- # Adapter for Insight Hub (formerly Bugsnag)
11
- # Note: The API endpoints still use bugsnag.com domains for backwards compatibility
12
7
  class InsightHubAdapter < Base
13
- NOTIFY_URI = "https://notify.bugsnag.com"
14
- SESSION_URI = "https://sessions.bugsnag.com"
8
+ API_URI = "https://notify.bugsnag.com"
15
9
  JSON_CONTENT_TYPE = "application/json"
16
10
 
17
11
  def initialize(name, settings = {})
18
- super(name, settings)
19
- @api_key = settings[:api_key] || ENV["INSIGHT_HUB_API_KEY"] || ENV["BUGSNAG_API_KEY"]
20
- @release_stage = settings[:release_stage] || Lapsoss.configuration.environment
21
- @app_version = settings[:app_version]
22
- @app_type = settings[:app_type] || "ruby"
12
+ super
13
+ @api_key = settings[:api_key] || ENV.fetch("INSIGHT_HUB_API_KEY", nil)
23
14
 
24
15
  validate_settings!
25
16
 
26
- @notify_client = create_http_client(NOTIFY_URI)
27
- @session_client = create_http_client(SESSION_URI) if settings[:enable_sessions]
17
+ @client = create_http_client(API_URI)
28
18
  @backtrace_processor = BacktraceProcessor.new
29
19
  end
30
20
 
@@ -34,134 +24,113 @@ module Lapsoss
34
24
  payload = build_payload(event)
35
25
  return unless payload
36
26
 
37
- headers = {
38
- "Content-Type" => JSON_CONTENT_TYPE,
39
- "Bugsnag-Api-Key" => @api_key,
40
- "Bugsnag-Payload-Version" => "5.0",
41
- "Bugsnag-Sent-At" => Time.now.utc.iso8601
42
- }
43
-
44
- begin
45
- @notify_client.post("/", body: JSON.generate(payload), headers: headers)
46
- rescue DeliveryError => e
47
- # Log the error and potentially notify error handler
48
- Lapsoss.configuration.logger&.error("[Lapsoss::InsightHubAdapter] Failed to deliver event: #{e.message}")
49
- Lapsoss.configuration.error_handler&.call(e)
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
+ })
50
32
 
51
- # Re-raise to let the caller know delivery failed
52
- raise
53
- end
54
- end
55
-
56
- def shutdown
57
- @notify_client&.shutdown
58
- @session_client&.shutdown
59
- super
33
+ handle_response(response, event)
34
+ rescue StandardError => e
35
+ handle_delivery_error(e)
60
36
  end
61
37
 
62
38
  def capabilities
63
39
  super.merge(
64
- code_context: true,
65
40
  breadcrumbs: true,
41
+ user_tracking: true,
42
+ custom_context: true,
43
+ release_tracking: true,
66
44
  sessions: true
67
45
  )
68
46
  end
69
47
 
48
+ def validate!
49
+ validate_settings!
50
+ true
51
+ end
52
+
70
53
  private
71
54
 
72
55
  def build_payload(event)
73
56
  {
74
57
  apiKey: @api_key,
75
- payloadVersion: "5.0",
58
+ payloadVersion: "5",
76
59
  notifier: {
77
- name: "Lapsoss",
60
+ name: "Lapsoss Ruby",
78
61
  version: Lapsoss::VERSION,
79
- url: "https://github.com/seuros/lapsoss"
62
+ url: "https://github.com/yourusername/lapsoss"
80
63
  },
81
- events: [build_event(event)]
64
+ events: [ build_event(event) ]
82
65
  }
83
66
  end
84
67
 
85
68
  def build_event(event)
86
- base_event = {
87
- app: {
88
- version: @app_version,
89
- releaseStage: @release_stage,
90
- type: @app_type
91
- },
92
- device: {
93
- hostname: Socket.gethostname,
94
- runtimeVersions: {
95
- ruby: RUBY_VERSION
96
- }
97
- },
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",
98
77
  severity: map_severity(event.level),
99
78
  unhandled: event.context[:unhandled] || false,
100
- severityReason: {
101
- type: event.context[:unhandled] ? "unhandledException" : "handledException"
102
- },
103
- user: build_user(event),
104
- context: event.context[:context],
105
- groupingHash: event.context[:fingerprint],
106
- breadcrumbs: build_breadcrumbs(event),
107
- metaData: event.context[:extra] || {}
108
- }
109
-
110
- case event.type
111
- when :exception
112
- base_event.merge(build_exception_data(event))
113
- when :message
114
- base_event.merge(build_message_data(event))
115
- end
79
+ metaData: event.context[:custom] || {}
80
+ }.compact
116
81
  end
117
82
 
118
- def build_exception_data(event)
119
- {
120
- exceptions: [{
121
- errorClass: event.exception.class.name,
122
- message: event.exception.message,
123
- stacktrace: build_stacktrace(event.exception),
124
- type: "ruby"
125
- }]
126
- }
127
- end
83
+ def build_exceptions(event)
84
+ return [] unless event.type == :exception && event.exception
128
85
 
129
- def build_message_data(event)
130
- {
131
- exceptions: [{
132
- # Insight Hub (Bugsnag) requires exception format for messages - this isn't a real exception
133
- # but rather a way to send structured log messages through their error API
134
- errorClass: "LogMessage", # Clear indication this is a log message
135
- message: event.message,
136
- stacktrace: [], # No fake backtrace for log messages
137
- type: "log" # Mark as log type, not ruby exception
138
- }]
139
- }
86
+ [ {
87
+ errorClass: event.exception_type,
88
+ message: event.message,
89
+ stacktrace: build_stacktrace(event.exception),
90
+ type: "ruby"
91
+ } ]
140
92
  end
141
93
 
142
94
  def build_stacktrace(exception)
143
- return [] unless exception
144
-
145
- frames = @backtrace_processor.process_exception(exception)
95
+ frames = @backtrace_processor.process_exception(exception, follow_cause: true)
146
96
  @backtrace_processor.format_frames(frames, :bugsnag)
147
97
  end
148
98
 
99
+ def build_app_data(event)
100
+ {
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
106
+ end
107
+
108
+ def build_device_data
109
+ {
110
+ hostname: Socket.gethostname,
111
+ osName: RUBY_PLATFORM,
112
+ runtimeVersions: {
113
+ ruby: RUBY_VERSION
114
+ }
115
+ }
116
+ end
149
117
 
150
118
  def build_breadcrumbs(event)
151
119
  breadcrumbs = event.context[:breadcrumbs] || []
120
+
152
121
  breadcrumbs.map do |crumb|
153
122
  {
154
- timestamp: crumb[:timestamp]&.iso8601 || Time.now.utc.iso8601,
155
- name: crumb[:message] || crumb[:name],
123
+ timestamp: crumb[:timestamp]&.iso8601,
124
+ name: crumb[:message],
156
125
  type: crumb[:type] || "manual",
157
126
  metaData: crumb[:data] || {}
158
127
  }
159
128
  end
160
129
  end
161
130
 
162
- def build_user(event)
131
+ def build_user_data(event)
163
132
  user = event.context[:user]
164
- return unless user
133
+ return nil unless user
165
134
 
166
135
  {
167
136
  id: user[:id]&.to_s,
@@ -173,17 +142,49 @@ module Lapsoss
173
142
  def map_severity(level)
174
143
  case level
175
144
  when :debug, :info then "info"
176
- when :warning, :warn then "warning"
177
- when :error, :fatal, :critical then "error"
145
+ when :warning then "warning"
146
+ when :error, :fatal then "error"
178
147
  else "error"
179
148
  end
180
149
  end
181
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)
177
+ end
178
+ end
179
+
182
180
  def validate_settings!
183
181
  validate_presence!(@api_key, "Insight Hub API key")
184
182
  validate_api_key!(@api_key, "Insight Hub API key", format: :alphanumeric) if @api_key
185
- validate_environment!(@app_version, "Insight Hub app version") if @app_version
186
- validate_type!(@release_stage, [String, Symbol], "Insight Hub release stage") if @release_stage
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)
187
188
  end
188
189
  end
189
190
  end
@@ -7,7 +7,7 @@ module Lapsoss
7
7
  class LoggerAdapter < Base
8
8
  def initialize(name, settings = {})
9
9
  @logger = settings[:logger] || Logger.new($stdout)
10
- super(name, settings)
10
+ super
11
11
  end
12
12
 
13
13
  def capabilities
@@ -28,12 +28,12 @@ module Lapsoss
28
28
  end
29
29
 
30
30
  # Log breadcrumbs if present in the event context
31
- if event.context[:breadcrumbs]&.any?
32
- event.context[:breadcrumbs].each do |breadcrumb|
33
- breadcrumb_msg = "[BREADCRUMB] [#{breadcrumb[:type].upcase}] #{breadcrumb[:message]}"
34
- breadcrumb_msg += " | #{breadcrumb[:metadata].inspect}" unless breadcrumb[:metadata].empty?
35
- @logger.debug(breadcrumb_msg)
36
- end
31
+ return unless event.context[:breadcrumbs]&.any?
32
+
33
+ event.context[:breadcrumbs].each do |breadcrumb|
34
+ breadcrumb_msg = "[BREADCRUMB] [#{breadcrumb[:type].upcase}] #{breadcrumb[:message]}"
35
+ breadcrumb_msg += " | #{breadcrumb[:metadata].inspect}" unless breadcrumb[:metadata].empty?
36
+ @logger.debug(breadcrumb_msg)
37
37
  end
38
38
  end
39
39
 
@@ -3,8 +3,6 @@
3
3
  require "json"
4
4
  require "socket"
5
5
  require "securerandom"
6
- require_relative "../http_client"
7
- require_relative "../backtrace_processor"
8
6
 
9
7
  module Lapsoss
10
8
  module Adapters
@@ -14,8 +12,8 @@ module Lapsoss
14
12
  JSON_CONTENT_TYPE = "application/json"
15
13
 
16
14
  def initialize(name, settings = {})
17
- super(name, settings)
18
- @access_token = settings[:access_token] || ENV["ROLLBAR_ACCESS_TOKEN"]
15
+ super
16
+ @access_token = settings[:access_token] || ENV.fetch("ROLLBAR_ACCESS_TOKEN", nil)
19
17
  @environment = settings[:environment] || Lapsoss.configuration.environment || "development"
20
18
 
21
19
  validate_settings!
@@ -30,62 +28,50 @@ module Lapsoss
30
28
  payload = build_payload(event)
31
29
  return unless payload
32
30
 
33
- path = "/api/#{API_VERSION}/item/"
34
- headers = {
35
- "Content-Type" => JSON_CONTENT_TYPE,
36
- "X-Rollbar-Access-Token" => @access_token
37
- }
38
-
39
- begin
40
- @client.post(path, body: JSON.generate(payload), headers: headers)
41
- rescue DeliveryError => e
42
- # Log the error and potentially notify error handler
43
- Lapsoss.configuration.logger&.error("[Lapsoss::RollbarAdapter] Failed to deliver event: #{e.message}")
44
- Lapsoss.configuration.error_handler&.call(e)
45
-
46
- # Re-raise to let the caller know delivery failed
47
- raise
48
- end
49
- 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
+ })
50
35
 
51
- def shutdown
52
- @client&.shutdown
53
- super
36
+ handle_response(response, event)
37
+ rescue StandardError => e
38
+ handle_delivery_error(e)
54
39
  end
55
40
 
56
41
  def capabilities
57
42
  super.merge(
58
- code_context: true
43
+ breadcrumbs: true,
44
+ user_tracking: true,
45
+ custom_context: true,
46
+ release_tracking: true
59
47
  )
60
48
  end
61
49
 
50
+ def validate!
51
+ validate_settings!
52
+ true
53
+ end
54
+
62
55
  private
63
56
 
64
57
  def build_payload(event)
65
- data = {
66
- environment: @environment,
67
- body: build_body(event),
68
- level: map_level(event.level),
69
- timestamp: event.timestamp.to_i,
70
- platform: "ruby",
71
- language: "ruby",
72
- framework: detect_framework,
73
- context: event.context[:context],
74
- request: event.context[:request],
75
- person: build_person(event),
76
- custom: event.context[:extra] || {},
77
- title: event.message || (event.exception&.message if event.type == :exception),
78
- uuid: SecureRandom.uuid,
79
- notifier: {
80
- name: "lapsoss",
81
- 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] || {}
82
73
  }
83
74
  }
84
-
85
- # Only add fingerprint if it exists
86
- data[:fingerprint] = event.context[:fingerprint].to_s if event.context[:fingerprint]
87
-
88
- { data: data }
89
75
  end
90
76
 
91
77
  def build_body(event)
@@ -95,9 +81,9 @@ module Lapsoss
95
81
  trace: {
96
82
  frames: build_backtrace_frames(event.exception),
97
83
  exception: {
98
- class: event.exception.class.name,
99
- message: event.exception.message,
100
- description: event.exception.message
84
+ class: event.exception_type,
85
+ message: event.message,
86
+ description: event.exception.to_s
101
87
  }
102
88
  }
103
89
  }
@@ -119,9 +105,9 @@ module Lapsoss
119
105
  formatted_frames.reverse
120
106
  end
121
107
 
122
- def build_person(event)
108
+ def build_person_data(event)
123
109
  user = event.context[:user]
124
- return unless user
110
+ return nil unless user
125
111
 
126
112
  {
127
113
  id: user[:id]&.to_s,
@@ -130,13 +116,22 @@ module Lapsoss
130
116
  }.compact
131
117
  end
132
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
+
133
128
  def map_level(level)
134
129
  case level
135
130
  when :debug then "debug"
136
131
  when :info then "info"
137
- when :warning, :warn then "warning"
132
+ when :warning then "warning"
138
133
  when :error then "error"
139
- when :fatal, :critical then "critical"
134
+ when :fatal then "critical"
140
135
  else "error"
141
136
  end
142
137
  end
@@ -144,14 +139,58 @@ module Lapsoss
144
139
  def detect_framework
145
140
  return "rails" if defined?(Rails)
146
141
  return "sinatra" if defined?(Sinatra)
142
+
147
143
  "ruby"
148
144
  end
149
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
157
+
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)
182
+ end
183
+
150
184
  def validate_settings!
151
185
  validate_presence!(@access_token, "Rollbar access token")
152
186
  validate_api_key!(@access_token, "Rollbar access token", format: :alphanumeric) if @access_token
153
187
  validate_environment!(@environment, "Rollbar environment") if @environment
154
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)
193
+ end
155
194
  end
156
195
  end
157
196
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
- require_relative "../http_client"
5
- require_relative "../backtrace_processor"
6
4
 
7
5
  module Lapsoss
8
6
  module Adapters
@@ -10,10 +8,10 @@ module Lapsoss
10
8
  PROTOCOL_VERSION = 7
11
9
  CONTENT_TYPE = "application/x-sentry-envelope"
12
10
  GZIP_THRESHOLD = 1024 * 30 # 30KB
13
- USER_AGENT = "lapsoss/#{Lapsoss::VERSION}"
11
+ USER_AGENT = "lapsoss/#{Lapsoss::VERSION}".freeze
14
12
 
15
13
  def initialize(name, settings = {})
16
- super(name, settings)
14
+ super
17
15
  validate_settings!
18
16
  return unless settings[:dsn]
19
17
 
@@ -69,7 +67,7 @@ module Lapsoss
69
67
  item_header = { type: item_type, content_type: "application/json" }
70
68
  item_payload = build_event_payload(event)
71
69
 
72
- [header, item_header, item_payload]
70
+ [ header, item_header, item_payload ]
73
71
  end
74
72
 
75
73
  def serialize_envelope(envelope)
@@ -82,9 +80,9 @@ module Lapsoss
82
80
  ].join("\n")
83
81
 
84
82
  if body.bytesize >= GZIP_THRESHOLD
85
- [Zlib.gzip(body), true]
83
+ [ Zlib.gzip(body), true ]
86
84
  else
87
- [body, false]
85
+ [ body, false ]
88
86
  end
89
87
  end
90
88
 
@@ -107,11 +105,11 @@ module Lapsoss
107
105
  when :exception
108
106
  {
109
107
  exception: {
110
- values: [{
108
+ values: [ {
111
109
  type: event.exception.class.name,
112
110
  value: event.exception.message,
113
111
  stacktrace: { frames: parse_backtrace(event.exception.backtrace) }
114
- }]
112
+ } ]
115
113
  }
116
114
  }
117
115
  when :message
@@ -151,9 +149,7 @@ module Lapsoss
151
149
  api_path = uri.path
152
150
 
153
151
  # For standard Sentry DSNs (just /project_id), build the envelope path
154
- if path_parts.length == 1 && project_id.match?(/^\d+$/)
155
- api_path = "/api/#{project_id}/envelope/"
156
- end
152
+ api_path = "/api/#{project_id}/envelope/" if path_parts.length == 1 && project_id.match?(/^\d+$/)
157
153
 
158
154
  {
159
155
  public_key: uri.user,
@@ -186,11 +182,9 @@ module Lapsoss
186
182
  end
187
183
 
188
184
  def validate_settings!
189
- if @settings[:dsn]
190
- validate_dsn!(@settings[:dsn], "Sentry DSN")
191
- else
192
- raise ValidationError, "Sentry DSN is required"
193
- end
185
+ raise ValidationError, "Sentry DSN is required" unless @settings[:dsn]
186
+
187
+ validate_dsn!(@settings[:dsn], "Sentry DSN")
194
188
  end
195
189
  end
196
190
  end