tracetells 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: 5537eb8ff5c01294ae19f4cfbecc455d69b6908e28afc72a5598d690a04d3753
4
+ data.tar.gz: 7d8e577665f1846717d2ce5c214425735479208178ebc5afa54cd86c8bd3b474
5
+ SHA512:
6
+ metadata.gz: 78067850640ffa59dc4c9149107d5a8b80f069d33e301d9a50c562c9478068f4b3eda695c95289124659958218d2e364b2aa236dfc5be8a544ee68d3109fdb8a
7
+ data.tar.gz: 4e0b84ef279742c39dd0f7db79247f9ca28cdfe4ad47ae7e66489ddd06ad4d8cc41c6f00868023ac1b87f94150e06a25e222c01c6baf29325557184c3176ff46
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deyan Dobrinov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # tracetells
2
+
3
+ Report a Ruby server's errors to [Trace](https://tracetells.com): what failed, where in your code, on which route, since which release. Trace groups and counts them, shows how many visits each one broke in the browser, and tells you whether a fix stopped it. No runtime dependencies; Ruby 3.1+.
4
+
5
+ ```ruby
6
+ # Gemfile
7
+ gem "tracetells"
8
+ ```
9
+
10
+ **Rails:** nothing else. The gem configures itself from the environment, adds its Rack middleware, and subscribes to `Rails.error`, which sees unhandled errors in controllers and jobs (Rails 7+). Routes come from Rails (7.1+): `/orders/:id`, never the URL. Errors Rails answers with a 4xx (`RecordNotFound`, `RoutingError`) aren't reported.
11
+
12
+ **Rack and Sinatra:**
13
+
14
+ ```ruby
15
+ # config.ru
16
+ require "tracetells"
17
+ Tracetells.configure
18
+ use Tracetells::Rack # reports exceptions with their route and re-raises them
19
+ ```
20
+
21
+ Report an error you handled with `Tracetells.report(error)`, or `Tracetells.report_message("Billing", "no plan for account")`. During a request, its route goes with it.
22
+
23
+ ## Configuration
24
+
25
+ | Variable | Default | |
26
+ |---|---|---|
27
+ | `TRACE_SERVER_KEY` | – | The website's server key. Your coding agent creates it (`/trace:setup server errors`), or an admin in the dashboard. Keep it in your credentials, never in the repository. Without it the gem does nothing. |
28
+ | `TRACE_ENDPOINT` | `https://app.tracetells.com` | Your own server, when you self-host Trace |
29
+ | `TRACE_ENVIRONMENT` | `RAILS_ENV`, `RACK_ENV`, else `production` | |
30
+ | `TRACE_RELEASE` | `SOURCE_VERSION`, `HEROKU_SLUG_COMMIT`, `RENDER_GIT_COMMIT`, `GIT_COMMIT` or `GITHUB_SHA`, if set | The release the code belongs to |
31
+
32
+ Or in code: `Tracetells.configure(key:, endpoint:, environment:, release:)`.
33
+
34
+ ## What it sends, and what it never does
35
+
36
+ **Sent:** the error's class, its message (with up to three causes), the backtrace (files relative to your project, lines, methods, and whether each frame is your code), the route template, the HTTP method and status, the environment, the release, the Ruby version, when it happened and how often.
37
+
38
+ **Never sent:** request or response bodies, params, query strings, headers, cookies, sessions, environment variables, source code, host names or who was signed in. There is no option to send them.
39
+
40
+ Messages and file paths are scrubbed before they leave: emails, tokens, card numbers, IBANs and phone-like numbers are replaced, and home directories are cut from paths. Trace's server scrubs everything again. Keep personal data out of exception messages anyway.
41
+
42
+ ## How it behaves
43
+
44
+ - Errors are sent from a background thread, in batches, at most once a second. Requests never wait for it. After a fork (Puma, Unicorn) a new thread starts by itself.
45
+ - The first occurrence of an error is sent right away; repeats within the next minute are sent once, as a count.
46
+ - A full queue, a network error, or Trace answering 429 or 5xx: errors are dropped, and the client backs off (5 seconds, doubling up to 5 minutes). It never raises into your code.
47
+ - At exit it sends what's queued, waiting at most two seconds.
48
+
49
+ Licence: MIT.
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tracetells
4
+ # Collects errors and sends them from a background thread: in batches, at
5
+ # most once a second, with repeats within a minute folded into a count, and
6
+ # backing off when Trace is unreachable or asks to slow down.
7
+ class Client
8
+ QUEUE = 1000
9
+ BATCH = 100
10
+ FOLD = 60
11
+ SEND_EVERY = 1.0
12
+
13
+ attr_reader :environment, :release
14
+
15
+ def initialize(key:, endpoint:, environment:, release:)
16
+ @key = key
17
+ @uri = URI("#{endpoint}/v1/errors")
18
+ @environment = environment.to_s
19
+ @release = release.to_s
20
+ @queue = Thread::Queue.new
21
+ @recent = {}
22
+ @outgoing = []
23
+ @paused_until = 0
24
+ @backoff = 5
25
+ @lock = Mutex.new
26
+ @pid = Process.pid
27
+ end
28
+
29
+ def report(error, request)
30
+ enqueue(event(error.class.name || "Error", Scrub.message(chain(error)), error.backtrace || caller(2), request))
31
+ end
32
+
33
+ def report_message(class_name, message, request)
34
+ enqueue(event(class_name.to_s, Scrub.message(message), caller(2), request))
35
+ end
36
+
37
+ def flush(timeout)
38
+ done = Thread::Queue.new
39
+ start
40
+ @queue << [:flush, done]
41
+ done.pop(timeout: timeout)
42
+ end
43
+
44
+ def shutdown
45
+ @queue << [:stop, nil]
46
+ end
47
+
48
+ private
49
+
50
+ def chain(error)
51
+ msg = error.message.to_s
52
+ cause = error.cause
53
+ 3.times do
54
+ break unless cause
55
+
56
+ msg += ": #{cause.message}"
57
+ cause = cause.cause
58
+ end
59
+ msg
60
+ end
61
+
62
+ def event(class_name, message, backtrace, request)
63
+ request ||= {}
64
+ status = request[:status].to_i
65
+ {
66
+ "class" => Scrub.truncate(Scrub.one_line(class_name), 200),
67
+ "message" => Scrub.truncate(message, 300),
68
+ "backtrace" => frames(backtrace),
69
+ "route" => request[:route].to_s.empty? ? "" : Scrub.route(request[:route]),
70
+ "method" => request[:method].to_s.upcase,
71
+ "status" => status.between?(100, 599) ? status : nil,
72
+ "occurred_at" => (Time.now.to_f * 1000).to_i,
73
+ "count" => 1
74
+ }
75
+ end
76
+
77
+ RUBY_LIB = RbConfig::CONFIG["rubylibdir"].to_s
78
+
79
+ # "/srv/app/app/models/order.rb:12:in 'Order#total'" (Ruby 3.4+ quotes, older ones backtick-quote).
80
+ def frames(lines)
81
+ Array(lines).first(60).filter_map do |line|
82
+ m = /\A(.+?):(\d+)(?::in [`'](.+)')?\z/.match(line.to_s)
83
+ next unless m
84
+
85
+ file = m[1]
86
+ next if file.include?("/lib/tracetells")
87
+
88
+ dependency = file.include?("/gems/") || file.start_with?("<internal:") || (!RUBY_LIB.empty? && file.start_with?(RUBY_LIB))
89
+ { "file" => Scrub.file(file), "line" => m[2].to_i, "function" => Scrub.truncate(Scrub.one_line(m[3].to_s), 200), "in_app" => !dependency }
90
+ end.first(40)
91
+ end
92
+
93
+ def enqueue(e)
94
+ start
95
+ @queue << [:event, e] if @queue.size < QUEUE
96
+ end
97
+
98
+ # After a fork (Puma, Unicorn) the thread is gone: start a new one.
99
+ def start
100
+ @lock.synchronize do
101
+ if @pid != Process.pid
102
+ @pid = Process.pid
103
+ @thread = nil
104
+ end
105
+ @thread = Thread.new { run } unless @thread&.alive?
106
+ end
107
+ end
108
+
109
+ def run
110
+ Thread.current.name = "tracetells"
111
+ Thread.current.report_on_exception = false
112
+ last_send = monotonic
113
+ loop do
114
+ kind, payload = @queue.pop(timeout: SEND_EVERY)
115
+ flushing = nil
116
+ case kind
117
+ when :event then admit(payload)
118
+ when :flush then flushing = payload
119
+ when :stop then return
120
+ end
121
+ fold_expired(!flushing.nil?)
122
+ if !@outgoing.empty? && (flushing || @outgoing.size >= BATCH || monotonic - last_send >= SEND_EVERY)
123
+ send_all
124
+ last_send = monotonic
125
+ end
126
+ flushing&.push(true)
127
+ end
128
+ rescue StandardError
129
+ nil
130
+ end
131
+
132
+ def admit(e)
133
+ key = fold_key(e)
134
+ seen = @recent[key]
135
+ if seen && monotonic - seen[:start] < FOLD
136
+ seen[:repeats] += 1
137
+ seen[:latest] = e
138
+ else
139
+ @recent[key] = { start: monotonic, repeats: 0, latest: nil }
140
+ @outgoing << e
141
+ end
142
+ end
143
+
144
+ def fold_key(e)
145
+ top = e["backtrace"].find { |f| f["in_app"] }
146
+ [e["class"], top ? "#{top['file']}|#{top['function']}" : e["message"], e["route"], e["status"]].join("|")
147
+ end
148
+
149
+ # Minutes that ended: their repeats go out as one error with a count.
150
+ def fold_expired(all)
151
+ now = monotonic
152
+ @recent.delete_if do |_, r|
153
+ ended = now - r[:start] >= FOLD
154
+ if (ended || all) && r[:latest] && r[:repeats].positive?
155
+ @outgoing << r[:latest].merge("count" => r[:repeats])
156
+ r[:repeats] = 0
157
+ r[:latest] = nil
158
+ end
159
+ ended
160
+ end
161
+ end
162
+
163
+ def send_all
164
+ until @outgoing.empty?
165
+ batch = @outgoing.shift(BATCH)
166
+ next if monotonic < @paused_until # backing off: drop rather than pile up
167
+
168
+ runtime = { "name" => RUBY_ENGINE, "version" => RUBY_VERSION }
169
+ body = JSON.generate("errors" => batch.map { |e| e.merge("environment" => @environment, "release" => @release, "runtime" => runtime) })
170
+ begin
171
+ res = Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == "https", open_timeout: 5, read_timeout: 5, write_timeout: 5) do |http|
172
+ http.post(@uri.path, body, "authorization" => "Bearer #{@key}", "content-type" => "application/json")
173
+ end
174
+ code = res.code.to_i
175
+ raise "HTTP #{code}" if code == 429 || code >= 500
176
+
177
+ @backoff = 5
178
+ rescue StandardError
179
+ @paused_until = monotonic + @backoff
180
+ @backoff = [@backoff * 2, 300].min
181
+ end
182
+ end
183
+ end
184
+
185
+ def monotonic
186
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tracetells
4
+ # Rack middleware: reports exceptions raised while handling a request, with
5
+ # its route, method and status, and re-raises them so the app's own error
6
+ # handling is unchanged. In Rails the Railtie adds it.
7
+ class Rack
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ previous = Thread.current[:tracetells_env]
14
+ Thread.current[:tracetells_env] = env
15
+ @app.call(env)
16
+ rescue Exception => e # rubocop:disable Lint/RescueException
17
+ Tracetells.report(e, request: self.class.request_info(env, status: 500)) unless env["tracetells.reported"]
18
+ raise
19
+ ensure
20
+ Thread.current[:tracetells_env] = previous
21
+ end
22
+
23
+ # The route template when the framework knows it (Rails 7.1+, Sinatra), else
24
+ # the path, which is scrubbed of ids before it's sent. Nothing else of the request.
25
+ def self.request_info(env, status: nil)
26
+ route = env["action_dispatch.route_uri_pattern"] || env["sinatra.route"]&.split(" ", 2)&.last || env["PATH_INFO"].to_s
27
+ { route: route.to_s.sub(/\(\.:format\)\z/, ""), method: env["REQUEST_METHOD"], status: status }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tracetells
4
+ # In Rails: configures from the environment, adds the Rack middleware, and
5
+ # subscribes to Rails.error, which sees unhandled errors in controllers and
6
+ # jobs (Rails 7+). Handled errors reported with `handled: true` are left out
7
+ # unless their severity is :error, and so are errors Rails answers with a 4xx.
8
+ class Railtie < ::Rails::Railtie
9
+ initializer "tracetells.middleware" do |app|
10
+ Tracetells.configure unless Tracetells.client
11
+ app.middleware.insert_before(0, Tracetells::Rack)
12
+ end
13
+
14
+ config.after_initialize do
15
+ Rails.error.subscribe(Tracetells::Railtie::Subscriber.new) if Rails.respond_to?(:error)
16
+ end
17
+
18
+ class Subscriber
19
+ def report(error, handled:, severity:, context:, source: nil)
20
+ return if handled && severity != :error
21
+
22
+ env = Thread.current[:tracetells_env]
23
+ status = status_for(error)
24
+ # 404s and other client errors (RecordNotFound, RoutingError) aren't server errors.
25
+ return if env && status < 500
26
+
27
+ request = env ? Tracetells::Rack.request_info(env, status: status) : nil
28
+ env["tracetells.reported"] = true if env
29
+ Tracetells.report(error, request: request)
30
+ end
31
+
32
+ private
33
+
34
+ def status_for(error)
35
+ if defined?(ActionDispatch::ExceptionWrapper)
36
+ ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
37
+ else
38
+ 500
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tracetells
4
+ # The same scrubbing Trace's server does (crates/trace-core/src/privacy.rs
5
+ # and errors.rs), done here first so nothing personal leaves the server.
6
+ # clients/test-vectors.json holds the cases every client must agree on.
7
+ module Scrub
8
+ EMAIL = /[a-z0-9._%+-]+(@|%40)[a-z0-9.-]+\.[a-z]{2,}/i
9
+ JWT = /\beyJ[\w-]+\.[\w-]+\.[\w-]+/
10
+ URL_QUERY = %r{(https?://[^\s?#]+)[?#][^\s]*}
11
+ IBAN = /\b[A-Z]{2}\d{2}(?:\s?[A-Z0-9]{4}){2,7}(?:\s?[A-Z0-9]{1,4})?\b/
12
+ CARD = /\b(?:\d[ -]?){12,18}\d\b/
13
+ TOKEN = %r{[A-Za-z0-9_\-+/=]{20,}}
14
+ LONG_NUMBER = /\+?\d(?:[\s().-]{0,2}\d){6,}/
15
+ HIDDEN = /[\u0000-\u001f\u007f-\u009f\u00ad\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/
16
+
17
+ DEPENDENCY_ROOTS = ["node_modules/", "gems/", "registry/src/", "site-packages/", "vendor/"].freeze
18
+ PROJECT_ROOTS = ["src/", "app/", "lib/", "crates/", "packages/", "config/", "bin/", "spec/", "test/", "tests/"].freeze
19
+
20
+ module_function
21
+
22
+ def token?(s)
23
+ s.match?(/\d/) && s.match?(/[A-Za-z]/)
24
+ end
25
+
26
+ # On one line: control and invisible characters become spaces, backticks quotes.
27
+ def one_line(s)
28
+ s.to_s.scrub("?").gsub(HIDDEN, " ").tr("`", "'").squeeze(" ").strip
29
+ end
30
+
31
+ def truncate(s, max)
32
+ s.length > max ? s[0, max] : s
33
+ end
34
+
35
+ # Emails, tokens, card numbers, IBANs, phone-like numbers and URL query strings.
36
+ def message(s)
37
+ one_line(s)
38
+ .gsub(URL_QUERY, '\1?[redacted]')
39
+ .gsub(EMAIL, "[email]")
40
+ .gsub(JWT, "[token]")
41
+ .gsub(IBAN, "[iban]")
42
+ .gsub(CARD, "[card]")
43
+ .gsub(TOKEN) { |m| token?(m) ? "[token]" : m }
44
+ .gsub(LONG_NUMBER, "[number]")
45
+ end
46
+
47
+ # A route as Trace stores it: no query string, segments that look like ids,
48
+ # emails or tokens replaced. Route parameters (:id, {id}) pass through.
49
+ def route(path)
50
+ p = one_line(path.to_s.strip).gsub(" ", "%20").split(/[?#]/).first.to_s
51
+ return "" unless p.start_with?("/")
52
+
53
+ out = p.split("/", -1).map do |seg|
54
+ if seg.length > 2 && seg.start_with?("{") && seg.end_with?("}")
55
+ ":#{seg[1..-2].delete_prefix('*')}"
56
+ elsif seg.empty? || seg.start_with?(":") || seg == "*"
57
+ seg
58
+ elsif seg.include?("@") || seg.downcase.include?("%40")
59
+ ":email"
60
+ elsif seg.include?("=") || (seg.count(".") >= 2 && seg.length >= 24)
61
+ ":token"
62
+ elsif seg.count("0-9") >= 3 || (seg.length >= 16 && token?(seg)) || (seg.length >= 16 && seg.match?(/\A\h+\z/))
63
+ ":id"
64
+ else
65
+ seg
66
+ end
67
+ end.join("/")
68
+ truncate(out, 200)
69
+ end
70
+
71
+ # A frame's file, relative to the project or the dependency it is in.
72
+ def file(name)
73
+ f = one_line(name).tr("\\", "/")
74
+ f = f.delete_prefix("file://")
75
+ f = f.split(/[?#]/).first.to_s
76
+ absolute = f.start_with?("/") || f[1] == ":" || f.start_with?("~/")
77
+ rel =
78
+ if !absolute
79
+ f
80
+ elsif (dep = DEPENDENCY_ROOTS.filter_map { |m| (i = f.rindex(m)) && [i, m] }.max_by(&:first))
81
+ i, m = dep
82
+ if m == "registry/src/"
83
+ rest = f[(i + m.length)..]
84
+ (slash = rest.index("/")) ? rest[(slash + 1)..] : rest
85
+ else
86
+ f[i..]
87
+ end
88
+ elsif (i = PROJECT_ROOTS.lazy.map { |m| f.index("/#{m}") }.find(&:itself))
89
+ f[(i + 1)..]
90
+ else
91
+ f.split("/").last.to_s
92
+ end
93
+ truncate(rel.split("/", -1).map { |seg| seg.include?("@") ? ":email" : seg }.join("/"), 200)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,3 @@
1
+ module Tracetells
2
+ VERSION = "0.1.0"
3
+ end
data/lib/tracetells.rb ADDED
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Report a Ruby server's errors to Trace (https://tracetells.com).
4
+ #
5
+ # Tracetells.configure # TRACE_SERVER_KEY, TRACE_ENDPOINT, TRACE_ENVIRONMENT, TRACE_RELEASE
6
+ # use Tracetells::Rack # in config.ru; Rails apps get it, and Rails.error, on their own
7
+ #
8
+ # What is sent: the error's class, its message, the backtrace (files relative
9
+ # to your project, lines, methods), the route template, the HTTP method and
10
+ # status, the environment, the release, the Ruby version, when and how often.
11
+ # Never sent: request or response bodies, params, query strings, headers,
12
+ # cookies, sessions, environment variables, source code, host names or who was
13
+ # signed in. There is no option to send them. Messages and paths are scrubbed first.
14
+
15
+ require "json"
16
+ require "net/http"
17
+ require "uri"
18
+ require "rbconfig"
19
+ require_relative "tracetells/version"
20
+ require_relative "tracetells/scrub"
21
+ require_relative "tracetells/client"
22
+ require_relative "tracetells/rack"
23
+ require_relative "tracetells/railtie" if defined?(::Rails::Railtie)
24
+
25
+ module Tracetells
26
+ class << self
27
+ # Starts the client. Without a key it does nothing. Options override the environment.
28
+ def configure(key: nil, endpoint: nil, environment: nil, release: nil)
29
+ env = ->(k) { (v = ENV[k].to_s.strip).empty? ? nil : v }
30
+ key ||= env.("TRACE_SERVER_KEY")
31
+ return @client = nil unless key
32
+
33
+ @client&.shutdown
34
+ @client = Client.new(
35
+ key: key,
36
+ endpoint: (endpoint || env.("TRACE_ENDPOINT") || "https://app.tracetells.com").sub(%r{/+\z}, ""),
37
+ environment: environment || env.("TRACE_ENVIRONMENT") || env.("RAILS_ENV") || env.("RACK_ENV") || "production",
38
+ release: release || %w[TRACE_RELEASE SOURCE_VERSION HEROKU_SLUG_COMMIT RENDER_GIT_COMMIT GIT_COMMIT GITHUB_SHA].lazy.map { |k| env.(k) }.find(&:itself) || ""
39
+ )
40
+ end
41
+
42
+ def client
43
+ @client
44
+ end
45
+
46
+ # Reports an error you handled. `request` takes :route, :method and :status.
47
+ def report(error, request: nil)
48
+ @client&.report(error, request || current_request)
49
+ rescue StandardError
50
+ nil
51
+ end
52
+
53
+ def report_message(class_name, message, request: nil)
54
+ @client&.report_message(class_name, message, request || current_request)
55
+ rescue StandardError
56
+ nil
57
+ end
58
+
59
+ # Sends what's queued, waiting at most `timeout` seconds.
60
+ def flush(timeout = 2)
61
+ @client&.flush(timeout)
62
+ end
63
+
64
+ # The request this thread is handling, as Tracetells::Rack saw it.
65
+ def current_request
66
+ env = Thread.current[:tracetells_env]
67
+ env && Tracetells::Rack.request_info(env)
68
+ end
69
+ end
70
+ end
71
+
72
+ at_exit { Tracetells.flush(2) }
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tracetells
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Deyan Dobrinov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Sends the class, message, backtrace, route, status and release of your
13
+ app's errors to Trace, which groups and counts them and ties them to the visits
14
+ they broke. Never request bodies, params, headers, cookies or users.
15
+ email: hello@tracetells.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/tracetells.rb
23
+ - lib/tracetells/client.rb
24
+ - lib/tracetells/rack.rb
25
+ - lib/tracetells/railtie.rb
26
+ - lib/tracetells/scrub.rb
27
+ - lib/tracetells/version.rb
28
+ homepage: https://tracetells.com/docs/server-errors/
29
+ licenses:
30
+ - MIT
31
+ metadata:
32
+ source_code_uri: https://github.com/dobrinov/tracetells/tree/main/clients/ruby
33
+ rubygems_mfa_required: 'true'
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '3.1'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 4.0.3
49
+ specification_version: 4
50
+ summary: 'Report Ruby server errors to Trace (tracetells.com): error facts only, scrubbed,
51
+ in the background'
52
+ test_files: []