lapsoss 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +855 -0
- data/lib/lapsoss/adapters/appsignal_adapter.rb +136 -0
- data/lib/lapsoss/adapters/base.rb +88 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +190 -0
- data/lib/lapsoss/adapters/logger_adapter.rb +67 -0
- data/lib/lapsoss/adapters/rollbar_adapter.rb +157 -0
- data/lib/lapsoss/adapters/sentry_adapter.rb +197 -0
- data/lib/lapsoss/backtrace_frame.rb +258 -0
- data/lib/lapsoss/backtrace_processor.rb +346 -0
- data/lib/lapsoss/client.rb +115 -0
- data/lib/lapsoss/configuration.rb +310 -0
- data/lib/lapsoss/current.rb +9 -0
- data/lib/lapsoss/event.rb +107 -0
- data/lib/lapsoss/exclusions.rb +429 -0
- data/lib/lapsoss/fingerprinter.rb +217 -0
- data/lib/lapsoss/http_client.rb +79 -0
- data/lib/lapsoss/middleware.rb +353 -0
- data/lib/lapsoss/pipeline.rb +131 -0
- data/lib/lapsoss/railtie.rb +72 -0
- data/lib/lapsoss/registry.rb +114 -0
- data/lib/lapsoss/release_tracker.rb +553 -0
- data/lib/lapsoss/router.rb +36 -0
- data/lib/lapsoss/sampling.rb +332 -0
- data/lib/lapsoss/scope.rb +110 -0
- data/lib/lapsoss/scrubber.rb +170 -0
- data/lib/lapsoss/user_context.rb +355 -0
- data/lib/lapsoss/validators.rb +142 -0
- data/lib/lapsoss/version.rb +5 -0
- data/lib/lapsoss.rb +76 -0
- metadata +217 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lapsoss
|
|
4
|
+
# Enhanced user context handling with privacy controls
|
|
5
|
+
class UserContext
|
|
6
|
+
SENSITIVE_FIELDS = %i[
|
|
7
|
+
email phone mobile telephone
|
|
8
|
+
address street city state zip postal_code country
|
|
9
|
+
ssn social_security_number
|
|
10
|
+
credit_card card_number cvv
|
|
11
|
+
password password_confirmation
|
|
12
|
+
secret token api_key
|
|
13
|
+
ip_address last_login_ip
|
|
14
|
+
birth_date date_of_birth dob
|
|
15
|
+
salary income wage
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(privacy_mode: false, allowed_fields: nil, field_transformers: {})
|
|
19
|
+
@privacy_mode = privacy_mode
|
|
20
|
+
@allowed_fields = allowed_fields&.map(&:to_sym)
|
|
21
|
+
@field_transformers = field_transformers
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def process_user_data(user_data)
|
|
25
|
+
return {} unless user_data.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
processed = {}
|
|
28
|
+
|
|
29
|
+
user_data.each do |key, value|
|
|
30
|
+
key_sym = key.to_sym
|
|
31
|
+
|
|
32
|
+
# Skip if not in allowed fields list (when specified)
|
|
33
|
+
if @allowed_fields && !@allowed_fields.include?(key_sym)
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Apply privacy filtering
|
|
38
|
+
if @privacy_mode && sensitive_field?(key_sym)
|
|
39
|
+
processed[key] = apply_privacy_filter(key_sym, value)
|
|
40
|
+
else
|
|
41
|
+
processed[key] = transform_field(key_sym, value)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
processed
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merge_user_data(existing_data, new_data)
|
|
49
|
+
existing_processed = process_user_data(existing_data || {})
|
|
50
|
+
new_processed = process_user_data(new_data || {})
|
|
51
|
+
|
|
52
|
+
existing_processed.merge(new_processed)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_user_id(user_data)
|
|
56
|
+
return nil unless user_data.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
# Try common user ID fields in order of preference
|
|
59
|
+
%i[id user_id uuid guid].each do |field|
|
|
60
|
+
value = user_data[field] || user_data[field.to_s]
|
|
61
|
+
return value if value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extract_user_segment(user_data)
|
|
68
|
+
return nil unless user_data.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
segments = {}
|
|
71
|
+
|
|
72
|
+
# 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"])
|
|
77
|
+
|
|
78
|
+
# Check role-based segments
|
|
79
|
+
if role = (user_data[:role] || user_data["role"])
|
|
80
|
+
segments[:role] = role.to_s.downcase
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check plan-based segments
|
|
84
|
+
if plan = (user_data[:plan] || user_data["plan"])
|
|
85
|
+
segments[:plan] = plan.to_s.downcase
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
segments.compact
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def sanitize_for_logging(user_data)
|
|
92
|
+
return {} unless user_data.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
sanitized = {}
|
|
95
|
+
|
|
96
|
+
user_data.each do |key, value|
|
|
97
|
+
key_sym = key.to_sym
|
|
98
|
+
|
|
99
|
+
if sensitive_field?(key_sym)
|
|
100
|
+
sanitized[key] = "[REDACTED]"
|
|
101
|
+
elsif value.is_a?(Hash)
|
|
102
|
+
sanitized[key] = sanitize_for_logging(value)
|
|
103
|
+
elsif value.is_a?(Array)
|
|
104
|
+
sanitized[key] = value.map { |v| v.is_a?(Hash) ? sanitize_for_logging(v) : v }
|
|
105
|
+
else
|
|
106
|
+
sanitized[key] = value
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sanitized
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def sensitive_field?(field)
|
|
116
|
+
SENSITIVE_FIELDS.include?(field) || field.to_s.match?(/password|secret|token|key|ssn|credit|card/i)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def apply_privacy_filter(field, value)
|
|
120
|
+
case field
|
|
121
|
+
when :email
|
|
122
|
+
mask_email(value)
|
|
123
|
+
when :phone, :mobile, :telephone
|
|
124
|
+
mask_phone(value)
|
|
125
|
+
when :ip_address, :last_login_ip
|
|
126
|
+
mask_ip(value)
|
|
127
|
+
else
|
|
128
|
+
"[FILTERED]"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def transform_field(field, value)
|
|
133
|
+
if transformer = @field_transformers[field]
|
|
134
|
+
transformer.call(value)
|
|
135
|
+
else
|
|
136
|
+
value
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def mask_email(email)
|
|
141
|
+
return "[INVALID_EMAIL]" unless email.is_a?(String) && email.include?("@")
|
|
142
|
+
|
|
143
|
+
local, domain = email.split("@", 2)
|
|
144
|
+
masked_local = local.length > 2 ? "#{local[0]}***#{local[-1]}" : "***"
|
|
145
|
+
"#{masked_local}@#{domain}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def mask_phone(phone)
|
|
149
|
+
return "[INVALID_PHONE]" unless phone.is_a?(String)
|
|
150
|
+
|
|
151
|
+
# Remove all non-digits
|
|
152
|
+
digits = phone.gsub(/\D/, "")
|
|
153
|
+
return "[INVALID_PHONE]" if digits.length < 4
|
|
154
|
+
|
|
155
|
+
# Show last 4 digits
|
|
156
|
+
"*" * (digits.length - 4) + digits[-4..-1]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def mask_ip(ip)
|
|
160
|
+
return "[INVALID_IP]" unless ip.is_a?(String)
|
|
161
|
+
|
|
162
|
+
if ip.include?(":")
|
|
163
|
+
# IPv6 - mask last 4 groups
|
|
164
|
+
parts = ip.split(":")
|
|
165
|
+
parts[-4..-1] = ["****"] * 4 if parts.length >= 4
|
|
166
|
+
parts.join(":")
|
|
167
|
+
else
|
|
168
|
+
# IPv4 - mask last octet
|
|
169
|
+
parts = ip.split(".")
|
|
170
|
+
return "[INVALID_IP]" if parts.length != 4
|
|
171
|
+
parts[-1] = "***"
|
|
172
|
+
parts.join(".")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
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
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Lapsoss
|
|
6
|
+
module Validators
|
|
7
|
+
class ValidationError < StandardError; end
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def validate_presence!(value, name)
|
|
12
|
+
if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
13
|
+
raise ValidationError, "#{name} is required and cannot be nil or empty"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_type!(value, types, name)
|
|
18
|
+
types = Array(types)
|
|
19
|
+
unless types.any? { |type| value.is_a?(type) }
|
|
20
|
+
type_names = types.map(&:name).join(' or ')
|
|
21
|
+
raise ValidationError, "#{name} must be a #{type_names}, got #{value.class} (#{value.inspect})"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate_callable!(value, name)
|
|
26
|
+
unless value.nil? || value.respond_to?(:call)
|
|
27
|
+
raise ValidationError, "#{name} must be callable (respond_to? :call) or nil, got #{value.class}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_numeric_range!(value, range, name)
|
|
32
|
+
validate_type!(value, [Numeric], name)
|
|
33
|
+
unless range.cover?(value)
|
|
34
|
+
raise ValidationError, "#{name} must be between #{range.min} and #{range.max}, got #{value}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_url!(value, name)
|
|
39
|
+
return if value.nil?
|
|
40
|
+
|
|
41
|
+
validate_type!(value, [String], name)
|
|
42
|
+
|
|
43
|
+
begin
|
|
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
|
|
51
|
+
rescue URI::InvalidURIError => e
|
|
52
|
+
raise ValidationError, "#{name} is not a valid URL: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_dsn!(dsn_string, name = "DSN")
|
|
57
|
+
return if dsn_string.nil?
|
|
58
|
+
|
|
59
|
+
validate_type!(dsn_string, [String], name)
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
uri = URI.parse(dsn_string)
|
|
63
|
+
|
|
64
|
+
# Check required components
|
|
65
|
+
validate_presence!(uri.scheme, "#{name} scheme")
|
|
66
|
+
validate_presence!(uri.host, "#{name} host")
|
|
67
|
+
validate_presence!(uri.user, "#{name} public key")
|
|
68
|
+
|
|
69
|
+
# Validate scheme
|
|
70
|
+
unless %w[http https].include?(uri.scheme)
|
|
71
|
+
raise ValidationError, "#{name} must use http or https scheme"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Extract project ID from path
|
|
75
|
+
path_parts = uri.path&.split("/") || []
|
|
76
|
+
project_id = path_parts.last
|
|
77
|
+
validate_presence!(project_id, "#{name} project ID")
|
|
78
|
+
|
|
79
|
+
# Validate project ID is numeric
|
|
80
|
+
unless project_id.match?(/^\d+$/)
|
|
81
|
+
raise ValidationError, "#{name} project ID must be numeric, got '#{project_id}'"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
rescue URI::InvalidURIError => e
|
|
85
|
+
raise ValidationError, "#{name} is not a valid URI: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_api_key!(value, name, format: nil)
|
|
90
|
+
validate_presence!(value, name)
|
|
91
|
+
validate_type!(value, [String], name)
|
|
92
|
+
|
|
93
|
+
case format
|
|
94
|
+
when :uuid
|
|
95
|
+
unless value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
|
96
|
+
raise ValidationError, "#{name} must be a valid UUID format"
|
|
97
|
+
end
|
|
98
|
+
when :hex
|
|
99
|
+
unless value.match?(/\A[0-9a-f]+\z/i)
|
|
100
|
+
raise ValidationError, "#{name} must be a valid hexadecimal string"
|
|
101
|
+
end
|
|
102
|
+
when :alphanumeric
|
|
103
|
+
unless value.match?(/\A[a-z0-9]+\z/i)
|
|
104
|
+
raise ValidationError, "#{name} must be alphanumeric"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_environment!(value, name = "environment")
|
|
110
|
+
return if value.nil?
|
|
111
|
+
|
|
112
|
+
validate_type!(value, [String, Symbol], name)
|
|
113
|
+
|
|
114
|
+
env_string = value.to_s
|
|
115
|
+
unless env_string.match?(/\A[a-z0-9_-]+\z/i)
|
|
116
|
+
raise ValidationError, "#{name} must contain only alphanumeric characters, underscores, and hyphens"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_sample_rate!(value, name = "sample_rate")
|
|
121
|
+
return if value.nil?
|
|
122
|
+
|
|
123
|
+
validate_numeric_range!(value, 0.0..1.0, name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validate_timeout!(value, name = "timeout")
|
|
127
|
+
return if value.nil?
|
|
128
|
+
|
|
129
|
+
validate_type!(value, [Numeric], name)
|
|
130
|
+
if value <= 0
|
|
131
|
+
raise ValidationError, "#{name} must be positive, got #{value}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_retries!(value, name = "max_retries")
|
|
136
|
+
return if value.nil?
|
|
137
|
+
|
|
138
|
+
validate_type!(value, [Integer], name)
|
|
139
|
+
validate_numeric_range!(value, 0..10, name)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/lapsoss.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lapsoss/version"
|
|
4
|
+
require_relative "lapsoss/configuration"
|
|
5
|
+
require_relative "lapsoss/registry"
|
|
6
|
+
require_relative "lapsoss/event"
|
|
7
|
+
require_relative "lapsoss/router"
|
|
8
|
+
require_relative "lapsoss/adapters/base"
|
|
9
|
+
require_relative "lapsoss/current"
|
|
10
|
+
require_relative "lapsoss/client"
|
|
11
|
+
require_relative "lapsoss/fingerprinter"
|
|
12
|
+
require_relative "lapsoss/backtrace_processor"
|
|
13
|
+
require_relative "lapsoss/scrubber"
|
|
14
|
+
require_relative "lapsoss/pipeline"
|
|
15
|
+
require_relative "lapsoss/sampling"
|
|
16
|
+
require_relative "lapsoss/user_context"
|
|
17
|
+
require_relative "lapsoss/exclusions"
|
|
18
|
+
require_relative "lapsoss/release_tracker"
|
|
19
|
+
require_relative "lapsoss/railtie" if defined?(Rails)
|
|
20
|
+
|
|
21
|
+
module Lapsoss
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
|
|
24
|
+
class DeliveryError < Error
|
|
25
|
+
attr_reader :response, :cause
|
|
26
|
+
|
|
27
|
+
def initialize(message, response: nil, cause: nil)
|
|
28
|
+
super(message)
|
|
29
|
+
@response = response
|
|
30
|
+
@cause = cause
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
attr_reader :client
|
|
36
|
+
|
|
37
|
+
def configuration
|
|
38
|
+
@configuration ||= Configuration.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def configure
|
|
42
|
+
yield(configuration)
|
|
43
|
+
configuration.validate!
|
|
44
|
+
configuration.apply!
|
|
45
|
+
@client = Client.new(configuration)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def capture_exception(exception, **context)
|
|
49
|
+
client.capture_exception(exception, **context)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def capture_message(message, level: :info, **context)
|
|
53
|
+
client.capture_message(message, level: level, **context)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_breadcrumb(message, type: :default, **metadata)
|
|
57
|
+
client.add_breadcrumb(message, type: type, **metadata)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def with_scope(context = {}, &block)
|
|
61
|
+
client.with_scope(context, &block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def current_scope
|
|
65
|
+
client.current_scope
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def flush(timeout: 2)
|
|
69
|
+
client.flush(timeout: timeout)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def shutdown
|
|
73
|
+
client.shutdown
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|