closeyourit-ruby 0.10.0 → 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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/closeyourit/background_worker.rb +14 -14
  3. data/lib/closeyourit/breadcrumb.rb +2 -2
  4. data/lib/closeyourit/breadcrumb_buffer.rb +2 -2
  5. data/lib/closeyourit/client.rb +24 -24
  6. data/lib/closeyourit/configuration.rb +102 -102
  7. data/lib/closeyourit/event.rb +6 -6
  8. data/lib/closeyourit/events/error_event.rb +16 -16
  9. data/lib/closeyourit/events/job_metric_event.rb +8 -8
  10. data/lib/closeyourit/events/log_event.rb +17 -17
  11. data/lib/closeyourit/events/message_event.rb +4 -4
  12. data/lib/closeyourit/events/performance_issue_event.rb +4 -4
  13. data/lib/closeyourit/events/slow_method_event.rb +6 -6
  14. data/lib/closeyourit/events/slow_query_event.rb +4 -4
  15. data/lib/closeyourit/instrumenter.rb +6 -6
  16. data/lib/closeyourit/line_cache.rb +4 -4
  17. data/lib/closeyourit/log_buffer.rb +10 -10
  18. data/lib/closeyourit/log_device.rb +18 -18
  19. data/lib/closeyourit/monitor.rb +2 -2
  20. data/lib/closeyourit/performance/request_profile.rb +5 -5
  21. data/lib/closeyourit/performance/rollup.rb +3 -3
  22. data/lib/closeyourit/rails/active_job_extension.rb +33 -31
  23. data/lib/closeyourit/rails/capture_exceptions.rb +3 -3
  24. data/lib/closeyourit/rails/error_subscriber.rb +4 -4
  25. data/lib/closeyourit/rails/log_broadcast.rb +12 -12
  26. data/lib/closeyourit/rails/net_http_patch.rb +22 -22
  27. data/lib/closeyourit/rails/query_source.rb +3 -3
  28. data/lib/closeyourit/rails/railtie.rb +32 -52
  29. data/lib/closeyourit/rails/request_body.rb +7 -7
  30. data/lib/closeyourit/rails/request_context.rb +27 -27
  31. data/lib/closeyourit/scope.rb +29 -29
  32. data/lib/closeyourit/scrubber.rb +47 -48
  33. data/lib/closeyourit/sidekiq/error_handler.rb +2 -2
  34. data/lib/closeyourit/sidekiq/job_metrics_middleware.rb +10 -11
  35. data/lib/closeyourit/stats.rb +8 -8
  36. data/lib/closeyourit/subscribers/job_performance.rb +30 -19
  37. data/lib/closeyourit/subscribers/request_performance.rb +4 -4
  38. data/lib/closeyourit/subscribers/slow_query.rb +42 -20
  39. data/lib/closeyourit/trace_context.rb +22 -22
  40. data/lib/closeyourit/transport.rb +22 -22
  41. data/lib/closeyourit/usage_registry.rb +17 -16
  42. data/lib/closeyourit/version.rb +1 -1
  43. data/lib/closeyourit-ruby.rb +125 -125
  44. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bc659e436ad9b08b9861d4898e6876a3b753ec88ebb7f8e81075729d958fcb4
4
- data.tar.gz: 2bb509651ce4aa880a40f2d49f29c9584bb1bfde984b6fba3065c1a954800f82
3
+ metadata.gz: 866fd0b6c34af299e0e538ddb181b1f6b3fd961da1e638226a81ad8b8ea15c0b
4
+ data.tar.gz: eaf0d4530e9015f7353790aeffe104f04a2f80d46c4c7622ad7dad3302bd75fe
5
5
  SHA512:
6
- metadata.gz: d3cfd52ac4b7ddda5a5a92485ebe1fb4be25b808e706b11af7c5e0c6767fff19af3aa3958a8a3d9e6d94ec6ec36162a942fa439700bd16d07d52987d34a4328e
7
- data.tar.gz: fe22c4cae5a0e94f4eeaf8284d00c0a42d51a0d8c3830dcd703abcf9774eaf31c4e5e81c0c4b8ba6331a1e3d331078136b2cf05e09316325c5f545acaa8b9c17
6
+ metadata.gz: b4193d36f1123b89bdf11998ebcd5ea445b0b84c325ccae654d434d2d94158d6dd9c920b0e879167f0fba5cbaa7615f42a3d2cd11f3f61d8ade9f3eaee18917f
7
+ data.tar.gz: 7a4d366fbf67b5720d878e2e1febbcda5500c6063d8f654e450c10b68789964a7c8b15390e23d4516d9220ea6cb2ffbde619900ec2871fe7b8b20fd23c967b8b
@@ -3,9 +3,9 @@
3
3
  require "concurrent"
4
4
 
5
5
  module CloseYourIt
6
- # Esegue l'invio fire-and-forget. Con `threads == 0` esegue sincrono (test/dev);
7
- # altrimenti usa una thread-pool con coda bounded e `fallback_policy: :discard`
8
- # (se la coda è piena l'evento si perde, mai backpressure sulla request).
6
+ # Runs the send fire-and-forget. With `threads == 0` it runs synchronously (test/dev);
7
+ # otherwise it uses a thread pool with a bounded queue and `fallback_policy: :discard`
8
+ # (when the queue is full the event is lost, never backpressure on the request).
9
9
  class BackgroundWorker
10
10
  attr_reader :executor
11
11
 
@@ -13,29 +13,29 @@ module CloseYourIt
13
13
  @executor = build_executor(threads.to_i, max_queue)
14
14
  end
15
15
 
16
- # Ritorna true se l'evento è stato accettato (o eseguito sincrono), false se scartato
17
- # perché la coda era piena (`fallback_policy: :discard`). Mai backpressure sulla request.
16
+ # Returns true if the event was accepted (or run synchronously), false if dropped
17
+ # because the queue was full (`fallback_policy: :discard`). Never backpressure on the request.
18
18
  def perform(&block)
19
19
  accepted = @executor.post do
20
20
  block.call
21
21
  rescue Exception => e # rubocop:disable Lint/RescueException
22
- # Mai propagare: la telemetria non deve poter crashare l'app ospite.
22
+ # Never propagate: telemetry must not be able to crash the host app.
23
23
  CloseYourIt.internal_logger.error("CloseYourIt background worker: #{e.class}: #{e.message}")
24
24
  end
25
25
 
26
26
  unless accepted
27
27
  CloseYourIt.stats.increment(:dropped)
28
- CloseYourIt.internal_logger.warn("CloseYourIt background worker: coda piena, evento scartato")
28
+ CloseYourIt.internal_logger.warn("CloseYourIt background worker: queue full, event dropped")
29
29
  CloseYourIt.notify_diagnostic(:drop, reason: :queue_full)
30
30
  end
31
31
 
32
32
  accepted
33
33
  end
34
34
 
35
- # Drena la coda attendendo fino a `timeout` secondi. Il default è il tetto di durata di UNA POST
36
- # (Transport::MAX_REQUEST_SECONDS): aspettare meno abbandona invii ancora sani, ed è quello che
37
- # succedeva con il vecchio secondo fisso (CYRB-24). Se il drain scade, i task rimasti in coda sono
38
- # persi: contali come `dropped`, altrimenti lo snapshot di fine vita dichiara zero perdite.
35
+ # Drains the queue waiting up to `timeout` seconds. The default is the duration cap of ONE POST
36
+ # (Transport::MAX_REQUEST_SECONDS): waiting less abandons still healthy sends, which is what
37
+ # happened with the old fixed one second (CYRB-24). If the drain expires, the tasks left in the
38
+ # queue are lost: count them as `dropped`, otherwise the end-of-life snapshot reports zero losses.
39
39
  def shutdown(timeout = Transport::MAX_REQUEST_SECONDS)
40
40
  return unless @executor.respond_to?(:shutdown)
41
41
 
@@ -47,12 +47,12 @@ module CloseYourIt
47
47
 
48
48
  private
49
49
 
50
- # Task accodati e mai eseguiti al momento della scadenza del drain. `queue_length` non esiste
51
- # sull'ImmediateExecutor (sincrono, niente coda) → zero residui.
50
+ # Tasks queued and never run when the drain expired. `queue_length` does not exist on the
51
+ # ImmediateExecutor (synchronous, no queue) → zero left over.
52
52
  def report_abandoned(timeout)
53
53
  abandoned = @executor.respond_to?(:queue_length) ? @executor.queue_length.to_i : 0
54
54
  CloseYourIt.internal_logger.warn(
55
- "CloseYourIt background worker: drain scaduto dopo #{timeout}s, #{abandoned} eventi abbandonati"
55
+ "CloseYourIt background worker: drain expired after #{timeout}s, #{abandoned} events abandoned"
56
56
  )
57
57
  abandoned.times do
58
58
  CloseYourIt.stats.increment(:dropped)
@@ -3,8 +3,8 @@
3
3
  require "time"
4
4
 
5
5
  module CloseYourIt
6
- # Singola briciola di contesto (query, navigazione, evento custom) precedente a un errore.
7
- # Forma evento Sentry (`breadcrumbs.values[]`). Il `data` è già scrubato a monte (module API).
6
+ # A single context breadcrumb (query, navigation, custom event) preceding an error.
7
+ # Sentry event shape (`breadcrumbs.values[]`). `data` is already scrubbed upstream (module API).
8
8
  class Breadcrumb
9
9
  def initialize(message: nil, category: nil, type: "default", level: "info", data: {}, timestamp: nil)
10
10
  @timestamp = timestamp || Time.now.utc.iso8601
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- # Ring buffer limitato di breadcrumb. Vive nello Scope (un buffer per execution-context),
5
- # scritto solo dal thread proprietario → niente mutex. Oltre `max_size` droppa il più vecchio.
4
+ # Bounded breadcrumb ring buffer. It lives in the Scope (one buffer per execution context),
5
+ # written only by the owning thread → no mutex. Beyond `max_size` it drops the oldest.
6
6
  class BreadcrumbBuffer
7
7
  def initialize(max_size)
8
8
  @max_size = max_size.to_i
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- # Compone Transport + BackgroundWorker: applica `before_send` e dispatcha
5
- # l'invio in modo fire-and-forget.
4
+ # Combines Transport + BackgroundWorker: applies `before_send` and dispatches
5
+ # the send fire-and-forget.
6
6
  class Client
7
- # Tetto di log per singola richiesta a /logs. Il backend rifiuta un batch oltre questo limite
8
- # (413 R413-LOG-002) scartando l'INTERA richiesta — e il buffer è già stato drenato → log persi.
9
- # Deve restare ≤ del limite server (LOGS_MAX_BATCH backend = 1000). Vedi #flush_logs.
7
+ # Log cap per single request to /logs. The backend rejects a batch beyond this limit
8
+ # (413 R413-LOG-002) dropping the WHOLE request — and the buffer has already been drained → lost
9
+ # logs. Must stay ≤ the server limit (backend LOGS_MAX_BATCH = 1000). See #flush_logs.
10
10
  LOGS_MAX_BATCH = 1000
11
11
 
12
12
  def initialize(configuration)
@@ -22,7 +22,7 @@ module CloseYourIt
22
22
  payload = event.to_h
23
23
  payload = @configuration.before_send.call(payload) if @configuration.before_send
24
24
  if payload.nil?
25
- # before_send ha scartato l'evento (ritorna nil): scarto voluto, reso visibile (CYRB-12).
25
+ # before_send dropped the event (returned nil): an intentional drop, made visible (CYRB-12).
26
26
  CloseYourIt.stats.increment(:dropped)
27
27
  CloseYourIt.notify_diagnostic(:drop, reason: :before_send)
28
28
  return nil
@@ -36,21 +36,21 @@ module CloseYourIt
36
36
  end
37
37
  payload
38
38
  rescue StandardError => e
39
- # La telemetria non deve MAI propagare nel path dell'app ospite: capture_event è invocato dal
40
- # subscriber sql.active_record, che gira nel thread della query. to_h (scrubber su bind
41
- # malformato) e before_send sono valutati qui in modo sincrono → se sollevano, assorbiamo,
42
- # logghiamo e scartiamo l'evento invece di disturbare la query ospite. Vedi CYRB-2.
39
+ # Telemetry must NEVER propagate into the host app's path: capture_event is called by the
40
+ # sql.active_record subscriber, which runs on the query's thread. to_h (scrubber on a malformed
41
+ # bind) and before_send are evaluated here synchronously → if they raise, we absorb, log and
42
+ # drop the event instead of disturbing the host query. See CYRB-2.
43
43
  CloseYourIt.internal_logger.error("CloseYourIt client: #{e.class}: #{e.message}")
44
44
  CloseYourIt.stats.increment(:dropped)
45
45
  CloseYourIt.notify_diagnostic(:drop, reason: :error, error: e.class.name)
46
46
  nil
47
47
  end
48
48
 
49
- # Invia un batch di log come ARRAY a /logs (l'endpoint accetta singolo o array). before_send è
50
- # applicato a ciascun payload; quelli scartati (nil) non vengono inviati. I payload oltre
51
- # LOGS_MAX_BATCH sono spezzati in più POST sequenziali (un chunk = un POST), così un flush grande
52
- # non viene rigettato in blocco dal backend e perso — vedi R3 / LOGS_MAX_BATCH. Un flush entro il
53
- # limite resta un singolo POST.
49
+ # Sends a batch of logs as an ARRAY to /logs (the endpoint accepts a single item or an array).
50
+ # before_send is applied to each payload; dropped ones (nil) are not sent. Payloads beyond
51
+ # LOGS_MAX_BATCH are split into several sequential POSTs (one chunk = one POST), so a large flush
52
+ # is not rejected as a whole by the backend and lost — see R3 / LOGS_MAX_BATCH. A flush within
53
+ # the limit stays a single POST.
54
54
  def flush_logs(events)
55
55
  return nil if events.nil? || events.empty?
56
56
 
@@ -68,8 +68,8 @@ module CloseYourIt
68
68
  payloads
69
69
  end
70
70
 
71
- # CYSK-29 — il flush della telemetria d'uso: UNA POST per finestra, payload privo di dati utente
72
- # per costruzione (route = Controller#action). Fire-and-forget via worker, come tutto il resto.
71
+ # CYSK-29 — the usage telemetry flush: ONE POST per window, a payload free of user data by
72
+ # construction (route = Controller#action). Fire-and-forget via the worker, like everything else.
73
73
  def flush_usage(symbols:, truncated:, window_started_at:, window_ended_at:)
74
74
  payload = {
75
75
  environment: @configuration.environment.to_s,
@@ -90,22 +90,22 @@ module CloseYourIt
90
90
 
91
91
  private
92
92
 
93
- # Costruisce il payload di UNA voce di log. `to_h` (scrubber su un byte non UTF-8) e `before_send`
94
- # possono sollevare: qui il buffer è già stato drenato, quindi una sola voce rotta valutata
95
- # insieme alle altre porterebbe via l'intero batch di voci sane. Ogni voce è isolata e scartata
96
- # da sola, come fa il client JS (CYRB-24). `nil` = voce da non spedire, già contabilizzata.
93
+ # Builds the payload of ONE log entry. `to_h` (scrubber on a non-UTF-8 byte) and `before_send`
94
+ # may raise: here the buffer has already been drained, so a single broken entry evaluated together
95
+ # with the others would take the whole batch of healthy entries with it. Each entry is isolated
96
+ # and dropped on its own, like the JS client does (CYRB-24). `nil` = entry not to send, already counted.
97
97
  def build_log_payload(event)
98
98
  payload = event.to_h
99
99
  payload = @configuration.before_send.call(payload) if @configuration.before_send
100
100
  return payload unless payload.nil?
101
101
 
102
- # Scarto voluto di before_send: contabilizzato come in #capture_event, altrimenti sparirebbe
103
- # silenziosamente dai contatori (CYRB-12).
102
+ # Intentional before_send drop: counted as in #capture_event, otherwise it would silently
103
+ # disappear from the counters (CYRB-12).
104
104
  CloseYourIt.stats.increment(:dropped)
105
105
  CloseYourIt.notify_diagnostic(:drop, reason: :before_send)
106
106
  nil
107
107
  rescue StandardError => e
108
- CloseYourIt.internal_logger.error("CloseYourIt client: log scartato — #{e.class}: #{e.message}")
108
+ CloseYourIt.internal_logger.error("CloseYourIt client: log dropped — #{e.class}: #{e.message}")
109
109
  CloseYourIt.stats.increment(:dropped)
110
110
  CloseYourIt.notify_diagnostic(:drop, reason: :error, error: e.class.name)
111
111
  nil
@@ -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_/,
@@ -57,140 +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
- # 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
+ @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).
170
170
  @propagate_trace_context = false
171
171
  @trace_propagation_allowlist = []
172
172
 
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.
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.
175
175
  @project_root = nil
176
176
 
177
177
  @filter_parameters = []
178
178
  @scrub_message_patterns = []
179
179
  end
180
180
 
181
- # 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).
182
182
  def excluded_exceptions=(list)
183
183
  @excluded_exceptions = Array(list).map { |item| item.is_a?(Regexp) ? item : item.to_s }
184
184
  end
185
185
 
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.
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.
188
188
  def excluded_log_patterns=(list)
189
189
  @excluded_log_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
190
190
  end
191
191
 
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.
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.
194
194
  def excluded_query_patterns=(list)
195
195
  @excluded_query_patterns = Array(list).map { |item| item.is_a?(Regexp) ? item : Regexp.new(Regexp.escape(item.to_s)) }
196
196
  end
@@ -199,8 +199,8 @@ module CloseYourIt
199
199
  @filter_parameters = Array(list)
200
200
  end
201
201
 
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.
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.
204
204
  def trace_propagation_allowlist=(list)
205
205
  @trace_propagation_allowlist = Array(list)
206
206
  end
@@ -213,8 +213,8 @@ module CloseYourIt
213
213
  environment.to_s == "production"
214
214
  end
215
215
 
216
- # Il client invia solo con credenziali complete (endpoint + token + project_id) e trasporto
217
- # 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).
218
218
  def enabled?
219
219
  return false if blank?(endpoint_url) || blank?(token) || blank?(project_id)
220
220
  return false if insecure_endpoint? && production?
@@ -222,8 +222,8 @@ module CloseYourIt
222
222
  true
223
223
  end
224
224
 
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`.
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`.
227
227
  def validate!
228
228
  CloseYourIt.internal_logger.warn(insecure_endpoint_message) if insecure_endpoint?
229
229
  CloseYourIt.internal_logger.warn(malformed_project_id_message) if malformed_project_id?
@@ -231,22 +231,22 @@ module CloseYourIt
231
231
  self
232
232
  end
233
233
 
234
- # 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).
235
235
  def release
236
236
  return @release unless @release.nil?
237
237
 
238
238
  @release = detect_release
239
239
  end
240
240
 
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).
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).
243
243
  def project_root
244
244
  return @project_root unless @project_root.nil?
245
245
 
246
246
  @project_root = detect_project_root
247
247
  end
248
248
 
249
- # 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.
250
250
  def detect_project_root
251
251
  return ::Rails.root.to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
252
252
 
@@ -255,12 +255,12 @@ module CloseYourIt
255
255
  Dir.pwd
256
256
  end
257
257
 
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).
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).
260
260
  SEMVER_TAG = /\Av?\d+\.\d+\.\d+([-+.].+)?\z/
261
261
 
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.
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.
264
264
  def detect_release
265
265
  detect_tag ||
266
266
  ENV["KAMAL_VERSION"] ||
@@ -275,8 +275,8 @@ module CloseYourIt
275
275
 
276
276
  private
277
277
 
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).
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).
280
280
  def git_revision
281
281
  return nil unless File.directory?(".git")
282
282
 
@@ -286,9 +286,9 @@ module CloseYourIt
286
286
  nil
287
287
  end
288
288
 
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.
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.
292
292
  def detect_tag
293
293
  tag = ENV["APP_GIT_TAG"] || ENV["GIT_TAG"]
294
294
  return nil if blank?(tag)
@@ -303,17 +303,17 @@ module CloseYourIt
303
303
  !uri.nil? && uri.scheme != "https"
304
304
  end
305
305
 
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à.
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.
308
308
  def malformed_project_id?
309
309
  !blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
310
310
  end
311
311
 
312
312
  def malformed_project_id_message
313
- "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."
314
314
  end
315
315
 
316
- # 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.
317
317
  def malformed_endpoint?
318
318
  return false if blank?(endpoint_url)
319
319
 
@@ -322,12 +322,12 @@ module CloseYourIt
322
322
  end
323
323
 
324
324
  def malformed_endpoint_message
325
- "CloseYourIt: endpoint_url (#{endpoint_url}) non è un URL valido (host mancante)."
325
+ "CloseYourIt: endpoint_url (#{endpoint_url}) is not a valid URL (missing host)."
326
326
  end
327
327
 
328
328
  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}"
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}"
331
331
  end
332
332
 
333
333
  def parsed_endpoint