streess 0.1.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: cda80db362d88dec67489e6937f7f26d2807e5c7608b7ed92e1af8abee6be823
4
+ data.tar.gz: 837cc29a0b2fdfdbf599bd431624c8c5b44ed4d48ff86324a2ed04f33312b266
5
+ SHA512:
6
+ metadata.gz: 49ddc166f17ed38c848e826945d882fb19207b94dc17b9a5b87f68ed0818a1526ab41041672f91c21cb2d742e04e0eaa07e374093311272973b0ea4cc6ce72ae
7
+ data.tar.gz: f4bd772562879ddaba889e61051a864da5f8e6153d973b1a47f906621830e29873470e244c8249754733b618cbfe393381413e3ddf3f7bf2e65b621aba505a59
@@ -0,0 +1,50 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Streess
6
+ class Client
7
+ class << self
8
+ # Test hook: set to a lambda to intercept HTTP calls
9
+ attr_accessor :http_handler
10
+
11
+ def send_error(payload)
12
+ config = Streess.configuration
13
+ uri = URI.join(config.endpoint, "/api/v1/errors")
14
+
15
+ request = Net::HTTP::Post.new(uri.path)
16
+ request["Content-Type"] = "application/json"
17
+ request["X-Streess-Key"] = config.api_key
18
+ request.body = JSON.generate(payload)
19
+
20
+ response = perform_request(uri, request)
21
+ response.code == "201"
22
+ rescue => e
23
+ log_error("Streess: failed to send error - #{e.class}: #{e.message}")
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ def perform_request(uri, request)
30
+ if http_handler
31
+ return http_handler.call(uri, request)
32
+ end
33
+
34
+ http = Net::HTTP.new(uri.host, uri.port)
35
+ http.use_ssl = (uri.scheme == "https")
36
+ http.open_timeout = 5
37
+ http.read_timeout = 5
38
+ http.request(request)
39
+ end
40
+
41
+ def log_error(message)
42
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
43
+ Rails.logger.error(message)
44
+ else
45
+ $stderr.puts(message)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ module Streess
2
+ class Configuration
3
+ attr_accessor :api_key, :endpoint, :current_user, :filtered_params
4
+
5
+ DEFAULT_FILTERED_PARAMS = [ :password, :token, :secret ].freeze
6
+
7
+ def initialize
8
+ @api_key = nil
9
+ @endpoint = nil
10
+ @current_user = nil
11
+ @filtered_params = DEFAULT_FILTERED_PARAMS.dup
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "../streess"
2
+ require_relative "payload_builder"
3
+ require_relative "client"
4
+
5
+ module Streess
6
+ class Middleware
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ @app.call(env)
13
+ rescue Exception => exception
14
+ report_error(exception, env)
15
+ raise
16
+ end
17
+
18
+ private
19
+
20
+ def report_error(exception, env)
21
+ config = Streess.configuration
22
+ return unless config.api_key && !config.api_key.empty?
23
+
24
+ payload = PayloadBuilder.build(exception, env)
25
+ Client.send_error(payload)
26
+ rescue
27
+ # Never interfere with the original exception propagation
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ module Streess
2
+ class ParamFilter
3
+ FILTERED_VALUE = "[FILTERED]"
4
+
5
+ def initialize(filtered_keys = [])
6
+ @filtered_keys = filtered_keys.map { |k| k.to_s.downcase }
7
+ end
8
+
9
+ def filter(hash)
10
+ return hash unless hash.is_a?(Hash)
11
+
12
+ hash.each_with_object({}) do |(key, value), result|
13
+ if filtered_key?(key)
14
+ result[key] = FILTERED_VALUE
15
+ elsif value.is_a?(Hash)
16
+ result[key] = filter(value)
17
+ else
18
+ result[key] = value
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def filtered_key?(key)
26
+ @filtered_keys.include?(key.to_s.downcase)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,94 @@
1
+ require_relative "param_filter"
2
+
3
+ module Streess
4
+ class PayloadBuilder
5
+ BACKTRACE_REGEX = /^(.+):(\d+):in '?(.+?)'?$/
6
+
7
+ class << self
8
+ def build(exception, env)
9
+ filter = ParamFilter.new(Streess.configuration.filtered_params)
10
+
11
+ {
12
+ error: {
13
+ exception_class: exception.class.name,
14
+ message: exception.message,
15
+ occurred_at: Time.now.iso8601,
16
+ backtrace: parse_backtrace(exception),
17
+ request: build_request(env, filter),
18
+ client: build_client(env),
19
+ user: build_user(env),
20
+ environment: build_environment
21
+ }
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ def parse_backtrace(exception)
28
+ return [] unless exception.backtrace
29
+
30
+ exception.backtrace.map do |line|
31
+ match = line.match(BACKTRACE_REGEX)
32
+ if match
33
+ { file: match[1], line: match[2].to_i, method: match[3] }
34
+ else
35
+ { file: line, line: 0, method: "unknown" }
36
+ end
37
+ end
38
+ end
39
+
40
+ def build_request(env, filter)
41
+ params = extract_params(env)
42
+
43
+ {
44
+ method: env["REQUEST_METHOD"],
45
+ path: env["PATH_INFO"],
46
+ host: env["HTTP_HOST"] || env["SERVER_NAME"],
47
+ query_string: env["QUERY_STRING"],
48
+ params: filter.filter(params),
49
+ headers: extract_headers(env)
50
+ }
51
+ end
52
+
53
+ def extract_params(env)
54
+ params = {}
55
+ params.merge!(env["rack.request.form_hash"]) if env["rack.request.form_hash"].is_a?(Hash)
56
+ params.merge!(env["rack.request.query_hash"]) if env["rack.request.query_hash"].is_a?(Hash)
57
+ params
58
+ end
59
+
60
+ def extract_headers(env)
61
+ env.each_with_object({}) do |(key, value), headers|
62
+ next unless key.is_a?(String) && key.start_with?("HTTP_")
63
+ next if key == "HTTP_HOST"
64
+
65
+ header_name = key.sub("HTTP_", "").split("_").map(&:capitalize).join("-")
66
+ headers[header_name] = value
67
+ end
68
+ end
69
+
70
+ def build_client(env)
71
+ {
72
+ user_agent: env["HTTP_USER_AGENT"],
73
+ ip_address: env["REMOTE_ADDR"]
74
+ }
75
+ end
76
+
77
+ def build_user(env)
78
+ user_proc = Streess.configuration.current_user
79
+ return nil unless user_proc
80
+
81
+ user_proc.call(env)
82
+ rescue
83
+ nil
84
+ end
85
+
86
+ def build_environment
87
+ {
88
+ ruby_version: RUBY_VERSION,
89
+ ruby_platform: RUBY_PLATFORM
90
+ }
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,15 @@
1
+ require_relative "../streess"
2
+ require_relative "middleware"
3
+
4
+ module Streess
5
+ if defined?(Rails::Railtie)
6
+ class Railtie < Rails::Railtie
7
+ # Insert middleware during config phase (before the stack freezes).
8
+ # The middleware itself checks if api_key is configured at request
9
+ # time, so it's safe to insert unconditionally.
10
+ initializer "streess.insert_middleware" do |app|
11
+ app.config.middleware.use(Streess::Middleware)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "streess"
2
+ require_relative "streess/railtie"
data/lib/streess.rb ADDED
@@ -0,0 +1,17 @@
1
+ require_relative "streess/configuration"
2
+
3
+ module Streess
4
+ class << self
5
+ def configure
6
+ yield(configuration)
7
+ end
8
+
9
+ def configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ def reset_configuration!
14
+ @configuration = nil
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: streess
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kasper Høglund
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-04-04 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ description: Rack middleware and client library for reporting errors to a Streess
41
+ server.
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - lib/streess-ruby.rb
47
+ - lib/streess.rb
48
+ - lib/streess/client.rb
49
+ - lib/streess/configuration.rb
50
+ - lib/streess/middleware.rb
51
+ - lib/streess/param_filter.rb
52
+ - lib/streess/payload_builder.rb
53
+ - lib/streess/railtie.rb
54
+ licenses:
55
+ - MIT
56
+ metadata: {}
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '3.0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.6.2
72
+ specification_version: 4
73
+ summary: Ruby client for Streess error tracking
74
+ test_files: []