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 +4 -4
- data/README.md +4 -0
- data/lib/apidepth/collector.rb +16 -8
- data/lib/apidepth/net_http_instrumentation.rb +4 -2
- data/lib/apidepth/rate_limit_headers.rb +1 -0
- data/lib/apidepth/registry_loader.rb +33 -4
- data/lib/apidepth/vendor_registry.rb +21 -0
- data/lib/apidepth/version.rb +1 -1
- metadata +23 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee27b15f831798b976f293eda67c90c9cccab735e75cb82a225d3ea511331622
|
|
4
|
+
data.tar.gz: 2ed0a290e8acab029c51eb12a116d295f8dfaa1390eef934cb61e5f9344b63ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b32de3265dc5a4add529dd10741d21aa935ba18fccf669614fb74b9b06ed5d8125200d0ec26be7cf0c9349b8f6cc9a56d173ed4b2ba89acd551b5684bd411aa8
|
|
7
|
+
data.tar.gz: bf7106cbfdf0a003824b625680a047978f1f8a27c862ce8df52551f316405e47dbd4444e3e64361f35cedf6317f74575a9998ed766afdbdd6459d56a5a719404
|
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# apidepth
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/apidepth)
|
|
4
|
+
[](https://rubygems.org/gems/apidepth)
|
|
5
|
+
[](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.
|
data/lib/apidepth/collector.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
17
|
-
|
|
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)
|
data/lib/apidepth/version.rb
CHANGED
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.
|
|
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:
|
|
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/
|
|
124
|
-
changelog_uri: https://github.com/
|
|
125
|
-
bug_tracker_uri: https://github.com/
|
|
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:
|
|
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: []
|