shieldpress-tracker 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ba3c8cf44d7637a43997a65762cebe985040105944be0f767009d6e6587769a
4
+ data.tar.gz: 955a4f2848f902e752e042de0d5ea9b7653083fb3bdc0958232aed4ef101659c
5
+ SHA512:
6
+ metadata.gz: 61cc4a6aa6a6584cf9b80d7197593a11a46bf19172c79bb516cd3565e0ee5fddfda80dbefb4b3fecbc19da3d6d0e9f019f0e6288973a5954f68d6c8ccc8a6da4
7
+ data.tar.gz: '0148c792dbdd13c93964fbab0b257908eb383b79c62fa00d28233b0603f19640145b0611a0ffccc66e33d2b15f9e34b5baebf4d1a89d9582dd2c7a03d83d1dbc'
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 - 2026-07-19
4
+
5
+ - Initial RubyGems release.
6
+ - Added system, runtime, HTTP, error, security, environment and dependency collectors.
7
+ - Added periodic reporting, heartbeat, request filtering and Rack middleware.
8
+ - Added automatic Rails middleware registration after initialization.
9
+ - Added recursive credential redaction before report serialization.
data/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShieldPress
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies, subject to the conditions of the MIT License.
6
+
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ <p align="center">
2
+ <a href="https://shieldpress.net/">
3
+ <img src="https://shieldpress.net/logo.png" alt="ShieldPress" width="500">
4
+ </a>
5
+ </p>
6
+
7
+ # shieldpress-tracker for Ruby
8
+
9
+ Monitoring, performance and application-security telemetry for Ruby 2.7+, Rack and Rails. It uses only Ruby standard-library dependencies.
10
+
11
+ Dashboard: [https://shieldpress.net/](https://shieldpress.net/)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ gem install shieldpress-tracker
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```ruby
22
+ require 'shieldpress_tracker'
23
+
24
+ tracker = ShieldPress.init(
25
+ api_key: 'YOUR_SHIELDPRESS_API_KEY',
26
+ site_id: 'auto',
27
+ app_name: 'my-ruby-app'
28
+ )
29
+ ```
30
+
31
+ Configuration can also use `SHIELDPRESS_API_KEY`, `SHIELDPRESS_SITE_ID`, `SHIELDPRESS_API_URL`, `SHIELDPRESS_APP_NAME` and `SHIELDPRESS_APP_VERSION` environment variables.
32
+
33
+ ## Rack
34
+
35
+ ```ruby
36
+ use ShieldPressTracker::Middleware, ShieldPress.instance
37
+ run MyApp
38
+ ```
39
+
40
+ Rails automatically adds the Rack middleware when `ShieldPress.init` runs from an initializer.
41
+
42
+ ## Manual instrumentation
43
+
44
+ ```ruby
45
+ tracker.record_request(path: '/orders', method: 'POST', status_code: 201, duration_ms: 42.5)
46
+ tracker.capture_error(StandardError.new('Invalid order'), order_id: 123)
47
+ tracker.analyze_request(url: '/login', method: 'POST', headers: headers, body: body, ip: '203.0.113.2')
48
+ tracker.record_auth_failure(ip: '203.0.113.2', url: '/login', reason: 'bad password')
49
+ tracker.record_rate_limit(ip: '203.0.113.2', url: '/api')
50
+ tracker.record_cors_violation(ip: '203.0.113.2', origin: 'https://untrusted.example', url: '/api')
51
+ tracker.flush
52
+ tracker.stop
53
+ ```
54
+
55
+ Options: `api_url`, `report_interval`, `system_metrics`, `http_tracking`, `error_tracking`, `security_tracking`, `dep_audit`, `dep_audit_interval`, `runtime_metrics`, `env_security`, `heartbeat`, `app_version`, `environment`, `tags`, `ignore_paths`, `max_error_buffer`, `max_security_buffer`, and `debug`.
56
+
57
+ The SDK never raises a network/reporting failure into the host application. Reports are sent to `/api/tracker/report` with Bearer authentication.
@@ -0,0 +1,120 @@
1
+ require "etc"
2
+ require "json"
3
+ require "rbconfig"
4
+ require "socket"
5
+ require "time"
6
+
7
+ module ShieldPressTracker
8
+ module Helpers
9
+ def self.percentile(values, percentile)
10
+ return 0 if values.empty?
11
+ values.sort[[(percentile / 100.0 * values.length).ceil - 1, 0].max]
12
+ end
13
+ end
14
+
15
+ class HttpCollector
16
+ def initialize
17
+ @records, @started, @mutex = [], Process.clock_gettime(Process::CLOCK_MONOTONIC), Mutex.new
18
+ end
19
+ def record(path:, method:, status_code:, duration_ms:)
20
+ @mutex.synchronize { @records.shift(1000) if @records.length >= 10_000; @records << [path, method, status_code.to_i, duration_ms.to_f] }
21
+ rescue StandardError
22
+ nil
23
+ end
24
+ def flush
25
+ records = @mutex.synchronize { rows = @records; @records = []; rows }
26
+ return nil if records.empty?
27
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC); elapsed = [now - @started, 0.001].max; @started = now
28
+ durations, statuses, grouped = records.map(&:last), Hash.new(0), Hash.new { |h, k| h[k] = [] }
29
+ records.each { |path, method, status, ms| statuses[status.to_s] += 1; grouped[[method, path]] << [status, ms] }
30
+ top = grouped.map do |(method, path), rows|
31
+ times = rows.map(&:last)
32
+ { path: path, method: method, count: rows.length, avgMs: (times.sum / times.length).round,
33
+ p95Ms: Helpers.percentile(times, 95), errorCount: rows.count { |r| r[0] >= 400 } }
34
+ end.sort_by { |x| -x[:count] }.first(20)
35
+ { totalRequests: records.length, totalErrors: records.count { |r| r[2] >= 500 }, avgResponseMs: (durations.sum / durations.length).round,
36
+ p50ResponseMs: Helpers.percentile(durations, 50), p95ResponseMs: Helpers.percentile(durations, 95),
37
+ p99ResponseMs: Helpers.percentile(durations, 99), maxResponseMs: durations.max, statusCodes: statuses,
38
+ topPaths: top, requestsPerSecond: (records.length / elapsed).round(2) }
39
+ rescue StandardError
40
+ nil
41
+ end
42
+ end
43
+
44
+ class ErrorCollector
45
+ def initialize(maximum); @maximum, @events, @mutex = maximum, [], Mutex.new end
46
+ def capture(error, meta = {})
47
+ event = { message: error.to_s[0, 4096], type: error.class.name, timestamp: Time.now.utc.iso8601 }
48
+ event[:stack] = Array(error.backtrace).join("\n")[0, 4096] if error.respond_to?(:backtrace)
49
+ meta = meta.dup
50
+ { url: :url, method: :method, status_code: :statusCode }.each { |from, to| event[to] = meta.delete(from) if meta.key?(from) }
51
+ event[:meta] = meta unless meta.empty?
52
+ @mutex.synchronize { @events << event; @events = @events.last(@maximum) }
53
+ rescue StandardError
54
+ nil
55
+ end
56
+ def flush; @mutex.synchronize { rows = @events; @events = []; rows } end
57
+ end
58
+
59
+ class SecurityCollector
60
+ RULES = [
61
+ [:sql_injection, :critical, /\b(union|select|drop|delete)\b.*\b(from|table|where)\b|--|\/\*/i],
62
+ [:xss_attempt, :high, /<script|javascript:|onerror\s*=/i],
63
+ [:command_injection, :critical, /[;&|`]\s*(rm|curl|wget|bash|sh|python)\b|\$\(/i],
64
+ [:suspicious_path, :high, /\.\.\/|%2e%2e|\/\.env|\/\.git/i],
65
+ [:bot_detected, :medium, /sqlmap|nikto|nmap|burp|nuclei|wpscan/i]
66
+ ].freeze
67
+ def initialize(maximum); @maximum, @events, @ips, @mutex = maximum, [], {}, Mutex.new end
68
+ def record(type:, severity:, message:, **data)
69
+ event = { type: type, severity: severity, message: message.to_s[0, 4096], timestamp: Time.now.utc.iso8601 }.merge(data.compact)
70
+ @mutex.synchronize do
71
+ @events << event; @events = @events.last(@maximum)
72
+ if data[:ip]; row = (@ips[data[:ip]] ||= { count: 0, types: [] }); row[:count] += 1; row[:types] |= [type]; end
73
+ end
74
+ end
75
+ def analyze(url:, method: "GET", headers: {}, body: nil, ip: nil, query: nil)
76
+ headers = headers.transform_keys { |k| k.to_s.downcase }; text = "#{url} #{body} #{query.to_json}"; ua = headers["user-agent"].to_s
77
+ RULES.each do |type, severity, pattern|
78
+ target = type == :bot_detected ? ua : text
79
+ record(type: type, severity: severity, message: "#{type.to_s.tr('_', ' ')} detected in request", ip: ip, url: url,
80
+ method: method, userAgent: ua, payload: %i[sql_injection xss_attempt command_injection].include?(type) ? text[0, 500] : nil) if pattern.match?(target)
81
+ end
82
+ record(type: :oversized_payload, severity: :medium, message: "Request payload exceeds 10MB", ip: ip, url: url, method: method) if headers["content-length"].to_i > 10 * 1024 * 1024
83
+ rescue StandardError
84
+ nil
85
+ end
86
+ def flush
87
+ events = @mutex.synchronize { rows = @events; @events = []; rows }
88
+ return nil if events.empty? && @ips.empty?
89
+ severity = %i[critical high medium low info].to_h { |x| [x, 0] }; types = Hash.new(0)
90
+ events.each { |e| severity[e[:severity]] += 1; types[e[:type]] += 1 }
91
+ ips = @ips.map { |ip, row| { ip: ip, count: row[:count], types: row[:types] } }.sort_by { |x| -x[:count] }.first(10)
92
+ { totalEvents: events.length, bySeverity: severity, byType: types, topOffendingIps: ips, events: events.last(50) }
93
+ rescue StandardError
94
+ nil
95
+ end
96
+ end
97
+
98
+ module Metrics
99
+ def self.system
100
+ { cpuPercent: 0, cpuCount: Etc.nprocessors, memUsedMb: 0, memTotalMb: 0, memPercent: 0,
101
+ uptimeSeconds: Process.clock_gettime(Process::CLOCK_MONOTONIC).round, loadAvg: [0, 0, 0], platform: RUBY_PLATFORM,
102
+ rubyVersion: RUBY_VERSION, pid: Process.pid, osName: RbConfig::CONFIG["host_os"], osArch: RbConfig::CONFIG["host_cpu"],
103
+ hostname: Socket.gethostname, cwd: Dir.pwd, execPath: RbConfig.ruby }
104
+ end
105
+ def self.runtime
106
+ { threadCount: Thread.list.length, gcCount: GC.stat[:count], heapLiveSlots: GC.stat[:heap_live_slots], rubyEngine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby" }
107
+ end
108
+ def self.environment
109
+ names = ENV.keys.select { |k| k.match?(/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL/i) }.first(50)
110
+ { rubyVersion: RUBY_VERSION, rubyPlatform: RUBY_PLATFORM, debugModeEnabled: !!$DEBUG,
111
+ envVarWarnings: names.map { |k| "Potential secret present in environment variable: #{k}" }, securityHeaders: [], opensslVersion: defined?(OpenSSL) ? OpenSSL::OPENSSL_VERSION : nil }
112
+ end
113
+ def self.dependencies
114
+ specs = Gem.loaded_specs.values
115
+ { totalDeps: specs.length, totalVulnerabilities: 0, bySeverity: {}, vulnerabilities: [], outdatedCount: 0,
116
+ lastAuditAt: Time.now.utc.iso8601, packages: specs.first(200).map { |s| { name: s.name, version: s.version.to_s } } }
117
+ end
118
+ end
119
+ end
120
+
@@ -0,0 +1,30 @@
1
+ module ShieldPressTracker
2
+ class Config
3
+ DEFAULTS = {
4
+ api_url: "https://api.shieldpress.net", report_interval: 60, system_metrics: true,
5
+ http_tracking: true, error_tracking: true, security_tracking: true, dep_audit: true,
6
+ dep_audit_interval: 3600, runtime_metrics: true, env_security: true, heartbeat: true,
7
+ app_name: "ruby-app", app_version: "0.0.0", environment: "production", tags: {},
8
+ ignore_paths: ["/health", "/healthz", "/ready", "/favicon.ico", "/assets/*"],
9
+ max_error_buffer: 100, max_security_buffer: 500, debug: false
10
+ }.freeze
11
+
12
+ attr_reader(*DEFAULTS.keys, :api_key, :site_id)
13
+
14
+ def initialize(values = {})
15
+ values = values.transform_keys(&:to_sym)
16
+ @api_key = values[:api_key] || ENV["SHIELDPRESS_API_KEY"] || ENV["SHIELDPRESS_SECRET"]
17
+ @site_id = values[:site_id] || ENV["SHIELDPRESS_SITE_ID"] || ENV["SHIELDPRESS_APP_ID"]
18
+ raise ArgumentError, "[ShieldPress] api_key is required" if @api_key.to_s.empty?
19
+ raise ArgumentError, "[ShieldPress] site_id is required" if @site_id.to_s.empty?
20
+ DEFAULTS.each do |key, default|
21
+ env = { api_url: "SHIELDPRESS_API_URL", app_name: "SHIELDPRESS_APP_NAME", app_version: "SHIELDPRESS_APP_VERSION" }[key]
22
+ value = values.key?(key) ? values[key] : (env && ENV[env]) || default
23
+ instance_variable_set("@#{key}", value)
24
+ end
25
+ @api_url = @api_url.sub(%r{/$}, "")
26
+ @environment = values[:environment] || ENV["APP_ENV"] || ENV["RACK_ENV"] || "production"
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,20 @@
1
+ module ShieldPressTracker
2
+ class Middleware
3
+ def initialize(app, tracker = nil); @app, @tracker = app, tracker || ShieldPress.instance end
4
+ def call(env)
5
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC); path = env["PATH_INFO"] || "/"; method = env["REQUEST_METHOD"] || "GET"
6
+ headers = env.select { |k, _| k.start_with?("HTTP_") }.to_h { |k, v| [k.delete_prefix("HTTP_").tr("_", "-").downcase, v] }
7
+ @tracker.analyze_request(url: path, method: method, headers: headers, ip: env["REMOTE_ADDR"], query: env["QUERY_STRING"])
8
+ status, response_headers, body = @app.call(env)
9
+ [status, response_headers, body]
10
+ rescue StandardError => e
11
+ @tracker.capture_error(e, url: path, method: method); raise
12
+ ensure
13
+ if started
14
+ duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000
15
+ @tracker.record_request(path: path, method: method, status_code: status || 500, duration_ms: duration)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,10 @@
1
+ if defined?(Rails::Railtie)
2
+ module ShieldPressTracker
3
+ class Railtie < Rails::Railtie
4
+ initializer "shieldpress_tracker.middleware" do |app|
5
+ app.middleware.use ShieldPressTracker::Middleware if ShieldPress.configured?
6
+ end
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,42 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module ShieldPressTracker
6
+ class Reporter
7
+ SENSITIVE_KEY = /authorization|cookie|password|passwd|secret|token|api[_-]?key|credential/i
8
+ SENSITIVE_TEXT = /(bearer\s+)[A-Za-z0-9._~+\-\/=]+|([?&](?:password|passwd|secret|token|api[_-]?key)=)[^&\s]+/i
9
+
10
+ def redact(value, depth = 0)
11
+ return "[MAX_DEPTH]" if depth > 12
12
+ case value
13
+ when Hash
14
+ value.each_with_object({}) { |(key, item), out| out[key] = key.to_s.match?(SENSITIVE_KEY) ? "[REDACTED]" : redact(item, depth + 1) }
15
+ when Array
16
+ value.first(500).map { |item| redact(item, depth + 1) }
17
+ when String
18
+ value.gsub(SENSITIVE_TEXT) { "#{Regexp.last_match(1) || Regexp.last_match(2)}[REDACTED]" }[0, 16_384]
19
+ else
20
+ value
21
+ end
22
+ end
23
+
24
+ def initialize(api_url, api_key, debug = false); @uri, @api_key, @debug = URI(api_url + "/api/tracker/report"), api_key, debug end
25
+ def send(payload)
26
+ payload = redact(payload)
27
+ body = JSON.generate(payload)
28
+ if body.bytesize > 512 * 1024
29
+ payload[:_truncated] = true; payload[:errors] = Array(payload[:errors]).first(20)
30
+ payload[:security][:events] = Array(payload[:security][:events]).first(10) if payload[:security]
31
+ body = JSON.generate(payload)
32
+ end
33
+ request = Net::HTTP::Post.new(@uri); request["Content-Type"] = "application/json"
34
+ request["Authorization"] = "Bearer #{@api_key}"; request["User-Agent"] = "shieldpress-tracker-ruby/1.0.0"; request.body = body
35
+ response = Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == "https", open_timeout: 5, read_timeout: 10) { |http| http.request(request) }
36
+ response.code.to_i.between?(200, 299)
37
+ rescue StandardError => e
38
+ warn("[ShieldPress] report failed: #{e.message}") if @debug
39
+ false
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,64 @@
1
+ require "uri"
2
+ require_relative "config"
3
+ require_relative "collectors"
4
+ require_relative "reporter"
5
+
6
+ module ShieldPressTracker
7
+ class Tracker
8
+ attr_reader :config, :http_collector, :error_collector, :security_collector
9
+ def initialize(config = {})
10
+ @config = Config.new(config); @http_collector = HttpCollector.new
11
+ @error_collector = ErrorCollector.new(@config.max_error_buffer); @security_collector = SecurityCollector.new(@config.max_security_buffer)
12
+ @reporter = Reporter.new(@config.api_url, @config.api_key, @config.debug); @stopped = false; @last_audit = Time.at(0)
13
+ end
14
+ def start
15
+ return self if @thread&.alive?
16
+ @stopped = false
17
+ @thread = Thread.new do
18
+ Thread.current.name = "shieldpress-reporter" if Thread.current.respond_to?(:name=)
19
+ until @stopped; sleep(@config.report_interval.to_i); flush unless @stopped; end
20
+ end
21
+ @thread.abort_on_exception = false
22
+ self
23
+ end
24
+ def stop(flush: true); @stopped = true; @thread&.kill; self.flush if flush; self end
25
+ def ignored?(path)
26
+ path = URI.parse(path).path rescue path
27
+ @config.ignore_paths.any? { |pattern| pattern.end_with?("/*") ? path.start_with?(pattern[0..-2]) : path == pattern }
28
+ end
29
+ def record_request(path:, method: "GET", status_code: 200, duration_ms: 0)
30
+ @http_collector.record(path: path, method: method, status_code: status_code, duration_ms: duration_ms) if @config.http_tracking && !ignored?(path)
31
+ end
32
+ def analyze_request(url:, method: "GET", headers: {}, body: nil, ip: nil, query: nil)
33
+ @security_collector.analyze(url: url, method: method, headers: headers, body: body, ip: ip, query: query) if @config.security_tracking && !ignored?(url)
34
+ end
35
+ def capture_error(error, meta = {}); @error_collector.capture(error, meta) if @config.error_tracking end
36
+ def record_auth_failure(ip: nil, url: nil, method: nil, reason: nil)
37
+ @security_collector.record(type: :auth_failure, severity: :medium, message: "Authentication failed#{reason ? ": #{reason}" : ""}", ip: ip, url: url, method: method)
38
+ end
39
+ def record_rate_limit(ip: nil, url: nil, method: nil, limit: nil)
40
+ @security_collector.record(type: :rate_limit_hit, severity: :low, message: "Rate limit triggered", ip: ip, url: url, method: method, meta: { limit: limit })
41
+ end
42
+ def record_cors_violation(ip: nil, origin: nil, url: nil)
43
+ @security_collector.record(type: :cors_violation, severity: :medium, message: "CORS policy blocked request from origin: #{origin || 'unknown'}", ip: ip, url: url)
44
+ end
45
+ def record_security_event(type:, severity:, message:, **data)
46
+ @security_collector.record(type: type, severity: severity, message: message, **data)
47
+ end
48
+ def flush
49
+ c = @config
50
+ payload = { siteId: c.site_id, appName: c.app_name, appVersion: c.app_version, environment: c.environment,
51
+ tags: c.tags, timestamp: Time.now.utc.iso8601 }
52
+ payload[:system] = Metrics.system if c.system_metrics
53
+ http = @http_collector.flush if c.http_tracking; payload[:http] = http if http
54
+ errors = @error_collector.flush if c.error_tracking; payload[:errors] = errors unless errors.nil? || errors.empty?
55
+ security = @security_collector.flush if c.security_tracking; payload[:security] = security if security
56
+ payload[:runtime] = Metrics.runtime if c.runtime_metrics; payload[:envSecurity] = Metrics.environment if c.env_security
57
+ if c.dep_audit && Time.now - @last_audit >= c.dep_audit_interval.to_i; payload[:depAudit] = Metrics.dependencies; @last_audit = Time.now; end
58
+ payload[:heartbeat] = true if c.heartbeat
59
+ @reporter.send(payload)
60
+ rescue StandardError
61
+ false
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,4 @@
1
+ module ShieldPressTracker
2
+ VERSION = "1.0.0"
3
+ end
4
+
@@ -0,0 +1,14 @@
1
+ require_relative "shieldpress_tracker/version"
2
+ require_relative "shieldpress_tracker/tracker"
3
+ require_relative "shieldpress_tracker/middleware"
4
+
5
+ module ShieldPress
6
+ class << self
7
+ def init(**config); @instance&.stop(flush: false); @instance = ShieldPressTracker::Tracker.new(config).start end
8
+ def instance; @instance || raise("ShieldPress.init must be called first") end
9
+ def configured?; !@instance.nil? end
10
+ end
11
+ end
12
+
13
+ require_relative "shieldpress_tracker/railtie" if defined?(Rails::Railtie)
14
+
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shieldpress-tracker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ShieldPress
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - hello@shieldpress.net
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE
22
+ - README.md
23
+ - lib/shieldpress_tracker.rb
24
+ - lib/shieldpress_tracker/collectors.rb
25
+ - lib/shieldpress_tracker/config.rb
26
+ - lib/shieldpress_tracker/middleware.rb
27
+ - lib/shieldpress_tracker/railtie.rb
28
+ - lib/shieldpress_tracker/reporter.rb
29
+ - lib/shieldpress_tracker/tracker.rb
30
+ - lib/shieldpress_tracker/version.rb
31
+ homepage: https://shieldpress.net
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ source_code_uri: https://github.com/vithanhlam/ShieldPress-Tracker-Ruby-Gem
36
+ changelog_uri: https://github.com/vithanhlam/ShieldPress-Tracker-Ruby-Gem/blob/main/CHANGELOG.md
37
+ rubygems_mfa_required: 'true'
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.7.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.2.33
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Monitoring, performance and security SDK for Ruby applications
57
+ test_files: []