closeyourit-ruby 0.9.4 → 0.10.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/lib/closeyourit/background_worker.rb +28 -9
  4. data/lib/closeyourit/breadcrumb.rb +2 -2
  5. data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
  6. data/lib/closeyourit/client.rb +42 -29
  7. data/lib/closeyourit/configuration.rb +105 -101
  8. data/lib/closeyourit/event.rb +6 -6
  9. data/lib/closeyourit/events/error_event.rb +16 -16
  10. data/lib/closeyourit/events/job_metric_event.rb +8 -8
  11. data/lib/closeyourit/events/log_event.rb +17 -17
  12. data/lib/closeyourit/events/message_event.rb +4 -4
  13. data/lib/closeyourit/events/performance_issue_event.rb +4 -4
  14. data/lib/closeyourit/events/slow_method_event.rb +6 -6
  15. data/lib/closeyourit/events/slow_query_event.rb +4 -4
  16. data/lib/closeyourit/instrumenter.rb +12 -4
  17. data/lib/closeyourit/line_cache.rb +4 -4
  18. data/lib/closeyourit/log_buffer.rb +10 -10
  19. data/lib/closeyourit/log_device.rb +18 -18
  20. data/lib/closeyourit/monitor.rb +2 -2
  21. data/lib/closeyourit/performance/request_profile.rb +5 -5
  22. data/lib/closeyourit/performance/rollup.rb +3 -3
  23. data/lib/closeyourit/rails/active_job_extension.rb +33 -31
  24. data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
  25. data/lib/closeyourit/rails/error_subscriber.rb +4 -4
  26. data/lib/closeyourit/rails/log_broadcast.rb +12 -12
  27. data/lib/closeyourit/rails/net_http_patch.rb +22 -22
  28. data/lib/closeyourit/rails/query_source.rb +3 -3
  29. data/lib/closeyourit/rails/railtie.rb +32 -52
  30. data/lib/closeyourit/rails/request_body.rb +7 -7
  31. data/lib/closeyourit/rails/request_context.rb +27 -27
  32. data/lib/closeyourit/scope.rb +29 -29
  33. data/lib/closeyourit/scrubber.rb +47 -48
  34. data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
  35. data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
  36. data/lib/closeyourit/stats.rb +8 -8
  37. data/lib/closeyourit/subscribers/job_performance.rb +30 -19
  38. data/lib/closeyourit/subscribers/request_performance.rb +4 -4
  39. data/lib/closeyourit/subscribers/slow_query.rb +42 -20
  40. data/lib/closeyourit/trace_context.rb +22 -22
  41. data/lib/closeyourit/transport.rb +25 -20
  42. data/lib/closeyourit/usage_registry.rb +17 -16
  43. data/lib/closeyourit/version.rb +1 -1
  44. data/lib/closeyourit-ruby.rb +125 -125
  45. metadata +1 -1
@@ -4,22 +4,22 @@ require "uri"
4
4
  require "concurrent"
5
5
 
6
6
  module CloseYourIt
7
- # Tiene tutte le opzioni del client. Costruita da `CloseYourIt.init { |c| ... }`.
8
- # Senza `endpoint_url`/`token`/`project_id` (o con `http://` in produzione) il client è no-op.
7
+ # Holds all client options. Built by `CloseYourIt.init { |c| ... }`.
8
+ # Without `endpoint_url`/`token`/`project_id` (or with `http://` in production) the client is a no-op.
9
9
  class Configuration
10
10
  DEFAULT_EXCLUDED_EXCEPTIONS = %w[
11
11
  ActionController::RoutingError
12
12
  ActiveRecord::RecordNotFound
13
13
  ].freeze
14
14
 
15
- # Header HTTP catturati nel contesto request (mai Authorization/Cookie → niente PII/segreti).
15
+ # HTTP headers captured in the request context (never Authorization/Cookie → no PII/secrets).
16
16
  DEFAULT_REQUEST_HEADER_ALLOWLIST = %w[Accept Content-Type User-Agent Referer].freeze
17
17
 
18
- # Tabelle di servizio del Solid stack: sono infrastruttura del framework, non codice
19
- # dell'applicazione. Una loro query lenta non si può correggere leggendo il proprio repo — dice
20
- # solo che il database è in contesa, cosa che le query dell'app raccontano già. Escluse per
21
- # DEFAULT dalla misura dei rallentamenti perché altrimenti la sommergono: su closeyourit-rails il
22
- # 2026-07-30 erano circa 2.740 campioni su 4.400 (62%), tutti fra i 250 e i 655 ms (CYRB-18).
18
+ # Solid stack service tables: they are framework infrastructure, not application code. A slow
19
+ # query on them cannot be fixed by reading your own repo — it only says the database is under
20
+ # contention, which the app's queries already show. Excluded by DEFAULT from slowdown measurement
21
+ # because otherwise they flood it: on closeyourit-rails on 2026-07-30 they were about 2,740
22
+ # samples out of 4,400 (62%), all between 250 and 655 ms (CYRB-18).
23
23
  DEFAULT_EXCLUDED_QUERY_PATTERNS = [
24
24
  /\bsolid_queue_/,
25
25
  /\bsolid_cache_/,
@@ -27,7 +27,7 @@ module CloseYourIt
27
27
  ].freeze
28
28
 
29
29
  attr_accessor :endpoint_url, :token, :project_id, :environment, :before_send, :on_diagnostic,
30
- :async_threads, :background_worker_max_queue,
30
+ :async_threads, :background_worker_max_queue, :shutdown_timeout,
31
31
  :slow_query_threshold_ms, :slow_method_threshold_ms,
32
32
  :send_pii, :obfuscate_sql, :send_server_name,
33
33
  :capture_query_bindings, :capture_method_arguments,
@@ -57,136 +57,140 @@ module CloseYourIt
57
57
  @excluded_exceptions = DEFAULT_EXCLUDED_EXCEPTIONS.dup
58
58
  @before_send = nil
59
59
 
60
- # Hook diagnostico opt-in: `->(event, details) { ... }` invocato a ogni tappa del ciclo di vita
61
- # di un evento (:enqueue, :send, :drop, :timeout, :shutdown) con dettagli privi di dati sensibili
62
- # (es. `{ reason: :queue_full }`, `{ status: 429 }`). Locale e non ricorsivo: non invia telemetria
63
- # e non può innescare loop di auto-monitoraggio (vedi CloseYourIt.notify_diagnostic, CYRB-12).
60
+ # Opt-in diagnostic hook: `->(event, details) { ... }` called at every stage of an event's
61
+ # lifecycle (:enqueue, :send, :drop, :timeout, :shutdown) with details free of sensitive data
62
+ # (e.g. `{ reason: :queue_full }`, `{ status: 429 }`). Local and non-recursive: it sends no
63
+ # telemetry and cannot trigger self-monitoring loops (see CloseYourIt.notify_diagnostic, CYRB-12).
64
64
  @on_diagnostic = nil
65
65
 
66
66
  @async_threads = default_threads
67
67
  @background_worker_max_queue = 30
68
-
69
- # Intercetta SIGTERM per garantire il flush di fine-vita (deploy/Kamal): SIGTERM di default
70
- # termina il processo SENZA eseguire gli at_exit. OPT-IN perché sovrascrive un eventuale handler
71
- # TERM dell'app ospite. Vedi CloseYourIt.shutdown / register_shutdown_flush (CYRB-5).
68
+ # How long to wait, on process exit, for in-flight sends to complete. Default: the duration cap
69
+ # of ONE POST, so the last batch is not abandoned halfway (CYRB-24).
70
+ # Lower it if process exit must be faster than delivery.
71
+ @shutdown_timeout = Transport::MAX_REQUEST_SECONDS
72
+
73
+ # Intercepts SIGTERM to guarantee the end-of-life flush (deploy/Kamal): by default SIGTERM ends
74
+ # the process WITHOUT running at_exit. OPT-IN because it overrides any TERM handler of the host
75
+ # app. See CloseYourIt.shutdown / register_shutdown_flush (CYRB-5).
72
76
  @trap_signals = false
73
77
 
74
78
  @slow_query_threshold_ms = 100
75
79
  @slow_method_threshold_ms = 200
76
- # Query da NON misurare come rallentamento (match sul testo SQL). Default: le tabelle di servizio
77
- # del Solid stack. Chi vuole misurarle davvero azzera la lista.
80
+ # Queries NOT to measure as slowdowns (match on the SQL text). Default: the Solid stack service
81
+ # tables. Whoever really wants to measure them clears the list.
78
82
  @excluded_query_patterns = DEFAULT_EXCLUDED_QUERY_PATTERNS.dup
79
83
 
80
84
  @send_pii = false
81
85
  @obfuscate_sql = true
82
86
  @send_server_name = true
83
87
 
84
- # Contesto HTTP della richiesta (method/url/header allowlist). Body/query/IP solo con send_pii.
88
+ # HTTP request context (method/url/header allowlist). Body/query/IP only with send_pii.
85
89
  @capture_request = true
86
90
  @request_header_allowlist = DEFAULT_REQUEST_HEADER_ALLOWLIST.dup
87
91
 
88
- # Righe di sorgente attorno a ogni frame dello stacktrace (pre/context/post). 0 = disattivo.
92
+ # Source lines around each stacktrace frame (pre/context/post). 0 = disabled.
89
93
  @context_lines = 3
90
94
 
91
- # Params del body della richiesta nell'evento (`request.data`), estratti LAZY solo quando
92
- # l'errore accade, sanitizzati e scrubbati (denylist + filter_parameters). Il backend
93
- # ri-scruba difensivamente. Upload → placeholder, cap 64 KB.
95
+ # Request body params in the event (`request.data`), extracted LAZILY only when the error
96
+ # happens, sanitized and scrubbed (denylist + filter_parameters). The backend scrubs again
97
+ # defensively. Uploads → placeholder, 64 KB cap.
94
98
  @capture_request_body = true
95
99
 
96
- # Breadcrumbs: cronologia (query offuscate, eventi custom) allegata all'errore.
100
+ # Breadcrumbs: history (obfuscated queries, custom events) attached to the error.
97
101
  @breadcrumbs_enabled = true
98
102
  @max_breadcrumbs = 100
99
103
 
100
- # Sampling probabilistico di errori/messaggi (1.0 = invia tutto, 0.0 = niente).
104
+ # Probabilistic sampling of errors/messages (1.0 = send everything, 0.0 = nothing).
101
105
  @sample_rate = 1.0
102
106
 
103
- # Cattura errori handled (Rails.error.report) e degli ActiveJob/Sidekiq (oggi persi).
107
+ # Captures handled errors (Rails.error.report) and ActiveJob/Sidekiq errors (otherwise lost).
104
108
  @capture_handled_errors = true
105
109
  @report_active_job_errors = true
106
110
 
107
- # Cattura valori dei parametri — opt-in, default OFF (privacy). I bind/argomenti possono contenere PII.
111
+ # Captures parameter values — opt-in, default OFF (privacy). Binds/arguments may contain PII.
108
112
  @capture_query_bindings = false
109
113
  @capture_method_arguments = false
110
114
 
111
- # Log strutturati (CloseYourIt.log / .logger). Master switch + sampling + batching dedicati.
115
+ # Structured logs (CloseYourIt.log / .logger). Dedicated master switch + sampling + batching.
112
116
  @logs_enabled = true
113
117
  @logs_sample_rate = 1.0
114
118
  @logs_batch_size = 50
115
119
  @logs_flush_interval = 5
116
120
 
117
- # CYSK-29 — telemetria d'uso: quali rotte/job/chiavi girano davvero. Il payload è privo di
118
- # dati utente per costruzione (route = Controller#action, mai l'URL), quindi il default è ON:
119
- # trenta giorni di raccolta facoltativa hanno insegnato che facoltativo significa mai.
120
- # Il registro si svuota a ogni flush: il tetto limita i simboli DISTINTI per finestra.
121
+ # CYSK-29 — usage telemetry: which routes/jobs/keys actually run. The payload carries no user
122
+ # data by construction (route = Controller#action, never the URL), so the default is ON:
123
+ # thirty days of optional collection taught us that optional means never.
124
+ # The registry is emptied on every flush: the cap limits DISTINCT symbols per window.
121
125
  @usage_enabled = true
122
126
  @usage_flush_interval = 300
123
127
  @usage_max_symbols = 2000
124
- # Broadcast opt-in di Rails.logger → CloseYourIt.log (default OFF; spedisce solo ≥ soglia).
128
+ # Opt-in Rails.logger broadcast → CloseYourIt.log (default OFF; sends only ≥ threshold).
125
129
  @capture_rails_logs = false
126
130
  @logs_min_level = :info
127
- # Soglia DEDICATA del broadcast Rails.logger, distinta da logs_min_level (che governa
128
- # CloseYourIt.log/.logger, dove è il dev a scegliere cosa loggare). Default :warn — conservativo:
129
- # senza, in produzione ad alto traffico OGNI riga info del framework (Started GET, Rendered, ...)
130
- # inonderebbe lo stream con decine di migliaia di log-entry/min (CYRB-7). Chi vuole anche gli info
131
- # del framework la abbassa esplicitamente (es. :info).
131
+ # DEDICATED threshold of the Rails.logger broadcast, separate from logs_min_level (which governs
132
+ # CloseYourIt.log/.logger, where the dev chooses what to log). Default :warn — conservative:
133
+ # without it, in high-traffic production EVERY framework info line (Started GET, Rendered, ...)
134
+ # would flood the stream with tens of thousands of log entries/min (CYRB-7). Whoever also wants
135
+ # the framework info lines lowers it explicitly (e.g. :info).
132
136
  @capture_rails_logs_min_level = :warn
133
- # Rumore del broadcast Rails.logger che NON è un'eccezione, e quindi excluded_exceptions non può
134
- # coprire: righe ripetute del framework o di una gemma. Regexp sul testo del messaggio, default
135
- # vuoto. Vale SOLO per il broadcast automatico, mai per CloseYourIt.log esplicito.
137
+ # Rails.logger broadcast noise that is NOT an exception, so excluded_exceptions cannot cover it:
138
+ # repeated framework or gem lines. Regexp on the message text, empty by default. Applies ONLY
139
+ # to the automatic broadcast, never to an explicit CloseYourIt.log.
136
140
  @excluded_log_patterns = []
137
141
 
138
- # Performance issue detection (verdetti aggregati: N+1, slow request, HTTP esterne lente).
139
- # OPT-IN, default OFF: profila OGNI query della richiesta → overhead non trascurabile, va attivato
140
- # consapevolmente per-app. Le soglie sono conservative (poco rumore). Vedi Performance::Rollup.
142
+ # Performance issue detection (aggregate verdicts: N+1, slow request, slow external HTTP).
143
+ # OPT-IN, default OFF: it profiles EVERY query of the request → non-negligible overhead, enable it
144
+ # deliberately per app. Thresholds are conservative (little noise). See Performance::Rollup.
141
145
  @detect_performance_issues = false
142
- @n_plus_one_threshold = 10 # stesso fingerprint+call-site eseguito > N volte in una richiesta
143
- @query_count_threshold = 100 # troppe query totali in una richiesta
144
- @query_time_threshold_ms = 500 # tempo DB totale per richiesta oltre cui = high_query_count
145
- @slow_request_threshold_ms = 1000 # durata totale della richiesta
146
- @slow_external_threshold_ms = 1000 # singola chiamata HTTP esterna
147
- @capture_external_http = true # strumenta Net::HTTP (solo se detect_performance_issues)
148
-
149
- # Metriche dei background job: durata di esecuzione e attesa in coda (queue latency) per ActiveJob
150
- # e Sidekiq. ON di default (a differenza di detect_performance_issues): la visibilità dei job
151
- # lenti/in ritardo/ritentati non deve richiedere strumentazione manuale in ogni app (CYRB-14), e
152
- # l'overhead è basso — una notifica per job, non il profiling di ogni query. Il rumore è tenuto a
153
- # bada dalle soglie (job normali sotto soglia = niente metrica) e dal sampling. La label è il nome
154
- # della classe del job; gli argomenti non vengono MAI inviati.
146
+ @n_plus_one_threshold = 10 # same fingerprint+call-site run > N times in one request
147
+ @query_count_threshold = 100 # too many total queries in one request
148
+ @query_time_threshold_ms = 500 # total DB time per request beyond which = high_query_count
149
+ @slow_request_threshold_ms = 1000 # total request duration
150
+ @slow_external_threshold_ms = 1000 # single external HTTP call
151
+ @capture_external_http = true # instruments Net::HTTP (only with detect_performance_issues)
152
+
153
+ # Background job metrics: execution duration and queue wait (queue latency) for ActiveJob and
154
+ # Sidekiq. ON by default (unlike detect_performance_issues): visibility of slow/late/retried jobs
155
+ # must not require manual instrumentation in every app (CYRB-14), and the overhead is low — one
156
+ # notification per job, not profiling of every query. Noise is kept down by the thresholds
157
+ # (normal jobs under threshold = no metric) and by sampling. The label is the job class name;
158
+ # arguments are NEVER sent.
155
159
  @monitor_jobs = true
156
- @slow_job_threshold_ms = 5000 # durata del perform oltre cui = slow_job
157
- @job_queue_latency_threshold_ms = 60_000 # attesa enqueue→esecuzione oltre cui = job_queue_latency
158
- @jobs_sample_rate = 1.0 # frazione dei candidati oltre soglia effettivamente inviata
159
-
160
- # Propagazione W3C trace context (traceparent/tracestate) verso i servizi esterni chiamati via
161
- # Net::HTTP. OPT-IN, default OFF: iniettare header d'uscita attraversa un trust boundary e va deciso
162
- # per-app. È limitata PER DESTINAZIONE dalla allowlist (host esatti, case-insensitive, o Regexp per i
163
- # sottodomini); lista vuota = nessuna destinazione. Mai verso host non elencati, mai come `baggage`
164
- # (che può portare PII). In ingresso un traceparent valido diventa il trace_id degli eventi
165
- # CloseYourIt → gli errori/metriche della richiesta si correlano alla traccia distribuita (CYRB-15).
160
+ @slow_job_threshold_ms = 5000 # perform duration beyond which = slow_job
161
+ @job_queue_latency_threshold_ms = 60_000 # enqueue→execution wait beyond which = job_queue_latency
162
+ @jobs_sample_rate = 1.0 # fraction of over-threshold candidates actually sent
163
+
164
+ # W3C trace context propagation (traceparent/tracestate) to external services called via
165
+ # Net::HTTP. OPT-IN, default OFF: injecting outgoing headers crosses a trust boundary and must be
166
+ # decided per app. It is limited PER DESTINATION by the allowlist (exact hosts, case-insensitive,
167
+ # or Regexp for subdomains); empty list = no destination. Never to unlisted hosts, never as
168
+ # `baggage` (which may carry PII). Inbound, a valid traceparent becomes the trace_id of the
169
+ # CloseYourIt events → the request's errors/metrics correlate with the distributed trace (CYRB-15).
166
170
  @propagate_trace_context = false
167
171
  @trace_propagation_allowlist = []
168
172
 
169
- # Radice del progetto: base per il filename relativo dei frame (culprit cross-SDK). Lazy:
170
- # auto-rilevata da Rails.root o Dir.pwd al primo accesso se non impostata esplicitamente.
173
+ # Project root: base for the relative frame filename (cross-SDK culprit). Lazy: auto-detected
174
+ # from Rails.root or Dir.pwd on first access when not set explicitly.
171
175
  @project_root = nil
172
176
 
173
177
  @filter_parameters = []
174
178
  @scrub_message_patterns = []
175
179
  end
176
180
 
177
- # Classi/stringhe → nome (String); i Regexp restano Regexp (match per pattern su nome/messaggio).
181
+ # Classes/strings → name (String); Regexps stay Regexps (pattern match on name/message).
178
182
  def excluded_exceptions=(list)
179
183
  @excluded_exceptions = Array(list).map { |item| item.is_a?(Regexp) ? item : item.to_s }
180
184
  end
181
185
 
182
- # Pattern del broadcast Rails.logger da scartare. Le stringhe diventano Regexp (match letterale
183
- # sul testo): chi scrive `config.excluded_log_patterns = ["Rendered layout"]` intende quello.
186
+ # Rails.logger broadcast patterns to drop. Strings become Regexps (literal match on the text):
187
+ # whoever writes `config.excluded_log_patterns = ["Rendered layout"]` means exactly that.
184
188
  def excluded_log_patterns=(list)
185
189
  @excluded_log_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
186
190
  end
187
191
 
188
- # Query da non misurare. Stessa normalizzazione di excluded_log_patterns: String = testo letterale
189
- # (un nome di tabella si scrive così, non come pattern), Regexp = pattern.
192
+ # Queries not to measure. Same normalization as excluded_log_patterns: String = literal text
193
+ # (a table name is written that way, not as a pattern), Regexp = pattern.
190
194
  def excluded_query_patterns=(list)
191
195
  @excluded_query_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
192
196
  end
@@ -195,8 +199,8 @@ module CloseYourIt
195
199
  @filter_parameters = Array(list)
196
200
  end
197
201
 
198
- # Destinazioni autorizzate a ricevere gli header di trace W3C. String = host esatto (match
199
- # case-insensitive), Regexp = pattern (per sottodomini/famiglie di host). Lista vuota = nessuno.
202
+ # Destinations allowed to receive the W3C trace headers. String = exact host (case-insensitive
203
+ # match), Regexp = pattern (for subdomains/host families). Empty list = none.
200
204
  def trace_propagation_allowlist=(list)
201
205
  @trace_propagation_allowlist = Array(list)
202
206
  end
@@ -209,8 +213,8 @@ module CloseYourIt
209
213
  environment.to_s == "production"
210
214
  end
211
215
 
212
- # Il client invia solo con credenziali complete (endpoint + token + project_id) e trasporto
213
- # sicuro (http:// ammesso fuori produzione).
216
+ # The client sends only with complete credentials (endpoint + token + project_id) and a secure
217
+ # transport (http:// allowed outside production).
214
218
  def enabled?
215
219
  return false if blank?(endpoint_url) || blank?(token) || blank?(project_id)
216
220
  return false if insecure_endpoint? && production?
@@ -218,8 +222,8 @@ module CloseYourIt
218
222
  true
219
223
  end
220
224
 
221
- # Logga i warning di configurazione (es. endpoint http://, project_id/endpoint malformati).
222
- # Non solleva mai: coerente con la filosofia no-op del client. Chiamata da `CloseYourIt.init`.
225
+ # Logs configuration warnings (e.g. http:// endpoint, malformed project_id/endpoint).
226
+ # Never raises: consistent with the client's no-op philosophy. Called by `CloseYourIt.init`.
223
227
  def validate!
224
228
  CloseYourIt.internal_logger.warn(insecure_endpoint_message) if insecure_endpoint?
225
229
  CloseYourIt.internal_logger.warn(malformed_project_id_message) if malformed_project_id?
@@ -227,22 +231,22 @@ module CloseYourIt
227
231
  self
228
232
  end
229
233
 
230
- # Release effettiva: quella impostata, altrimenti auto-rilevata (ENV di deploy/CI o git).
234
+ # Effective release: the one set, otherwise auto-detected (deploy/CI ENV or git).
231
235
  def release
232
236
  return @release unless @release.nil?
233
237
 
234
238
  @release = detect_release
235
239
  end
236
240
 
237
- # Radice del progetto effettiva: quella impostata, altrimenti auto-rilevata (Rails.root o Dir.pwd).
238
- # Usata per rendere `frame.filename` relativo (culprit confrontabile cross-SDK — CYRB-4).
241
+ # Effective project root: the one set, otherwise auto-detected (Rails.root or Dir.pwd).
242
+ # Used to make `frame.filename` relative (culprit comparable across SDKs — CYRB-4).
239
243
  def project_root
240
244
  return @project_root unless @project_root.nil?
241
245
 
242
246
  @project_root = detect_project_root
243
247
  end
244
248
 
245
- # Rails.root quando l'app gira sotto Rails, altrimenti la working directory. Mai solleva.
249
+ # Rails.root when the app runs under Rails, otherwise the working directory. Never raises.
246
250
  def detect_project_root
247
251
  return ::Rails.root.to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
248
252
 
@@ -251,12 +255,12 @@ module CloseYourIt
251
255
  Dir.pwd
252
256
  end
253
257
 
254
- # Un tag semver (con `v` opzionale) è preferito allo short SHA come release: converge con quello
255
- # che registra la CI (che tagga), mentre lo short SHA crea release duplicate lato backend (CYRB-9).
258
+ # A semver tag (optional `v`) is preferred to the short SHA as release: it converges with what the
259
+ # CI records (it tags), while the short SHA creates duplicate releases on the backend (CYRB-9).
256
260
  SEMVER_TAG = /\Av?\d+\.\d+\.\d+([-+.].+)?\z/
257
261
 
258
- # Auto-rilevamento release: prima un tag semver (APP_GIT_TAG/GIT_TAG), poi lo short SHA dalle env
259
- # di deploy/CI o dal git. Mai solleva.
262
+ # Release auto-detection: first a semver tag (APP_GIT_TAG/GIT_TAG), then the short SHA from the
263
+ # deploy/CI env or from git. Never raises.
260
264
  def detect_release
261
265
  detect_tag ||
262
266
  ENV["KAMAL_VERSION"] ||
@@ -271,8 +275,8 @@ module CloseYourIt
271
275
 
272
276
  private
273
277
 
274
- # `.git` è una directory in un checkout normale, un file in un worktree → File.directory?
275
- # è false nei worktree, così i test non lanciano subprocess git (deterministico).
278
+ # `.git` is a directory in a normal checkout and a file in a worktree → File.directory? is false
279
+ # in worktrees, so tests do not spawn git subprocesses (deterministic).
276
280
  def git_revision
277
281
  return nil unless File.directory?(".git")
278
282
 
@@ -282,9 +286,9 @@ module CloseYourIt
282
286
  nil
283
287
  end
284
288
 
285
- # Tag semver da APP_GIT_TAG poi GIT_TAG: accetta solo un valore non-blank con forma semver
286
- # (v opzionale + MAJOR.MINOR.PATCH + eventuale suffisso). Scarta `unknown`, vuoto, nomi di
287
- # branch, ecc. → nil, così detect_release cade sulla catena SHA.
289
+ # Semver tag from APP_GIT_TAG then GIT_TAG: accepts only a non-blank value shaped like semver
290
+ # (optional v + MAJOR.MINOR.PATCH + optional suffix). Rejects `unknown`, empty, branch names,
291
+ # etc. → nil, so detect_release falls back to the SHA chain.
288
292
  def detect_tag
289
293
  tag = ENV["APP_GIT_TAG"] || ENV["GIT_TAG"]
290
294
  return nil if blank?(tag)
@@ -299,17 +303,17 @@ module CloseYourIt
299
303
  !uri.nil? && uri.scheme != "https"
300
304
  end
301
305
 
302
- # Avvisa se il project_id è valorizzato ma non sembra uno UUID (l'errore tipico è incollare
303
- # uno slug/nome al posto dell'id). Non blocca: il server è l'autorità sulla validità.
306
+ # Warns when project_id is set but does not look like a UUID (the typical mistake is pasting a
307
+ # slug/name instead of the id). It does not block: the server is the authority on validity.
304
308
  def malformed_project_id?
305
309
  !blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
306
310
  end
307
311
 
308
312
  def malformed_project_id_message
309
- "CloseYourIt: project_id (#{project_id}) non ha forma UUID — verifica di aver copiato l'id corretto."
313
+ "CloseYourIt: project_id (#{project_id}) is not shaped like a UUID — check that you copied the right id."
310
314
  end
311
315
 
312
- # Avvisa se endpoint_url è valorizzato ma non parsabile o privo di host.
316
+ # Warns when endpoint_url is set but not parsable or has no host.
313
317
  def malformed_endpoint?
314
318
  return false if blank?(endpoint_url)
315
319
 
@@ -318,12 +322,12 @@ module CloseYourIt
318
322
  end
319
323
 
320
324
  def malformed_endpoint_message
321
- "CloseYourIt: endpoint_url (#{endpoint_url}) non è un URL valido (host mancante)."
325
+ "CloseYourIt: endpoint_url (#{endpoint_url}) is not a valid URL (missing host)."
322
326
  end
323
327
 
324
328
  def insecure_endpoint_message
325
- tail = production? ? "Telemetria DISABILITATA in production." : "Consentito solo in sviluppo."
326
- "CloseYourIt: endpoint_url usa http:// non sicuro (#{endpoint_url}) — il token viaggerebbe in chiaro. #{tail}"
329
+ tail = production? ? "Telemetry DISABLED in production." : "Allowed only in development."
330
+ "CloseYourIt: endpoint_url uses insecure http:// (#{endpoint_url}) — the token would travel in clear text. #{tail}"
327
331
  end
328
332
 
329
333
  def parsed_endpoint
@@ -4,8 +4,8 @@ require "time"
4
4
  require "socket"
5
5
 
6
6
  module CloseYourIt
7
- # Base degli eventi di telemetria. Le sottoclassi implementano `#to_h` e `#ingest_path`
8
- # (il path API a cui l'evento va spedito: errori → /events, metriche → /metrics).
7
+ # Base of telemetry events. Subclasses implement `#to_h` and `#ingest_path`
8
+ # (the API path the event is sent to: errors → /events, metrics → /metrics).
9
9
  class Event
10
10
  def initialize(configuration)
11
11
  @configuration = configuration
@@ -13,11 +13,11 @@ module CloseYourIt
13
13
  end
14
14
 
15
15
  def to_h
16
- raise NotImplementedError, "#{self.class} deve implementare #to_h"
16
+ raise NotImplementedError, "#{self.class} must implement #to_h"
17
17
  end
18
18
 
19
19
  def ingest_path(_project_id)
20
- raise NotImplementedError, "#{self.class} deve implementare #ingest_path"
20
+ raise NotImplementedError, "#{self.class} must implement #ingest_path"
21
21
  end
22
22
 
23
23
  private
@@ -42,8 +42,8 @@ module CloseYourIt
42
42
  hash.reject { |_key, value| value.nil? }
43
43
  end
44
44
 
45
- # Fusione ricorsiva: gli Hash annidati vengono fusi (es. `contexts.runtime` preservato
46
- # mentre lo scope aggiunge `contexts.active_job`), gli scalari sovrascritti.
45
+ # Recursive merge: nested Hashes are merged (e.g. `contexts.runtime` kept while the scope adds
46
+ # `contexts.active_job`), scalars are overwritten.
47
47
  def deep_merge(base, override)
48
48
  base.merge(override) do |_key, old_value, new_value|
49
49
  if old_value.is_a?(Hash) && new_value.is_a?(Hash)
@@ -7,9 +7,9 @@ require_relative "../scrubber"
7
7
  require_relative "../line_cache"
8
8
 
9
9
  module CloseYourIt
10
- # Trasforma un'eccezione Ruby nel **payload evento Sentry** che il backend CloseYourIt ingerisce
11
- # (Errors::Ingest::Normalize). Usa `backtrace_locations` (niente regex) e mette la cause-chain in
12
- # `exception.values` ordinata dall'esterna alla principale (Sentry: values.last = il crash).
10
+ # Turns a Ruby exception into the **Sentry event payload** the CloseYourIt backend ingests
11
+ # (Errors::Ingest::Normalize). Uses `backtrace_locations` (no regex) and puts the cause chain in
12
+ # `exception.values` ordered from the outermost to the main one (Sentry: values.last = the crash).
13
13
  class ErrorEvent < Event
14
14
  def self.from_exception(exception, configuration:, handled: false, level: "error", contexts: nil)
15
15
  new(exception, configuration, handled: handled, level: level, contexts: contexts)
@@ -30,7 +30,7 @@ module CloseYourIt
30
30
  "timestamp" => @occurred_at,
31
31
  "platform" => "ruby",
32
32
  "level" => @level,
33
- # Correlazione log↔errori: stesso trace_id dei log della medesima richiesta (popolato dallo Scope).
33
+ # Log↔error correlation: same trace_id as the logs of the same request (populated by the Scope).
34
34
  "trace_id" => CloseYourIt::Scope.current.trace_id,
35
35
  "environment" => environment,
36
36
  "release" => @configuration.release,
@@ -39,9 +39,9 @@ module CloseYourIt
39
39
  "contexts" => { "runtime" => { "name" => "ruby", "version" => RUBY_VERSION } },
40
40
  "sdk" => sdk
41
41
  )
42
- # Fonde il contesto per-richiesta/job (user/tags/extra/contexts/request) raccolto nello Scope.
42
+ # Merges the per-request/job context (user/tags/extra/contexts/request) collected in the Scope.
43
43
  merged = deep_merge(base, CloseYourIt::Scope.current.to_event_hash)
44
- # Context extra passato esplicitamente (es. rails_error dall'ErrorReporter).
44
+ # Extra context passed explicitly (e.g. rails_error from the ErrorReporter).
45
45
  @contexts ? deep_merge(merged, { "contexts" => @contexts }) : merged
46
46
  end
47
47
 
@@ -51,7 +51,7 @@ module CloseYourIt
51
51
 
52
52
  private
53
53
 
54
- # Cause-chain → array Sentry: causa più esterna prima, eccezione principale ULTIMA.
54
+ # Cause chain → Sentry array: outermost cause first, main exception LAST.
55
55
  def exception_values
56
56
  chain = []
57
57
  seen = []
@@ -73,10 +73,10 @@ module CloseYourIt
73
73
  }
74
74
  end
75
75
 
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.
78
- # `filename` è relativo/basename (culprit confrontabile cross-SDK — CYRB-4); `abs_path` è il
79
- # path assoluto per il dettaglio e per leggere il sorgente delle context lines.
76
+ # Sentry-style: most recent frame last; keys filename/function/lineno/in_app/abs_path
77
+ # + source snippet (pre_context/context_line/post_context) when the file is readable.
78
+ # `filename` is relative/basename (culprit comparable across SDKs — CYRB-4); `abs_path` is the
79
+ # absolute path for the detail view and for reading the context lines' source.
80
80
  def frames(locations)
81
81
  return [] if locations.nil?
82
82
 
@@ -94,15 +94,15 @@ module CloseYourIt
94
94
  end
95
95
  end
96
96
 
97
- # Path da mostrare (Sentry `filename`): relativo alla project_root se il file è dentro l'app
98
- # (es. "app/models/user.rb"), altrimenti il basename (gem/stdlib/path esterni → "base.rb").
99
- # Allinea Ruby agli altri SDK (Dart/JS mandano basename/relativo), così il culprit
100
- # "filename in function" è confrontabile a colpo d'occhio su progetti poliglotti. Vedi CYRB-4.
97
+ # Path to display (Sentry `filename`): relative to project_root if the file is inside the app
98
+ # (e.g. "app/models/user.rb"), otherwise the basename (gem/stdlib/external paths → "base.rb").
99
+ # Aligns Ruby with the other SDKs (Dart/JS send basename/relative), so the culprit
100
+ # "filename in function" is comparable at a glance on polyglot projects. See CYRB-4.
101
101
  def relative_filename(path)
102
102
  return path if path.nil?
103
103
 
104
104
  root = @configuration.project_root.to_s
105
- prefix = root.empty? ? nil : File.join(root, "") # root con separatore finale, portabile
105
+ prefix = root.empty? ? nil : File.join(root, "") # root with trailing separator, portable
106
106
  return File.basename(path) unless prefix && path.start_with?(prefix)
107
107
 
108
108
  relative = path[prefix.length..]
@@ -4,15 +4,15 @@ require "securerandom"
4
4
  require_relative "../event"
5
5
 
6
6
  module CloseYourIt
7
- # Metrica `kind=performance_issue` per un background job che ha superato una soglia di DURATA
8
- # (`subtype=slow_job`) o di ATTESA in coda (`subtype=job_queue_latency`). Spedita sulla pipeline
9
- # metriche (`/api/v1/projects/:id/metrics`), come slow_query/slow_method e i verdetti performance.
7
+ # `kind=performance_issue` metric for a background job that exceeded a DURATION threshold
8
+ # (`subtype=slow_job`) or a queue WAIT threshold (`subtype=job_queue_latency`). Sent on the metrics
9
+ # pipeline (`/api/v1/projects/:id/metrics`), like slow_query/slow_method and performance verdicts.
10
10
  #
11
- # `label` è il nome della CLASSE del job — mai gli argomenti (privacy: come slow_method invia solo
12
- # label/durata). `attempt` rende visibili i retry; `trace_id` (= job_id ActiveJob / jid Sidekiq)
13
- # correla la metrica a log ed errori dello stesso job. `duration_ms` è il valore misurato del subtype
14
- # (durata del perform per slow_job, attesa in coda per job_queue_latency) — stessa convenzione del
15
- # Rollup. I campi nil vengono omessi.
11
+ # `label` is the job CLASS name — never the arguments (privacy: like slow_method it sends only
12
+ # label/duration). `attempt` makes retries visible; `trace_id` (= ActiveJob job_id / Sidekiq jid)
13
+ # correlates the metric with logs and errors of the same job. `duration_ms` is the subtype's
14
+ # measured value (perform duration for slow_job, queue wait for job_queue_latency) — same
15
+ # convention as the Rollup. Nil fields are omitted.
16
16
  class JobMetricEvent < Event
17
17
  def initialize(attrs, configuration)
18
18
  super(configuration)