closeyourit-ruby 0.10.0 → 0.11.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -1
  3. data/lib/closeyourit/background_worker.rb +14 -14
  4. data/lib/closeyourit/breadcrumb.rb +2 -2
  5. data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
  6. data/lib/closeyourit/client.rb +24 -24
  7. data/lib/closeyourit/configuration.rb +104 -103
  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 +11 -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 +6 -6
  17. data/lib/closeyourit/job_context.rb +61 -0
  18. data/lib/closeyourit/line_cache.rb +4 -4
  19. data/lib/closeyourit/log_buffer.rb +10 -10
  20. data/lib/closeyourit/log_device.rb +18 -18
  21. data/lib/closeyourit/monitor.rb +2 -2
  22. data/lib/closeyourit/performance/request_profile.rb +5 -5
  23. data/lib/closeyourit/performance/rollup.rb +3 -3
  24. data/lib/closeyourit/rails/active_job_extension.rb +36 -31
  25. data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
  26. data/lib/closeyourit/rails/error_subscriber.rb +4 -4
  27. data/lib/closeyourit/rails/log_broadcast.rb +12 -12
  28. data/lib/closeyourit/rails/net_http_patch.rb +22 -22
  29. data/lib/closeyourit/rails/query_source.rb +3 -3
  30. data/lib/closeyourit/rails/railtie.rb +29 -65
  31. data/lib/closeyourit/rails/request_body.rb +7 -7
  32. data/lib/closeyourit/rails/request_context.rb +27 -27
  33. data/lib/closeyourit/scope.rb +29 -29
  34. data/lib/closeyourit/scrubber.rb +50 -50
  35. data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
  36. data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +39 -17
  37. data/lib/closeyourit/stats.rb +8 -8
  38. data/lib/closeyourit/subscribers/job_performance.rb +102 -24
  39. data/lib/closeyourit/subscribers/request_performance.rb +4 -4
  40. data/lib/closeyourit/subscribers/slow_query.rb +42 -20
  41. data/lib/closeyourit/trace_context.rb +22 -22
  42. data/lib/closeyourit/transport.rb +22 -22
  43. data/lib/closeyourit/usage_registry.rb +17 -16
  44. data/lib/closeyourit/version.rb +1 -1
  45. data/lib/closeyourit-ruby.rb +125 -125
  46. metadata +2 -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_/,
@@ -41,7 +41,7 @@ module CloseYourIt
41
41
  :query_time_threshold_ms, :slow_request_threshold_ms, :slow_external_threshold_ms,
42
42
  :capture_external_http, :trap_signals,
43
43
  :monitor_jobs, :slow_job_threshold_ms, :job_queue_latency_threshold_ms,
44
- :jobs_sample_rate, :propagate_trace_context,
44
+ :jobs_sample_rate, :job_lifecycle_logs, :propagate_trace_context,
45
45
  :usage_enabled, :usage_flush_interval, :usage_max_symbols
46
46
  attr_writer :release, :project_root
47
47
  attr_reader :excluded_exceptions, :excluded_log_patterns, :excluded_query_patterns,
@@ -57,140 +57,141 @@ 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
- # Quanto attendere, alla chiusura del processo, che gli invii in volo completino. Default: il
69
- # tetto di durata di UNA POST, così l'ultimo batch non viene abbandonato a metà (CYRB-24).
70
- # Abbassalo se l'uscita del processo deve essere più rapida della consegna.
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
71
  @shutdown_timeout = Transport::MAX_REQUEST_SECONDS
72
72
 
73
- # Intercetta SIGTERM per garantire il flush di fine-vita (deploy/Kamal): SIGTERM di default
74
- # termina il processo SENZA eseguire gli at_exit. OPT-IN perché sovrascrive un eventuale handler
75
- # TERM dell'app ospite. Vedi CloseYourIt.shutdown / register_shutdown_flush (CYRB-5).
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).
76
76
  @trap_signals = false
77
77
 
78
78
  @slow_query_threshold_ms = 100
79
79
  @slow_method_threshold_ms = 200
80
- # Query da NON misurare come rallentamento (match sul testo SQL). Default: le tabelle di servizio
81
- # 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.
82
82
  @excluded_query_patterns = DEFAULT_EXCLUDED_QUERY_PATTERNS.dup
83
83
 
84
84
  @send_pii = false
85
85
  @obfuscate_sql = true
86
86
  @send_server_name = true
87
87
 
88
- # 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.
89
89
  @capture_request = true
90
90
  @request_header_allowlist = DEFAULT_REQUEST_HEADER_ALLOWLIST.dup
91
91
 
92
- # 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.
93
93
  @context_lines = 3
94
94
 
95
- # Params del body della richiesta nell'evento (`request.data`), estratti LAZY solo quando
96
- # l'errore accade, sanitizzati e scrubbati (denylist + filter_parameters). Il backend
97
- # 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.
98
98
  @capture_request_body = true
99
99
 
100
- # Breadcrumbs: cronologia (query offuscate, eventi custom) allegata all'errore.
100
+ # Breadcrumbs: history (obfuscated queries, custom events) attached to the error.
101
101
  @breadcrumbs_enabled = true
102
102
  @max_breadcrumbs = 100
103
103
 
104
- # 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).
105
105
  @sample_rate = 1.0
106
106
 
107
- # 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).
108
108
  @capture_handled_errors = true
109
109
  @report_active_job_errors = true
110
110
 
111
- # 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.
112
112
  @capture_query_bindings = false
113
113
  @capture_method_arguments = false
114
114
 
115
- # Log strutturati (CloseYourIt.log / .logger). Master switch + sampling + batching dedicati.
115
+ # Structured logs (CloseYourIt.log / .logger). Dedicated master switch + sampling + batching.
116
116
  @logs_enabled = true
117
117
  @logs_sample_rate = 1.0
118
118
  @logs_batch_size = 50
119
119
  @logs_flush_interval = 5
120
120
 
121
- # CYSK-29 — telemetria d'uso: quali rotte/job/chiavi girano davvero. Il payload è privo di
122
- # dati utente per costruzione (route = Controller#action, mai l'URL), quindi il default è ON:
123
- # trenta giorni di raccolta facoltativa hanno insegnato che facoltativo significa mai.
124
- # 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.
125
125
  @usage_enabled = true
126
126
  @usage_flush_interval = 300
127
127
  @usage_max_symbols = 2000
128
- # 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).
129
129
  @capture_rails_logs = false
130
130
  @logs_min_level = :info
131
- # Soglia DEDICATA del broadcast Rails.logger, distinta da logs_min_level (che governa
132
- # CloseYourIt.log/.logger, dove è il dev a scegliere cosa loggare). Default :warn — conservativo:
133
- # senza, in produzione ad alto traffico OGNI riga info del framework (Started GET, Rendered, ...)
134
- # inonderebbe lo stream con decine di migliaia di log-entry/min (CYRB-7). Chi vuole anche gli info
135
- # 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).
136
136
  @capture_rails_logs_min_level = :warn
137
- # Rumore del broadcast Rails.logger che NON è un'eccezione, e quindi excluded_exceptions non può
138
- # coprire: righe ripetute del framework o di una gemma. Regexp sul testo del messaggio, default
139
- # 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.
140
140
  @excluded_log_patterns = []
141
141
 
142
- # Performance issue detection (verdetti aggregati: N+1, slow request, HTTP esterne lente).
143
- # OPT-IN, default OFF: profila OGNI query della richiesta → overhead non trascurabile, va attivato
144
- # 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.
145
145
  @detect_performance_issues = false
146
- @n_plus_one_threshold = 10 # stesso fingerprint+call-site eseguito > N volte in una richiesta
147
- @query_count_threshold = 100 # troppe query totali in una richiesta
148
- @query_time_threshold_ms = 500 # tempo DB totale per richiesta oltre cui = high_query_count
149
- @slow_request_threshold_ms = 1000 # durata totale della richiesta
150
- @slow_external_threshold_ms = 1000 # singola chiamata HTTP esterna
151
- @capture_external_http = true # strumenta Net::HTTP (solo se detect_performance_issues)
152
-
153
- # Metriche dei background job: durata di esecuzione e attesa in coda (queue latency) per ActiveJob
154
- # e Sidekiq. ON di default (a differenza di detect_performance_issues): la visibilità dei job
155
- # lenti/in ritardo/ritentati non deve richiedere strumentazione manuale in ogni app (CYRB-14), e
156
- # l'overhead è basso — una notifica per job, non il profiling di ogni query. Il rumore è tenuto a
157
- # bada dalle soglie (job normali sotto soglia = niente metrica) e dal sampling. La label è il nome
158
- # 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.
159
159
  @monitor_jobs = true
160
- @slow_job_threshold_ms = 5000 # durata del perform oltre cui = slow_job
161
- @job_queue_latency_threshold_ms = 60_000 # attesa enqueue→esecuzione oltre cui = job_queue_latency
162
- @jobs_sample_rate = 1.0 # frazione dei candidati oltre soglia effettivamente inviata
163
-
164
- # Propagazione W3C trace context (traceparent/tracestate) verso i servizi esterni chiamati via
165
- # Net::HTTP. OPT-IN, default OFF: iniettare header d'uscita attraversa un trust boundary e va deciso
166
- # per-app. È limitata PER DESTINAZIONE dalla allowlist (host esatti, case-insensitive, o Regexp per i
167
- # sottodomini); lista vuota = nessuna destinazione. Mai verso host non elencati, mai come `baggage`
168
- # (che può portare PII). In ingresso un traceparent valido diventa il trace_id degli eventi
169
- # CloseYourIt → gli errori/metriche della richiesta si correlano alla traccia distribuita (CYRB-15).
160
+ @job_lifecycle_logs = false
161
+ @slow_job_threshold_ms = 5000 # perform duration beyond which = slow_job
162
+ @job_queue_latency_threshold_ms = 60_000 # enqueue→execution wait beyond which = job_queue_latency
163
+ @jobs_sample_rate = 1.0 # fraction of over-threshold candidates actually sent
164
+
165
+ # W3C trace context propagation (traceparent/tracestate) to external services called via
166
+ # Net::HTTP. OPT-IN, default OFF: injecting outgoing headers crosses a trust boundary and must be
167
+ # decided per app. It is limited PER DESTINATION by the allowlist (exact hosts, case-insensitive,
168
+ # or Regexp for subdomains); empty list = no destination. Never to unlisted hosts, never as
169
+ # `baggage` (which may carry PII). Inbound, a valid traceparent becomes the trace_id of the
170
+ # CloseYourIt events → the request's errors/metrics correlate with the distributed trace (CYRB-15).
170
171
  @propagate_trace_context = false
171
172
  @trace_propagation_allowlist = []
172
173
 
173
- # Radice del progetto: base per il filename relativo dei frame (culprit cross-SDK). Lazy:
174
- # auto-rilevata da Rails.root o Dir.pwd al primo accesso se non impostata esplicitamente.
174
+ # Project root: base for the relative frame filename (cross-SDK culprit). Lazy: auto-detected
175
+ # from Rails.root or Dir.pwd on first access when not set explicitly.
175
176
  @project_root = nil
176
177
 
177
178
  @filter_parameters = []
178
179
  @scrub_message_patterns = []
179
180
  end
180
181
 
181
- # Classi/stringhe → nome (String); i Regexp restano Regexp (match per pattern su nome/messaggio).
182
+ # Classes/strings → name (String); Regexps stay Regexps (pattern match on name/message).
182
183
  def excluded_exceptions=(list)
183
184
  @excluded_exceptions = Array(list).map { |item| item.is_a?(Regexp) ? item : item.to_s }
184
185
  end
185
186
 
186
- # Pattern del broadcast Rails.logger da scartare. Le stringhe diventano Regexp (match letterale
187
- # sul testo): chi scrive `config.excluded_log_patterns = ["Rendered layout"]` intende quello.
187
+ # Rails.logger broadcast patterns to drop. Strings become Regexps (literal match on the text):
188
+ # whoever writes `config.excluded_log_patterns = ["Rendered layout"]` means exactly that.
188
189
  def excluded_log_patterns=(list)
189
190
  @excluded_log_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
190
191
  end
191
192
 
192
- # Query da non misurare. Stessa normalizzazione di excluded_log_patterns: String = testo letterale
193
- # (un nome di tabella si scrive così, non come pattern), Regexp = pattern.
193
+ # Queries not to measure. Same normalization as excluded_log_patterns: String = literal text
194
+ # (a table name is written that way, not as a pattern), Regexp = pattern.
194
195
  def excluded_query_patterns=(list)
195
196
  @excluded_query_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
196
197
  end
@@ -199,8 +200,8 @@ module CloseYourIt
199
200
  @filter_parameters = Array(list)
200
201
  end
201
202
 
202
- # Destinazioni autorizzate a ricevere gli header di trace W3C. String = host esatto (match
203
- # case-insensitive), Regexp = pattern (per sottodomini/famiglie di host). Lista vuota = nessuno.
203
+ # Destinations allowed to receive the W3C trace headers. String = exact host (case-insensitive
204
+ # match), Regexp = pattern (for subdomains/host families). Empty list = none.
204
205
  def trace_propagation_allowlist=(list)
205
206
  @trace_propagation_allowlist = Array(list)
206
207
  end
@@ -213,8 +214,8 @@ module CloseYourIt
213
214
  environment.to_s == "production"
214
215
  end
215
216
 
216
- # Il client invia solo con credenziali complete (endpoint + token + project_id) e trasporto
217
- # sicuro (http:// ammesso fuori produzione).
217
+ # The client sends only with complete credentials (endpoint + token + project_id) and a secure
218
+ # transport (http:// allowed outside production).
218
219
  def enabled?
219
220
  return false if blank?(endpoint_url) || blank?(token) || blank?(project_id)
220
221
  return false if insecure_endpoint? && production?
@@ -222,8 +223,8 @@ module CloseYourIt
222
223
  true
223
224
  end
224
225
 
225
- # Logga i warning di configurazione (es. endpoint http://, project_id/endpoint malformati).
226
- # Non solleva mai: coerente con la filosofia no-op del client. Chiamata da `CloseYourIt.init`.
226
+ # Logs configuration warnings (e.g. http:// endpoint, malformed project_id/endpoint).
227
+ # Never raises: consistent with the client's no-op philosophy. Called by `CloseYourIt.init`.
227
228
  def validate!
228
229
  CloseYourIt.internal_logger.warn(insecure_endpoint_message) if insecure_endpoint?
229
230
  CloseYourIt.internal_logger.warn(malformed_project_id_message) if malformed_project_id?
@@ -231,22 +232,22 @@ module CloseYourIt
231
232
  self
232
233
  end
233
234
 
234
- # Release effettiva: quella impostata, altrimenti auto-rilevata (ENV di deploy/CI o git).
235
+ # Effective release: the one set, otherwise auto-detected (deploy/CI ENV or git).
235
236
  def release
236
237
  return @release unless @release.nil?
237
238
 
238
239
  @release = detect_release
239
240
  end
240
241
 
241
- # Radice del progetto effettiva: quella impostata, altrimenti auto-rilevata (Rails.root o Dir.pwd).
242
- # Usata per rendere `frame.filename` relativo (culprit confrontabile cross-SDK — CYRB-4).
242
+ # Effective project root: the one set, otherwise auto-detected (Rails.root or Dir.pwd).
243
+ # Used to make `frame.filename` relative (culprit comparable across SDKs — CYRB-4).
243
244
  def project_root
244
245
  return @project_root unless @project_root.nil?
245
246
 
246
247
  @project_root = detect_project_root
247
248
  end
248
249
 
249
- # Rails.root quando l'app gira sotto Rails, altrimenti la working directory. Mai solleva.
250
+ # Rails.root when the app runs under Rails, otherwise the working directory. Never raises.
250
251
  def detect_project_root
251
252
  return ::Rails.root.to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
252
253
 
@@ -255,12 +256,12 @@ module CloseYourIt
255
256
  Dir.pwd
256
257
  end
257
258
 
258
- # Un tag semver (con `v` opzionale) è preferito allo short SHA come release: converge con quello
259
- # che registra la CI (che tagga), mentre lo short SHA crea release duplicate lato backend (CYRB-9).
259
+ # A semver tag (optional `v`) is preferred to the short SHA as release: it converges with what the
260
+ # CI records (it tags), while the short SHA creates duplicate releases on the backend (CYRB-9).
260
261
  SEMVER_TAG = /\Av?\d+\.\d+\.\d+([-+.].+)?\z/
261
262
 
262
- # Auto-rilevamento release: prima un tag semver (APP_GIT_TAG/GIT_TAG), poi lo short SHA dalle env
263
- # di deploy/CI o dal git. Mai solleva.
263
+ # Release auto-detection: first a semver tag (APP_GIT_TAG/GIT_TAG), then the short SHA from the
264
+ # deploy/CI env or from git. Never raises.
264
265
  def detect_release
265
266
  detect_tag ||
266
267
  ENV["KAMAL_VERSION"] ||
@@ -275,8 +276,8 @@ module CloseYourIt
275
276
 
276
277
  private
277
278
 
278
- # `.git` è una directory in un checkout normale, un file in un worktree → File.directory?
279
- # è false nei worktree, così i test non lanciano subprocess git (deterministico).
279
+ # `.git` is a directory in a normal checkout and a file in a worktree → File.directory? is false
280
+ # in worktrees, so tests do not spawn git subprocesses (deterministic).
280
281
  def git_revision
281
282
  return nil unless File.directory?(".git")
282
283
 
@@ -286,9 +287,9 @@ module CloseYourIt
286
287
  nil
287
288
  end
288
289
 
289
- # Tag semver da APP_GIT_TAG poi GIT_TAG: accetta solo un valore non-blank con forma semver
290
- # (v opzionale + MAJOR.MINOR.PATCH + eventuale suffisso). Scarta `unknown`, vuoto, nomi di
291
- # branch, ecc. → nil, così detect_release cade sulla catena SHA.
290
+ # Semver tag from APP_GIT_TAG then GIT_TAG: accepts only a non-blank value shaped like semver
291
+ # (optional v + MAJOR.MINOR.PATCH + optional suffix). Rejects `unknown`, empty, branch names,
292
+ # etc. → nil, so detect_release falls back to the SHA chain.
292
293
  def detect_tag
293
294
  tag = ENV["APP_GIT_TAG"] || ENV["GIT_TAG"]
294
295
  return nil if blank?(tag)
@@ -303,17 +304,17 @@ module CloseYourIt
303
304
  !uri.nil? && uri.scheme != "https"
304
305
  end
305
306
 
306
- # Avvisa se il project_id è valorizzato ma non sembra uno UUID (l'errore tipico è incollare
307
- # uno slug/nome al posto dell'id). Non blocca: il server è l'autorità sulla validità.
307
+ # Warns when project_id is set but does not look like a UUID (the typical mistake is pasting a
308
+ # slug/name instead of the id). It does not block: the server is the authority on validity.
308
309
  def malformed_project_id?
309
310
  !blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
310
311
  end
311
312
 
312
313
  def malformed_project_id_message
313
- "CloseYourIt: project_id (#{project_id}) non ha forma UUID — verifica di aver copiato l'id corretto."
314
+ "CloseYourIt: project_id (#{project_id}) is not shaped like a UUID — check that you copied the right id."
314
315
  end
315
316
 
316
- # Avvisa se endpoint_url è valorizzato ma non parsabile o privo di host.
317
+ # Warns when endpoint_url is set but not parsable or has no host.
317
318
  def malformed_endpoint?
318
319
  return false if blank?(endpoint_url)
319
320
 
@@ -322,12 +323,12 @@ module CloseYourIt
322
323
  end
323
324
 
324
325
  def malformed_endpoint_message
325
- "CloseYourIt: endpoint_url (#{endpoint_url}) non è un URL valido (host mancante)."
326
+ "CloseYourIt: endpoint_url (#{endpoint_url}) is not a valid URL (missing host)."
326
327
  end
327
328
 
328
329
  def insecure_endpoint_message
329
- tail = production? ? "Telemetria DISABILITATA in production." : "Consentito solo in sviluppo."
330
- "CloseYourIt: endpoint_url usa http:// non sicuro (#{endpoint_url}) — il token viaggerebbe in chiaro. #{tail}"
330
+ tail = production? ? "Telemetry DISABLED in production." : "Allowed only in development."
331
+ "CloseYourIt: endpoint_url uses insecure http:// (#{endpoint_url}) — the token would travel in clear text. #{tail}"
331
332
  end
332
333
 
333
334
  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)
@@ -33,6 +33,9 @@ module CloseYourIt
33
33
  "queue" => @attrs[:queue],
34
34
  "adapter" => @attrs[:adapter],
35
35
  "attempt" => @attrs[:attempt],
36
+ "contexts" => (@attrs[:job_context] && { "job" => @attrs[:job_context].merge(
37
+ "measurement" => (@attrs[:subtype] == "slow_job" ? "duration" : "queue_wait")
38
+ ) }),
36
39
  "sdk" => sdk
37
40
  )
38
41
  end