errsight 0.2.2

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.
@@ -0,0 +1,248 @@
1
+ module Errsight
2
+ # Sidekiq integration. Three pieces:
3
+ #
4
+ # 1. ClientMiddleware — runs on enqueue, snapshots the current scope
5
+ # (user/tags/breadcrumbs) into the job payload so the worker side can
6
+ # restore it when the job runs.
7
+ # 2. ServerMiddleware — runs around `perform`, rehydrates the snapshot,
8
+ # reports any exception with structured Sidekiq context, and re-raises
9
+ # so Sidekiq's retry/death machinery still fires.
10
+ # 3. error_handlers entry — Sidekiq's safety net for exceptions raised
11
+ # *outside* the middleware chain (fetch errors, middleware itself
12
+ # raising). Dedup-aware so middleware-captured exceptions aren't
13
+ # reported twice.
14
+ #
15
+ # We do NOT enqueue Errsight's HTTP delivery as Sidekiq jobs. Delivery
16
+ # stays on the SDK's own background flush thread so error reporting still
17
+ # works when the customer's Sidekiq is broken (Redis down, workers stuck).
18
+ module Sidekiq
19
+ # Cap propagated breadcrumbs so we don't bloat job payloads in Redis. A
20
+ # job carrying 50 crumbs × 200 bytes × 1M jobs/day = 10 GB/day, which is
21
+ # not a cost we should silently impose on the customer's Redis bill.
22
+ MAX_PROPAGATED_BREADCRUMBS = 20
23
+
24
+ # Hard cap on per-arg serialized size. Jobs occasionally carry large
25
+ # payloads (file blobs, base64 images) and we'd rather drop them than
26
+ # blow past the API's 512KB ingestion limit.
27
+ MAX_ARG_BYTES = 4_096
28
+
29
+ class << self
30
+ # Idempotent. Safe to call multiple times: the @configured flag and
31
+ # chain.exists? guards prevent double-registration if the host requires
32
+ # us once via Bundler and once via the Railtie.
33
+ def configure_integration!
34
+ return unless defined?(::Sidekiq)
35
+ return if @configured
36
+ @configured = true
37
+
38
+ ::Sidekiq.configure_server do |config|
39
+ config.server_middleware do |chain|
40
+ # Prepend so we wrap *all* other server middleware — exceptions
41
+ # raised by other middleware are caught here too.
42
+ chain.prepend(ServerMiddleware) unless chain.exists?(ServerMiddleware)
43
+ end
44
+ config.client_middleware do |chain|
45
+ chain.add(ClientMiddleware) unless chain.exists?(ClientMiddleware)
46
+ end
47
+ register_error_handler(config)
48
+ end
49
+
50
+ ::Sidekiq.configure_client do |config|
51
+ config.client_middleware do |chain|
52
+ chain.add(ClientMiddleware) unless chain.exists?(ClientMiddleware)
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Sidekiq 7+ exposes per-config error_handlers; pre-7 used the global
60
+ # Sidekiq.error_handlers array. Support both so users on older Sidekiq
61
+ # don't have to wire the safety net manually.
62
+ def register_error_handler(config)
63
+ # The 3rd arg (config) was added in Sidekiq 7. Default it so the
64
+ # same proc works under Sidekiq 6.
65
+ handler = proc do |exception, ctx, _config = nil|
66
+ next unless exception.is_a?(Exception)
67
+ # If the server middleware already reported this exception, skip —
68
+ # otherwise a single failed job lands in the issue list twice.
69
+ seen = Thread.current[:errsight_captured_exceptions] ||= []
70
+ next if seen.include?(exception.object_id)
71
+ seen << exception.object_id
72
+
73
+ Errsight.capture_exception(
74
+ exception,
75
+ metadata: { sidekiq_context: stringify_keys(ctx) },
76
+ tags: { "sidekiq.source" => "error_handler" }
77
+ )
78
+ rescue StandardError
79
+ # Never let our handler tip Sidekiq into a meta-error loop.
80
+ end
81
+
82
+ if config.respond_to?(:error_handlers)
83
+ config.error_handlers << handler
84
+ elsif ::Sidekiq.respond_to?(:error_handlers)
85
+ ::Sidekiq.error_handlers << handler
86
+ end
87
+ end
88
+
89
+ def stringify_keys(hash)
90
+ return {} unless hash.is_a?(Hash)
91
+ hash.each_with_object({}) { |(k, v), out| out[k.to_s] = v }
92
+ end
93
+ end
94
+
95
+ class ClientMiddleware
96
+ include ::Sidekiq::ClientMiddleware if defined?(::Sidekiq::ClientMiddleware)
97
+
98
+ def call(_worker_class, job, _queue, _redis_pool)
99
+ snapshot = scope_snapshot
100
+ job["errsight_scope"] = snapshot unless snapshot.empty?
101
+ yield
102
+ end
103
+
104
+ private
105
+
106
+ # Delegate to Scope#to_h, which encodes the "user breadcrumbs only;
107
+ # DB crumbs stay process-local" propagation rule. Cap the propagated
108
+ # crumbs to keep job payloads small in Redis on high-throughput queues.
109
+ def scope_snapshot
110
+ hash = Errsight.current_scope.to_h
111
+ if hash["breadcrumbs"].is_a?(Array) && hash["breadcrumbs"].size > MAX_PROPAGATED_BREADCRUMBS
112
+ hash["breadcrumbs"] = hash["breadcrumbs"].last(MAX_PROPAGATED_BREADCRUMBS)
113
+ end
114
+ hash
115
+ end
116
+ end
117
+
118
+ class ServerMiddleware
119
+ include ::Sidekiq::ServerMiddleware if defined?(::Sidekiq::ServerMiddleware)
120
+
121
+ def call(worker, job, queue)
122
+ Errsight.with_scope(scope_for_job(job)) do
123
+ run_with_capture(worker, job, queue) { yield }
124
+ end
125
+ ensure
126
+ # Match the dedup-set lifecycle from CaptureMiddleware: cleared at
127
+ # job boundary so the next job on the same Sidekiq thread starts
128
+ # fresh and isn't tricked into skipping a legitimately new error.
129
+ Thread.current[:errsight_captured_exceptions] = nil
130
+ end
131
+
132
+ private
133
+
134
+ # Start from the worker thread's root scope (which carries any
135
+ # process-wide tags set at boot) and overlay the snapshot shipped by
136
+ # the enqueuing process. Job context wins on conflicts because the
137
+ # enqueuer knew which user/request triggered the work.
138
+ def scope_for_job(job)
139
+ scope = Errsight.hub.current_scope.dup
140
+ snapshot = job["errsight_scope"]
141
+ scope.merge!(::Errsight::Scope.from_h(snapshot)) if snapshot.is_a?(Hash)
142
+ scope
143
+ end
144
+
145
+ def run_with_capture(worker, job, queue)
146
+ yield
147
+ rescue Exception => exception
148
+ capture(exception, worker, job, queue)
149
+ raise
150
+ end
151
+
152
+ def capture(exception, worker, job, queue)
153
+ seen = Thread.current[:errsight_captured_exceptions] ||= []
154
+ return if seen.include?(exception.object_id)
155
+ seen << exception.object_id
156
+
157
+ Errsight.capture_exception(
158
+ exception,
159
+ tags: build_tags(worker, job, queue),
160
+ metadata: { sidekiq: build_metadata(job) }
161
+ )
162
+ rescue StandardError
163
+ # Never let our own capture failure suppress the job's exception —
164
+ # Sidekiq still needs to see the original to retry.
165
+ end
166
+
167
+ def build_tags(worker, job, queue)
168
+ {
169
+ "sidekiq.worker" => worker_name(job, worker),
170
+ "sidekiq.queue" => queue.to_s,
171
+ "sidekiq.jid" => job["jid"].to_s,
172
+ "sidekiq.retry_count" => job["retry_count"].to_s
173
+ }.reject { |_, v| v.to_s.strip.empty? }
174
+ end
175
+
176
+ def build_metadata(job)
177
+ {
178
+ "args" => filter_args(args_for_metadata(job)),
179
+ "queue" => job["queue"],
180
+ "jid" => job["jid"],
181
+ "enqueued_at" => job["enqueued_at"],
182
+ "created_at" => job["created_at"],
183
+ "retry" => job["retry"],
184
+ "retry_count" => job["retry_count"]
185
+ }.compact
186
+ end
187
+
188
+ def worker_name(job, worker)
189
+ # ActiveJob jobs land here with class = "ActiveJob::QueueAdapters::
190
+ # SidekiqAdapter::JobWrapper" and the real class in `wrapped`.
191
+ # Surface the real class so the issues UI doesn't group every AJ job
192
+ # under a single useless wrapper name.
193
+ job["wrapped"] || job["class"] || worker&.class&.name || "Unknown"
194
+ end
195
+
196
+ def args_for_metadata(job)
197
+ if job["wrapped"]
198
+ # ActiveJob: real args live at args[0]["arguments"].
199
+ wrapper = Array(job["args"]).first
200
+ wrapper.is_a?(Hash) ? (wrapper["arguments"] || []) : []
201
+ else
202
+ job["args"] || []
203
+ end
204
+ end
205
+
206
+ def filter_args(args)
207
+ return [] unless args.is_a?(Array)
208
+ args.map { |arg| filter_one(arg) }
209
+ end
210
+
211
+ def filter_one(arg)
212
+ case arg
213
+ when Hash then filter_hash(arg)
214
+ when Array then arg.map { |a| filter_one(a) }
215
+ else truncate_value(arg)
216
+ end
217
+ end
218
+
219
+ def filter_hash(hash)
220
+ filter = self.class.parameter_filter
221
+ normalized = hash.transform_keys(&:to_s)
222
+ filtered = filter ? filter.filter(normalized) : normalized
223
+ filtered.transform_values { |v| filter_one(v) }
224
+ rescue StandardError
225
+ { "_unfilterable" => "[#{hash.class.name}]" }
226
+ end
227
+
228
+ def truncate_value(value)
229
+ str = value.to_s
230
+ return value if str.bytesize <= MAX_ARG_BYTES
231
+ "[truncated #{value.class.name} #{str.bytesize}b]"
232
+ end
233
+
234
+ # Cached at class level — Rails.application.config.filter_parameters
235
+ # is effectively immutable after boot; rebuilding the ParameterFilter
236
+ # per job is wasted allocation on the hot path.
237
+ def self.parameter_filter
238
+ return @parameter_filter if defined?(@parameter_filter)
239
+ @parameter_filter =
240
+ if defined?(::ActiveSupport::ParameterFilter) && defined?(::Rails) && ::Rails.application
241
+ ::ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ Errsight::Sidekiq.configure_integration! if defined?(::Sidekiq)
@@ -0,0 +1,107 @@
1
+ module Errsight
2
+ # Reads source files to attach `pre_context`, `context_line`, and
3
+ # `post_context` to in_app backtrace frames — the snippet of code around
4
+ # the failing line that turns "stack trace at user.rb:42" into "here's
5
+ # what user.rb:42 looked like when it crashed."
6
+ #
7
+ # Reference: sentry-ruby's `Sentry::LineCache`. Their cache is per-process
8
+ # bounded LRU; ours is the same idea, smaller, no dependencies.
9
+ module SourceContext
10
+ # 5 lines before, 5 lines after — sentry's default and a comfortable
11
+ # debugging window. Larger values bloat events without adding value.
12
+ PRE_CONTEXT_LINES = 5
13
+ POST_CONTEXT_LINES = 5
14
+
15
+ # Cap each emitted line to keep events bounded. A 1MB minified JS line
16
+ # accidentally landing in a Ruby backtrace (it happens via Sprockets
17
+ # asset pipeline errors) shouldn't blow our 512KB ingestion limit.
18
+ MAX_LINE_BYTES = 256
19
+
20
+ # LRU bound. 100 files × ~100 lines avg × ~80 bytes per line = ~800KB
21
+ # cache footprint per process. Cheap. A request typically reads <10
22
+ # unique files for context; the cache absorbs cross-request sharing.
23
+ CACHE_SIZE = 100
24
+
25
+ class << self
26
+ # Returns { pre_context:, context_line:, post_context: } or nil if the
27
+ # file can't be read (missing, eval'd, internal, permission denied,
28
+ # malformed encoding, …). Never raises — source-context failure must
29
+ # not cascade into a failed event capture.
30
+ def fetch(filename, lineno)
31
+ return nil unless filename.is_a?(String) && !filename.empty?
32
+ return nil unless lineno.is_a?(Integer) && lineno > 0
33
+ # Synthetic frames have no readable source.
34
+ return nil if filename.start_with?("<", "(")
35
+
36
+ lines = read_lines(filename)
37
+ return nil unless lines
38
+
39
+ idx = lineno - 1
40
+ return nil if idx < 0 || idx >= lines.size
41
+
42
+ {
43
+ pre_context: slice_with_truncation(lines, [ idx - PRE_CONTEXT_LINES, 0 ].max, idx - 1),
44
+ context_line: truncate(lines[idx]),
45
+ post_context: slice_with_truncation(lines, idx + 1, [ idx + POST_CONTEXT_LINES, lines.size - 1 ].min)
46
+ }
47
+ rescue StandardError
48
+ nil
49
+ end
50
+
51
+ # Test-only: drop the cache so a test that mutates a fixture file
52
+ # gets the fresh contents on the next fetch.
53
+ def reset_cache!
54
+ @cache = nil
55
+ @order = nil
56
+ end
57
+
58
+ private
59
+
60
+ def read_lines(filename)
61
+ @cache ||= {}
62
+ @order ||= []
63
+
64
+ if @cache.key?(filename)
65
+ # LRU touch: move to most-recent end.
66
+ @order.delete(filename)
67
+ @order << filename
68
+ return @cache[filename]
69
+ end
70
+
71
+ return nil unless File.file?(filename) && File.readable?(filename)
72
+
73
+ # Force UTF-8; if the file has invalid bytes, fall back to binary
74
+ # so we don't raise. Source files in production should be clean
75
+ # UTF-8 but customer fixtures sometimes aren't.
76
+ contents = File.read(filename, mode: "r:utf-8")
77
+ contents = contents.scrub("?") unless contents.valid_encoding?
78
+
79
+ lines = contents.lines.map { |l| l.chomp }
80
+ @cache[filename] = lines
81
+ @order << filename
82
+
83
+ # Evict oldest until we're at or below the cap.
84
+ while @order.size > CACHE_SIZE
85
+ evicted = @order.shift
86
+ @cache.delete(evicted)
87
+ end
88
+
89
+ lines
90
+ rescue StandardError
91
+ # File errors (permission denied, vanished mid-read, etc.) — return
92
+ # nil so fetch returns nil so the frame just lacks source context.
93
+ nil
94
+ end
95
+
96
+ def slice_with_truncation(lines, from, to)
97
+ return [] if to < from
98
+ lines[from..to].to_a.map { |l| truncate(l) }
99
+ end
100
+
101
+ def truncate(line)
102
+ return line.to_s if line.nil?
103
+ line.bytesize > MAX_LINE_BYTES ? line.byteslice(0, MAX_LINE_BYTES) + "…[truncated]" : line
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,3 @@
1
+ module Errsight
2
+ VERSION = "0.2.2"
3
+ end
data/lib/errsight.rb ADDED
@@ -0,0 +1,193 @@
1
+ require "securerandom"
2
+ require "errsight/version"
3
+ require "errsight/configuration"
4
+ require "errsight/scope"
5
+ require "errsight/hub"
6
+ require "errsight/backtrace"
7
+ require "errsight/source_context"
8
+ require "errsight/client"
9
+ require "errsight/logger"
10
+ require "errsight/middleware"
11
+ require "errsight/capture_middleware"
12
+ require "errsight/railtie" if defined?(Rails)
13
+
14
+ module Errsight
15
+ class Error < StandardError; end
16
+ class ConfigurationError < Error; end
17
+
18
+ class << self
19
+ def configuration
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ def configure
24
+ yield configuration
25
+ configuration.validate!
26
+ configuration
27
+ end
28
+
29
+ def client
30
+ @client ||= Client.new(configuration)
31
+ end
32
+
33
+ def hub
34
+ Hub.current
35
+ end
36
+
37
+ def current_scope
38
+ hub.current_scope
39
+ end
40
+
41
+ # Push a fresh scope for the duration of the block, then pop on exit.
42
+ # Used by request and job middleware so user/tags/breadcrumbs set during
43
+ # one unit of work don't leak to the next one handled by the same thread.
44
+ # Pass an explicit Scope to install a pre-built one (e.g. rehydrated
45
+ # from a Sidekiq job payload).
46
+ def with_scope(scope = nil, &block)
47
+ hub.with_scope(scope, &block)
48
+ end
49
+
50
+ # Public scope mutators delegate to the current (top-of-stack) scope.
51
+ # Pre-scope-stack callers wrote directly to Thread.current and relied on
52
+ # the request middleware to clear; those reads bled across requests on
53
+ # long-lived Puma/Sidekiq threads. The stack guarantees cleanup at the
54
+ # block boundary regardless of caller hygiene.
55
+ def set_user(user); current_scope.set_user(user); end
56
+ def clear_user; current_scope.clear_user; end
57
+ def set_tag(key, value); current_scope.set_tag(key, value); end
58
+ def set_tags(tags); current_scope.set_tags(tags); end
59
+ def clear_tags; current_scope.clear_tags; end
60
+ def add_breadcrumb(**kwargs); current_scope.add_breadcrumb(**kwargs); end
61
+ def clear_breadcrumbs; current_scope.clear_breadcrumbs; end
62
+
63
+ def log(level:, message:, backtrace: nil, environment: nil, metadata: {},
64
+ occurred_at: nil, fingerprint: nil, user: nil, tags: nil, release: nil)
65
+ return unless configuration.enabled?
66
+ return if level_below_threshold?(level)
67
+
68
+ scope = current_scope
69
+ event = {
70
+ ingestion_id: SecureRandom.uuid,
71
+ level: level.to_s,
72
+ message: message.to_s,
73
+ backtrace: backtrace,
74
+ environment: environment || configuration.environment,
75
+ metadata: metadata,
76
+ occurred_at: (occurred_at || Time.now).iso8601(3),
77
+ release: release || configuration.release,
78
+ user: user || scope.user,
79
+ tags: merge_tags(scope.tags, tags),
80
+ breadcrumbs: scope.breadcrumbs
81
+ }
82
+ event[:fingerprint] = fingerprint if fingerprint
83
+ event.compact!
84
+
85
+ event = run_before_send(event)
86
+ return if event.nil?
87
+
88
+ client.enqueue(event)
89
+ end
90
+
91
+ def capture_exception(exception, metadata: {}, fingerprint: nil, user: nil, tags: nil)
92
+ return unless exception.is_a?(Exception)
93
+
94
+ enriched_metadata = metadata.merge(exception_class: exception.class.to_s)
95
+ causes = walk_exception_causes(exception)
96
+ enriched_metadata[:exception_causes] = causes if causes.any?
97
+
98
+ frames = build_frames(exception)
99
+ enriched_metadata[:exception_frames] = frames if frames.any?
100
+
101
+ log(
102
+ level: :error,
103
+ message: "#{exception.class}: #{exception.message}",
104
+ backtrace: exception.backtrace&.join("\n"),
105
+ metadata: enriched_metadata,
106
+ fingerprint: fingerprint,
107
+ user: user,
108
+ tags: tags
109
+ )
110
+ end
111
+
112
+ private
113
+
114
+ # Hard cap on cause-chain depth. Exception#cause can technically loop if
115
+ # someone constructs a pathological chain manually; the seen-set guards
116
+ # against that, but the depth cap is the load-bearing protection.
117
+ MAX_CAUSE_DEPTH = 5
118
+ MAX_CAUSE_BACKTRACE_FRAMES = 20
119
+
120
+ # Parse the exception's backtrace into structured frames + attach
121
+ # source context to in_app frames. Ships in metadata[:exception_frames]
122
+ # alongside the legacy `backtrace` string so the backend can render
123
+ # whichever it knows how to show. Source context is the
124
+ # debugging-experience differentiator: stacks become "here's the line
125
+ # of your code that failed, with 5 lines of surrounding context."
126
+ def build_frames(exception)
127
+ raw = exception.backtrace
128
+ return [] unless raw.is_a?(Array) && raw.any?
129
+
130
+ frames = Backtrace.parse(raw)
131
+ frames.each do |frame|
132
+ next unless frame[:in_app] && frame[:abs_path]
133
+ ctx = SourceContext.fetch(frame[:abs_path], frame[:lineno])
134
+ frame.merge!(ctx) if ctx
135
+ end
136
+ frames
137
+ rescue StandardError
138
+ # Frame parsing must never break capture. Fall back to no structured
139
+ # frames; the legacy backtrace string still ships.
140
+ []
141
+ end
142
+
143
+ # Ruby chains exceptions via #cause when one rescue raises another. The
144
+ # outer exception's class+message is what the user sees in the issue
145
+ # title, but the inner cause is usually what actually broke (Net::Read
146
+ # Timeout under PaymentGatewayError). Surface the chain so the issue
147
+ # detail page can show "rescued from <inner>".
148
+ def walk_exception_causes(exception)
149
+ causes = []
150
+ current = exception.cause
151
+ seen = { exception.object_id => true }
152
+ while current && causes.size < MAX_CAUSE_DEPTH && !seen[current.object_id]
153
+ seen[current.object_id] = true
154
+ causes << {
155
+ class: current.class.to_s,
156
+ message: current.message.to_s,
157
+ backtrace: current.backtrace&.first(MAX_CAUSE_BACKTRACE_FRAMES)
158
+ }
159
+ current = current.cause
160
+ end
161
+ causes
162
+ end
163
+
164
+ # Customer-supplied final-mile filter. If it returns nil, the event is
165
+ # dropped silently. If it raises, we log the error and pass the event
166
+ # through unmodified — silently dropping production errors because the
167
+ # customer's filter has a bug is worse than the bug itself.
168
+ def run_before_send(event)
169
+ filter = configuration.before_send
170
+ return event unless filter.respond_to?(:call)
171
+ result = filter.call(event)
172
+ result.is_a?(Hash) ? result : nil
173
+ rescue StandardError => e
174
+ configuration.logger&.warn(
175
+ "[Errsight] before_send raised #{e.class}: #{e.message} — passing event through unmodified"
176
+ )
177
+ event
178
+ end
179
+
180
+ def merge_tags(scope_tags, per_call)
181
+ return scope_tags if per_call.nil? || per_call.empty?
182
+ scope_tags.merge(per_call.transform_keys(&:to_s).transform_values(&:to_s))
183
+ end
184
+
185
+ def level_below_threshold?(level)
186
+ level_value(level) < level_value(configuration.min_level)
187
+ end
188
+
189
+ def level_value(level)
190
+ %i[debug info warning error fatal].index(level.to_sym) || 0
191
+ end
192
+ end
193
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: errsight
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Errsight
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: concurrent-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ description: A lightweight Ruby gem that hooks into Rails.logger and sends logs/errors
27
+ to the Errsight API.
28
+ email:
29
+ - support@errsight.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - errsight.gemspec
38
+ - lib/errsight.rb
39
+ - lib/errsight/backtrace.rb
40
+ - lib/errsight/capture_middleware.rb
41
+ - lib/errsight/client.rb
42
+ - lib/errsight/configuration.rb
43
+ - lib/errsight/hub.rb
44
+ - lib/errsight/integrations/active_job.rb
45
+ - lib/errsight/integrations/active_record.rb
46
+ - lib/errsight/integrations/rails_error_reporter.rb
47
+ - lib/errsight/logger.rb
48
+ - lib/errsight/middleware.rb
49
+ - lib/errsight/railtie.rb
50
+ - lib/errsight/scope.rb
51
+ - lib/errsight/sidekiq.rb
52
+ - lib/errsight/source_context.rb
53
+ - lib/errsight/version.rb
54
+ homepage: https://errsight.com
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://errsight.com
59
+ source_code_uri: https://github.com/errsight/errsight-ruby
60
+ bug_tracker_uri: https://github.com/errsight/errsight-ruby/issues
61
+ changelog_uri: https://github.com/errsight/errsight-ruby/blob/main/CHANGELOG.md
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '3.0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 4.0.3
77
+ specification_version: 4
78
+ summary: Ruby/Rails client for Errsight error tracking
79
+ test_files: []