lapsoss 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +71 -7
- data/lib/lapsoss/adapters/appsignal_adapter.rb +18 -12
- data/lib/lapsoss/adapters/base.rb +19 -0
- data/lib/lapsoss/adapters/concerns/envelope_builder.rb +127 -0
- data/lib/lapsoss/adapters/concerns/http_delivery.rb +130 -0
- data/lib/lapsoss/adapters/concerns/level_mapping.rb +65 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +21 -21
- data/lib/lapsoss/adapters/rollbar_adapter.rb +64 -122
- data/lib/lapsoss/adapters/sentry_adapter.rb +77 -143
- data/lib/lapsoss/backtrace_processor.rb +1 -1
- data/lib/lapsoss/breadcrumb.rb +59 -0
- data/lib/lapsoss/client.rb +3 -5
- data/lib/lapsoss/configuration.rb +26 -31
- data/lib/lapsoss/event.rb +90 -96
- data/lib/lapsoss/fingerprinter.rb +57 -49
- data/lib/lapsoss/merged_scope.rb +1 -6
- data/lib/lapsoss/middleware/release_tracker.rb +11 -98
- data/lib/lapsoss/pipeline_builder.rb +2 -2
- data/lib/lapsoss/rails_error_subscriber.rb +3 -4
- data/lib/lapsoss/rails_middleware.rb +2 -2
- data/lib/lapsoss/railtie.rb +13 -2
- data/lib/lapsoss/registry.rb +7 -7
- data/lib/lapsoss/router.rb +1 -3
- data/lib/lapsoss/scope.rb +1 -6
- data/lib/lapsoss/scrubber.rb +15 -148
- data/lib/lapsoss/validators.rb +63 -92
- data/lib/lapsoss/version.rb +1 -1
- metadata +8 -24
- data/CHANGELOG.md +0 -5
- data/lib/lapsoss/exclusion_configuration.rb +0 -30
- data/lib/lapsoss/exclusion_presets.rb +0 -249
- data/lib/lapsoss/middleware/sample_filter.rb +0 -23
- data/lib/lapsoss/middleware/sampling_middleware.rb +0 -18
- data/lib/lapsoss/middleware/user_context_enhancer.rb +0 -46
- data/lib/lapsoss/release_providers.rb +0 -110
- data/lib/lapsoss/sampling/adaptive_sampler.rb +0 -46
- data/lib/lapsoss/sampling/composite_sampler.rb +0 -26
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +0 -30
- data/lib/lapsoss/sampling/exception_type_sampler.rb +0 -44
- data/lib/lapsoss/sampling/health_based_sampler.rb +0 -19
- data/lib/lapsoss/sampling/sampling_factory.rb +0 -69
- data/lib/lapsoss/sampling/time_based_sampler.rb +0 -44
- data/lib/lapsoss/sampling/user_based_sampler.rb +0 -42
- data/lib/lapsoss/user_context.rb +0 -175
- data/lib/lapsoss/user_context_integrations.rb +0 -39
- data/lib/lapsoss/user_context_middleware.rb +0 -50
- data/lib/lapsoss/user_context_provider.rb +0 -93
- data/lib/lapsoss/utils.rb +0 -13
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Lapsoss
|
4
|
-
module Sampling
|
5
|
-
class TimeBasedSampler < Base
|
6
|
-
def initialize(schedule: {})
|
7
|
-
@schedule = schedule
|
8
|
-
@default_rate = schedule.fetch(:default, 1.0)
|
9
|
-
end
|
10
|
-
|
11
|
-
def sample?(_event, _hint = {})
|
12
|
-
now = Time.zone.now
|
13
|
-
rate = find_rate_for_time(now)
|
14
|
-
rate > rand
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def find_rate_for_time(time)
|
20
|
-
hour = time.hour
|
21
|
-
day_of_week = time.wday # 0 = Sunday
|
22
|
-
|
23
|
-
# Check specific hour
|
24
|
-
hour_key = :"hour_#{hour}"
|
25
|
-
return @schedule[hour_key] if @schedule.key?(hour_key)
|
26
|
-
|
27
|
-
# Check day of week
|
28
|
-
day_names = %i[sunday monday tuesday wednesday thursday friday saturday]
|
29
|
-
day_key = day_names[day_of_week]
|
30
|
-
return @schedule[day_key] if @schedule.key?(day_key)
|
31
|
-
|
32
|
-
# Check business hours
|
33
|
-
if @schedule.key?(:business_hours) && (9..17).cover?(hour) && (1..5).cover?(day_of_week)
|
34
|
-
return @schedule[:business_hours]
|
35
|
-
end
|
36
|
-
|
37
|
-
# Check weekends
|
38
|
-
return @schedule[:weekends] if @schedule.key?(:weekends) && [ 0, 6 ].include?(day_of_week)
|
39
|
-
|
40
|
-
@default_rate
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Lapsoss
|
4
|
-
module Sampling
|
5
|
-
class UserBasedSampler < Base
|
6
|
-
def initialize(rates: {})
|
7
|
-
@rates = rates
|
8
|
-
@default_rate = rates.fetch(:default, 1.0)
|
9
|
-
end
|
10
|
-
|
11
|
-
def sample?(event, _hint = {})
|
12
|
-
user = event.context[:user]
|
13
|
-
return @default_rate > rand unless user
|
14
|
-
|
15
|
-
rate = find_rate_for_user(user)
|
16
|
-
rate > rand
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def find_rate_for_user(user)
|
22
|
-
# Check specific user ID
|
23
|
-
user_id = user[:id] || user["id"]
|
24
|
-
return @rates[user_id] if user_id && @rates.key?(user_id)
|
25
|
-
|
26
|
-
# Check user segments
|
27
|
-
@rates.each do |segment, rate|
|
28
|
-
case segment
|
29
|
-
when :internal, "internal"
|
30
|
-
return rate if user[:internal] || user["internal"]
|
31
|
-
when :premium, "premium"
|
32
|
-
return rate if user[:premium] || user["premium"]
|
33
|
-
when :beta, "beta"
|
34
|
-
return rate if user[:beta] || user["beta"]
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
@default_rate
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
data/lib/lapsoss/user_context.rb
DELETED
@@ -1,175 +0,0 @@
|
|
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
|
-
next if @allowed_fields&.exclude?(key_sym)
|
34
|
-
|
35
|
-
# Apply privacy filtering
|
36
|
-
processed[key] = if @privacy_mode && sensitive_field?(key_sym)
|
37
|
-
apply_privacy_filter(key_sym, value)
|
38
|
-
else
|
39
|
-
transform_field(key_sym, value)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
processed
|
44
|
-
end
|
45
|
-
|
46
|
-
def merge_user_data(existing_data, new_data)
|
47
|
-
existing_processed = process_user_data(existing_data || {})
|
48
|
-
new_processed = process_user_data(new_data || {})
|
49
|
-
|
50
|
-
existing_processed.merge(new_processed)
|
51
|
-
end
|
52
|
-
|
53
|
-
def extract_user_id(user_data)
|
54
|
-
return nil unless user_data.is_a?(Hash)
|
55
|
-
|
56
|
-
# Try common user ID fields in order of preference
|
57
|
-
%i[id user_id uuid guid].each do |field|
|
58
|
-
value = user_data[field] || user_data[field.to_s]
|
59
|
-
return value if value
|
60
|
-
end
|
61
|
-
|
62
|
-
nil
|
63
|
-
end
|
64
|
-
|
65
|
-
def extract_user_segment(user_data)
|
66
|
-
return nil unless user_data.is_a?(Hash)
|
67
|
-
|
68
|
-
segments = {}
|
69
|
-
|
70
|
-
# Check for common user segments
|
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?
|
75
|
-
|
76
|
-
# Check role-based segments
|
77
|
-
if role = user_data[:role] || user_data["role"]
|
78
|
-
segments[:role] = role.to_s.downcase
|
79
|
-
end
|
80
|
-
|
81
|
-
# Check plan-based segments
|
82
|
-
if plan = user_data[:plan] || user_data["plan"]
|
83
|
-
segments[:plan] = plan.to_s.downcase
|
84
|
-
end
|
85
|
-
|
86
|
-
segments.compact
|
87
|
-
end
|
88
|
-
|
89
|
-
def sanitize_for_logging(user_data)
|
90
|
-
return {} unless user_data.is_a?(Hash)
|
91
|
-
|
92
|
-
sanitized = {}
|
93
|
-
|
94
|
-
user_data.each do |key, value|
|
95
|
-
key_sym = key.to_sym
|
96
|
-
|
97
|
-
sanitized[key] = if sensitive_field?(key_sym)
|
98
|
-
"[REDACTED]"
|
99
|
-
elsif value.is_a?(Hash)
|
100
|
-
sanitize_for_logging(value)
|
101
|
-
elsif value.is_a?(Array)
|
102
|
-
value.map { |v| v.is_a?(Hash) ? sanitize_for_logging(v) : v }
|
103
|
-
else
|
104
|
-
value
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
sanitized
|
109
|
-
end
|
110
|
-
|
111
|
-
private
|
112
|
-
|
113
|
-
def sensitive_field?(field)
|
114
|
-
SENSITIVE_FIELDS.include?(field) || field.to_s.match?(/password|secret|token|key|ssn|credit|card/i)
|
115
|
-
end
|
116
|
-
|
117
|
-
def apply_privacy_filter(field, value)
|
118
|
-
case field
|
119
|
-
when :email
|
120
|
-
mask_email(value)
|
121
|
-
when :phone, :mobile, :telephone
|
122
|
-
mask_phone(value)
|
123
|
-
when :ip_address, :last_login_ip
|
124
|
-
mask_ip(value)
|
125
|
-
else
|
126
|
-
"[FILTERED]"
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def transform_field(field, value)
|
131
|
-
if transformer = @field_transformers[field]
|
132
|
-
transformer.call(value)
|
133
|
-
else
|
134
|
-
value
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def mask_email(email)
|
139
|
-
return "[INVALID_EMAIL]" unless email.is_a?(String) && email.include?("@")
|
140
|
-
|
141
|
-
local, domain = email.split("@", 2)
|
142
|
-
masked_local = local.length > 2 ? "#{local[0]}***#{local[-1]}" : "***"
|
143
|
-
"#{masked_local}@#{domain}"
|
144
|
-
end
|
145
|
-
|
146
|
-
def mask_phone(phone)
|
147
|
-
return "[INVALID_PHONE]" unless phone.is_a?(String)
|
148
|
-
|
149
|
-
# Remove all non-digits
|
150
|
-
digits = phone.gsub(/\D/, "")
|
151
|
-
return "[INVALID_PHONE]" if digits.length < 4
|
152
|
-
|
153
|
-
# Show last 4 digits
|
154
|
-
("*" * (digits.length - 4)) + digits[-4..]
|
155
|
-
end
|
156
|
-
|
157
|
-
def mask_ip(ip)
|
158
|
-
return "[INVALID_IP]" unless ip.is_a?(String)
|
159
|
-
|
160
|
-
if ip.include?(":")
|
161
|
-
# IPv6 - mask last 4 groups
|
162
|
-
parts = ip.split(":")
|
163
|
-
parts[-4..-1] = [ "****" ] * 4 if parts.length >= 4
|
164
|
-
parts.join(":")
|
165
|
-
else
|
166
|
-
# IPv4 - mask last octet
|
167
|
-
parts = ip.split(".")
|
168
|
-
return "[INVALID_IP]" if parts.length != 4
|
169
|
-
|
170
|
-
parts[-1] = "***"
|
171
|
-
parts.join(".")
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
@@ -1,39 +0,0 @@
|
|
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
|
@@ -1,50 +0,0 @@
|
|
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
|
@@ -1,93 +0,0 @@
|
|
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
|