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
@@ -2,29 +2,30 @@
2
2
 
3
3
  module CloseYourIt
4
4
  module Rails
5
- # Incluso in ActiveJob::Base (via railtie `on_load(:active_job)`): cattura gli errori dei job
6
- # (oggi persi) con il contesto del job. La logica vive in `.monitor`/`.report_discarded` per essere
7
- # testabile senza ActiveSupport/ActiveJob.
5
+ # Included in ActiveJob::Base (via the railtie's `on_load(:active_job)`): captures job errors
6
+ # (otherwise lost) with the job context. The logic lives in `.monitor`/`.report_discarded` so it
7
+ # is testable without ActiveSupport/ActiveJob.
8
8
  #
9
- # CYRB-19: `around_perform` gira DENTRO `perform_now`, mentre `retry_on`/`discard_on` (rescue_from)
10
- # sono valutati FUORI, dopo i callback. Catturare nell'around_perform significa quindi segnalare
11
- # OGNI tentativo — anche quelli che verranno ritentati con successo — perché il reporter vede
12
- # l'errore prima che il retry possa zittirlo, e ogni tentativo solleva una nuova istanza (la
13
- # deduplica interna, per-istanza, non interviene). Su ActiveJob 7.1+ deleghiamo la segnalazione ad
14
- # `after_discard`, che Rails invoca al fallimento definitivo (retry_on esauriti o eccezione non
15
- # gestita), mai sui tentativi che `retry_on` ritenta → una sola occorrenza per job.
9
+ # CYRB-19: `around_perform` runs INSIDE `perform_now`, while `retry_on`/`discard_on` (rescue_from)
10
+ # are evaluated OUTSIDE, after the callbacks. Capturing in around_perform therefore means reporting
11
+ # EVERY attempt — even those that will be retried successfully — because the reporter sees the
12
+ # error before the retry can silence it, and every attempt raises a new instance (the internal,
13
+ # per-instance dedup does not kick in). On ActiveJob 7.1+ we delegate reporting to `after_discard`,
14
+ # which Rails calls on final failure (retry_on exhausted or unhandled exception), never on the
15
+ # attempts `retry_on` retries → a single occurrence per job.
16
16
  #
17
- # Limiti noti (per costruzione di ActiveJob, non del client):
18
- # - `after_discard` NON scatta per gli errori intercettati da un `rescue_from` custom: sono gestiti
19
- # dall'app, quindi non li segnaliamo più come "non gestiti" (prima lo facevamo, impropriamente).
20
- # - I retry a livello di ADAPTER (es. Sidekiq) SENZA `retry_on` risollevano l'errore non gestito:
21
- # `after_discard` scatta a ogni esecuzione, quindi lì la de-duplicazione per tentativo non si
22
- # applica (comportamento invariato rispetto a prima).
23
- # Sulle versioni prive di `after_discard` restiamo al fallback legacy (cattura nell'around_perform).
17
+ # Known limits (by ActiveJob's design, not the client's):
18
+ # - `after_discard` does NOT fire for errors caught by a custom `rescue_from`: they are handled by
19
+ # the app, so we no longer report them as "unhandled" (we used to, improperly).
20
+ # - ADAPTER-level retries (e.g. Sidekiq) WITHOUT `retry_on` re-raise the unhandled error:
21
+ # `after_discard` fires on every execution, so per-attempt dedup does not apply there
22
+ # (behavior unchanged from before).
23
+ # On versions without `after_discard` we keep the legacy fallback (capture in around_perform).
24
24
  module ActiveJobExtension
25
- # Scope arricchito durante il `perform` tramandato ad `after_discard`. Legato all'ISTANZA del job
26
- # (ogni retry ne crea una nuova, deserializzata) → vive esattamente quanto serve, niente bleed tra
27
- # job o thread; il reset di fine `perform` lo sgancia solo dallo storage, non muta l'oggetto.
25
+ # Scope enriched during `perform`, handed over to `after_discard`. Bound to the job INSTANCE
26
+ # (every retry creates a new, deserialized one) → it lives exactly as long as needed, no bleed
27
+ # between jobs or threads; the end-of-`perform` reset only detaches it from storage, it does not
28
+ # mutate the object.
28
29
  STASHED_SCOPE_IVAR = :@__closeyourit_stashed_scope
29
30
 
30
31
  def self.included(base)
@@ -39,9 +40,10 @@ module CloseYourIt
39
40
  end
40
41
  end
41
42
 
42
- # Esegue il job arricchendo lo scope con tag/context; resetta lo scope a fine job (no bleed tra
43
- # job sullo stesso thread). Su ActiveJob 7.1+ NON cattura l'errore (lo fa `after_discard` solo al
44
- # fallimento definitivo): qui tramanda lo scope al job e ri-solleva. Sul fallback legacy cattura.
43
+ # Runs the job enriching the scope with tags/context; resets the scope at the end of the job (no
44
+ # bleed between jobs on the same thread). On ActiveJob 7.1+ it does NOT capture the error
45
+ # (`after_discard` does, only on final failure): here it hands the scope to the job and re-raises.
46
+ # On the legacy fallback it captures.
45
47
  def self.monitor(job)
46
48
  return yield unless CloseYourIt.configuration.report_active_job_errors
47
49
 
@@ -60,10 +62,10 @@ module CloseYourIt
60
62
  end
61
63
  end
62
64
 
63
- # Aggancio `after_discard` (ActiveJob 7.1+): il job è definitivamente fallito, quindi segnaliamo
64
- # l'errore UNA sola volta (handled:false). Riprendiamo lo scope arricchito durante il `perform`
65
- # (tramandato da `monitor`) così il report conserva breadcrumb/tag/contesti raccolti nel job;
66
- # `apply_job_scope` rinfresca i campi standard con `executions` finale senza perdere i custom.
65
+ # `after_discard` hook (ActiveJob 7.1+): the job has failed for good, so we report the error ONLY
66
+ # once (handled:false). We restore the scope enriched during `perform` (handed over by `monitor`)
67
+ # so the report keeps the breadcrumbs/tags/contexts collected in the job; `apply_job_scope`
68
+ # refreshes the standard fields with the final `executions` without losing the custom ones.
67
69
  def self.report_discarded(job, exception)
68
70
  return unless CloseYourIt.configuration.report_active_job_errors
69
71
 
@@ -78,8 +80,8 @@ module CloseYourIt
78
80
  end
79
81
  end
80
82
 
81
- # true quando ActiveJob espone `after_discard` (7.1+): la segnalazione è delegata lì, così
82
- # `monitor` non cattura i tentativi intermedi. false → fallback legacy (cattura nell'around_perform).
83
+ # true when ActiveJob exposes `after_discard` (7.1+): reporting is delegated there, so `monitor`
84
+ # does not capture intermediate attempts. false → legacy fallback (capture in around_perform).
83
85
  def self.report_on_discard?(job)
84
86
  job.class.respond_to?(:after_discard)
85
87
  end
@@ -93,8 +95,8 @@ module CloseYourIt
93
95
  context["executions"] = job.executions if job.respond_to?(:executions)
94
96
  CloseYourIt.set_context("active_job", context) unless context.empty?
95
97
 
96
- # Un trace_id stabile per esecuzione (job_id è unico per run ActiveJob): log ed errore dello
97
- # stesso job condividono il trace_id → correlazione log↔errore anche fuori dal ciclo richiesta.
98
+ # A stable trace_id per execution (job_id is unique per ActiveJob run): logs and error of the
99
+ # same job share the trace_id → log↔error correlation outside the request cycle too.
98
100
  CloseYourIt::Scope.current.trace_id = job.job_id if job.respond_to?(:job_id)
99
101
  end
100
102
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module CloseYourIt
4
4
  module Rails
5
- # Rack middleware: cattura le eccezioni non gestite, le invia a CloseYourIt
6
- # e le **ri-solleva** (l'app continua a gestirle come prima). Rack puro, nessuna
7
- # dipendenza da Rails → testabile in isolamento.
5
+ # Rack middleware: captures unhandled exceptions, sends them to CloseYourIt
6
+ # and **re-raises** them (the app keeps handling them as before). Pure Rack, no
7
+ # Rails dependency → testable in isolation.
8
8
  class CaptureExceptions
9
9
  def initialize(app)
10
10
  @app = app
@@ -2,13 +2,13 @@
2
2
 
3
3
  module CloseYourIt
4
4
  module Rails
5
- # Sottoscrittore di `ActiveSupport::ErrorReporter` (Rails 7+): cattura gli errori HANDLED
6
- # riportati via `Rails.error.report`/`Rails.error.handle`. Gli unhandled passano già dal
7
- # middleware Rack → la dedup (ivar sull'istanza) evita il doppio invio.
5
+ # `ActiveSupport::ErrorReporter` subscriber (Rails 7+): captures HANDLED errors reported via
6
+ # `Rails.error.report`/`Rails.error.handle`. Unhandled ones already go through the Rack
7
+ # middleware → dedup (ivar on the instance) avoids double sending.
8
8
  class ErrorSubscriber
9
9
  SEVERITY_TO_LEVEL = { error: "error", warning: "warning", info: "info" }.freeze
10
10
 
11
- # Sorgenti interne rumorose da non inoltrare (evita loop/duplicati).
11
+ # Noisy internal sources not to forward (avoids loops/duplicates).
12
12
  IGNORED_SOURCES = %w[closeyourit].freeze
13
13
 
14
14
  def report(error, handled:, severity:, context:, source: nil)
@@ -4,26 +4,26 @@ require "logger"
4
4
 
5
5
  module CloseYourIt
6
6
  module Rails
7
- # Sink Logger-compatibile agganciato a Rails.logger (broadcast opt-in, `config.capture_rails_logs`):
8
- # ogni log dell'app ≥ soglia (`capture_rails_logs_min_level`, default :warn) viene re-inoltrato a
9
- # CloseYourIt.log → ingest /logs. Sotto soglia: no-op (nessun invio). Il default è conservativo
10
- # (:warn) per non inondare lo stream col rumore info del framework (CYRB-7). Non scrive su alcun
11
- # device (inoltra soltanto).
7
+ # Logger-compatible sink attached to Rails.logger (opt-in broadcast, `config.capture_rails_logs`):
8
+ # every app log ≥ threshold (`capture_rails_logs_min_level`, default :warn) is forwarded to
9
+ # CloseYourIt.log → ingest /logs. Below threshold: no-op (nothing sent). The default is
10
+ # conservative (:warn) so the stream is not flooded with framework info noise (CYRB-7). It writes
11
+ # to no device (it only forwards).
12
12
  class LogBroadcast < ::Logger
13
13
  SEVERITY_LEVELS = { 0 => "debug", 1 => "info", 2 => "warning", 3 => "error", 4 => "fatal", 5 => "fatal" }.freeze
14
14
  LEVEL_BY_SYMBOL = { debug: 0, info: 1, warn: 2, warning: 2, error: 3, fatal: 4 }.freeze
15
15
 
16
16
  def initialize(min_level = :warn)
17
- super(nil) # nessun device: inoltra soltanto
17
+ super(nil) # no device: it only forwards
18
18
  self.level = LEVEL_BY_SYMBOL.fetch(min_level.to_sym, 2)
19
19
  end
20
20
 
21
- # Sovrascrive il punto unico di ::Logger: filtra per soglia e per esclusioni, poi re-inoltra a
22
- # CloseYourIt.log. Il secondo filtro esiste perché la soglia da sola non distingue il rumore: una
23
- # `ActionController::RoutingError` da favicon mancante è un `logger.error` come un altro, e senza
24
- # controllare il TESTO rientrava dal canale log dopo essere stata esclusa da quello degli errori
25
- # (CYRB-17). Il gating vive qui e non in `CloseYourIt.log`: quel metodo serve anche il dev che
26
- # scrive un log di proposito, e le sue righe non si silenziano.
21
+ # Overrides ::Logger's single entry point: filters by threshold and by exclusions, then forwards
22
+ # to CloseYourIt.log. The second filter exists because the threshold alone cannot tell noise
23
+ # apart: an `ActionController::RoutingError` from a missing favicon is a `logger.error` like any
24
+ # other, and without checking the TEXT it came back through the log channel after being excluded
25
+ # from the errors one (CYRB-17). Gating lives here and not in `CloseYourIt.log`: that method also
26
+ # serves the dev who writes a log on purpose, and their lines are never silenced.
27
27
  def add(severity, message = nil, progname = nil)
28
28
  severity ||= ::Logger::UNKNOWN
29
29
  return true if severity < level
@@ -5,15 +5,15 @@ require_relative "../scope"
5
5
 
6
6
  module CloseYourIt
7
7
  module Rails
8
- # Prepended a Net::HTTP: cronometra ogni chiamata esterna e la spinge nel RequestProfile dello
9
- # Scope, così la finestra della richiesta può rilevare le HTTP esterne lente. Trasparente
10
- # (restituisce la risposta originale) e difensivo (no-op se la telemetria è off; mai solleva per
11
- # colpa del profiling). Esclude le chiamate verso l'endpoint CloseYourIt stesso (niente loop).
8
+ # Prepended to Net::HTTP: times every external call and pushes it into the Scope's RequestProfile,
9
+ # so the request window can detect slow external HTTP. Transparent (returns the original response)
10
+ # and defensive (no-op when telemetry is off; never raises because of profiling). Excludes calls to
11
+ # the CloseYourIt endpoint itself (no loop).
12
12
  module NetHTTPPatch
13
13
  def request(req, body = nil, &block)
14
14
  config = CloseYourIt.configuration
15
- # Propagazione W3C: indipendente dal profiling (ha il suo opt-in), va fatta PRIMA del round-trip
16
- # perché aggiunge header alla richiesta in uscita.
15
+ # W3C propagation: independent of profiling (it has its own opt-in), must happen BEFORE the
16
+ # round-trip because it adds headers to the outgoing request.
17
17
  inject_trace_context(config, req)
18
18
  return super unless config.detect_performance_issues && config.capture_external_http
19
19
 
@@ -28,12 +28,12 @@ module CloseYourIt
28
28
 
29
29
  private
30
30
 
31
- # Gestisce gli header di trace W3C sulla richiesta in uscita (solo con la propagazione opt-in ON).
32
- # Verso una destinazione autorizzata inietta traceparent/tracestate; verso qualsiasi altra li
33
- # RIMUOVE. La rimozione è la difesa contro il leak su redirect cross-host: se lo stesso oggetto
34
- # request viene riusato per seguire un redirect verso un host non in allowlist, gli header della
35
- # chiamata precedente non devono sopravvivere ("host esterni non ricevono header interni" — CYRB-15).
36
- # baggage non viene mai né letto né emesso. Difensivo: mai solleva per colpa della propagazione.
31
+ # Handles the W3C trace headers on the outgoing request (only with opt-in propagation ON).
32
+ # To an allowed destination it injects traceparent/tracestate; to any other it REMOVES them.
33
+ # Removal is the defense against leaks on cross-host redirects: if the same request object is
34
+ # reused to follow a redirect to a host not in the allowlist, the previous call's headers must not
35
+ # survive ("external hosts do not receive internal headers" — CYRB-15).
36
+ # baggage is never read nor emitted. Defensive: never raises because of propagation.
37
37
  def inject_trace_context(config, req)
38
38
  return unless config.propagate_trace_context
39
39
 
@@ -47,9 +47,9 @@ module CloseYourIt
47
47
  nil
48
48
  end
49
49
 
50
- # Il trace context da consegnare a QUESTA destinazione, o nil se non va propagato nulla: host
51
- # assente, endpoint CloseYourIt stesso (niente auto-propagazione), destinazione fuori allowlist,
52
- # o scope privo di contesto.
50
+ # The trace context to deliver to THIS destination, or nil if nothing must be propagated: missing
51
+ # host, the CloseYourIt endpoint itself (no self-propagation), destination outside the allowlist,
52
+ # or a scope without context.
53
53
  def deliverable_context(config)
54
54
  host = address
55
55
  return nil if host.nil? || own_endpoint?(config, host)
@@ -58,16 +58,16 @@ module CloseYourIt
58
58
  CloseYourIt::Scope.current.trace_context
59
59
  end
60
60
 
61
- # La destinazione è autorizzata a ricevere il trace context? String = host esatto (case-insensitive),
62
- # Regexp = pattern (sottodomini/famiglie). Lista vuota → sempre false (nessuna destinazione).
61
+ # Is the destination allowed to receive the trace context? String = exact host (case-insensitive),
62
+ # Regexp = pattern (subdomains/families). Empty list → always false (no destination).
63
63
  def destination_allowed?(config, host)
64
64
  config.trace_propagation_allowlist.any? do |pattern|
65
65
  pattern.is_a?(Regexp) ? pattern.match?(host) : pattern.to_s.casecmp?(host)
66
66
  end
67
67
  end
68
68
 
69
- # Rimuove gli header di trace W3C che una chiamata precedente sullo stesso oggetto request possa
70
- # aver lasciato. Solo i nostri header, mai altro; no-op se il request non li supporta.
69
+ # Removes the W3C trace headers a previous call on the same request object may have left.
70
+ # Only our headers, nothing else; no-op if the request does not support them.
71
71
  def strip_trace_headers(req)
72
72
  return unless req.respond_to?(:delete)
73
73
 
@@ -83,7 +83,7 @@ module CloseYourIt
83
83
  host: host, path: templatize_path(req), duration_ms: duration_ms
84
84
  )
85
85
  rescue StandardError
86
- # Il profiling non deve mai disturbare la chiamata ospite.
86
+ # Profiling must never disturb the host call.
87
87
  nil
88
88
  end
89
89
 
@@ -96,8 +96,8 @@ module CloseYourIt
96
96
  false
97
97
  end
98
98
 
99
- # Path senza query string, con uuid e run di ≥3 cifre → placeholder (stessa rotta = stessa
100
- # signature). La soglia ≥3 cifre preserva le versioni API tipo "/v1" e templatizza gli id reali
99
+ # Path without query string, with uuids and runs of ≥3 digits → placeholders (same route = same
100
+ # signature). The ≥3 digit threshold keeps API versions like "/v1" and templatizes real ids
101
101
  # ("/v1/charges/ch_12345" → "/v1/charges/ch_<n>", "/users/123" → "/users/<n>").
102
102
  def templatize_path(req)
103
103
  path = req.respond_to?(:path) ? req.path.to_s : ""
@@ -2,9 +2,9 @@
2
2
 
3
3
  module CloseYourIt
4
4
  module Rails
5
- # Call-site applicativo di una query lenta (privacy-safe → sempre inviato): primo frame della
6
- # backtrace ripulito da Rails.backtrace_cleaner (rimuove gem/framework, tiene il codice app),
7
- # senza il suffisso ":in '...'". Es. "app/models/order.rb:42".
5
+ # Application call site of a slow query (privacy-safe → always sent): first backtrace frame
6
+ # cleaned by Rails.backtrace_cleaner (removes gems/framework, keeps app code), without the
7
+ # ":in '...'" suffix. E.g. "app/models/order.rb:42".
8
8
  module QuerySource
9
9
  def self.from_caller(backtrace = caller)
10
10
  frame = ::Rails.backtrace_cleaner.clean(backtrace).first
@@ -15,13 +15,13 @@ require_relative "../sidekiq/job_metrics_middleware"
15
15
 
16
16
  module CloseYourIt
17
17
  module Rails
18
- # Aggancia il client a Rails: Rack middleware di cattura eccezioni +
19
- # subscriber `sql.active_record` per le query lente.
18
+ # Hooks the client into Rails: exception-capturing Rack middleware +
19
+ # `sql.active_record` subscriber for slow queries.
20
20
  class Railtie < ::Rails::Railtie
21
21
  initializer "closeyourit.use_rack_middleware" do |app|
22
22
  app.config.middleware.use CloseYourIt::Rails::CaptureExceptions
23
- # RequestContext deve AVVOLGERE CaptureExceptions: lo scope dev'essere già popolato
24
- # quando l'eccezione risale a CaptureExceptions.
23
+ # RequestContext must WRAP CaptureExceptions: the scope must already be populated
24
+ # when the exception bubbles up to CaptureExceptions.
25
25
  app.config.middleware.insert_before(
26
26
  CloseYourIt::Rails::CaptureExceptions,
27
27
  CloseYourIt::Rails::RequestContext
@@ -33,43 +33,22 @@ module CloseYourIt
33
33
 
34
34
  ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
35
35
  event = ActiveSupport::Notifications::Event.new(*args)
36
- subscriber.record(
37
- name: event.payload[:name],
38
- duration_ms: event.duration,
39
- sql: event.payload[:sql],
40
- cached: event.payload.fetch(:cached, false),
41
- connection: event.payload[:connection],
42
- binds: event.payload[:binds],
43
- type_casted_binds: event.payload[:type_casted_binds],
44
- source: CloseYourIt::Rails::QuerySource.from_caller
45
- )
46
- subscriber.breadcrumb(
47
- name: event.payload[:name],
48
- sql: event.payload[:sql],
49
- duration_ms: event.duration,
50
- cached: event.payload.fetch(:cached, false)
51
- )
52
- # Accumula la query nel profilo per-richiesta (detection N+1 a fine richiesta).
53
- subscriber.profile(
54
- name: event.payload[:name],
55
- sql: event.payload[:sql],
56
- duration_ms: event.duration,
57
- cached: event.payload.fetch(:cached, false),
58
- source: CloseYourIt::Rails::QuerySource.from_caller
59
- )
36
+ # Slow query, breadcrumb and per-request profile (N+1 detection). The call site is lazy:
37
+ # a backtrace for every query was the SDK's biggest cost on the host thread.
38
+ subscriber.notify(event.payload, event.duration, -> { CloseYourIt::Rails::QuerySource.from_caller })
60
39
  end
61
40
  end
62
41
 
63
- # A fine richiesta: trasforma il profilo accumulato in verdetti performance_issue (N+1, slow
64
- # request, HTTP esterne lente). Il subscriber è no-op se detect_performance_issues è OFF.
42
+ # At the end of the request: turns the collected profile into performance_issue verdicts (N+1,
43
+ # slow request, slow external HTTP). The subscriber is a no-op when detect_performance_issues is OFF.
65
44
  initializer "closeyourit.subscribe_request_performance" do
66
45
  perf = CloseYourIt::Subscribers::RequestPerformance.new
67
46
 
68
47
  ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
69
48
  event = ActiveSupport::Notifications::Event.new(*args)
70
49
  payload = event.payload
71
- # CYSK-29 — telemetria d'uso: la rotta è `Controller#action`, MAI l'URL. Il registro fa
72
- # una lookup e un increment; il gate usage_enabled sta dentro #record.
50
+ # CYSK-29 — usage telemetry: the route is `Controller#action`, NEVER the URL. The registry does
51
+ # one lookup and one increment; the usage_enabled gate lives inside #record.
73
52
  CloseYourIt.usage_registry.record("route", "#{payload[:controller]}##{payload[:action]}") if CloseYourIt.enabled?
74
53
  perf.record(
75
54
  route: "#{payload[:controller]}##{payload[:action]}",
@@ -78,24 +57,25 @@ module CloseYourIt
78
57
  end
79
58
  end
80
59
 
81
- # Strumenta Net::HTTP: rileva le chiamate esterne lente (solo con detect_performance_issues) e
82
- # propaga il trace context W3C (solo con propagate_trace_context). Prepend incondizionato — i due
83
- # opt-in sono valutati per-chiamata nel patch; con entrambi OFF è di fatto no-op (chiama super).
60
+ # Instruments Net::HTTP: detects slow external calls (only with detect_performance_issues) and
61
+ # propagates the W3C trace context (only with propagate_trace_context). Unconditional prepend —
62
+ # the two opt-ins are evaluated per call in the patch; with both OFF it is in effect a no-op (calls super).
84
63
  initializer "closeyourit.instrument_net_http" do
85
64
  require "net/http"
86
65
  ::Net::HTTP.prepend(CloseYourIt::Rails::NetHTTPPatch) unless ::Net::HTTP.ancestors.include?(CloseYourIt::Rails::NetHTTPPatch)
87
66
  end
88
67
 
89
- # Cattura gli errori di ActiveJob/Solid Queue (around_perform).
68
+ # Captures ActiveJob/Solid Queue errors (around_perform).
90
69
  initializer "closeyourit.active_job" do
91
70
  ActiveSupport.on_load(:active_job) do
92
71
  include CloseYourIt::Rails::ActiveJobExtension
93
72
  end
94
73
  end
95
74
 
96
- # Misura durata e attesa in coda dei job ActiveJob via notifiche ActiveSupport: `perform_start`
97
- # dà l'attesa (now - enqueued_at) appena il job parte, `perform` dà la durata dell'esecuzione a
98
- # fine job. Oltre soglia → metriche slow_job / job_queue_latency. No-op se monitor_jobs è OFF.
75
+ # Measures duration and queue wait of ActiveJob jobs via ActiveSupport notifications:
76
+ # `perform_start` gives the wait (now - enqueued_at) as soon as the job starts, `perform` gives
77
+ # the execution duration at the end. Beyond threshold → slow_job / job_queue_latency metrics.
78
+ # No-op when monitor_jobs is OFF.
99
79
  initializer "closeyourit.subscribe_active_job_performance" do
100
80
  jobs = CloseYourIt::Subscribers::JobPerformance.new
101
81
 
@@ -107,28 +87,28 @@ module CloseYourIt
107
87
  ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
108
88
  event = ActiveSupport::Notifications::Event.new(*args)
109
89
  job = event.payload[:job]
110
- # CYSK-29 — anche i job dichiarano di essere girati: kind `job`, simbolo = la classe.
90
+ # CYSK-29 — jobs also declare they ran: kind `job`, symbol = the class.
111
91
  CloseYourIt.usage_registry.record("job", job.class.name) if job && CloseYourIt.enabled?
112
92
  jobs.active_job_performed(job, event.duration) if job
113
93
  end
114
94
  end
115
95
 
116
- # Cattura gli errori HANDLED riportati via Rails.error.report (Rails 7+).
96
+ # Captures HANDLED errors reported via Rails.error.report (Rails 7+).
117
97
  initializer "closeyourit.error_reporter" do
118
98
  if ::Rails.respond_to?(:error) && ::Rails.error.respond_to?(:subscribe)
119
99
  ::Rails.error.subscribe(CloseYourIt::Rails::ErrorSubscriber.new)
120
100
  end
121
101
  end
122
102
 
123
- # Broadcast opt-in di Rails.logger → CloseYourIt.log (config.capture_rails_logs, default OFF).
124
- # Spedisce solo i log dell'app ≥ capture_rails_logs_min_level: soglia DEDICATA (default :warn),
125
- # distinta da logs_min_level, così il broadcast non inonda lo stream col rumore info del framework
126
- # (Started GET, Rendered, ... — una riga per richiesta ad alto traffico, CYRB-7). Richiede
103
+ # Opt-in Rails.logger → CloseYourIt.log broadcast (config.capture_rails_logs, default OFF).
104
+ # Sends only app logs ≥ capture_rails_logs_min_level: a DEDICATED threshold (default :warn),
105
+ # separate from logs_min_level, so the broadcast does not flood the stream with framework info
106
+ # noise (Started GET, Rendered, ... — one line per request under high traffic, CYRB-7). Requires
127
107
  # BroadcastLogger (Rails 7.1+).
128
- # `after: :load_config_initializers`: config.capture_rails_logs è impostato in
129
- # config/initializers/closeyourit.rb (CloseYourIt.init), che gira DOPO gli initializer dei
130
- # railtie. Senza questo `after:` il check leggerebbe il default (false) e il broadcast non
131
- # verrebbe mai agganciato → i log dell'app non arriverebbero a CloseYourIt.
108
+ # `after: :load_config_initializers`: config.capture_rails_logs is set in
109
+ # config/initializers/closeyourit.rb (CloseYourIt.init), which runs AFTER the railtie
110
+ # initializers. Without this `after:` the check would read the default (false) and the broadcast
111
+ # would never be attached → the app logs would never reach CloseYourIt.
132
112
  initializer "closeyourit.capture_rails_logs", after: :load_config_initializers do
133
113
  config = CloseYourIt.configuration
134
114
  if config.capture_rails_logs && ::Rails.logger.respond_to?(:broadcast_to)
@@ -136,9 +116,9 @@ module CloseYourIt
136
116
  end
137
117
  end
138
118
 
139
- # Cattura gli errori dei job Sidekiq + misura durata/attesa via server middleware (solo se
140
- # Sidekiq è presente). Il middleware è no-op effettivo se monitor_jobs è OFF (la decisione vive
141
- # in JobPerformance#record).
119
+ # Captures Sidekiq job errors + measures duration/wait via server middleware (only when Sidekiq
120
+ # is present). The middleware is effectively a no-op when monitor_jobs is OFF (the decision
121
+ # lives in JobPerformance#record).
142
122
  initializer "closeyourit.sidekiq" do
143
123
  if defined?(::Sidekiq) && ::Sidekiq.respond_to?(:configure_server)
144
124
  ::Sidekiq.configure_server do |sidekiq_config|
@@ -6,11 +6,11 @@ require_relative "../scrubber"
6
6
 
7
7
  module CloseYourIt
8
8
  module Rails
9
- # Estrae i params del body della richiesta (`request.data`) al momento dell'EVENTO — mai
10
- # eagerly a ogni richiesta (zero overhead sul percorso felice). Preferisce i params già
11
- # parsati da Rails/Rack presenti in env; ripiega sulla rilettura di rack.input (con rewind)
12
- # solo per JSON/form ≤ MAX_BODY_BYTES. Output sanitizzato (upload → "[FILE: …]", oggetti →
13
- # "[OBJECT: …]", stringhe troncate) e scrubbato con la stessa denylist del resto del client.
9
+ # Extracts the request body params (`request.data`) at EVENT time — never eagerly on every
10
+ # request (zero overhead on the happy path). Prefers the params already parsed by Rails/Rack in
11
+ # env; falls back to re-reading rack.input (with rewind) only for JSON/form ≤ MAX_BODY_BYTES.
12
+ # Sanitized output (uploads → "[FILE: …]", objects → "[OBJECT: …]", truncated strings) and
13
+ # scrubbed with the same denylist as the rest of the client.
14
14
  module RequestBody
15
15
  MAX_BODY_BYTES = 65_536
16
16
  MAX_DEPTH = 8
@@ -31,7 +31,7 @@ module CloseYourIt
31
31
 
32
32
  private
33
33
 
34
- # Params già parsati a monte (ActionDispatch o Rack): nessuna rilettura del body.
34
+ # Params already parsed upstream (ActionDispatch or Rack): no body re-read.
35
35
  def parsed_params(env)
36
36
  env["action_dispatch.request.request_parameters"] || env["rack.request.form_hash"]
37
37
  end
@@ -45,7 +45,7 @@ module CloseYourIt
45
45
  value = JSON.parse(raw)
46
46
  value.is_a?(Hash) ? value : { "_json" => value }
47
47
  when FORM_TYPE
48
- # Fallback flat (stdlib): il percorso reale con nesting passa dai params già parsati.
48
+ # Flat fallback (stdlib): the real path with nesting goes through the already parsed params.
49
49
  URI.decode_www_form(raw).to_h
50
50
  end
51
51
  rescue JSON::ParserError, ArgumentError