apidepth 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f499652eea585017c268ea82a997a9e11af46e36c912b6f672fa91fd7d354eb4
4
- data.tar.gz: aa70866eaf7876101236b986472ad14b24ea68bcaa9a38a68ed901d88e1f06b5
3
+ metadata.gz: ee27b15f831798b976f293eda67c90c9cccab735e75cb82a225d3ea511331622
4
+ data.tar.gz: 2ed0a290e8acab029c51eb12a116d295f8dfaa1390eef934cb61e5f9344b63ce
5
5
  SHA512:
6
- metadata.gz: 22a04205584e05a111b2422209733b4c06b0008dada3840caa340915d2d9ffb7195dec530c4791b692990812f357b4148a714da904ada5fba15e5b20f854460e
7
- data.tar.gz: 607dca2a4455e60fcf21bd3502382107a3840b02ca23102a0dac4fc4c903c62101bf46b099210591182aa6350f229ffc0209f650a71076060c1e482b83340cee
6
+ metadata.gz: b32de3265dc5a4add529dd10741d21aa935ba18fccf669614fb74b9b06ed5d8125200d0ec26be7cf0c9349b8f6cc9a56d173ed4b2ba89acd551b5684bd411aa8
7
+ data.tar.gz: bf7106cbfdf0a003824b625680a047978f1f8a27c862ce8df52551f316405e47dbd4444e3e64361f35cedf6317f74575a9998ed766afdbdd6459d56a5a719404
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # apidepth
2
2
 
3
+ [![Gem Version](https://img.shields.io/gem/v/apidepth)](https://rubygems.org/gems/apidepth)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-ruby)](https://rubygems.org/gems/apidepth)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
3
7
  Most API monitoring tools measure latency from their servers to the vendor. That's not what your users feel. Apidepth instruments `Net::HTTP` directly — every outbound call your app makes to Stripe, OpenAI, or Twilio is timed at the socket level, from your server. Then it benchmarks your numbers against anonymized fleet data, so when Stripe is slow you can tell if it's you or everyone.
4
8
 
5
9
  No payload capture. No credentials touch our infrastructure. No changes to your application code beyond a one-time initializer.
@@ -189,6 +189,8 @@ module Apidepth
189
189
  end
190
190
 
191
191
  def drain_queue
192
+ return [].freeze if @queue.empty?
193
+
192
194
  events = []
193
195
  events << @queue.pop(true) while events.size < MAX_BATCH_SIZE
194
196
  events
@@ -240,13 +242,7 @@ module Apidepth
240
242
 
241
243
  key = Apidepth.configuration.api_key
242
244
  if key.nil? || key.empty?
243
- unless @warned_no_key
244
- @warned_no_key = true
245
- Apidepth.logger&.warn(
246
- "[Apidepth] No API key configured — events are being dropped. " \
247
- "Visit www.apidepth.io to create an account and get your key."
248
- )
249
- end
245
+ warn_no_api_key!
250
246
  return
251
247
  end
252
248
 
@@ -292,7 +288,7 @@ module Apidepth
292
288
 
293
289
  if host.match?(/\A\d+\z/)
294
290
  int = host.to_i
295
- if int >= 0 && int <= 0xFFFFFFFF
291
+ if int.between?(0, 0xFFFFFFFF)
296
292
  host = [int >> 24, (int >> 16) & 0xFF, (int >> 8) & 0xFF,
297
293
  int & 0xFF].join(".")
298
294
  end
@@ -305,6 +301,18 @@ module Apidepth
305
301
  "addresses (got #{url.host.inspect})."
306
302
  end
307
303
 
304
+ def warn_no_api_key!
305
+ @stats_mutex.synchronize do
306
+ unless @warned_no_key
307
+ @warned_no_key = true
308
+ Apidepth.logger&.warn(
309
+ "[Apidepth] No API key configured — events are being dropped. " \
310
+ "Visit www.apidepth.io to create an account and get your key."
311
+ )
312
+ end
313
+ end
314
+ end
315
+
308
316
  def validate_api_key!(key)
309
317
  return if key.nil? || key.empty?
310
318
  return unless key.match?(/[\r\n]/)
@@ -88,7 +88,8 @@ module Apidepth
88
88
  }.merge(rl || {})
89
89
  )
90
90
  )
91
- rescue StandardError
91
+ rescue StandardError => e
92
+ Apidepth.logger&.debug("[Apidepth] Instrumentation error: #{e.class}: #{e.message}")
92
93
  nil
93
94
  end
94
95
 
@@ -110,7 +111,8 @@ module Apidepth
110
111
  ts: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
111
112
  )
112
113
  )
113
- rescue StandardError
114
+ rescue StandardError => e
115
+ Apidepth.logger&.debug("[Apidepth] Instrumentation error: #{e.class}: #{e.message}")
114
116
  nil
115
117
  end
116
118
  end
@@ -72,6 +72,7 @@ module Apidepth
72
72
  headers.each do |name|
73
73
  val = response[name]
74
74
  next unless val
75
+ next unless val.strip.match?(/\A\d+\z/)
75
76
 
76
77
  n = val.strip.to_i
77
78
  return n if n >= 0
@@ -12,14 +12,32 @@ module Apidepth
12
12
  # registry (remote → disk cache → bundled baseline already loaded by
13
13
  # VendorRegistry.initialize_registry) and starts the background
14
14
  # refresh thread.
15
+ #
16
+ # Startup strategy: apply the on-disk cache synchronously so the in-process
17
+ # registry is populated immediately (no blocking network call on the boot
18
+ # thread). The initial remote fetch is dispatched to a background thread so
19
+ # that slow or unreachable endpoints (CI, air-gapped environments) do not
20
+ # block Rails boot for up to 8 seconds (open_timeout + read_timeout).
21
+ # The background refresh loop (start_refresh_thread) is unchanged.
15
22
  def self.load_and_start
16
- registry = fetch_remote || load_from_disk
17
- VendorRegistry.replace(registry) if registry
23
+ # Apply disk cache immediately — zero network latency, registry is ready
24
+ # before the application begins serving requests.
25
+ disk_registry = load_from_disk
26
+ VendorRegistry.replace(disk_registry) if disk_registry
27
+
28
+ # Fetch the freshest registry from the network in the background so the
29
+ # boot thread is never blocked by the remote request.
30
+ Thread.new do
31
+ registry = fetch_remote
32
+ VendorRegistry.replace(registry) if registry
33
+ end.tap do |t|
34
+ t.abort_on_exception = false
35
+ t.name = "apidepth-registry-init"
36
+ end
37
+
18
38
  start_refresh_thread
19
39
  end
20
40
 
21
- private
22
-
23
41
  def self.start_refresh_thread
24
42
  Thread.new do
25
43
  loop do
@@ -213,6 +231,17 @@ module Apidepth
213
231
  raise ArgumentError, "registry_cache_path must not contain '..' traversal segments (got #{path.inspect})"
214
232
  end
215
233
 
234
+ # Reset mutable class-level warn state under @mutex.
235
+ # Called by tests instead of raw instance_variable_set so that state
236
+ # changes go through the same lock used in production code paths.
237
+ def self.reset_state!
238
+ @mutex.synchronize do
239
+ @conflict_vendors = {}
240
+ @warned_stale = {}
241
+ @warned_conflict = {}
242
+ end
243
+ end
244
+
216
245
  # Ruby's `private` keyword does not apply to `def self.method` — those remain
217
246
  # public class methods regardless of placement inside a private block.
218
247
  # private_class_method is the correct idiom.
@@ -64,6 +64,10 @@ module Apidepth
64
64
  [%r{/[a-z0-9]{24,}}, "/:token"]
65
65
  ].freeze
66
66
 
67
+ # True when the runtime supports Regexp.timeout (introduced in Ruby 3.2).
68
+ # Used by apply_vendor_normalizers to enable ReDoS protection when available.
69
+ RUBY_GTE_3_2 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2")
70
+
67
71
  class << self
68
72
  def identify(host, raw_path)
69
73
  hosts, patterns = @mutex.synchronize { [@hosts, @patterns] }
@@ -169,11 +173,28 @@ module Apidepth
169
173
  path.split("?").first
170
174
  end
171
175
 
176
+ # First-match-wins: iteration stops at the first pattern that matches the
177
+ # path. Vendor authors must order rules from most-specific to least-specific
178
+ # to ensure that narrower patterns (e.g. /v1/charges/:id) are tested before
179
+ # broader catch-alls (e.g. /v1/:resource/:id). A less-specific rule placed
180
+ # earlier will shadow any more-specific rules that follow it.
181
+ #
182
+ # ReDoS protection: on Ruby >= 3.2 we apply a per-match timeout of 1ms so
183
+ # that a pathological pattern from a compromised or misconfigured registry
184
+ # cannot stall the request thread indefinitely. On older Ruby, Regexp.timeout
185
+ # is not available — use a trusted, internally-reviewed registry source.
172
186
  def apply_vendor_normalizers(rules, path)
187
+ if RUBY_GTE_3_2
188
+ saved_timeout = Regexp.timeout
189
+ Regexp.timeout = 0.001
190
+ end
191
+
173
192
  rules.each do |pattern, replacement|
174
193
  return path.gsub(pattern, replacement) if path.match?(pattern)
175
194
  end
176
195
  path
196
+ ensure
197
+ Regexp.timeout = saved_timeout if RUBY_GTE_3_2
177
198
  end
178
199
 
179
200
  def apply_generic_normalizers(path)
@@ -1,5 +1,5 @@
1
1
  # lib/apidepth/version.rb
2
2
 
3
3
  module Apidepth
4
- VERSION = "0.2.3".freeze
4
+ VERSION = "0.3.0".freeze
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apidepth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Apidepth
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-05-27 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: json
@@ -93,6 +94,20 @@ dependencies:
93
94
  - - "~>"
94
95
  - !ruby/object:Gem::Version
95
96
  version: '1.65'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.22'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.22'
96
111
  description: Know if your API slowness is your code or the vendor's. Apidepth instruments
97
112
  Net::HTTP to track real production latency to Stripe, OpenAI, Twilio and others
98
113
  — then benchmarks your p95 against anonymized fleet data so you can see if it's
@@ -120,10 +135,11 @@ licenses:
120
135
  - MIT
121
136
  metadata:
122
137
  homepage_uri: https://apidepth.io
123
- source_code_uri: https://github.com/cmwright33/apidepth-ruby
124
- changelog_uri: https://github.com/cmwright33/apidepth-ruby/blob/main/CHANGELOG.md
125
- bug_tracker_uri: https://github.com/cmwright33/apidepth-ruby/issues
138
+ source_code_uri: https://github.com/apidepth-io/apidepth-ruby
139
+ changelog_uri: https://github.com/apidepth-io/apidepth-ruby/blob/main/CHANGELOG.md
140
+ bug_tracker_uri: https://github.com/apidepth-io/apidepth-ruby/issues
126
141
  rubygems_mfa_required: 'true'
142
+ post_install_message:
127
143
  rdoc_options: []
128
144
  require_paths:
129
145
  - lib
@@ -138,7 +154,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
154
  - !ruby/object:Gem::Version
139
155
  version: '0'
140
156
  requirements: []
141
- rubygems_version: 4.0.11
157
+ rubygems_version: 3.5.22
158
+ signing_key:
142
159
  specification_version: 4
143
160
  summary: Know if your API slowness is your code or the vendor's
144
161
  test_files: []