sciosano 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/lib/sciosano/breadcrumb.rb +121 -0
- data/lib/sciosano/client.rb +230 -0
- data/lib/sciosano/configuration.rb +157 -0
- data/lib/sciosano/context.rb +67 -0
- data/lib/sciosano/integrations/active_job.rb +134 -0
- data/lib/sciosano/integrations/rack.rb +128 -0
- data/lib/sciosano/integrations/rails.rb +151 -0
- data/lib/sciosano/integrations/sidekiq.rb +164 -0
- data/lib/sciosano/report.rb +175 -0
- data/lib/sciosano/version.rb +5 -0
- data/lib/sciosano.rb +213 -0
- data/sciosano.gemspec +40 -0
- metadata +163 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sciosano
|
|
4
|
+
module Integrations
|
|
5
|
+
# Rack middleware for capturing exceptions in Rack applications
|
|
6
|
+
class RackMiddleware
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
# Add request breadcrumb
|
|
13
|
+
add_request_breadcrumb(env)
|
|
14
|
+
|
|
15
|
+
# Set request context
|
|
16
|
+
Sciosano.client&.with_context(request: build_request_context(env)) do
|
|
17
|
+
@app.call(env)
|
|
18
|
+
end
|
|
19
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
20
|
+
# Capture exception but re-raise
|
|
21
|
+
capture_exception(e, env)
|
|
22
|
+
raise
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def add_request_breadcrumb(env)
|
|
28
|
+
return unless Sciosano.client
|
|
29
|
+
|
|
30
|
+
method = env["REQUEST_METHOD"]
|
|
31
|
+
path = env["PATH_INFO"]
|
|
32
|
+
|
|
33
|
+
Sciosano.add_breadcrumb(
|
|
34
|
+
type: :http,
|
|
35
|
+
category: "request",
|
|
36
|
+
message: "#{method} #{path}",
|
|
37
|
+
data: {
|
|
38
|
+
method: method,
|
|
39
|
+
path: path,
|
|
40
|
+
query_string: env["QUERY_STRING"]
|
|
41
|
+
}.compact
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_request_context(env)
|
|
46
|
+
request = Rack::Request.new(env)
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
url: request.url,
|
|
50
|
+
route: env["PATH_INFO"],
|
|
51
|
+
method: request.request_method,
|
|
52
|
+
query_string: request.query_string,
|
|
53
|
+
remote_ip: request.ip,
|
|
54
|
+
user_agent: request.user_agent,
|
|
55
|
+
referer: request.referer,
|
|
56
|
+
headers: extract_headers(env)
|
|
57
|
+
}.compact
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_headers(env)
|
|
61
|
+
headers = {}
|
|
62
|
+
env.each do |key, value|
|
|
63
|
+
next unless key.start_with?("HTTP_")
|
|
64
|
+
next if sensitive_header?(key)
|
|
65
|
+
|
|
66
|
+
header_name = key.sub(/^HTTP_/, "").split("_").map(&:capitalize).join("-")
|
|
67
|
+
headers[header_name] = value
|
|
68
|
+
end
|
|
69
|
+
headers
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sensitive_header?(key)
|
|
73
|
+
%w[HTTP_COOKIE HTTP_AUTHORIZATION HTTP_X_API_KEY].include?(key)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def capture_exception(exception, env)
|
|
77
|
+
return unless Sciosano.client
|
|
78
|
+
|
|
79
|
+
# Add additional request context
|
|
80
|
+
request = Rack::Request.new(env)
|
|
81
|
+
|
|
82
|
+
Sciosano.capture_exception(exception, {
|
|
83
|
+
request: {
|
|
84
|
+
url: request.url,
|
|
85
|
+
method: request.request_method,
|
|
86
|
+
params: filtered_params(request.params)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def filtered_params(params)
|
|
92
|
+
return {} unless params.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
Sciosano.configuration.filter_parameters.each do |filter|
|
|
95
|
+
params = filter_hash(params, filter)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
params
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def filter_hash(hash, filter)
|
|
102
|
+
hash.transform_values do |value|
|
|
103
|
+
case value
|
|
104
|
+
when Hash
|
|
105
|
+
filter_hash(value, filter)
|
|
106
|
+
else
|
|
107
|
+
value
|
|
108
|
+
end
|
|
109
|
+
end.transform_keys do |key|
|
|
110
|
+
if matches_filter?(key, filter)
|
|
111
|
+
"[FILTERED]"
|
|
112
|
+
else
|
|
113
|
+
key
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def matches_filter?(key, filter)
|
|
119
|
+
case filter
|
|
120
|
+
when Regexp
|
|
121
|
+
filter.match?(key.to_s)
|
|
122
|
+
else
|
|
123
|
+
key.to_s.downcase.include?(filter.to_s.downcase)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sciosano
|
|
4
|
+
module Integrations
|
|
5
|
+
# Rails integration via Railtie
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer "Sciosano.configure_rails_initialization", before: :build_middleware_stack do |app|
|
|
8
|
+
# Insert Rack middleware BEFORE ShowExceptions so we can capture errors
|
|
9
|
+
# before Rails renders error pages
|
|
10
|
+
app.middleware.insert_before(
|
|
11
|
+
ActionDispatch::ShowExceptions,
|
|
12
|
+
Sciosano::Integrations::RackMiddleware
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Subscribe to ActiveSupport notifications for breadcrumbs
|
|
17
|
+
initializer "Sciosano.subscribe_to_notifications" do
|
|
18
|
+
# Subscribe to SQL queries
|
|
19
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
|
20
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
21
|
+
|
|
22
|
+
# Skip SCHEMA queries and very fast queries
|
|
23
|
+
next if event.payload[:name] == "SCHEMA"
|
|
24
|
+
next if event.duration < 1 # Skip queries under 1ms
|
|
25
|
+
|
|
26
|
+
Sciosano.add_breadcrumb(
|
|
27
|
+
type: :default,
|
|
28
|
+
category: "query",
|
|
29
|
+
message: event.payload[:name] || "SQL Query",
|
|
30
|
+
data: {
|
|
31
|
+
sql: event.payload[:sql]&.slice(0, 500),
|
|
32
|
+
duration: event.duration.round(2),
|
|
33
|
+
cached: event.payload[:cached]
|
|
34
|
+
}.compact
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Subscribe to controller actions
|
|
39
|
+
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
|
|
40
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
41
|
+
|
|
42
|
+
Sciosano.add_breadcrumb(
|
|
43
|
+
type: :default,
|
|
44
|
+
category: "controller",
|
|
45
|
+
message: "#{event.payload[:controller]}##{event.payload[:action]}",
|
|
46
|
+
data: {
|
|
47
|
+
controller: event.payload[:controller],
|
|
48
|
+
action: event.payload[:action],
|
|
49
|
+
method: event.payload[:method],
|
|
50
|
+
path: event.payload[:path],
|
|
51
|
+
status: event.payload[:status],
|
|
52
|
+
duration: event.duration.round(2)
|
|
53
|
+
}.compact
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Subscribe to view rendering
|
|
58
|
+
ActiveSupport::Notifications.subscribe("render_template.action_view") do |*args|
|
|
59
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
60
|
+
|
|
61
|
+
# Only track significant renders (over 5ms)
|
|
62
|
+
next if event.duration < 5
|
|
63
|
+
|
|
64
|
+
Sciosano.add_breadcrumb(
|
|
65
|
+
type: :default,
|
|
66
|
+
category: "view",
|
|
67
|
+
message: "Render #{event.payload[:identifier]}",
|
|
68
|
+
data: {
|
|
69
|
+
template: event.payload[:identifier],
|
|
70
|
+
duration: event.duration.round(2)
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Subscribe to cache operations
|
|
76
|
+
ActiveSupport::Notifications.subscribe(/cache_(read|write|delete)\.active_support/) do |*args|
|
|
77
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
78
|
+
|
|
79
|
+
Sciosano.add_breadcrumb(
|
|
80
|
+
type: :default,
|
|
81
|
+
category: "cache",
|
|
82
|
+
message: "Cache #{event.name.split('.').first.sub('cache_', '')}",
|
|
83
|
+
data: {
|
|
84
|
+
key: event.payload[:key]&.to_s&.slice(0, 100),
|
|
85
|
+
hit: event.payload[:hit]
|
|
86
|
+
}.compact
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Subscribe to mailer events
|
|
91
|
+
ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |*args|
|
|
92
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
93
|
+
|
|
94
|
+
Sciosano.add_breadcrumb(
|
|
95
|
+
type: :default,
|
|
96
|
+
category: "mailer",
|
|
97
|
+
message: "Send #{event.payload[:mailer]}",
|
|
98
|
+
data: {
|
|
99
|
+
mailer: event.payload[:mailer],
|
|
100
|
+
to: event.payload[:to]&.first,
|
|
101
|
+
subject: event.payload[:subject]&.slice(0, 100)
|
|
102
|
+
}.compact
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Set user from controller
|
|
108
|
+
initializer "Sciosano.controller_extensions" do
|
|
109
|
+
ActiveSupport.on_load(:action_controller) do
|
|
110
|
+
include Sciosano::Integrations::ControllerExtensions
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
config.after_initialize do
|
|
115
|
+
# Auto-configure from Rails
|
|
116
|
+
if Sciosano.configuration.api_key && !Sciosano.client
|
|
117
|
+
Sciosano.configure do |config|
|
|
118
|
+
config.environment = Rails.env
|
|
119
|
+
config.release ||= Rails.application.class.module_parent_name
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Controller extensions for setting user context
|
|
126
|
+
module ControllerExtensions
|
|
127
|
+
extend ActiveSupport::Concern
|
|
128
|
+
|
|
129
|
+
included do
|
|
130
|
+
around_action :sciosano_set_context, if: -> { Sciosano.client }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def sciosano_set_context
|
|
136
|
+
# Set user if method exists
|
|
137
|
+
if respond_to?(:current_user, true) && current_user
|
|
138
|
+
Sciosano.set_user(
|
|
139
|
+
id: current_user.try(:id),
|
|
140
|
+
email: current_user.try(:email),
|
|
141
|
+
name: current_user.try(:name) || current_user.try(:full_name)
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
yield
|
|
146
|
+
ensure
|
|
147
|
+
Sciosano.clear_user
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sciosano
|
|
4
|
+
module Integrations
|
|
5
|
+
# Sidekiq middleware for capturing job errors
|
|
6
|
+
module Sidekiq
|
|
7
|
+
# Server middleware (runs on job worker)
|
|
8
|
+
class ServerMiddleware
|
|
9
|
+
def call(worker, job, queue)
|
|
10
|
+
# Add job breadcrumb
|
|
11
|
+
add_job_breadcrumb(worker, job, queue)
|
|
12
|
+
|
|
13
|
+
# Set job context
|
|
14
|
+
Sciosano.client&.with_context(
|
|
15
|
+
extra: {
|
|
16
|
+
sidekiq_job: job["class"],
|
|
17
|
+
sidekiq_queue: queue,
|
|
18
|
+
sidekiq_jid: job["jid"],
|
|
19
|
+
sidekiq_retry_count: job["retry_count"]
|
|
20
|
+
}
|
|
21
|
+
) do
|
|
22
|
+
yield
|
|
23
|
+
end
|
|
24
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
25
|
+
capture_exception(e, worker, job, queue)
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def add_job_breadcrumb(worker, job, queue)
|
|
32
|
+
return unless Sciosano.client
|
|
33
|
+
|
|
34
|
+
Sciosano.add_breadcrumb(
|
|
35
|
+
type: :default,
|
|
36
|
+
category: "sidekiq",
|
|
37
|
+
message: "Processing #{job['class']}",
|
|
38
|
+
data: {
|
|
39
|
+
job_class: job["class"],
|
|
40
|
+
queue: queue,
|
|
41
|
+
jid: job["jid"],
|
|
42
|
+
args_count: job["args"]&.size || 0
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def capture_exception(exception, worker, job, queue)
|
|
48
|
+
return unless Sciosano.client
|
|
49
|
+
|
|
50
|
+
# Include job context in report
|
|
51
|
+
Sciosano.capture_exception(exception, {
|
|
52
|
+
tags: {
|
|
53
|
+
"sidekiq.queue" => queue,
|
|
54
|
+
"sidekiq.job" => job["class"]
|
|
55
|
+
},
|
|
56
|
+
extra: {
|
|
57
|
+
sidekiq: {
|
|
58
|
+
job_class: job["class"],
|
|
59
|
+
queue: queue,
|
|
60
|
+
jid: job["jid"],
|
|
61
|
+
args: sanitize_args(job["args"]),
|
|
62
|
+
retry_count: job["retry_count"],
|
|
63
|
+
created_at: job["created_at"],
|
|
64
|
+
enqueued_at: job["enqueued_at"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def sanitize_args(args)
|
|
71
|
+
return [] unless args.is_a?(Array)
|
|
72
|
+
|
|
73
|
+
args.map do |arg|
|
|
74
|
+
case arg
|
|
75
|
+
when Hash
|
|
76
|
+
filter_sensitive_data(arg)
|
|
77
|
+
when String
|
|
78
|
+
arg.length > 200 ? "#{arg[0..200]}..." : arg
|
|
79
|
+
else
|
|
80
|
+
arg
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def filter_sensitive_data(hash)
|
|
86
|
+
hash.transform_values do |value|
|
|
87
|
+
case value
|
|
88
|
+
when Hash
|
|
89
|
+
filter_sensitive_data(value)
|
|
90
|
+
else
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
end.transform_keys do |key|
|
|
94
|
+
if sensitive_key?(key)
|
|
95
|
+
"[FILTERED]"
|
|
96
|
+
else
|
|
97
|
+
key
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def sensitive_key?(key)
|
|
103
|
+
key.to_s.downcase.match?(/password|secret|token|key|auth|credit|card/)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Client middleware (runs when job is enqueued)
|
|
108
|
+
class ClientMiddleware
|
|
109
|
+
def call(worker_class, job, queue, redis_pool)
|
|
110
|
+
# Add enqueue breadcrumb
|
|
111
|
+
Sciosano.add_breadcrumb(
|
|
112
|
+
type: :default,
|
|
113
|
+
category: "sidekiq",
|
|
114
|
+
message: "Enqueued #{worker_class}",
|
|
115
|
+
data: {
|
|
116
|
+
job_class: worker_class.to_s,
|
|
117
|
+
queue: queue,
|
|
118
|
+
jid: job["jid"]
|
|
119
|
+
}
|
|
120
|
+
) if Sciosano.client
|
|
121
|
+
|
|
122
|
+
yield
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Register Sidekiq middleware
|
|
127
|
+
def self.setup
|
|
128
|
+
::Sidekiq.configure_server do |config|
|
|
129
|
+
config.server_middleware do |chain|
|
|
130
|
+
chain.add ServerMiddleware
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
config.client_middleware do |chain|
|
|
134
|
+
chain.add ClientMiddleware
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Also register as error handler for guaranteed capture
|
|
138
|
+
# This catches errors even if middleware chain has timing issues
|
|
139
|
+
config.error_handlers << proc do |exception, context|
|
|
140
|
+
next unless Sciosano.client
|
|
141
|
+
|
|
142
|
+
Sciosano.capture_exception(exception, {
|
|
143
|
+
tags: {
|
|
144
|
+
"sidekiq.error_handler" => "true"
|
|
145
|
+
},
|
|
146
|
+
extra: {
|
|
147
|
+
sidekiq_context: context
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
::Sidekiq.configure_client do |config|
|
|
154
|
+
config.client_middleware do |chain|
|
|
155
|
+
chain.add ClientMiddleware
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Auto-setup when loaded
|
|
164
|
+
Sciosano::Integrations::Sidekiq.setup if defined?(::Sidekiq)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sciosano
|
|
4
|
+
# Report represents an error or message to be sent to sciosano
|
|
5
|
+
class Report
|
|
6
|
+
attr_reader :id, :report_type, :source, :error, :user_description,
|
|
7
|
+
:context, :environment, :breadcrumbs, :tags, :timestamp
|
|
8
|
+
|
|
9
|
+
def initialize(attrs = {})
|
|
10
|
+
@id = attrs[:id] || SecureRandom.uuid
|
|
11
|
+
@report_type = attrs[:report_type] || "error"
|
|
12
|
+
@source = attrs[:source] || "backend"
|
|
13
|
+
@error = attrs[:error]
|
|
14
|
+
@user_description = attrs[:user_description]
|
|
15
|
+
@context = attrs[:context] || {}
|
|
16
|
+
@environment = attrs[:environment] || {}
|
|
17
|
+
@breadcrumbs = attrs[:breadcrumbs] || []
|
|
18
|
+
@tags = attrs[:tags] || {}
|
|
19
|
+
@timestamp = attrs[:timestamp] || Time.now.utc.iso8601(3)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Build report from an exception
|
|
23
|
+
#
|
|
24
|
+
# @param exception [Exception]
|
|
25
|
+
# @param context [Context]
|
|
26
|
+
# @param breadcrumbs [Array<Breadcrumb>]
|
|
27
|
+
# @param configuration [Configuration]
|
|
28
|
+
# @return [Report]
|
|
29
|
+
def self.build_from_exception(exception, context:, breadcrumbs:, configuration:)
|
|
30
|
+
new(
|
|
31
|
+
report_type: "error",
|
|
32
|
+
source: "backend",
|
|
33
|
+
error: build_error_hash(exception),
|
|
34
|
+
context: build_context_hash(context, configuration),
|
|
35
|
+
environment: build_environment_hash(configuration),
|
|
36
|
+
breadcrumbs: breadcrumbs.map(&:to_h),
|
|
37
|
+
tags: context.tags
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Build report from a message
|
|
42
|
+
#
|
|
43
|
+
# @param message [String]
|
|
44
|
+
# @param level [Symbol]
|
|
45
|
+
# @param context [Context]
|
|
46
|
+
# @param breadcrumbs [Array<Breadcrumb>]
|
|
47
|
+
# @param configuration [Configuration]
|
|
48
|
+
# @return [Report]
|
|
49
|
+
def self.build_from_message(message, level:, context:, breadcrumbs:, configuration:)
|
|
50
|
+
new(
|
|
51
|
+
report_type: "feedback",
|
|
52
|
+
source: "backend",
|
|
53
|
+
user_description: message,
|
|
54
|
+
context: build_context_hash(context, configuration),
|
|
55
|
+
environment: build_environment_hash(configuration),
|
|
56
|
+
breadcrumbs: breadcrumbs.map(&:to_h),
|
|
57
|
+
tags: context.tags.merge("level" => level.to_s)
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Convert to JSON for API
|
|
62
|
+
#
|
|
63
|
+
# @return [String]
|
|
64
|
+
def to_json(*_args)
|
|
65
|
+
JSON.generate(to_h)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Convert to hash
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash]
|
|
71
|
+
def to_h
|
|
72
|
+
{
|
|
73
|
+
reportType: report_type,
|
|
74
|
+
source: source,
|
|
75
|
+
error: error,
|
|
76
|
+
userDescription: user_description,
|
|
77
|
+
context: context,
|
|
78
|
+
environment: environment,
|
|
79
|
+
breadcrumbs: breadcrumbs,
|
|
80
|
+
tags: tags,
|
|
81
|
+
timestamp: timestamp
|
|
82
|
+
}.compact
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class << self
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def build_error_hash(exception)
|
|
89
|
+
hash = {
|
|
90
|
+
name: exception.class.name,
|
|
91
|
+
message: exception.message,
|
|
92
|
+
file: extract_file(exception.backtrace),
|
|
93
|
+
line: extract_line(exception.backtrace)
|
|
94
|
+
}.compact
|
|
95
|
+
|
|
96
|
+
# stackTrace is required, always include it (even if empty string)
|
|
97
|
+
hash[:stackTrace] = format_backtrace(exception.backtrace)
|
|
98
|
+
hash
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def format_backtrace(backtrace)
|
|
102
|
+
return "" unless backtrace
|
|
103
|
+
|
|
104
|
+
backtrace.first(50).join("\n")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extract_file(backtrace)
|
|
108
|
+
return nil unless backtrace&.first
|
|
109
|
+
|
|
110
|
+
match = backtrace.first.match(/^(.+):(\d+)/)
|
|
111
|
+
match&.[](1)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def extract_line(backtrace)
|
|
115
|
+
return nil unless backtrace&.first
|
|
116
|
+
|
|
117
|
+
match = backtrace.first.match(/^(.+):(\d+)/)
|
|
118
|
+
match&.[](2)&.to_i
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_context_hash(context, configuration)
|
|
122
|
+
hash = {
|
|
123
|
+
userId: context.user&.dig(:id)&.to_s,
|
|
124
|
+
userEmail: context.user&.dig(:email),
|
|
125
|
+
custom: filter_params(context.extra, configuration.filter_parameters),
|
|
126
|
+
url: context.request&.dig(:url) || ""
|
|
127
|
+
}.compact
|
|
128
|
+
|
|
129
|
+
# url is required, ensure it's always present even after compact
|
|
130
|
+
hash[:url] ||= ""
|
|
131
|
+
|
|
132
|
+
# Add route if available
|
|
133
|
+
if context.request&.dig(:route)
|
|
134
|
+
hash[:route] = context.request[:route]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
hash
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_environment_hash(configuration)
|
|
141
|
+
{
|
|
142
|
+
rubyVersion: RUBY_VERSION,
|
|
143
|
+
rubyPlatform: RUBY_PLATFORM,
|
|
144
|
+
hostname: Socket.gethostname,
|
|
145
|
+
nodeEnv: configuration.environment,
|
|
146
|
+
appVersion: configuration.release,
|
|
147
|
+
railsVersion: defined?(Rails) ? Rails.version : nil,
|
|
148
|
+
sidekiqVersion: defined?(Sidekiq) ? Sidekiq::VERSION : nil
|
|
149
|
+
}.compact
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def filter_params(hash, filters)
|
|
153
|
+
return hash if filters.empty?
|
|
154
|
+
|
|
155
|
+
hash.transform_values do |value|
|
|
156
|
+
case value
|
|
157
|
+
when Hash
|
|
158
|
+
filter_params(value, filters)
|
|
159
|
+
else
|
|
160
|
+
value
|
|
161
|
+
end
|
|
162
|
+
end.reject do |key, _|
|
|
163
|
+
filters.any? do |filter|
|
|
164
|
+
case filter
|
|
165
|
+
when Regexp
|
|
166
|
+
filter.match?(key.to_s)
|
|
167
|
+
else
|
|
168
|
+
key.to_s.downcase.include?(filter.to_s.downcase)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|