closeyourit-ruby 0.3.4 → 0.5.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 +4 -4
- data/README.md +3 -3
- data/lib/closeyourit/configuration.rb +10 -1
- data/lib/closeyourit/events/error_event.rb +17 -2
- data/lib/closeyourit/line_cache.rb +41 -0
- data/lib/closeyourit/rails/request_body.rb +87 -0
- data/lib/closeyourit/rails/request_context.rb +21 -1
- data/lib/closeyourit/scope.rb +32 -3
- data/lib/closeyourit/version.rb +1 -1
- data/lib/closeyourit-ruby.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6dc227e626e4b47b931a80698aa8b2567bacb033dad2e5e42d6c8497890debe2
|
|
4
|
+
data.tar.gz: d012c3b1272b238e796ff7cdb1cf687276932a0c5196f14fbd0d5393edf9d938
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4cd8b4aaa3cb4ff246d2607d3f65ec71c696c37b8ccb9525979c99175bcf5f1c35f051574d034b8cb7198fb62362c2683e74e076989cf3e2ea3ae34408385a7
|
|
7
|
+
data.tar.gz: 0b54035901e710005c1cc45339a30f0df0277e0d0708c9faff417a536eba0000b77b2a2bbb9c7b608b8528b4ac04b65f7f16b31dbd8320ec1e2d37f9465db2a4
|
data/README.md
CHANGED
|
@@ -26,12 +26,12 @@ Caratteristiche:
|
|
|
26
26
|
|
|
27
27
|
## Installazione
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Pubblicata su [RubyGems](https://rubygems.org/gems/closeyourit-ruby):
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
32
|
# Gemfile del progetto da monitorare
|
|
33
|
-
gem "closeyourit-ruby"
|
|
34
|
-
# in sviluppo locale:
|
|
33
|
+
gem "closeyourit-ruby"
|
|
34
|
+
# in sviluppo locale, dal checkout:
|
|
35
35
|
# gem "closeyourit-ruby", path: "../closeyourit-ruby"
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -20,7 +20,8 @@ module CloseYourIt
|
|
|
20
20
|
:slow_query_threshold_ms, :slow_method_threshold_ms,
|
|
21
21
|
:send_pii, :obfuscate_sql, :send_server_name,
|
|
22
22
|
:capture_query_bindings, :capture_method_arguments,
|
|
23
|
-
:capture_request, :request_header_allowlist,
|
|
23
|
+
:capture_request, :request_header_allowlist, :context_lines,
|
|
24
|
+
:capture_request_body,
|
|
24
25
|
:breadcrumbs_enabled, :max_breadcrumbs, :sample_rate,
|
|
25
26
|
:capture_handled_errors, :report_active_job_errors,
|
|
26
27
|
:logs_enabled, :logs_sample_rate, :logs_batch_size, :logs_flush_interval,
|
|
@@ -55,6 +56,14 @@ module CloseYourIt
|
|
|
55
56
|
@capture_request = true
|
|
56
57
|
@request_header_allowlist = DEFAULT_REQUEST_HEADER_ALLOWLIST.dup
|
|
57
58
|
|
|
59
|
+
# Righe di sorgente attorno a ogni frame dello stacktrace (pre/context/post). 0 = disattivo.
|
|
60
|
+
@context_lines = 3
|
|
61
|
+
|
|
62
|
+
# Params del body della richiesta nell'evento (`request.data`), estratti LAZY solo quando
|
|
63
|
+
# l'errore accade, sanitizzati e scrubbati (denylist + filter_parameters). Il backend
|
|
64
|
+
# ri-scruba difensivamente. Upload → placeholder, cap 64 KB.
|
|
65
|
+
@capture_request_body = true
|
|
66
|
+
|
|
58
67
|
# Breadcrumbs: cronologia (query offuscate, eventi custom) allegata all'errore.
|
|
59
68
|
@breadcrumbs_enabled = true
|
|
60
69
|
@max_breadcrumbs = 100
|
|
@@ -4,6 +4,7 @@ require "securerandom"
|
|
|
4
4
|
require "socket"
|
|
5
5
|
require_relative "../event"
|
|
6
6
|
require_relative "../scrubber"
|
|
7
|
+
require_relative "../line_cache"
|
|
7
8
|
|
|
8
9
|
module CloseYourIt
|
|
9
10
|
# Trasforma un'eccezione Ruby nel **payload evento Sentry** che il backend CloseYourIt ingerisce
|
|
@@ -72,21 +73,35 @@ module CloseYourIt
|
|
|
72
73
|
}
|
|
73
74
|
end
|
|
74
75
|
|
|
75
|
-
# Sentry-style: frame più recente per ultimo; chiavi filename/function/lineno/in_app/abs_path
|
|
76
|
+
# Sentry-style: frame più recente per ultimo; chiavi filename/function/lineno/in_app/abs_path
|
|
77
|
+
# + snippet di sorgente (pre_context/context_line/post_context) quando il file è leggibile.
|
|
76
78
|
def frames(locations)
|
|
77
79
|
return [] if locations.nil?
|
|
78
80
|
|
|
81
|
+
context_lines = @configuration.context_lines.to_i
|
|
79
82
|
locations.reverse.map do |loc|
|
|
80
|
-
{
|
|
83
|
+
frame = {
|
|
81
84
|
"filename" => loc.path,
|
|
82
85
|
"abs_path" => loc.path,
|
|
83
86
|
"function" => loc.label,
|
|
84
87
|
"lineno" => loc.lineno,
|
|
85
88
|
"in_app" => in_app?(loc.path)
|
|
86
89
|
}
|
|
90
|
+
add_context!(frame, loc.path, loc.lineno, context_lines) if context_lines.positive?
|
|
91
|
+
frame
|
|
87
92
|
end
|
|
88
93
|
end
|
|
89
94
|
|
|
95
|
+
def add_context!(frame, path, lineno, count)
|
|
96
|
+
lines = LineCache.lines(path)
|
|
97
|
+
return if lines.nil? || lineno.nil? || lineno < 1 || lineno > lines.size
|
|
98
|
+
|
|
99
|
+
index = lineno - 1
|
|
100
|
+
frame["pre_context"] = lines[[ index - count, 0 ].max...index]
|
|
101
|
+
frame["context_line"] = lines[index]
|
|
102
|
+
frame["post_context"] = lines[index + 1, count] || []
|
|
103
|
+
end
|
|
104
|
+
|
|
90
105
|
def in_app?(path)
|
|
91
106
|
return false if path.nil?
|
|
92
107
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloseYourIt
|
|
4
|
+
# Cache in-process delle righe dei file sorgente, per le context lines dei frame
|
|
5
|
+
# (pre_context/context_line/post_context). Bounded: oltre MAX_FILES si svuota per intero —
|
|
6
|
+
# semplice e sufficiente (i file di un'app in errore sono pochi e ricorrenti). Thread-safe.
|
|
7
|
+
module LineCache
|
|
8
|
+
MAX_FILES = 200
|
|
9
|
+
|
|
10
|
+
@cache = {}
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Righe del file (chomp-ate), oppure nil se non leggibile o path sintetico ("(eval)", "(irb)").
|
|
15
|
+
def lines(path)
|
|
16
|
+
return nil if path.nil? || path.empty? || path.start_with?("(")
|
|
17
|
+
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
return @cache[path] if @cache.key?(path)
|
|
20
|
+
|
|
21
|
+
@cache.clear if @cache.size >= MAX_FILES
|
|
22
|
+
@cache[path] = read_lines(path)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clear
|
|
27
|
+
@mutex.synchronize { @cache.clear }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def read_lines(path)
|
|
33
|
+
return nil unless File.readable?(path)
|
|
34
|
+
|
|
35
|
+
File.readlines(path, chomp: true)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
require_relative "../scrubber"
|
|
6
|
+
|
|
7
|
+
module CloseYourIt
|
|
8
|
+
module Rails
|
|
9
|
+
# Estrae i params del body della richiesta (`request.data`) al momento dell'EVENTO — mai
|
|
10
|
+
# eagerly a ogni richiesta (zero overhead sul percorso felice). Preferisce i params già
|
|
11
|
+
# parsati da Rails/Rack presenti in env; ripiega sulla rilettura di rack.input (con rewind)
|
|
12
|
+
# solo per JSON/form ≤ MAX_BODY_BYTES. Output sanitizzato (upload → "[FILE: …]", oggetti →
|
|
13
|
+
# "[OBJECT: …]", stringhe troncate) e scrubbato con la stessa denylist del resto del client.
|
|
14
|
+
module RequestBody
|
|
15
|
+
MAX_BODY_BYTES = 65_536
|
|
16
|
+
MAX_DEPTH = 8
|
|
17
|
+
MAX_STRING = 1024
|
|
18
|
+
|
|
19
|
+
FORM_TYPE = "application/x-www-form-urlencoded"
|
|
20
|
+
JSON_TYPE = "application/json"
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def extract(env)
|
|
24
|
+
params = parsed_params(env) || raw_params(env)
|
|
25
|
+
return nil if params.nil? || params.empty?
|
|
26
|
+
|
|
27
|
+
Scrubber.new(CloseYourIt.configuration).filter_params(sanitize(params, 0))
|
|
28
|
+
rescue StandardError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Params già parsati a monte (ActionDispatch o Rack): nessuna rilettura del body.
|
|
35
|
+
def parsed_params(env)
|
|
36
|
+
env["action_dispatch.request.request_parameters"] || env["rack.request.form_hash"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def raw_params(env)
|
|
40
|
+
raw = read_body(env)
|
|
41
|
+
return nil if raw.nil? || raw.empty?
|
|
42
|
+
|
|
43
|
+
case env["CONTENT_TYPE"].to_s.split(";").first.to_s.strip.downcase
|
|
44
|
+
when JSON_TYPE
|
|
45
|
+
value = JSON.parse(raw)
|
|
46
|
+
value.is_a?(Hash) ? value : { "_json" => value }
|
|
47
|
+
when FORM_TYPE
|
|
48
|
+
# Fallback flat (stdlib): il percorso reale con nesting passa dai params già parsati.
|
|
49
|
+
URI.decode_www_form(raw).to_h
|
|
50
|
+
end
|
|
51
|
+
rescue JSON::ParserError, ArgumentError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read_body(env)
|
|
56
|
+
input = env["rack.input"]
|
|
57
|
+
return nil if input.nil?
|
|
58
|
+
|
|
59
|
+
length = env["CONTENT_LENGTH"].to_i
|
|
60
|
+
return nil if length <= 0 || length > MAX_BODY_BYTES
|
|
61
|
+
|
|
62
|
+
input.rewind if input.respond_to?(:rewind)
|
|
63
|
+
raw = input.read(MAX_BODY_BYTES)
|
|
64
|
+
input.rewind if input.respond_to?(:rewind)
|
|
65
|
+
raw
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sanitize(value, depth)
|
|
69
|
+
return "[TRUNCATED]" if depth > MAX_DEPTH
|
|
70
|
+
|
|
71
|
+
case value
|
|
72
|
+
when Hash then value.each_with_object({}) { |(key, val), acc| acc[key.to_s] = sanitize(val, depth + 1) }
|
|
73
|
+
when Array then value.map { |item| sanitize(item, depth + 1) }
|
|
74
|
+
when String then value.size > MAX_STRING ? "#{value[0, MAX_STRING]}…" : value
|
|
75
|
+
when Numeric, true, false, nil then value
|
|
76
|
+
else
|
|
77
|
+
if value.respond_to?(:original_filename)
|
|
78
|
+
"[FILE: #{value.original_filename}]"
|
|
79
|
+
else
|
|
80
|
+
"[OBJECT: #{value.class.name}]"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require "rack/utils"
|
|
4
5
|
|
|
5
6
|
module CloseYourIt
|
|
6
7
|
module Rails
|
|
@@ -12,6 +13,10 @@ module CloseYourIt
|
|
|
12
13
|
# Header con prefisso non-HTTP_ in env Rack.
|
|
13
14
|
RAW_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
|
|
14
15
|
|
|
16
|
+
# Cookie di correlazione scritto dal browser SDK (session replay): id opaco della
|
|
17
|
+
# sessione di replay, trasportato da ogni richiesta same-origin.
|
|
18
|
+
REPLAY_COOKIE = "cyi_replay"
|
|
19
|
+
|
|
15
20
|
def initialize(app)
|
|
16
21
|
@app = app
|
|
17
22
|
end
|
|
@@ -20,7 +25,14 @@ module CloseYourIt
|
|
|
20
25
|
if enabled?
|
|
21
26
|
# trace_id sempre (correlazione log↔errori), anche con capture_request OFF.
|
|
22
27
|
CloseYourIt::Scope.current.trace_id = trace_id_for(env)
|
|
23
|
-
|
|
28
|
+
# Correlazione errore server ↔ session replay: l'id dal cookie finisce sullo scope
|
|
29
|
+
# → contexts.replay.replay_id dell'evento (stesso punto del percorso JS).
|
|
30
|
+
CloseYourIt::Scope.current.replay_session_id = replay_session_id_for(env)
|
|
31
|
+
if CloseYourIt.configuration.capture_request
|
|
32
|
+
CloseYourIt::Scope.current.request = build_request(env)
|
|
33
|
+
# Riferimento all'env per l'estrazione LAZY del body (request.data) a evento costruito.
|
|
34
|
+
CloseYourIt::Scope.current.rack_env = env
|
|
35
|
+
end
|
|
24
36
|
end
|
|
25
37
|
@app.call(env)
|
|
26
38
|
ensure
|
|
@@ -43,6 +55,14 @@ module CloseYourIt
|
|
|
43
55
|
SecureRandom.uuid
|
|
44
56
|
end
|
|
45
57
|
|
|
58
|
+
# Estrae `cyi_replay` dal cookie della richiesta (nil se assente). Rack puro.
|
|
59
|
+
def replay_session_id_for(env)
|
|
60
|
+
cookie = env["HTTP_COOKIE"]
|
|
61
|
+
return nil if cookie.nil? || cookie.empty?
|
|
62
|
+
|
|
63
|
+
Rack::Utils.parse_cookies_header(cookie)[REPLAY_COOKIE].to_s.then { |id| id.empty? ? nil : id }
|
|
64
|
+
end
|
|
65
|
+
|
|
46
66
|
# Forma `request` Sentry. URL senza query string; header solo dall'allowlist (mai
|
|
47
67
|
# Authorization/Cookie). query_string + IP solo con send_pii (opt-in).
|
|
48
68
|
def build_request(env)
|
data/lib/closeyourit/scope.rb
CHANGED
|
@@ -35,7 +35,7 @@ module CloseYourIt
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
attr_accessor :request, :trace_id
|
|
38
|
+
attr_accessor :request, :trace_id, :rack_env, :replay_session_id
|
|
39
39
|
attr_reader :user, :tags, :extra, :contexts, :breadcrumbs
|
|
40
40
|
|
|
41
41
|
def initialize
|
|
@@ -78,7 +78,9 @@ module CloseYourIt
|
|
|
78
78
|
@extra = {}
|
|
79
79
|
@contexts = {}
|
|
80
80
|
@request = nil
|
|
81
|
+
@rack_env = nil
|
|
81
82
|
@trace_id = nil
|
|
83
|
+
@replay_session_id = nil
|
|
82
84
|
@breadcrumbs = BreadcrumbBuffer.new(CloseYourIt.configuration.max_breadcrumbs)
|
|
83
85
|
@performance_profile = nil
|
|
84
86
|
end
|
|
@@ -92,14 +94,23 @@ module CloseYourIt
|
|
|
92
94
|
"user" => serialize_user,
|
|
93
95
|
"tags" => scrub(presence(@tags)),
|
|
94
96
|
"extra" => scrub(presence(@extra)),
|
|
95
|
-
"contexts" =>
|
|
96
|
-
"request" =>
|
|
97
|
+
"contexts" => contexts_payload,
|
|
98
|
+
"request" => request_payload,
|
|
97
99
|
"breadcrumbs" => breadcrumbs_payload
|
|
98
100
|
}.reject { |_key, value| value.nil? }
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
private
|
|
102
104
|
|
|
105
|
+
# contexts utente + `replay.replay_id` (session replay) quando presente sullo scope: il
|
|
106
|
+
# backend legge contexts.replay.replay_id per legare l'errore server al video (stesso punto
|
|
107
|
+
# del percorso JS). replay/replay_id non sono chiavi sensibili → sopravvivono allo Scrubber.
|
|
108
|
+
def contexts_payload
|
|
109
|
+
merged = @contexts.dup
|
|
110
|
+
merged["replay"] = { "replay_id" => @replay_session_id } if @replay_session_id
|
|
111
|
+
scrub(presence(merged))
|
|
112
|
+
end
|
|
113
|
+
|
|
103
114
|
# Redige i valori delle chiavi sensibili preservando la struttura (es. contexts.runtime resta
|
|
104
115
|
# intatto, solo i valori sotto chiavi sensibili diventano [FILTERED]). Riusa lo Scrubber della
|
|
105
116
|
# configurazione, lo stesso percorso di breadcrumb.data e degli attributi di log.
|
|
@@ -109,6 +120,24 @@ module CloseYourIt
|
|
|
109
120
|
Scrubber.new(CloseYourIt.configuration).filter_params(hash)
|
|
110
121
|
end
|
|
111
122
|
|
|
123
|
+
# Request context + body params (`request.data`) estratti LAZY qui — cioè solo quando un
|
|
124
|
+
# evento viene davvero costruito, mai sul percorso felice della richiesta.
|
|
125
|
+
def request_payload
|
|
126
|
+
return nil if @request.nil?
|
|
127
|
+
|
|
128
|
+
data = request_body_data
|
|
129
|
+
data ? @request.merge("data" => data) : @request
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def request_body_data
|
|
133
|
+
return nil unless CloseYourIt.configuration.capture_request_body
|
|
134
|
+
return nil if @rack_env.nil?
|
|
135
|
+
|
|
136
|
+
Rails::RequestBody.extract(@rack_env)
|
|
137
|
+
rescue StandardError
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
112
141
|
# `user.id` sempre; email/ip_address/username solo con `send_pii` (il backend li strippa
|
|
113
142
|
# comunque — difesa in profondità).
|
|
114
143
|
def serialize_user
|
data/lib/closeyourit/version.rb
CHANGED
data/lib/closeyourit-ruby.rb
CHANGED
|
@@ -28,6 +28,7 @@ require_relative "closeyourit/monitor"
|
|
|
28
28
|
require_relative "closeyourit/client"
|
|
29
29
|
require_relative "closeyourit/rails/capture_exceptions"
|
|
30
30
|
require_relative "closeyourit/rails/request_context"
|
|
31
|
+
require_relative "closeyourit/rails/request_body"
|
|
31
32
|
require_relative "closeyourit/rails/log_broadcast"
|
|
32
33
|
require_relative "closeyourit/rails/net_http_patch"
|
|
33
34
|
require_relative "closeyourit/rails/active_job_extension"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: closeyourit-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessio Bussolari
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- lib/closeyourit/events/slow_method_event.rb
|
|
48
48
|
- lib/closeyourit/events/slow_query_event.rb
|
|
49
49
|
- lib/closeyourit/instrumenter.rb
|
|
50
|
+
- lib/closeyourit/line_cache.rb
|
|
50
51
|
- lib/closeyourit/log_buffer.rb
|
|
51
52
|
- lib/closeyourit/log_device.rb
|
|
52
53
|
- lib/closeyourit/monitor.rb
|
|
@@ -59,6 +60,7 @@ files:
|
|
|
59
60
|
- lib/closeyourit/rails/net_http_patch.rb
|
|
60
61
|
- lib/closeyourit/rails/query_source.rb
|
|
61
62
|
- lib/closeyourit/rails/railtie.rb
|
|
63
|
+
- lib/closeyourit/rails/request_body.rb
|
|
62
64
|
- lib/closeyourit/rails/request_context.rb
|
|
63
65
|
- lib/closeyourit/scope.rb
|
|
64
66
|
- lib/closeyourit/scrubber.rb
|