closeyourit-ruby

Client di telemetria per CloseYourIt. Una gemma che il tuo progetto Rails installa per inviare a CloseYourIt:

  • Eccezioni → in formato evento Sentry, sul path Bearer /api/v1/projects/:id/events. Finiscono nel error-tracker (raggruppate, triage, promote-to-ticket) come quelle di un SDK Sentry.
  • Query/metodi lenti → sul path /api/v1/projects/:id/metrics (pipeline metriche dedicata, raggruppate per signature con aggregati di durata) — ciò che Sentry/GlitchTip non danno bene.

È il client primario di CloseYourIt: replica e migliora ciò che fa un SDK Sentry (request context, user/tag/contesto, breadcrumbs, errori nei background job, errori handled, sampling, messaggi) senza dipendere dagli SDK Sentry.

Caratteristiche:

  • Contesto ricco sull'errore: request HTTP (method/url/header), user/tag/contesto custom, breadcrumbs (cronologia query offuscate prima del crash).
  • Cattura automatica di: eccezioni non gestite (Rack), errori in ActiveJob/Sidekiq (oggi persi), errori handled (Rails.error.report), query/metodi lenti.
  • capture_message, sampling (sample_rate), ignore per Regexp, release detection automatica.
  • Invio fire-and-forget: thread pool in background, non blocca la request, non crasha mai l'app.
  • No-op se non configurata (sicura in sviluppo/test).
  • Privacy-by-default: niente PII a meno di abilitarla esplicitamente.

Design e contratto dati in PDR.md.

Installazione

Pubblicata su RubyGems:

# Gemfile del progetto da monitorare
gem "closeyourit-ruby"
# in sviluppo locale, dal checkout:
# gem "closeyourit-ruby", path: "../closeyourit-ruby"

Ottenere credenziali

In CloseYourIt, area Member → Project → tokens, crea un token: ottieni il Bearer secret (mostrato una volta) e l'UUID del progetto. Servono entrambi alla gemma.

Configurazione

# config/initializers/closeyourit.rb
CloseYourIt.init do |c|
  c.endpoint_url = ENV["CLOSEYOURIT_ENDPOINT_URL"]   # es. https://www.closeyour.it (host canonico)
  c.token        = ENV["CLOSEYOURIT_TOKEN"]          # Bearer secret del Projects::Token
  c.project_id   = ENV["CLOSEYOURIT_PROJECT_ID"]     # UUID del progetto su CloseYourIt
  c.environment  = Rails.env
end

Senza endpoint_url / token / project_id la gemma è no-op (nessun invio, nessun overhead). In production un endpoint_url http:// viene rifiutato (no-op + warning): il token viaggerebbe in chiaro.

Endpoint: usa l'host canonico https://www.closeyour.it. La gemma segue fino a 2 redirect su POST (preservando metodo e body), quindi anche l'apex https://closeyour.it (301 → www) funziona — ma puntare direttamente a www risparmia un hop per ogni evento.

Opzioni

Opzione Default Descrizione
endpoint_url ENV["CLOSEYOURIT_ENDPOINT_URL"] URL base (la gemma appende i path per-progetto)
token ENV["CLOSEYOURIT_TOKEN"] Bearer secret del progetto
project_id ENV["CLOSEYOURIT_PROJECT_ID"] UUID progetto (nel path di ingest)
release ENV["CLOSEYOURIT_RELEASE"] Versione riportata negli errori (opzionale)
environment Rails.env / RACK_ENV / "development" Ambiente riportato negli eventi
excluded_exceptions RoutingError, RecordNotFound Eccezioni da NON inviare — String (nome classe) o Regexp (match su nome/messaggio)
before_send nil ->(payload) { ... } — scrub finale; ritorna payload o nil per scartare
sample_rate 1.0 Frazione di errori/messaggi inviata (1.0 tutto, 0.0 niente)
async_threads cpu/2 Thread di invio; 0 = sincrono (test)
trap_signals false Intercetta SIGTERM per garantire il flush di fine-vita (deploy/Kamal); opt-in perché sovrascrive un eventuale handler TERM dell'app ospite (vedi Flush di fine-vita)
slow_query_threshold_ms 100 Soglia query lente
slow_method_threshold_ms 200 Soglia metodi lenti
capture_request true Cattura il contesto HTTP della richiesta (method/url/header allowlist)
request_header_allowlist Accept, Content-Type, User-Agent, Referer Header inviati (mai Authorization/Cookie)
breadcrumbs_enabled true Cronologia (query offuscate + add_breadcrumb) allegata all'errore
max_breadcrumbs 100 Dimensione max del ring buffer breadcrumbs
capture_handled_errors true Cattura gli errori riportati via Rails.error.report
report_active_job_errors true Cattura gli errori dei job ActiveJob/Solid Queue/Sidekiq
send_pii false Master switch PII
obfuscate_sql true Maschera i literal nello SQL
filter_parameters [] Chiavi extra da redarre (mergiate con quelle di Rails)
scrub_message_patterns [] Regexp da redarre da messaggi d'eccezione e da message dei log
logs_enabled true Master switch dello stream di log strutturato
logs_min_level :info Soglia minima: i log sotto (es. debug) non vengono inviati
logs_sample_rate 1.0 Frazione di log inviata (1.0 tutto, 0.0 niente)
logs_batch_size 50 Log accumulati nel buffer prima di un flush a batch verso /logs
logs_flush_interval 5 Secondi tra i flush periodici del buffer log
capture_rails_logs false Inoltra anche Rails.logger allo stream — attenzione al volume (soglia capture_rails_logs_min_level)
capture_rails_logs_min_level :warn Soglia dedicata del broadcast Rails.logger (distinta da logs_min_level): default conservativo per non inondare lo stream col rumore info del framework
detect_performance_issues false Master switch dei verdetti performance (N+1, query count, request/HTTP esterne lente). Opt-in: profila ogni query → overhead (vedi Performance issues)
n_plus_one_threshold 10 Ripetizioni dello stesso [fingerprint, call-site] in una richiesta oltre cui = n_plus_one
query_count_threshold 100 Query totali in una richiesta oltre cui = high_query_count
query_time_threshold_ms 500 Tempo DB totale per richiesta (ms) oltre cui = high_query_count
slow_request_threshold_ms 1000 Durata totale della richiesta (ms) oltre cui = slow_request
slow_external_threshold_ms 1000 Durata di una singola HTTP esterna (ms) oltre cui = slow_external_http
capture_external_http true Strumenta Net::HTTP per rilevare le HTTP esterne (effettivo solo con detect_performance_issues)

Cosa cattura

Eccezioni (automatico, con Rails) → error-tracker

Il Railtie inserisce un Rack middleware che cattura le eccezioni non gestite, le invia come evento Sentry (exception.values[], level, event_id, stacktrace) e le ri-solleva (l'app continua a gestirle come prima). Cattura manuale ovunque:

begin
  rischioso!
rescue => e
  CloseYourIt.capture_exception(e)
  raise
end

Request context (automatico, con Rails)

Un Rack middleware allega a ogni evento il contesto HTTP della richiesta: method, url (senza query string) e gli header dell'allowlist (request_header_allowlist, mai Authorization/Cookie). Query string e IP solo con send_pii. Lo scope è resettato a fine richiesta (niente bleed tra request).

Background job + errori handled (automatico, con Rails)

  • ActiveJob / Solid Queue / Sidekiq: gli errori dei job (prima persi) vengono catturati con tag job.class/job.queue e contesto del job, poi ri-sollevati.
  • Rails.error.report (ActiveSupport ErrorReporter): gli errori handled vengono inviati con mechanism.handled = true e il level mappato dalla severity. La dedup garantisce un solo invio anche se la stessa eccezione passa da più punti (Rack + job + reporter).

Log strutturati (manuale/automatico) → stream log

Stream di log strutturati (level/message/attributes/logger) separato dagli errori, correlato via trace_id (stesso della richiesta). Bufferizzato e spedito a batch a /logs. message e attributes passano dallo scrubber (denylist chiavi + scrub_message_patterns) come gli errori.

# API esplicita — il keyword `logger:` è la sorgente del log; ogni altra keyword è un attributo
CloseYourIt.log(:info, "ordine creato", order_id: order.id, amount: order.amount)
CloseYourIt.log(:warning, "retry pagamento", attempt: 2)

# Interfaccia ::Logger-like (level via metodo)
CloseYourIt.logger.warn("cache miss", key: cache_key)
CloseYourIt.logger.error("job fallito", job: self.class.name)

La sorgente del log (campo logger) si imposta con .named — child logger immutabile, stesso idioma di dart/js. Il logger radice resta invariato:

payments = CloseYourIt.logger.named("payments")   # child con sorgente "payments"
payments.warn("retry", attempt: 3)                # logger="payments", attributes={attempt: 3}

Sull'interfaccia .logger una chiave logger: è sempre un attributo dati (non viene hijackata come sorgente): CloseYourIt.logger.warn("disco pieno", logger: "/dev/sda1") mette logger negli attributes. Sorgente (.named) e attributo logger possono coesistere senza collidere.

Ogni log sotto logs_min_level (default :info) è scartato prima di essere costruito o spedito — CloseYourIt.log e CloseYourIt.logger.* usano la mappa di livelli (debug < info < warning < error < fatal), identica agli SDK dart/js. Un CloseYourIt.logger.debug(...) in un loop caldo con la soglia di default non produce alcun invio.

Con Rails, capture_rails_logs = true inoltra anche Rails.logger allo stream, ma con una soglia dedicata capture_rails_logs_min_level (default :warn, distinta da logs_min_level): in produzione ad alto traffico Rails logga a :info ogni richiesta (Started GET, Rendered, ...) e un default a :info inonderebbe lo stream col rumore del framework — la soglia conservativa lo evita. Abbassala esplicitamente (es. config.capture_rails_logs_min_level = :info) solo se vuoi anche gli info del framework. I log dei background job ereditano il trace_id = job_id, così log ed errore dello stesso job si correlano.

Flush di fine-vita

Il buffer log viene svuotato automaticamente allo shutdown del processo: CloseYourIt.init registra un at_exit che drena anche il worker asincrono (attende gli invii in volo, non solo il flush del buffer), così i log sotto-batch di rake/CLI non vanno persi all'uscita. SIGTERM (deploy/Kamal) termina il processo senza eseguire gli at_exit: imposta trap_signals = true per intercettarlo e convertirlo in un exit pulito, oppure chiama CloseYourIt.shutdown esplicitamente prima di terminare.

Contesto, breadcrumbs e messaggi (manuale)

CloseYourIt.set_user(id: .id)              # solo id; email/ip solo se send_pii
CloseYourIt.set_tag(:tenant, current_tenant.slug)
CloseYourIt.set_context(:billing, { plan: "pro" })
CloseYourIt.set_extra(:cart_size, cart.size)
CloseYourIt.configure_scope { |s| s.set_tag(:area, "checkout") }

CloseYourIt.add_breadcrumb(message: "coupon applicato", category: "ui")  # cronologia pre-crash
CloseYourIt.capture_message("cache miss storm", level: "warning")        # messaggio diagnostico

Lo scope (user/tag/contesto/breadcrumbs) è per-richiesta/job e viene allegato automaticamente all'evento d'errore catturato nello stesso contesto di esecuzione.

Query lente (automatico, con Rails) → metriche

Il Railtie si iscrive a sql.active_record: ogni query oltre slow_query_threshold_ms (esclusi SCHEMA/CACHE) viene inviata come slow_query alla pipeline metriche. Lo SQL è offuscato (bind esclusi).

Metodi lenti → metriche

# Blocco ad-hoc
CloseYourIt.measure("checkout.total") do
  calcolo_pesante
end

# Macro su un metodo (wrap automatico, firma e valore di ritorno invariati)
class Report
  include CloseYourIt::Monitor
  def generate(...) = ...
  monitor :generate
end

Viene inviato un slow_method solo se la durata supera slow_method_threshold_ms. Gli argomenti del metodo non vengono mai inviati (solo label, durata, file:riga).

Performance issues (opt-in)

slow_query e slow_method sono metriche puntuali (una query/un metodo lenti). Oltre a queste, con detect_performance_issues = true la gemma profila ogni query della richiesta (fingerprint + call-site) e, a fine richiesta, emette 0..N verdetti performance_issue sulla pipeline metriche (/api/v1/projects/:id/metrics). Un verdetto non è una singola query lenta: è un pattern dell'intera richiesta, aggregato e correlato a log/errori via trace_id. I quattro subtype:

  • n_plus_one — lo stesso [fingerprint SQL, call-site] eseguito più di n_plus_one_threshold volte (default 10) nella stessa richiesta: il classico N+1. Un verdetto per gruppo, con lo SQL già offuscato e il source (file:riga) che lo ha originato.
  • high_query_count — troppe query in una richiesta: numero totale oltre query_count_threshold (default 100) oppure tempo DB totale oltre query_time_threshold_ms (default 500 ms).
  • slow_request — durata totale della richiesta oltre slow_request_threshold_ms (default 1000 ms), con route, query_count e tempo DB come contesto.
  • slow_external_http — una singola chiamata HTTP esterna (Net::HTTP) oltre slow_external_threshold_ms (default 1000 ms). Richiede capture_external_http (default true, che strumenta Net::HTTP solo quando la detection è attiva); host e path templatizzato, mai la query string.
CloseYourIt.init do |c|
  # …endpoint_url / token / project_id…
  c.detect_performance_issues = true
end

Con Rails il wiring è automatico (il Railtie si iscrive a process_action.action_controller): attivato il master switch, non serve altro codice. Le soglie sopra sono conservative (poco rumore) e tutte ritoccabili.

Overhead — perché è opt-in (default OFF). Per aggregare i verdetti la detection profila ogni query della richiesta (fingerprint + call-site): un costo non trascurabile, da attivare consapevolmente per-app. slow_query/slow_method restano attive e indipendenti da questo switch. (Pendant server-side dei verdetti performance_issue di closeyourit-js: qui i subtype sono quelli del backend Rails — N+1, query count, request/HTTP esterne lente.)

Privacy & PII

Privacy-by-default (send_pii = false). In sintesi:

  • SQL: inviato il template, mai i bind values; obfuscate_sql maschera anche i literal inline.
  • Chiavi sensibili (password, token, authorization, cookie, secret, api_key, csrf, credit_card, cvv, ssn, iban, …) → [FILTERED]; estendibili con filter_parameters.
  • Messaggi d'eccezione: inviati per il debug, ma redigibili con scrub_message_patterns / before_send.
  • Mai inviati: variabili locali dei frame, argomenti dei metodi, IP/cookie/Authorization, token (che viaggia solo nell'header su HTTPS).

Rischio residuo: exception.value e i nomi tabella/colonna nello SQL possono contenere dati di dominio. Usa before_send/scrub_message_patterns per azzerarli. Dettaglio in PDR.md §9.

Diagnostica

Il trasporto è fire-and-forget: non solleva mai e non blocca la request. Per non lasciare fallimenti silenziosi, ogni risposta HTTP non-2xx (es. 401 token errato, 404 progetto inesistente) viene loggata a warn, così come gli eventi scartati a coda piena. I contatori sono ispezionabili a runtime:

CloseYourIt.stats.to_h
# => { enqueued: 128, dropped: 0, sent: 126, failed: 2 }
  • enqueued — eventi accettati per l'invio
  • dropped — scartati perché la coda async era piena (mai backpressure)
  • sent — risposta HTTP 2xx
  • failed — errore di rete o status non-2xx (vedi i log a warn)

Sviluppo

bundle install
bundle exec rspec        # test (WebMock, niente rete reale)
bundle exec rubocop      # lint (omakase)
COVERAGE_ENFORCE=1 bundle exec rspec   # gate coverage ≥90% line