closeyourit-ruby 0.2.0 → 0.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d841fba2147566f662dce067e2d41c44d44cf6d402fb929fc3c73eb9a6af4a1
4
- data.tar.gz: cf7b435c2c9a336f107930ded5fc2ad197032a5d62eeca07f25594775a111bcc
3
+ metadata.gz: d98365053459eaded9732014e987b01e3fcf2ac60a21db83026243d076bc2dbf
4
+ data.tar.gz: 4cb2024f4681e57d415097b8af237156f3cc0c8b764a2d14ea958bb8824e1738
5
5
  SHA512:
6
- metadata.gz: a5172771940399ca5f3f2706e35d3c72d16942b79e0b2bbb89f0dfa184e4a66d45f7644704d45b6fb8f62f645b01bc84e75d7abe1d8a08ae0fced77e87781dcf
7
- data.tar.gz: dc9b5cc1387ecfb81b70a58815c0f79f7e68f1aeda98c2d3af14d0862f2dd28878a6803a1193e2737c4d8990b833c2aadf0d4461b393a26810957a1b6f0749d0
6
+ metadata.gz: de2f19a59df8c78825e0070e5c48c97424a7481d314219da675543da1ab71171ecfe7d5cd09cc0d81d48323be6d50435027e8a6bf901f121c6f5f3435b419b0d
7
+ data.tar.gz: 75ab998c2d1049bdcc047a96a54a01fe92cb50b820e81d1eac3795f4695416d7076956aac3defd82c520daaffd2dccf3726bb585cc189fb76090601928b3447f
data/README.md CHANGED
@@ -45,7 +45,7 @@ In CloseYourIt, area **Member → Project → tokens**, crea un token: ottieni i
45
45
  ```ruby
46
46
  # config/initializers/closeyourit.rb
47
47
  CloseYourIt.init do |c|
48
- c.endpoint_url = ENV["CLOSEYOURIT_ENDPOINT_URL"] # es. https://closeyour.it
48
+ c.endpoint_url = ENV["CLOSEYOURIT_ENDPOINT_URL"] # es. https://www.closeyour.it (host canonico)
49
49
  c.token = ENV["CLOSEYOURIT_TOKEN"] # Bearer secret del Projects::Token
50
50
  c.project_id = ENV["CLOSEYOURIT_PROJECT_ID"] # UUID del progetto su CloseYourIt
51
51
  c.environment = Rails.env
@@ -55,6 +55,10 @@ end
55
55
  **Senza `endpoint_url` / `token` / `project_id` la gemma è no-op** (nessun invio, nessun overhead). In
56
56
  `production` un `endpoint_url` `http://` viene rifiutato (no-op + warning): il token viaggerebbe in chiaro.
57
57
 
58
+ > **Endpoint:** usa l'host canonico **`https://www.closeyour.it`**. La gemma segue fino a 2 redirect su POST
59
+ > (preservando metodo e body), quindi anche l'apex `https://closeyour.it` (301 → www) funziona — ma puntare
60
+ > direttamente a www risparmia un hop per ogni evento.
61
+
58
62
  ### Opzioni
59
63
 
60
64
  | Opzione | Default | Descrizione |
@@ -167,6 +171,23 @@ Privacy-by-default (`send_pii = false`). In sintesi:
167
171
  > Rischio residuo: `exception.value` e i nomi tabella/colonna nello SQL possono contenere dati di
168
172
  > dominio. Usa `before_send`/`scrub_message_patterns` per azzerarli. Dettaglio in [`PDR.md` §9](PDR.md).
169
173
 
174
+ ## Diagnostica
175
+
176
+ Il trasporto è fire-and-forget: non solleva mai e non blocca la request. Per non lasciare
177
+ fallimenti silenziosi, ogni risposta HTTP non-2xx (es. `401` token errato, `404` progetto
178
+ inesistente) viene loggata a `warn`, così come gli eventi scartati a coda piena. I contatori
179
+ sono ispezionabili a runtime:
180
+
181
+ ```ruby
182
+ CloseYourIt.stats.to_h
183
+ # => { enqueued: 128, dropped: 0, sent: 126, failed: 2 }
184
+ ```
185
+
186
+ - `enqueued` — eventi accettati per l'invio
187
+ - `dropped` — scartati perché la coda async era piena (mai backpressure)
188
+ - `sent` — risposta HTTP 2xx
189
+ - `failed` — errore di rete o status non-2xx (vedi i log a `warn`)
190
+
170
191
  ## Sviluppo
171
192
 
172
193
  ```bash
@@ -13,13 +13,22 @@ 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
18
  def perform(&block)
17
- @executor.post do
19
+ accepted = @executor.post do
18
20
  block.call
19
21
  rescue Exception => e # rubocop:disable Lint/RescueException
20
22
  # Mai propagare: la telemetria non deve poter crashare l'app ospite.
21
23
  CloseYourIt.logger.error("CloseYourIt background worker: #{e.class}: #{e.message}")
22
24
  end
25
+
26
+ unless accepted
27
+ CloseYourIt.stats.increment(:dropped)
28
+ CloseYourIt.logger.warn("CloseYourIt background worker: coda piena, evento scartato")
29
+ end
30
+
31
+ accepted
23
32
  end
24
33
 
25
34
  def shutdown(timeout = 1)
@@ -19,7 +19,8 @@ module CloseYourIt
19
19
  return nil if payload.nil?
20
20
 
21
21
  path = event.ingest_path(@configuration.project_id)
22
- @worker.perform { @transport.send_event(payload, path: path) }
22
+ accepted = @worker.perform { @transport.send_event(payload, path: path) }
23
+ CloseYourIt.stats.increment(:enqueued) if accepted
23
24
  payload
24
25
  end
25
26
 
@@ -95,9 +95,12 @@ module CloseYourIt
95
95
  true
96
96
  end
97
97
 
98
- # Logga i warning di configurazione (es. endpoint http://). Chiamata da `CloseYourIt.init`.
98
+ # Logga i warning di configurazione (es. endpoint http://, project_id/endpoint malformati).
99
+ # Non solleva mai: coerente con la filosofia no-op del client. Chiamata da `CloseYourIt.init`.
99
100
  def validate!
100
101
  CloseYourIt.logger.warn(insecure_endpoint_message) if insecure_endpoint?
102
+ CloseYourIt.logger.warn(malformed_project_id_message) if malformed_project_id?
103
+ CloseYourIt.logger.warn(malformed_endpoint_message) if malformed_endpoint?
101
104
  self
102
105
  end
103
106
 
@@ -133,11 +136,35 @@ module CloseYourIt
133
136
  nil
134
137
  end
135
138
 
139
+ UUID_FORMAT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
140
+
136
141
  def insecure_endpoint?
137
142
  uri = parsed_endpoint
138
143
  !uri.nil? && uri.scheme != "https"
139
144
  end
140
145
 
146
+ # Avvisa se il project_id è valorizzato ma non sembra uno UUID (l'errore tipico è incollare
147
+ # uno slug/nome al posto dell'id). Non blocca: il server è l'autorità sulla validità.
148
+ def malformed_project_id?
149
+ !blank?(project_id) && !UUID_FORMAT.match?(project_id.to_s)
150
+ end
151
+
152
+ def malformed_project_id_message
153
+ "CloseYourIt: project_id (#{project_id}) non ha forma UUID — verifica di aver copiato l'id corretto."
154
+ end
155
+
156
+ # Avvisa se endpoint_url è valorizzato ma non parsabile o privo di host.
157
+ def malformed_endpoint?
158
+ return false if blank?(endpoint_url)
159
+
160
+ uri = parsed_endpoint
161
+ uri.nil? || blank?(uri.host)
162
+ end
163
+
164
+ def malformed_endpoint_message
165
+ "CloseYourIt: endpoint_url (#{endpoint_url}) non è un URL valido (host mancante)."
166
+ end
167
+
141
168
  def insecure_endpoint_message
142
169
  tail = production? ? "Telemetria DISABILITATA in production." : "Consentito solo in sviluppo."
143
170
  "CloseYourIt: endpoint_url usa http:// non sicuro (#{endpoint_url}) — il token viaggerebbe in chiaro. #{tail}"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module CloseYourIt
6
+ # Contatori diagnostici thread-safe del client: quanti eventi sono stati accodati,
7
+ # scartati (coda piena), spediti con successo o falliti (rete o status non-2xx).
8
+ # Servono a rendere visibili i fallimenti silenziosi del trasporto fire-and-forget.
9
+ #
10
+ # CloseYourIt.stats.to_h # => { enqueued: 12, dropped: 0, sent: 11, failed: 1 }
11
+ class Stats
12
+ COUNTERS = %i[enqueued dropped sent failed].freeze
13
+
14
+ def initialize
15
+ @counters = COUNTERS.to_h { |name| [ name, Concurrent::AtomicFixnum.new(0) ] }
16
+ end
17
+
18
+ def increment(name)
19
+ counter = @counters.fetch(name)
20
+ counter.increment
21
+ counter.value
22
+ end
23
+
24
+ def [](name)
25
+ @counters.fetch(name).value
26
+ end
27
+
28
+ def to_h
29
+ @counters.transform_values(&:value)
30
+ end
31
+
32
+ def reset!
33
+ @counters.each_value { |counter| counter.value = 0 }
34
+ self
35
+ end
36
+ end
37
+ end
@@ -10,14 +10,25 @@ module CloseYourIt
10
10
  class Transport
11
11
  OPEN_TIMEOUT = 2
12
12
  READ_TIMEOUT = 3
13
+ # Net::HTTP non segue i redirect: l'host canonico può rispondere 301 (es. apex → www).
14
+ # Ri-POSTiamo a Location preservando metodo + body, così l'evento non si perde in silenzio.
15
+ MAX_REDIRECTS = 2
13
16
 
14
17
  def initialize(configuration)
15
18
  @configuration = configuration
16
19
  end
17
20
 
18
21
  def send_event(payload, path:)
19
- post(payload, path)
22
+ response = post(payload, path)
23
+ if response.is_a?(Net::HTTPSuccess)
24
+ CloseYourIt.stats.increment(:sent)
25
+ else
26
+ CloseYourIt.stats.increment(:failed)
27
+ CloseYourIt.logger.warn("CloseYourIt transport: HTTP #{response.code} su #{path}")
28
+ end
29
+ response
20
30
  rescue StandardError => e
31
+ CloseYourIt.stats.increment(:failed)
21
32
  CloseYourIt.logger.error("CloseYourIt transport: #{e.class}: #{e.message}")
22
33
  nil
23
34
  end
@@ -25,7 +36,21 @@ module CloseYourIt
25
36
  private
26
37
 
27
38
  def post(payload, path)
39
+ body = JSON.generate(payload)
28
40
  uri = URI.parse("#{base_url}#{path}")
41
+ redirects = 0
42
+
43
+ loop do
44
+ response = post_once(uri, body)
45
+ location = response["location"] if response.is_a?(Net::HTTPRedirection)
46
+ return response unless location && redirects < MAX_REDIRECTS
47
+
48
+ redirects += 1
49
+ uri = redirect_uri(uri, location)
50
+ end
51
+ end
52
+
53
+ def post_once(uri, body)
29
54
  http = Net::HTTP.new(uri.host, uri.port)
30
55
  http.use_ssl = uri.scheme == "https"
31
56
  http.open_timeout = OPEN_TIMEOUT
@@ -35,11 +60,17 @@ module CloseYourIt
35
60
  request["Authorization"] = "Bearer #{@configuration.token}"
36
61
  request["Content-Type"] = "application/json"
37
62
  request["User-Agent"] = "closeyourit-ruby/#{VERSION}"
38
- request.body = JSON.generate(payload)
63
+ request.body = body
39
64
 
40
65
  http.request(request)
41
66
  end
42
67
 
68
+ # Location può essere assoluto (https://www.…) o relativo (/api/…): risolvilo sull'URI corrente.
69
+ def redirect_uri(current, location)
70
+ target = URI.parse(location)
71
+ target.relative? ? current + target : target
72
+ end
73
+
43
74
  def base_url
44
75
  @configuration.endpoint_url.to_s.chomp("/")
45
76
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CloseYourIt
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -7,6 +7,7 @@ require_relative "closeyourit/configuration"
7
7
  require_relative "closeyourit/breadcrumb"
8
8
  require_relative "closeyourit/scope"
9
9
  require_relative "closeyourit/scrubber"
10
+ require_relative "closeyourit/stats"
10
11
  require_relative "closeyourit/background_worker"
11
12
  require_relative "closeyourit/transport"
12
13
  require_relative "closeyourit/event"
@@ -144,6 +145,12 @@ module CloseYourIt
144
145
 
145
146
  attr_writer :logger
146
147
 
148
+ # Contatori diagnostici del client (accodati/scartati/spediti/falliti).
149
+ # CloseYourIt.stats.to_h # => { enqueued: …, dropped: …, sent: …, failed: … }
150
+ def stats
151
+ @stats ||= Stats.new
152
+ end
153
+
147
154
  private
148
155
 
149
156
  def client
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closeyourit-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessio Bussolari
@@ -55,6 +55,7 @@ files:
55
55
  - lib/closeyourit/scope.rb
56
56
  - lib/closeyourit/scrubber.rb
57
57
  - lib/closeyourit/sidekiq/error_handler.rb
58
+ - lib/closeyourit/stats.rb
58
59
  - lib/closeyourit/subscribers/slow_query.rb
59
60
  - lib/closeyourit/transport.rb
60
61
  - lib/closeyourit/version.rb