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
@@ -25,22 +25,20 @@ module Lapsoss
25
25
  @rails_parameter_filter = rails_parameter_filter
26
26
 
27
27
  # Only use custom scrubbing if Rails parameter filter is not available
28
- unless @rails_parameter_filter
29
- @scrub_fields = Array(config[:scrub_fields] || DEFAULT_SCRUB_FIELDS)
30
- @scrub_all = config[:scrub_all] || false
31
- @whitelist_fields = Array(config[:whitelist_fields] || [])
32
- @randomize_scrub_length = config[:randomize_scrub_length] || false
33
- @scrub_value = config[:scrub_value] || "**SCRUBBED**"
34
- end
28
+ return if @rails_parameter_filter
29
+
30
+ @scrub_fields = Array(config[:scrub_fields] || DEFAULT_SCRUB_FIELDS)
31
+ @scrub_all = config[:scrub_all] || false
32
+ @whitelist_fields = Array(config[:whitelist_fields] || [])
33
+ @randomize_scrub_length = config[:randomize_scrub_length] || false
34
+ @scrub_value = config[:scrub_value] || "**SCRUBBED**"
35
35
  end
36
36
 
37
37
  def scrub(data)
38
38
  return data if data.nil?
39
39
 
40
40
  # If Rails parameter filter is available, use it exclusively
41
- if @rails_parameter_filter
42
- return @rails_parameter_filter.filter(data)
43
- end
41
+ return @rails_parameter_filter.filter(data) if @rails_parameter_filter
44
42
 
45
43
  # Fallback to custom scrubbing logic only if Rails filter is not available
46
44
  @scrubbed_objects = {}.compare_by_identity
@@ -68,14 +66,14 @@ module Lapsoss
68
66
  hash.each_with_object({}) do |(key, value), result|
69
67
  key_string = key.to_s.downcase
70
68
 
71
- if should_scrub_field?(key_string)
72
- result[key] = generate_scrub_value(value)
69
+ result[key] = if should_scrub_field?(key_string)
70
+ generate_scrub_value(value)
73
71
  elsif value.is_a?(Hash)
74
- result[key] = scrub_recursive(value)
72
+ scrub_recursive(value)
75
73
  elsif value.is_a?(Array)
76
- result[key] = scrub_array(value)
74
+ scrub_array(value)
77
75
  else
78
- result[key] = scrub_value(value)
76
+ scrub_value(value)
79
77
  end
80
78
  end
81
79
  end
@@ -118,7 +116,7 @@ module Lapsoss
118
116
  PROTECTED_EVENT_FIELDS.include?(field_name.to_s)
119
117
  end
120
118
 
121
- def whitelisted_value?(value)
119
+ def whitelisted_value?(_value)
122
120
  # Basic implementation - could be extended
123
121
  false
124
122
  end
@@ -136,7 +134,7 @@ module Lapsoss
136
134
  original_filename: safe_call(attachment, :original_filename),
137
135
  size: safe_call(attachment, :size) || safe_call(attachment, :tempfile, :size)
138
136
  }
139
- rescue => e
137
+ rescue StandardError => e
140
138
  { __attachment__: true, error: "Failed to extract attachment info: #{e.message}" }
141
139
  end
142
140
 
@@ -146,7 +144,7 @@ module Lapsoss
146
144
  end
147
145
  end
148
146
 
149
- def generate_scrub_value(original_value)
147
+ def generate_scrub_value(_original_value)
150
148
  if @randomize_scrub_length
151
149
  "*" * rand(6..12)
152
150
  else
@@ -30,15 +30,13 @@ module Lapsoss
30
30
  key_sym = key.to_sym
31
31
 
32
32
  # Skip if not in allowed fields list (when specified)
33
- if @allowed_fields && !@allowed_fields.include?(key_sym)
34
- next
35
- end
33
+ next if @allowed_fields&.exclude?(key_sym)
36
34
 
37
35
  # Apply privacy filtering
38
- if @privacy_mode && sensitive_field?(key_sym)
39
- processed[key] = apply_privacy_filter(key_sym, value)
36
+ processed[key] = if @privacy_mode && sensitive_field?(key_sym)
37
+ apply_privacy_filter(key_sym, value)
40
38
  else
41
- processed[key] = transform_field(key_sym, value)
39
+ transform_field(key_sym, value)
42
40
  end
43
41
  end
44
42
 
@@ -70,18 +68,18 @@ module Lapsoss
70
68
  segments = {}
71
69
 
72
70
  # Check for common user segments
73
- segments[:internal] = !!(user_data[:internal] || user_data["internal"])
74
- segments[:premium] = !!(user_data[:premium] || user_data["premium"])
75
- segments[:beta] = !!(user_data[:beta] || user_data["beta"])
76
- segments[:admin] = !!(user_data[:admin] || user_data["admin"])
71
+ segments[:internal] = !(user_data[:internal] || user_data["internal"]).nil?
72
+ segments[:premium] = !(user_data[:premium] || user_data["premium"]).nil?
73
+ segments[:beta] = !(user_data[:beta] || user_data["beta"]).nil?
74
+ segments[:admin] = !(user_data[:admin] || user_data["admin"]).nil?
77
75
 
78
76
  # Check role-based segments
79
- if role = (user_data[:role] || user_data["role"])
77
+ if role = user_data[:role] || user_data["role"]
80
78
  segments[:role] = role.to_s.downcase
81
79
  end
82
80
 
83
81
  # Check plan-based segments
84
- if plan = (user_data[:plan] || user_data["plan"])
82
+ if plan = user_data[:plan] || user_data["plan"]
85
83
  segments[:plan] = plan.to_s.downcase
86
84
  end
87
85
 
@@ -96,14 +94,14 @@ module Lapsoss
96
94
  user_data.each do |key, value|
97
95
  key_sym = key.to_sym
98
96
 
99
- if sensitive_field?(key_sym)
100
- sanitized[key] = "[REDACTED]"
97
+ sanitized[key] = if sensitive_field?(key_sym)
98
+ "[REDACTED]"
101
99
  elsif value.is_a?(Hash)
102
- sanitized[key] = sanitize_for_logging(value)
100
+ sanitize_for_logging(value)
103
101
  elsif value.is_a?(Array)
104
- sanitized[key] = value.map { |v| v.is_a?(Hash) ? sanitize_for_logging(v) : v }
102
+ value.map { |v| v.is_a?(Hash) ? sanitize_for_logging(v) : v }
105
103
  else
106
- sanitized[key] = value
104
+ value
107
105
  end
108
106
  end
109
107
 
@@ -153,7 +151,7 @@ module Lapsoss
153
151
  return "[INVALID_PHONE]" if digits.length < 4
154
152
 
155
153
  # Show last 4 digits
156
- "*" * (digits.length - 4) + digits[-4..-1]
154
+ ("*" * (digits.length - 4)) + digits[-4..]
157
155
  end
158
156
 
159
157
  def mask_ip(ip)
@@ -162,194 +160,16 @@ module Lapsoss
162
160
  if ip.include?(":")
163
161
  # IPv6 - mask last 4 groups
164
162
  parts = ip.split(":")
165
- parts[-4..-1] = ["****"] * 4 if parts.length >= 4
163
+ parts[-4..-1] = [ "****" ] * 4 if parts.length >= 4
166
164
  parts.join(":")
167
165
  else
168
166
  # IPv4 - mask last octet
169
167
  parts = ip.split(".")
170
168
  return "[INVALID_IP]" if parts.length != 4
169
+
171
170
  parts[-1] = "***"
172
171
  parts.join(".")
173
172
  end
174
173
  end
175
174
  end
176
-
177
- # User context provider that integrates with various authentication systems
178
- class UserContextProvider
179
- def initialize(providers: {})
180
- @providers = providers
181
- end
182
-
183
- def get_user_context(event, hint = {})
184
- context = {}
185
-
186
- # Try each provider in order
187
- @providers.each do |name, provider|
188
- begin
189
- if provider_context = provider.call(event, hint)
190
- context.merge!(provider_context)
191
- end
192
- rescue StandardError => e
193
- # Log provider error but don't fail
194
- warn "User context provider #{name} failed: #{e.message}"
195
- end
196
- end
197
-
198
- context
199
- end
200
-
201
- # Built-in providers for common authentication systems
202
- def self.devise_provider
203
- lambda do |event, hint|
204
- return {} unless defined?(Devise) && defined?(Warden)
205
-
206
- # Try to get user from Warden (used by Devise)
207
- if request = hint[:request]
208
- user = request.env["warden"]&.user
209
- return {} unless user
210
-
211
- {
212
- id: user.id,
213
- email: user.email,
214
- username: user.respond_to?(:username) ? user.username : nil,
215
- created_at: user.created_at,
216
- role: user.respond_to?(:role) ? user.role : nil
217
- }.compact
218
- end
219
-
220
- {}
221
- end
222
- end
223
-
224
- def self.omniauth_provider
225
- lambda do |event, hint|
226
- return {} unless defined?(OmniAuth)
227
-
228
- if request = hint[:request]
229
- if auth_info = request.env["omniauth.auth"]
230
- {
231
- provider: auth_info["provider"],
232
- uid: auth_info["uid"],
233
- name: auth_info.dig("info", "name"),
234
- email: auth_info.dig("info", "email"),
235
- username: auth_info.dig("info", "nickname")
236
- }.compact
237
- end
238
- end
239
-
240
- {}
241
- end
242
- end
243
-
244
- def self.session_provider
245
- lambda do |event, hint|
246
- return {} unless hint[:request]
247
-
248
- request = hint[:request]
249
- session = request.session rescue {}
250
-
251
- {
252
- session_id: session[:session_id] || session["session_id"],
253
- user_id: session[:user_id] || session["user_id"],
254
- csrf_token: session[:_csrf_token] || session["_csrf_token"]
255
- }.compact
256
- end
257
- end
258
-
259
- def self.thread_local_provider
260
- lambda do |event, hint|
261
- # Get user from thread-local storage
262
- Thread.current[:current_user] || {}
263
- end
264
- end
265
- end
266
-
267
- # Integration with popular authentication gems
268
- module UserContextIntegrations
269
- def self.setup_devise_integration
270
- return unless defined?(Devise)
271
-
272
- # Add middleware to capture user context
273
- if defined?(Rails)
274
- Rails.application.config.middleware.use(UserContextMiddleware)
275
- end
276
- end
277
-
278
- def self.setup_clearance_integration
279
- return unless defined?(Clearance)
280
-
281
- # Clearance integration
282
- if defined?(Rails)
283
- Rails.application.config.middleware.use(UserContextMiddleware) do |middleware|
284
- middleware.user_provider = lambda do |request|
285
- request.env[:clearance].current_user if request.env[:clearance]
286
- end
287
- end
288
- end
289
- end
290
-
291
- def self.setup_authlogic_integration
292
- return unless defined?(Authlogic)
293
-
294
- # Authlogic integration
295
- if defined?(Rails)
296
- Rails.application.config.middleware.use(UserContextMiddleware) do |middleware|
297
- middleware.user_provider = lambda do |request|
298
- UserSession.find&.user if defined?(UserSession)
299
- end
300
- end
301
- end
302
- end
303
- end
304
-
305
- # Middleware to automatically capture user context
306
- class UserContextMiddleware
307
- def initialize(app, user_provider: nil)
308
- @app = app
309
- @user_provider = user_provider
310
- end
311
-
312
- def call(env)
313
- request = Rack::Request.new(env)
314
-
315
- # Capture user context
316
- user_context = extract_user_context(request)
317
-
318
- # Store in thread-local for access during request processing
319
- Thread.current[:lapsoss_user_context] = user_context
320
-
321
- @app.call(env)
322
- ensure
323
- Thread.current[:lapsoss_user_context] = nil
324
- end
325
-
326
- private
327
-
328
- def extract_user_context(request)
329
- if @user_provider
330
- user = @user_provider.call(request)
331
- return {} unless user
332
-
333
- context = {
334
- id: user.id,
335
- email: user.email,
336
- username: user.respond_to?(:username) ? user.username : nil
337
- }
338
-
339
- # Add role information if available
340
- if user.respond_to?(:role)
341
- context[:role] = user.role
342
- end
343
-
344
- # Add plan information if available
345
- if user.respond_to?(:plan)
346
- context[:plan] = user.plan
347
- end
348
-
349
- context.compact
350
- else
351
- {}
352
- end
353
- end
354
- end
355
175
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ # Integration with popular authentication gems
5
+ module UserContextIntegrations
6
+ def self.setup_devise_integration
7
+ return unless defined?(Devise)
8
+
9
+ # Add middleware to capture user context
10
+ Rails.application.config.middleware.use(UserContextMiddleware) if defined?(Rails)
11
+ end
12
+
13
+ def self.setup_clearance_integration
14
+ return unless defined?(Clearance)
15
+
16
+ # Clearance integration
17
+ return unless defined?(Rails)
18
+
19
+ Rails.application.config.middleware.use(UserContextMiddleware) do |middleware|
20
+ middleware.user_provider = lambda do |request|
21
+ request.env[:clearance].current_user if request.env[:clearance]
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.setup_authlogic_integration
27
+ return unless defined?(Authlogic)
28
+
29
+ # Authlogic integration
30
+ return unless defined?(Rails)
31
+
32
+ Rails.application.config.middleware.use(UserContextMiddleware) do |middleware|
33
+ middleware.user_provider = lambda do |_request|
34
+ UserSession.find&.user if defined?(UserSession)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ # Middleware to automatically capture user context
5
+ class UserContextMiddleware
6
+ def initialize(app, user_provider: nil)
7
+ @app = app
8
+ @user_provider = user_provider
9
+ end
10
+
11
+ def call(env)
12
+ request = Rack::Request.new(env)
13
+
14
+ # Capture user context
15
+ user_context = extract_user_context(request)
16
+
17
+ # Store in thread-local for access during request processing
18
+ Thread.current[:lapsoss_user_context] = user_context
19
+
20
+ @app.call(env)
21
+ ensure
22
+ Thread.current[:lapsoss_user_context] = nil
23
+ end
24
+
25
+ private
26
+
27
+ def extract_user_context(request)
28
+ if @user_provider
29
+ user = @user_provider.call(request)
30
+ return {} unless user
31
+
32
+ context = {
33
+ id: user.id,
34
+ email: user.email,
35
+ username: user.respond_to?(:username) ? user.username : nil
36
+ }
37
+
38
+ # Add role information if available
39
+ context[:role] = user.role if user.respond_to?(:role)
40
+
41
+ # Add plan information if available
42
+ context[:plan] = user.plan if user.respond_to?(:plan)
43
+
44
+ context.compact
45
+ else
46
+ {}
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ # User context provider that integrates with various authentication systems
5
+ class UserContextProvider
6
+ def initialize(providers: {})
7
+ @providers = providers
8
+ end
9
+
10
+ def get_user_context(event, hint = {})
11
+ context = {}
12
+
13
+ # Try each provider in order
14
+ @providers.each do |name, provider|
15
+ if provider_context = provider.call(event, hint)
16
+ context.merge!(provider_context)
17
+ end
18
+ rescue StandardError => e
19
+ # Log provider error but don't fail
20
+ warn "User context provider #{name} failed: #{e.message}"
21
+ end
22
+
23
+ context
24
+ end
25
+
26
+ # Built-in providers for common authentication systems
27
+ def self.devise_provider
28
+ lambda do |_event, hint|
29
+ return {} unless defined?(Devise) && defined?(Warden)
30
+
31
+ # Try to get user from Warden (used by Devise)
32
+ if request = hint[:request]
33
+ user = request.env["warden"]&.user
34
+ return {} unless user
35
+
36
+ {
37
+ id: user.id,
38
+ email: user.email,
39
+ username: user.respond_to?(:username) ? user.username : nil,
40
+ created_at: user.created_at,
41
+ role: user.respond_to?(:role) ? user.role : nil
42
+ }.compact
43
+ end
44
+
45
+ {}
46
+ end
47
+ end
48
+
49
+ def self.omniauth_provider
50
+ lambda do |_event, hint|
51
+ return {} unless defined?(OmniAuth)
52
+
53
+ if (request = hint[:request]) && (auth_info = request.env["omniauth.auth"])
54
+ {
55
+ provider: auth_info["provider"],
56
+ uid: auth_info["uid"],
57
+ name: auth_info.dig("info", "name"),
58
+ email: auth_info.dig("info", "email"),
59
+ username: auth_info.dig("info", "nickname")
60
+ }.compact
61
+ end
62
+
63
+ {}
64
+ end
65
+ end
66
+
67
+ def self.session_provider
68
+ lambda do |_event, hint|
69
+ return {} unless hint[:request]
70
+
71
+ request = hint[:request]
72
+ session = begin
73
+ request.session
74
+ rescue StandardError
75
+ {}
76
+ end
77
+
78
+ {
79
+ session_id: session[:session_id] || session["session_id"],
80
+ user_id: session[:user_id] || session["user_id"],
81
+ csrf_token: session[:_csrf_token] || session["_csrf_token"]
82
+ }.compact
83
+ end
84
+ end
85
+
86
+ def self.thread_local_provider
87
+ lambda do |_event, _hint|
88
+ # Get user from thread-local storage
89
+ Thread.current[:current_user] || {}
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Utils
5
+ module_function
6
+
7
+ def current_time
8
+ if Time.respond_to?(:zone) && Time.zone
9
+ end
10
+ Time.zone.now
11
+ end
12
+ end
13
+ end
@@ -17,7 +17,7 @@ module Lapsoss
17
17
  def validate_type!(value, types, name)
18
18
  types = Array(types)
19
19
  unless types.any? { |type| value.is_a?(type) }
20
- type_names = types.map(&:name).join(' or ')
20
+ type_names = types.map(&:name).join(" or ")
21
21
  raise ValidationError, "#{name} must be a #{type_names}, got #{value.class} (#{value.inspect})"
22
22
  end
23
23
  end
@@ -29,7 +29,7 @@ module Lapsoss
29
29
  end
30
30
 
31
31
  def validate_numeric_range!(value, range, name)
32
- validate_type!(value, [Numeric], name)
32
+ validate_type!(value, [ Numeric ], name)
33
33
  unless range.cover?(value)
34
34
  raise ValidationError, "#{name} must be between #{range.min} and #{range.max}, got #{value}"
35
35
  end
@@ -38,16 +38,12 @@ module Lapsoss
38
38
  def validate_url!(value, name)
39
39
  return if value.nil?
40
40
 
41
- validate_type!(value, [String], name)
41
+ validate_type!(value, [ String ], name)
42
42
 
43
43
  begin
44
44
  uri = URI.parse(value)
45
- unless uri.scheme && uri.host
46
- raise ValidationError, "#{name} must be a valid URL with scheme and host"
47
- end
48
- unless %w[http https].include?(uri.scheme)
49
- raise ValidationError, "#{name} must use http or https scheme"
50
- end
45
+ raise ValidationError, "#{name} must be a valid URL with scheme and host" unless uri.scheme && uri.host
46
+ raise ValidationError, "#{name} must use http or https scheme" unless %w[http https].include?(uri.scheme)
51
47
  rescue URI::InvalidURIError => e
52
48
  raise ValidationError, "#{name} is not a valid URL: #{e.message}"
53
49
  end
@@ -56,7 +52,7 @@ module Lapsoss
56
52
  def validate_dsn!(dsn_string, name = "DSN")
57
53
  return if dsn_string.nil?
58
54
 
59
- validate_type!(dsn_string, [String], name)
55
+ validate_type!(dsn_string, [ String ], name)
60
56
 
61
57
  begin
62
58
  uri = URI.parse(dsn_string)
@@ -67,9 +63,7 @@ module Lapsoss
67
63
  validate_presence!(uri.user, "#{name} public key")
68
64
 
69
65
  # Validate scheme
70
- unless %w[http https].include?(uri.scheme)
71
- raise ValidationError, "#{name} must use http or https scheme"
72
- end
66
+ raise ValidationError, "#{name} must use http or https scheme" unless %w[http https].include?(uri.scheme)
73
67
 
74
68
  # Extract project ID from path
75
69
  path_parts = uri.path&.split("/") || []
@@ -80,7 +74,6 @@ module Lapsoss
80
74
  unless project_id.match?(/^\d+$/)
81
75
  raise ValidationError, "#{name} project ID must be numeric, got '#{project_id}'"
82
76
  end
83
-
84
77
  rescue URI::InvalidURIError => e
85
78
  raise ValidationError, "#{name} is not a valid URI: #{e.message}"
86
79
  end
@@ -88,7 +81,7 @@ module Lapsoss
88
81
 
89
82
  def validate_api_key!(value, name, format: nil)
90
83
  validate_presence!(value, name)
91
- validate_type!(value, [String], name)
84
+ validate_type!(value, [ String ], name)
92
85
 
93
86
  case format
94
87
  when :uuid
@@ -96,20 +89,16 @@ module Lapsoss
96
89
  raise ValidationError, "#{name} must be a valid UUID format"
97
90
  end
98
91
  when :hex
99
- unless value.match?(/\A[0-9a-f]+\z/i)
100
- raise ValidationError, "#{name} must be a valid hexadecimal string"
101
- end
92
+ raise ValidationError, "#{name} must be a valid hexadecimal string" unless value.match?(/\A[0-9a-f]+\z/i)
102
93
  when :alphanumeric
103
- unless value.match?(/\A[a-z0-9]+\z/i)
104
- raise ValidationError, "#{name} must be alphanumeric"
105
- end
94
+ raise ValidationError, "#{name} must be alphanumeric" unless value.match?(/\A[a-z0-9]+\z/i)
106
95
  end
107
96
  end
108
97
 
109
98
  def validate_environment!(value, name = "environment")
110
99
  return if value.nil?
111
100
 
112
- validate_type!(value, [String, Symbol], name)
101
+ validate_type!(value, [ String, Symbol ], name)
113
102
 
114
103
  env_string = value.to_s
115
104
  unless env_string.match?(/\A[a-z0-9_-]+\z/i)
@@ -126,16 +115,14 @@ module Lapsoss
126
115
  def validate_timeout!(value, name = "timeout")
127
116
  return if value.nil?
128
117
 
129
- validate_type!(value, [Numeric], name)
130
- if value <= 0
131
- raise ValidationError, "#{name} must be positive, got #{value}"
132
- end
118
+ validate_type!(value, [ Numeric ], name)
119
+ raise ValidationError, "#{name} must be positive, got #{value}" if value <= 0
133
120
  end
134
121
 
135
122
  def validate_retries!(value, name = "max_retries")
136
123
  return if value.nil?
137
124
 
138
- validate_type!(value, [Integer], name)
125
+ validate_type!(value, [ Integer ], name)
139
126
  validate_numeric_range!(value, 0..10, name)
140
127
  end
141
128
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lapsoss
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end