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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fce4d4eb3a92009bf70daeb903091ec30b4274d926b52d5b8df44644a808c35e
4
- data.tar.gz: be83377677c8567c700d8ba5d6a47aa1664474db87aa750ab8a61dbed85fadb2
3
+ metadata.gz: 6dc227e626e4b47b931a80698aa8b2567bacb033dad2e5e42d6c8497890debe2
4
+ data.tar.gz: d012c3b1272b238e796ff7cdb1cf687276932a0c5196f14fbd0d5393edf9d938
5
5
  SHA512:
6
- metadata.gz: 4533f36e714d2c8ac1706a8bdce8652beed02384f0ce09f5965679c0d76a5e098b5ed006530bc88bbc4c92ed982ed373bd9dd5d5db04cf015770949deebe3e00
7
- data.tar.gz: 3500e6f71ea42ba8bb900d78dabde73941913e5b03459ab0f2e570b27c26b6637df27a786d5ea1eb6e63b7ffcb8c8b84d07b6299d5553d5711ea3700543449dd
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
- Uso interno → install via git o path (no RubyGems):
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", git: "https://github.com/bussolabs/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
- CloseYourIt::Scope.current.request = build_request(env) if CloseYourIt.configuration.capture_request
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)
@@ -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" => scrub(presence(@contexts)),
96
- "request" => @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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- VERSION = "0.3.4"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -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.3.4
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