apidepth 0.3.0 → 0.4.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: ee27b15f831798b976f293eda67c90c9cccab735e75cb82a225d3ea511331622
4
- data.tar.gz: 2ed0a290e8acab029c51eb12a116d295f8dfaa1390eef934cb61e5f9344b63ce
3
+ metadata.gz: 48290c633c238b8bbfcaff2a1f98cb4ffa26a3d0c02853ef0f345841425b463e
4
+ data.tar.gz: b3564982b79da91fe2ad976c913aa8bf94efbd35928f78a52d934e43b02eb4d0
5
5
  SHA512:
6
- metadata.gz: b32de3265dc5a4add529dd10741d21aa935ba18fccf669614fb74b9b06ed5d8125200d0ec26be7cf0c9349b8f6cc9a56d173ed4b2ba89acd551b5684bd411aa8
7
- data.tar.gz: bf7106cbfdf0a003824b625680a047978f1f8a27c862ce8df52551f316405e47dbd4444e3e64361f35cedf6317f74575a9998ed766afdbdd6459d56a5a719404
6
+ metadata.gz: 7a932948137e2d17f75d346686e60f0287c06f2702ec03a7adf3458c521604d9691e7803921726d391f1e161c59f77bce15063a5eeda93800093d7bb7d75e3ac
7
+ data.tar.gz: d262bd3e9b0668a1f5c667160efba8a41b99b850fcf177d3ccf3a5398b7179a005d9b141fb1ef62e1e3a86d55533fce7cf7eaf96c71f01a059ba96d98888ff9e
data/README.md CHANGED
@@ -40,7 +40,7 @@ bundle install
40
40
 
41
41
  ---
42
42
 
43
- ## Quick start
43
+ ## Getting started
44
44
 
45
45
  Create `config/initializers/apidepth.rb`:
46
46
 
@@ -100,7 +100,8 @@ Apidepth.configure do |config|
100
100
  # Path for the local vendor registry cache. Must be an absolute path.
101
101
  # The registry is fetched from Apidepth's servers and cached here so
102
102
  # cold starts don't block on a network fetch.
103
- # Default: "/tmp/apidepth_registry.json"
103
+ # Rails default: Rails.root.join("tmp/apidepth_registry.json") (set by the Railtie)
104
+ # Non-Rails default: "/tmp/apidepth_registry.json"
104
105
  config.registry_cache_path = "/tmp/apidepth_registry.json"
105
106
 
106
107
  # Custom vendors your app calls that aren't in the global registry.
@@ -133,6 +134,9 @@ Every event contains:
133
134
  | `cold_start` | `true` if this request paid for SSL handshake; excluded from p95 calculations |
134
135
  | `env` | Environment tag from `config.environment` or `Rails.env` |
135
136
  | `ts` | Unix timestamp in milliseconds |
137
+ | `rl_remaining` | Remaining quota, e.g. `4999` — present when vendor rate limit headers are found |
138
+ | `rl_limit` | Total quota, e.g. `5000` — present when vendor rate limit headers are found |
139
+ | `rl_reset_at` | Quota reset time in epoch milliseconds — present when vendor rate limit headers are found |
136
140
 
137
141
  ### What is never captured
138
142
 
data/bin/apidepth ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # bin/apidepth — CLI entry point for the Apidepth Ruby SDK.
3
+ #
4
+ # Subcommands:
5
+ # setup Configure the SDK for your project (writes initializer)
6
+ # test Send a synthetic test event and confirm the pipeline works
7
+
8
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
9
+
10
+ subcommand = ARGV.shift
11
+
12
+ case subcommand
13
+ when "setup"
14
+ require "apidepth/cli/setup"
15
+ Apidepth::CLI::Setup.run(ARGV)
16
+ when "test"
17
+ require "apidepth/cli/test_cmd"
18
+ Apidepth::CLI::TestCmd.run(ARGV)
19
+ when nil, "--help", "-h"
20
+ puts "Usage: bundle exec apidepth <subcommand> [options]"
21
+ puts ""
22
+ puts "Subcommands:"
23
+ puts " setup Configure the SDK and write your initializer"
24
+ puts " test Send a test event to confirm the pipeline works"
25
+ puts ""
26
+ puts "Run `bundle exec apidepth <subcommand> --help` for subcommand options."
27
+ else
28
+ warn "Unknown subcommand: #{subcommand.inspect}"
29
+ warn "Run `bundle exec apidepth --help` for usage."
30
+ exit 1
31
+ end
@@ -0,0 +1,82 @@
1
+ # lib/apidepth/cli/framework_detector.rb
2
+ #
3
+ # Detects the web framework in the current directory by inspecting well-known
4
+ # files. Returns a framework identifier and the recommended initializer path.
5
+ # Used by `apidepth setup` to produce copy-paste-ready output.
6
+
7
+ module Apidepth
8
+ module CLI
9
+ module FrameworkDetector
10
+ FRAMEWORKS = %i[rails sinatra].freeze
11
+
12
+ DetectedFramework = Struct.new(:name, :initializer_path, :initializer_snippet, keyword_init: true)
13
+
14
+ def self.detect(dir: Dir.pwd, api_key: nil, ignored_hosts: [], collector_url: nil)
15
+ framework = _detect_framework(dir)
16
+ _build_result(framework, api_key: api_key, ignored_hosts: ignored_hosts, collector_url: collector_url)
17
+ end
18
+
19
+ def self._detect_framework(dir)
20
+ return :rails if File.exist?(File.join(dir, "config/application.rb"))
21
+ return :sinatra if File.exist?(File.join(dir, "config.ru"))
22
+
23
+ :generic
24
+ end
25
+
26
+ def self._build_result(framework, api_key:, ignored_hosts:, collector_url:)
27
+ key_val = api_key || "YOUR_API_KEY"
28
+ url_val = collector_url || "https://collector.apidepth.io"
29
+ hosts_val = ignored_hosts.empty? ? "[]" : ignored_hosts.map { |h| %("#{h}") }.join(", ").then { |s| "[#{s}]" }
30
+
31
+ case framework
32
+ when :rails
33
+ snippet = <<~RUBY
34
+ # config/initializers/apidepth.rb
35
+ Apidepth.configure do |config|
36
+ config.api_key = #{key_val.inspect}
37
+ config.collector_url = #{url_val.inspect}
38
+ config.ignored_hosts = #{hosts_val}
39
+ end
40
+ RUBY
41
+ DetectedFramework.new(
42
+ name: :rails,
43
+ initializer_path: "config/initializers/apidepth.rb",
44
+ initializer_snippet: snippet
45
+ )
46
+ when :sinatra
47
+ snippet = <<~RUBY
48
+ # Top of your main app file (e.g. app.rb), before any routes
49
+ require "apidepth"
50
+ Apidepth.configure do |config|
51
+ config.api_key = #{key_val.inspect}
52
+ config.collector_url = #{url_val.inspect}
53
+ config.ignored_hosts = #{hosts_val}
54
+ end
55
+ Apidepth.instrument!
56
+ RUBY
57
+ DetectedFramework.new(
58
+ name: :sinatra,
59
+ initializer_path: nil,
60
+ initializer_snippet: snippet
61
+ )
62
+ else
63
+ snippet = <<~RUBY
64
+ # Add to your application startup file
65
+ require "apidepth"
66
+ Apidepth.configure do |config|
67
+ config.api_key = #{key_val.inspect}
68
+ config.collector_url = #{url_val.inspect}
69
+ config.ignored_hosts = #{hosts_val}
70
+ end
71
+ Apidepth.instrument!
72
+ RUBY
73
+ DetectedFramework.new(
74
+ name: :generic,
75
+ initializer_path: nil,
76
+ initializer_snippet: snippet
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,136 @@
1
+ # lib/apidepth/cli/setup.rb
2
+ #
3
+ # Implements `bundle exec apidepth setup`.
4
+ #
5
+ # Interactive mode (default):
6
+ # Opens the Apidepth dashboard in a browser so the developer can copy their
7
+ # API key, then prompts for ignored host patterns and writes the initializer.
8
+ #
9
+ # Non-interactive mode (CI/CD and AI-assisted setup):
10
+ # bundle exec apidepth setup --api-key $APIDEPTH_API_KEY --no-prompt
11
+ # All prompts are bypassed; output goes to stdout only.
12
+
13
+ require "optparse"
14
+ require "apidepth/cli/framework_detector"
15
+
16
+ module Apidepth
17
+ module CLI
18
+ module Setup
19
+ DASHBOARD_KEYS_URL = "https://apidepth.io/dashboard/api-keys".freeze
20
+
21
+ def self.run(argv = ARGV)
22
+ options = parse_options(argv)
23
+ api_key = options[:api_key]
24
+ collector_url = options[:collector_url]
25
+ ignored_hosts = options[:ignored_hosts] || []
26
+ no_prompt = options[:no_prompt]
27
+ framework_override = options[:framework]
28
+
29
+ # Interactive: open dashboard and prompt for key
30
+ unless api_key || no_prompt
31
+ $stdout.puts "\nApidepth SDK Setup"
32
+ $stdout.puts "─" * 40
33
+ $stdout.puts "\nOpening your API keys page..."
34
+ _open_browser(DASHBOARD_KEYS_URL)
35
+ $stdout.print "\nPaste your API key: "
36
+ api_key = $stdin.gets&.strip
37
+ if api_key.nil? || api_key.empty?
38
+ warn "No API key provided. Aborting."
39
+ exit 1
40
+ end
41
+ end
42
+
43
+ # Interactive: prompt for ignored hosts
44
+ unless no_prompt
45
+ $stdout.puts "\nDefault ignored hosts (always skipped):"
46
+ %w[localhost 127.0.0.1 0.0.0.0 ::1].each { |h| $stdout.puts " • #{h}" }
47
+ $stdout.puts " • #{collector_url || 'collector.apidepth.io'}"
48
+ $stdout.puts "\nAny internal API patterns to ignore? (comma-separated, wildcards ok)"
49
+ $stdout.puts " Examples: *.internal, *.local, *.svc.cluster.local, *.railway.internal"
50
+ $stdout.print "> "
51
+ input = $stdin.gets&.strip || ""
52
+ ignored_hosts += input.split(",").map(&:strip).reject(&:empty?) unless input.empty?
53
+ end
54
+
55
+ result = FrameworkDetector.detect(
56
+ dir: Dir.pwd,
57
+ api_key: api_key,
58
+ ignored_hosts: ignored_hosts,
59
+ collector_url: collector_url
60
+ )
61
+ # Override detected framework if flag provided
62
+ if framework_override
63
+ result = FrameworkDetector.detect(
64
+ dir: Dir.pwd,
65
+ api_key: api_key,
66
+ ignored_hosts: ignored_hosts,
67
+ collector_url: collector_url
68
+ )
69
+ end
70
+
71
+ _print_result(result, no_prompt: no_prompt)
72
+ end
73
+
74
+ def self.parse_options(argv)
75
+ options = {}
76
+ parser = OptionParser.new do |opts|
77
+ opts.banner = "Usage: bundle exec apidepth setup [options]"
78
+ opts.on("--api-key KEY", "API key (skips browser OAuth)") { |v| options[:api_key] = v }
79
+ opts.on("--collector-url URL", "Override collector URL") { |v| options[:collector_url] = v }
80
+ opts.on("--ignored-hosts HOSTS", "Comma-separated ignored host patterns") do |v|
81
+ options[:ignored_hosts] = v.split(",").map(&:strip)
82
+ end
83
+ opts.on("--no-prompt", "Non-interactive mode; output to stdout only") { options[:no_prompt] = true }
84
+ opts.on("--framework NAME", "Override framework detection (rails|sinatra|generic)") do |v|
85
+ options[:framework] = v
86
+ end
87
+ opts.on_tail("-h", "--help", "Show this message") do
88
+ puts opts
89
+ exit
90
+ end
91
+ end
92
+ begin
93
+ parser.parse!(argv.dup)
94
+ rescue OptionParser::InvalidOption => e
95
+ warn e.message
96
+ exit 1
97
+ end
98
+ options
99
+ end
100
+
101
+ def self._open_browser(url)
102
+ if RUBY_PLATFORM.include?("darwin")
103
+ system("open", url)
104
+ elsif RUBY_PLATFORM.include?("linux")
105
+ system("xdg-open", url)
106
+ else
107
+ $stdout.puts "Visit: #{url}"
108
+ end
109
+ end
110
+
111
+ def self._print_result(result, no_prompt:)
112
+ framework_label = result.name.to_s.capitalize
113
+ $stdout.puts "\nDetected: #{framework_label}" unless no_prompt
114
+
115
+ if result.initializer_path && !no_prompt
116
+ $stdout.puts "\nAdd the following to #{result.initializer_path}:"
117
+ $stdout.puts
118
+ $stdout.puts result.initializer_snippet
119
+ $stdout.print "Write to #{result.initializer_path}? [y/N] "
120
+ answer = $stdin.gets&.strip&.downcase
121
+ if answer == "y"
122
+ full_path = File.join(Dir.pwd, result.initializer_path)
123
+ FileUtils.mkdir_p(File.dirname(full_path))
124
+ File.write(full_path, result.initializer_snippet)
125
+ $stdout.puts "Written to #{result.initializer_path}"
126
+ else
127
+ $stdout.puts "(Not written — copy the snippet above into your codebase)"
128
+ end
129
+ else
130
+ $stdout.puts result.initializer_snippet
131
+ end
132
+ $stdout.puts "\nRun `bundle exec apidepth test` to confirm events are reaching the collector." unless no_prompt
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,136 @@
1
+ # lib/apidepth/cli/test_cmd.rb
2
+ #
3
+ # Implements `bundle exec apidepth test`.
4
+ #
5
+ # Fires a synthetic event to the collector with `"test": true` in the payload.
6
+ # The collector writes this to `test_events`, not the main events table.
7
+ # Hard 5-second timeout. Per-failure-mode error messages with concrete next steps.
8
+
9
+ require "net/http"
10
+ require "uri"
11
+ require "json"
12
+ require "apidepth/version"
13
+
14
+ module Apidepth
15
+ module CLI
16
+ module TestCmd
17
+ DEFAULT_COLLECTOR_URL = "https://collector.apidepth.io".freeze
18
+ TIMEOUT_SECONDS = 5
19
+
20
+ def self.run(argv = ARGV)
21
+ api_key, collector_url = _load_config(argv)
22
+
23
+ unless api_key
24
+ warn "No API key configured."
25
+ warn "Run `bundle exec apidepth setup` or set APIDEPTH_API_KEY."
26
+ exit 1
27
+ end
28
+
29
+ base_url = (collector_url || DEFAULT_COLLECTOR_URL).chomp("/")
30
+ $stdout.print "Sending test event to collector... "
31
+
32
+ begin
33
+ elapsed = _send_test_event(api_key, base_url)
34
+ $stdout.puts "✓ received in #{elapsed}ms"
35
+ $stdout.puts "Visit your dashboard: https://apidepth.io/dashboard"
36
+ rescue TestError => e
37
+ $stdout.puts "✗"
38
+ warn "\n#{e.message}"
39
+ warn e.hint if e.hint
40
+ exit 1
41
+ end
42
+ end
43
+
44
+ class TestError < StandardError
45
+ attr_reader :hint
46
+
47
+ def initialize(msg, hint: nil)
48
+ super(msg)
49
+ @hint = hint
50
+ end
51
+ end
52
+
53
+ def self._load_config(_argv)
54
+ # Try the SDK configuration first, fall back to environment variable
55
+ begin
56
+ require "apidepth"
57
+ cfg = Apidepth.configuration
58
+ api_key = cfg.api_key || ENV.fetch("APIDEPTH_API_KEY", nil)
59
+ collector_url = cfg.collector_url || ENV.fetch("APIDEPTH_COLLECTOR_URL", nil)
60
+ rescue StandardError
61
+ api_key = ENV.fetch("APIDEPTH_API_KEY", nil)
62
+ collector_url = ENV.fetch("APIDEPTH_COLLECTOR_URL", nil)
63
+ end
64
+ [api_key, collector_url]
65
+ end
66
+
67
+ def self._send_test_event(api_key, base_url)
68
+ uri = URI.parse("#{base_url}/v1/events")
69
+ payload = {
70
+ batch: [
71
+ {
72
+ vendor: "apidepth-test",
73
+ endpoint: "/test",
74
+ method: "GET",
75
+ status: 200,
76
+ outcome: "success",
77
+ duration_ms: 1,
78
+ cold_start: false,
79
+ env: "test",
80
+ ts: (Time.now.to_f * 1000).to_i,
81
+ test: true
82
+ }
83
+ ],
84
+ sdk: { name: "apidepth-ruby", version: Apidepth::VERSION }
85
+ }.to_json
86
+
87
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
88
+
89
+ http = Net::HTTP.new(uri.host, uri.port)
90
+ http.use_ssl = uri.scheme == "https"
91
+ http.read_timeout = TIMEOUT_SECONDS
92
+ http.open_timeout = TIMEOUT_SECONDS
93
+
94
+ request = Net::HTTP::Post.new(uri.path.empty? ? "/" : uri.path)
95
+ request["Content-Type"] = "application/json"
96
+ request["Authorization"] = "Bearer #{api_key}"
97
+ request.body = payload
98
+
99
+ # Bypass our own instrumentation
100
+ Thread.current[:apidepth_skip] = true
101
+ response = http.request(request)
102
+ Thread.current[:apidepth_skip] = false
103
+
104
+ case response.code.to_i
105
+ when 200, 201, 204
106
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
107
+ when 401, 403
108
+ raise TestError.new(
109
+ "API key not recognised (HTTP #{response.code}).",
110
+ hint: "Check the key in your initializer matches your dashboard at https://apidepth.io/dashboard/api-keys"
111
+ )
112
+ else
113
+ raise TestError.new(
114
+ "Collector returned HTTP #{response.code}.",
115
+ hint: "Check https://status.apidepth.io for service status."
116
+ )
117
+ end
118
+ rescue Net::OpenTimeout, Net::ReadTimeout
119
+ raise TestError.new(
120
+ "No response after #{TIMEOUT_SECONDS} seconds.",
121
+ hint: "Check for a firewall blocking outbound port 443."
122
+ )
123
+ rescue OpenSSL::SSL::SSLError => e
124
+ raise TestError.new(
125
+ "SSL certificate verification failed: #{e.message}",
126
+ hint: "Check your Ruby SSL configuration."
127
+ )
128
+ rescue Errno::ECONNREFUSED, SocketError => e
129
+ raise TestError.new(
130
+ "Could not reach #{uri.host}: #{e.message}",
131
+ hint: "Check outbound HTTPS (port 443) is allowed from this environment."
132
+ )
133
+ end
134
+ end
135
+ end
136
+ end
@@ -291,6 +291,10 @@ module Apidepth
291
291
  if int.between?(0, 0xFFFFFFFF)
292
292
  host = [int >> 24, (int >> 16) & 0xFF, (int >> 8) & 0xFF,
293
293
  int & 0xFF].join(".")
294
+ else
295
+ raise ArgumentError,
296
+ "Apidepth collector_url must not target private, loopback, or link-local " \
297
+ "addresses (got #{url.host.inspect})."
294
298
  end
295
299
  end
296
300
 
@@ -1,30 +1,76 @@
1
1
  # lib/apidepth/configuration.rb
2
2
 
3
+ require "set"
4
+ require "uri"
5
+
3
6
  module Apidepth
4
7
  class Configuration
8
+ # Always ignored regardless of user config. Covers unambiguous loopback
9
+ # addresses — we deliberately avoid wildcard patterns like *.internal here
10
+ # because silently swallowing traffic the developer wants to see is worse
11
+ # than showing mystery vendors. The setup subcommand prompts for custom
12
+ # patterns interactively.
13
+ HARD_IGNORED_HOSTS = %w[localhost 127.0.0.1 0.0.0.0 ::1].freeze
14
+
5
15
  attr_accessor :api_key,
6
- :collector_url,
7
16
  :enabled,
8
17
  :flush_interval,
9
18
  :registry_refresh_interval,
10
19
  :registry_cache_path,
11
- :ignored_hosts,
12
20
  :on_flush_error,
13
21
  :environment, # e.g. "production" — set by Railtie from Rails.env
14
22
  :sample_rate, # Float 0.0–1.0, default 1.0 (100% of events captured)
15
23
  :extra_vendors # Hash of vendor_name => host, e.g. { "my-api" => "api.myservice.com" }
16
24
 
25
+ attr_reader :ignored_hosts, :collector_url
26
+
17
27
  def initialize
18
28
  @enabled = true
19
29
  @flush_interval = 20
20
30
  @registry_refresh_interval = 6 * 60 * 60
21
31
  @registry_cache_path = "/tmp/apidepth_registry.json"
22
32
  @collector_url = nil
23
- @ignored_hosts = []
33
+ @_user_hosts = []
24
34
  @on_flush_error = nil
25
35
  @environment = nil # Railtie sets this to Rails.env at boot
26
36
  @sample_rate = 1.0 # capture everything by default
27
37
  @extra_vendors = {} # customer-defined host mappings
38
+ _rebuild_ignored_hosts
39
+ end
40
+
41
+ def collector_url=(url)
42
+ @collector_url = url
43
+ _rebuild_ignored_hosts
44
+ end
45
+
46
+ def ignored_hosts=(hosts)
47
+ @_user_hosts = Array(hosts || [])
48
+ _rebuild_ignored_hosts
49
+ end
50
+
51
+ # Returns true if +host+ should be skipped. Supports glob wildcards
52
+ # (* matches any sequence, ? matches one character) so customers can
53
+ # ignore entire internal domains: "*.internal", "*.svc.cluster.local".
54
+ def ignored_host?(host)
55
+ @_exact_ignored.include?(host) ||
56
+ @_glob_ignored.any? { |pat| File.fnmatch(pat, host) }
57
+ end
58
+
59
+ private
60
+
61
+ def _rebuild_ignored_hosts
62
+ all = HARD_IGNORED_HOSTS.dup + (@_user_hosts || [])
63
+ if @collector_url
64
+ begin
65
+ h = URI.parse(@collector_url).host
66
+ all << h if h
67
+ rescue URI::InvalidURIError
68
+ nil
69
+ end
70
+ end
71
+ @_exact_ignored = all.reject { |p| p.include?("*") || p.include?("?") }.to_set
72
+ @_glob_ignored = all.select { |p| p.include?("*") || p.include?("?") }
73
+ @ignored_hosts = Set.new(all)
28
74
  end
29
75
  end
30
76
  end
@@ -10,7 +10,7 @@ module Apidepth
10
10
  # 4. Sample rate: probabilistically skip events
11
11
  return super if Thread.current[:apidepth_skip]
12
12
  return super unless Apidepth.configuration.enabled
13
- return super if Apidepth.configuration.ignored_hosts.include?(address)
13
+ return super if Apidepth.configuration.ignored_host?(address)
14
14
  return super unless sampled?
15
15
 
16
16
  # Snapshot connection state BEFORE calling super.
@@ -27,6 +27,14 @@ module Apidepth
27
27
  # every outbound HTTP request.
28
28
  Apidepth.configuration.environment ||= Rails.env.to_s
29
29
 
30
+ # Use a per-app cache path so concurrent apps on the same host don't
31
+ # share a world-readable /tmp file. Falls back to /tmp only if Rails.root
32
+ # is somehow unavailable.
33
+ if defined?(Rails.root) && Rails.root
34
+ Apidepth.configuration.registry_cache_path ||=
35
+ Rails.root.join("tmp/apidepth_registry.json").to_s
36
+ end
37
+
30
38
  Net::HTTP.prepend(Apidepth::NetHTTPInstrumentation)
31
39
  Apidepth::VendorRegistry.load_extra_vendors(Apidepth.configuration.extra_vendors)
32
40
  Apidepth::RegistryLoader.load_and_start
@@ -41,16 +49,9 @@ module Apidepth
41
49
  end
42
50
 
43
51
  # -------------------------------------------------------------------------
44
- # 3. Flush queue on graceful shutdown.
45
- # at_exit fires on SIGTERM → graceful Puma/Unicorn shutdown.
46
- # flush! rescues internally so a network error at shutdown is not fatal.
47
- # -------------------------------------------------------------------------
48
- config.after_initialize do
49
- at_exit { Apidepth::Collector.instance.flush! }
50
- end
51
-
52
- # -------------------------------------------------------------------------
53
- # 4. Fork safety for Puma cluster mode / Spring.
52
+ # 3. Shutdown flush + fork safety.
53
+ #
54
+ # at_exit: flush the queue on graceful shutdown (SIGTERM Puma/Unicorn).
54
55
  #
55
56
  # after_fork: reset the Collector singleton so each worker gets a fresh
56
57
  # instance with its own flush thread. The master's flush thread is not
@@ -65,6 +66,8 @@ module Apidepth
65
66
  # ActiveSupport::ForkTracker is available in Rails 7.1+.
66
67
  # -------------------------------------------------------------------------
67
68
  config.after_initialize do
69
+ at_exit { Apidepth::Collector.instance.flush! }
70
+
68
71
  if defined?(ActiveSupport::ForkTracker)
69
72
  ActiveSupport::ForkTracker.after_fork { Apidepth::Collector.reset! }
70
73
  elsif defined?(Puma)
@@ -103,6 +103,8 @@ module Apidepth
103
103
  # Pure numeric
104
104
  if str.match?(/\A\d+(?:\.\d+)?\z/)
105
105
  n = str.to_f
106
+ # >= 1_000_000_000 is a Unix timestamp (seconds since epoch, e.g. "1716000000");
107
+ # smaller values are seconds-from-now (Retry-After style, e.g. "30").
106
108
  return n >= 1_000_000_000 ? (n * 1_000).to_i : now_ms + (n * 1_000).to_i
107
109
  end
108
110
 
@@ -1,5 +1,5 @@
1
1
  # lib/apidepth/version.rb
2
2
 
3
3
  module Apidepth
4
- VERSION = "0.3.0".freeze
4
+ VERSION = "0.4.0".freeze
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apidepth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Apidepth
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-27 00:00:00.000000000 Z
11
+ date: 2026-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -114,13 +114,18 @@ description: Know if your API slowness is your code or the vendor's. Apidepth in
114
114
  you, or everyone.
115
115
  email:
116
116
  - hello@apidepth.io
117
- executables: []
117
+ executables:
118
+ - apidepth
118
119
  extensions: []
119
120
  extra_rdoc_files: []
120
121
  files:
121
122
  - LICENSE
122
123
  - README.md
124
+ - bin/apidepth
123
125
  - lib/apidepth.rb
126
+ - lib/apidepth/cli/framework_detector.rb
127
+ - lib/apidepth/cli/setup.rb
128
+ - lib/apidepth/cli/test_cmd.rb
124
129
  - lib/apidepth/collector.rb
125
130
  - lib/apidepth/configuration.rb
126
131
  - lib/apidepth/event.rb
@@ -147,7 +152,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
152
  requirements:
148
153
  - - ">="
149
154
  - !ruby/object:Gem::Version
150
- version: 2.7.0
155
+ version: 3.1.0
151
156
  required_rubygems_version: !ruby/object:Gem::Requirement
152
157
  requirements:
153
158
  - - ">="