errorgap 0.9.0 → 0.10.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: c01da4468e72e4df60f1a720efa5f8c5873357a2681c3c8163b97c83de0489f4
4
- data.tar.gz: 11274b3058d605600c50fcac50f3e9dc7e9f118d5f8d1ab673d6b754d3e039d6
3
+ metadata.gz: f52bfa8726abcfbc207b98bc4756db649a619595a56298d4c06b73c63d7b8d08
4
+ data.tar.gz: bf7d11b852e1fffe27e928ccc259fa3589d35997e4d4eeabfa609c19020a16f6
5
5
  SHA512:
6
- metadata.gz: 0b438eac927e6e9fe023551f5106b73a4f4e160af91acc90940686fbae4924ba33216ff524d224145c23370cdc398b47bfac644920c28a9edeab8c4e2a29a201
7
- data.tar.gz: 521300e7615cce7765d24e98563368815b2cd7d6cb8397205988b3bcd2673d378bd14bad26ec5845d87dd20cc639b882fc865e4699efe287dee5dc078f76a93d
6
+ metadata.gz: 32f5f975ae8555dc20ee6cdde6618ebe5b001ecf89b51bb59e4f4b72cd3a2d64747c5814c60ab590fe26adbfb23f5923dd2c45511c2c22ad4c3b518877917368
7
+ data.tar.gz: 7aec10caf7eef47f0e593266cfe554ef67aab2c2fb32e03320cfd3e7c1fad88db40b9ececff9241dbb136858c3f85830fe895b6dc6442d789b474cf8c19252bd
data/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.10.0] - 2026-10-13
9
+
10
+ ### Added
11
+
12
+ - **Remote settings.** The gem follows the project's settings in errorgap
13
+ (Project settings › What SDKs send): when errors, performance data or logs
14
+ are switched off there, running apps stop sending them within ten minutes,
15
+ with no deploy. Read on a background thread from
16
+ `GET /api/projects/:slug/config`; if errorgap cannot be reached everything
17
+ stays on. `config.remote_settings = false` (or
18
+ `ERRORGAP_REMOTE_SETTINGS=false`) turns this off.
19
+ - **Resque.** Failed jobs are reported with their class, queue and arguments,
20
+ alongside Resque's own failure backend.
21
+ - **Delayed Job.** Jobs are timed as `job` transactions and their errors
22
+ reported with the job class, queue, id and attempt count.
23
+ - **Rake.** An error that ends a task is reported with the task's name and
24
+ flushed before Rake exits.
25
+
26
+ In Rails all three are installed for you. Elsewhere:
27
+ `Errorgap::Integrations::Resque.install`, `…::DelayedJob.install`,
28
+ `…::Rake.install`.
29
+
8
30
  ## [0.9.0] - 2026-10-13
9
31
 
10
32
  ### Added
data/README.md CHANGED
@@ -62,6 +62,18 @@ Errorgap::Integrations::ActiveJob.install
62
62
  Errorgap::Integrations::Sidekiq.install
63
63
  ```
64
64
 
65
+ Resque, Delayed Job and Rake tasks are covered the same way (in Rails with no
66
+ setup; elsewhere `Errorgap::Integrations::Resque.install`,
67
+ `Errorgap::Integrations::DelayedJob.install`,
68
+ `Errorgap::Integrations::Rake.install`).
69
+
70
+ ## Remote settings
71
+
72
+ Errors, performance data and logs can each be switched off for a project in
73
+ errorgap (Project settings › What SDKs send). Running apps follow within ten
74
+ minutes, with no deploy. If errorgap cannot be reached, everything stays on.
75
+ `config.remote_settings = false` ignores the project's switches.
76
+
65
77
  ## Users
66
78
 
67
79
  Errors raised in a request carry the signed-in Warden / Devise user's id and
@@ -23,6 +23,7 @@ module Errorgap
23
23
  :track_release,
24
24
  :release_sha,
25
25
  :release_branch,
26
+ :remote_settings,
26
27
  :auth_events,
27
28
  :auth_user,
28
29
  :app_name
@@ -62,6 +63,10 @@ module Errorgap
62
63
  # request (Warden / Devise).
63
64
  @capture_user = true
64
65
  @before_notify_hooks = []
66
+ # Follow the project's settings in errorgap (Project settings › What
67
+ # SDKs send): stop sending errors, performance data or logs when they
68
+ # are switched off there.
69
+ @remote_settings = ENV["ERRORGAP_REMOTE_SETTINGS"].to_s.strip.downcase != "false"
65
70
  # Sign-ins to this app (Security › Logins). Off until you opt in: they
66
71
  # carry user names and IPs.
67
72
  @auth_events = false
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Errorgap
4
+ module Integrations
5
+ # Delayed Job: every job runs as a `job` transaction and an error it
6
+ # raises is reported with the job's class, queue, id and attempt count,
7
+ # then re-raised so Delayed Job's retries work as before.
8
+ #
9
+ # Errorgap::Integrations::DelayedJob.install # after Delayed Job is loaded
10
+ module DelayedJob
11
+ ACTIVE_JOB_WRAPPER = "ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper"
12
+
13
+ class << self
14
+ def install
15
+ return false unless defined?(::Delayed::Plugin) && defined?(::Delayed::Worker)
16
+ return true if @installed
17
+
18
+ ::Delayed::Worker.plugins << plugin
19
+ @installed = true
20
+ end
21
+
22
+ def plugin
23
+ @plugin ||= Class.new(::Delayed::Plugin) do
24
+ callbacks do |lifecycle|
25
+ lifecycle.around(:invoke_job) do |job, *args, &block|
26
+ Errorgap::Integrations::DelayedJob.invoke(job) { block.call(job, *args) }
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def invoke(job, &block)
33
+ job_class = job_name(job)
34
+ # An ActiveJob run by Delayed Job is covered by the ActiveJob
35
+ # integration, which knows the real job class and its arguments.
36
+ return yield if job_class == ACTIVE_JOB_WRAPPER && ActiveJob.installed?
37
+
38
+ Errorgap.track_job(job_class, queue: (job.respond_to?(:queue) && job.queue) || "default") do
39
+ run(job, job_class, &block)
40
+ end
41
+ ensure
42
+ Errorgap.clear_user
43
+ end
44
+
45
+ private
46
+
47
+ def run(job, job_class)
48
+ yield
49
+ rescue Exception => exception # rubocop:disable Lint/RescueException
50
+ Errorgap.notify_once(
51
+ exception,
52
+ context: { component: "delayed_job", action: job_class },
53
+ environment: {
54
+ queue: job.respond_to?(:queue) ? job.queue : nil,
55
+ job_id: job.respond_to?(:id) ? job.id : nil,
56
+ attempts: job.respond_to?(:attempts) ? job.attempts : nil
57
+ }.compact
58
+ )
59
+ raise
60
+ end
61
+
62
+ # `Mailer.delay.welcome(user)` is a PerformableMethod: name it by
63
+ # what it calls rather than by the wrapper.
64
+ def job_name(job)
65
+ payload = job.payload_object
66
+ return payload.display_name.to_s if payload.respond_to?(:display_name)
67
+
68
+ payload.class.name.to_s
69
+ rescue StandardError
70
+ "Delayed::Job"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Errorgap
4
+ module Integrations
5
+ # Rake: report an error that ends a task, with the task's name, then
6
+ # re-raise it. The report is flushed before Rake exits.
7
+ #
8
+ # require "errorgap"
9
+ # Errorgap::Integrations::Rake.install # in the Rakefile
10
+ #
11
+ # In Rails this is installed for you.
12
+ module Rake
13
+ class << self
14
+ def install
15
+ return false unless defined?(::Rake::Task)
16
+ return true if @installed
17
+
18
+ ::Rake::Task.prepend(Patch)
19
+ @installed = true
20
+ end
21
+ end
22
+
23
+ module Patch
24
+ def execute(args = nil)
25
+ super
26
+ rescue SystemExit, Interrupt
27
+ raise
28
+ rescue Exception => exception # rubocop:disable Lint/RescueException
29
+ begin
30
+ Errorgap.notify_once(
31
+ exception,
32
+ context: { component: "rake", action: name.to_s },
33
+ params: { "arguments" => args.respond_to?(:to_a) ? args.to_a : nil }
34
+ )
35
+ Errorgap.flush
36
+ rescue StandardError
37
+ nil
38
+ end
39
+ raise
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Errorgap
4
+ module Integrations
5
+ # Resque: report failed jobs with their class, queue and arguments.
6
+ # Resque's own failure backend (the Failed tab) keeps working alongside.
7
+ #
8
+ # Errorgap::Integrations::Resque.install # after Resque is loaded
9
+ module Resque
10
+ class << self
11
+ def install
12
+ return false unless defined?(::Resque::Failure::Base)
13
+ return true if @installed
14
+
15
+ require "resque/failure/multiple"
16
+ current = ::Resque::Failure.backend
17
+ backends = current == ::Resque::Failure::Multiple ? ::Resque::Failure::Multiple.classes : [current]
18
+ ::Resque::Failure::Multiple.classes = (backends + [backend]).uniq
19
+ ::Resque::Failure.backend = ::Resque::Failure::Multiple
20
+ @installed = true
21
+ end
22
+
23
+ # The failure backend, defined on first use so loading errorgap does
24
+ # not need Resque.
25
+ def backend
26
+ @backend ||= Class.new(::Resque::Failure::Base) do
27
+ def save
28
+ job_class = payload.is_a?(Hash) ? payload["class"].to_s : ""
29
+ Errorgap.notify_once(
30
+ exception,
31
+ context: { component: "resque", action: job_class },
32
+ environment: { queue: queue.to_s },
33
+ params: { "arguments" => payload.is_a?(Hash) ? payload["args"] : nil }
34
+ )
35
+ end
36
+
37
+ def self.count(*)
38
+ 0
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -21,6 +21,7 @@ module Errorgap
21
21
  def log(message, level: "info", source: nil, environment: nil, occurred_at: nil, sync: false)
22
22
  return unless @configuration.logs_enabled
23
23
  return if @configuration.ignored_environment?
24
+ return unless Errorgap.remote_settings.enabled?(:logs)
24
25
 
25
26
  normalized = self.class.normalize_level(level)
26
27
  return if self.class.rank(normalized) < self.class.rank(self.class.normalize_level(@configuration.minimum_log_level))
@@ -19,6 +19,9 @@ module Errorgap
19
19
  def notify(error, context: {}, environment: {}, session: {}, params: {}, breadcrumbs: [], sync: false)
20
20
  @configuration.validate!
21
21
  return Response.new(status: 202, body: "ignored environment") if @configuration.ignored_environment?
22
+ unless Errorgap.remote_settings.enabled?(:errors)
23
+ return Response.new(status: 202, body: "errors are switched off for this project")
24
+ end
22
25
 
23
26
  notice = Notice.from_exception(
24
27
  error,
@@ -31,6 +31,13 @@ module Errorgap
31
31
  initializer "errorgap.install_job_integrations" do
32
32
  Errorgap::Integrations::ActiveJob.install
33
33
  Errorgap::Integrations::Sidekiq.install
34
+ Errorgap::Integrations::Resque.install
35
+ Errorgap::Integrations::DelayedJob.install
36
+ end
37
+
38
+ # `rails some:task` / `rake some:task` errors.
39
+ rake_tasks do
40
+ Errorgap::Integrations::Rake.install
34
41
  end
35
42
 
36
43
  # After config/initializers, so `auth_events` is readable here.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Errorgap
8
+ # The project's remote settings: what errorgap wants this app to send.
9
+ # Someone can turn errors, performance data or logs off for a project in
10
+ # errorgap (Project settings › What SDKs send) and running apps follow
11
+ # within a poll interval, with no deploy.
12
+ #
13
+ # Read from `GET /api/projects/:slug/config` on a background thread, never
14
+ # on the caller's. Until the first answer, and whenever errorgap cannot be
15
+ # reached, everything stays on.
16
+ class RemoteSettings
17
+ DEFAULT_INTERVAL = 600
18
+ MIN_INTERVAL = 60
19
+ KINDS = { errors: "errors_enabled", apm: "apm_enabled", logs: "logs_enabled" }.freeze
20
+
21
+ def initialize(configuration)
22
+ @configuration = configuration
23
+ @mutex = Mutex.new
24
+ @settings = {}
25
+ @interval = DEFAULT_INTERVAL
26
+ @thread = nil
27
+ @pid = nil
28
+ end
29
+
30
+ def configure(configuration)
31
+ @configuration = configuration
32
+ end
33
+
34
+ # False only when errorgap has said to stop sending this kind of data.
35
+ def enabled?(kind)
36
+ return true unless @configuration.remote_settings
37
+
38
+ ensure_polling
39
+ @settings[KINDS.fetch(kind)] != false
40
+ end
41
+
42
+ # Fetch now, on this thread. Returns the settings, or nil when they
43
+ # could not be read.
44
+ def refresh
45
+ uri = URI.join(base_url, "api/projects/#{@configuration.project_slug}/config")
46
+ request = Net::HTTP::Get.new(uri)
47
+ request["User-Agent"] = "errorgap-ruby/#{Errorgap::VERSION}"
48
+ request["X-Errorgap-Project-Key"] = @configuration.api_key.to_s unless @configuration.api_key.to_s.empty?
49
+ timeout = @configuration.timeout.to_f.positive? ? @configuration.timeout.to_f : 5
50
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https",
51
+ open_timeout: timeout, read_timeout: timeout) do |http|
52
+ http.request(request)
53
+ end
54
+ return nil unless response.code.to_i == 200
55
+
56
+ apply(JSON.parse(response.body))
57
+ rescue StandardError
58
+ nil
59
+ end
60
+
61
+ private
62
+
63
+ def apply(body)
64
+ return nil unless body.is_a?(Hash)
65
+
66
+ @settings = body.slice(*KINDS.values)
67
+ seconds = body["poll_interval_seconds"].to_i
68
+ @interval = seconds >= MIN_INTERVAL ? seconds : DEFAULT_INTERVAL
69
+ @settings
70
+ end
71
+
72
+ # A forked child (Puma workers, Sidekiq) has the settings but not the
73
+ # thread: start its own.
74
+ def ensure_polling
75
+ return if @pid == Process.pid && @thread&.alive?
76
+ return if @configuration.project_slug.to_s.strip.empty?
77
+
78
+ @mutex.synchronize do
79
+ return if @pid == Process.pid && @thread&.alive?
80
+
81
+ @pid = Process.pid
82
+ @thread = Thread.new do
83
+ loop do
84
+ Thread.current[:errorgap_sending] = true
85
+ refresh
86
+ sleep(@interval)
87
+ end
88
+ end
89
+ @thread.name = "errorgap-settings" if @thread.respond_to?(:name=)
90
+ @thread.report_on_exception = false if @thread.respond_to?(:report_on_exception=)
91
+ end
92
+ end
93
+
94
+ def base_url
95
+ endpoint = @configuration.endpoint.to_s
96
+ endpoint.end_with?("/") ? endpoint : "#{endpoint}/"
97
+ end
98
+ end
99
+ end
@@ -12,6 +12,7 @@ module Errorgap
12
12
 
13
13
  def deliver_async(transaction)
14
14
  return if @configuration.ignored_environment?
15
+ return unless Errorgap.remote_settings.enabled?(:apm)
15
16
  return unless should_sample?
16
17
 
17
18
  Errorgap.delivery.enqueue("transactions", transaction.to_h)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errorgap
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/errorgap.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "errorgap/configuration"
6
6
  require_relative "errorgap/sanitizer"
7
7
  require_relative "errorgap/release_detector"
8
8
  require_relative "errorgap/delivery"
9
+ require_relative "errorgap/remote_settings"
9
10
  require_relative "errorgap/breadcrumbs"
10
11
  require_relative "errorgap/notifier"
11
12
  require_relative "errorgap/notice"
@@ -20,6 +21,9 @@ require_relative "errorgap/rack_middleware"
20
21
  require_relative "errorgap/integrations/sidekiq"
21
22
  require_relative "errorgap/integrations/active_job"
22
23
  require_relative "errorgap/integrations/net_http"
24
+ require_relative "errorgap/integrations/resque"
25
+ require_relative "errorgap/integrations/delayed_job"
26
+ require_relative "errorgap/integrations/rake"
23
27
  require_relative "errorgap/version"
24
28
 
25
29
  module Errorgap
@@ -27,6 +31,7 @@ module Errorgap
27
31
  def configure
28
32
  yield(configuration)
29
33
  delivery.configure(configuration)
34
+ remote_settings.configure(configuration)
30
35
  notifier.configure(configuration)
31
36
  transacter.configure(configuration)
32
37
  log_delivery.configure(configuration)
@@ -42,6 +47,11 @@ module Errorgap
42
47
  @notifier ||= Notifier.new(configuration)
43
48
  end
44
49
 
50
+ # What errorgap wants this app to send (the project's remote settings).
51
+ def remote_settings
52
+ @remote_settings ||= RemoteSettings.new(configuration)
53
+ end
54
+
45
55
  # The shared sender behind notices, transactions, logs and sign-ins.
46
56
  def delivery
47
57
  @delivery ||= Delivery.new(configuration)
@@ -210,6 +220,7 @@ module Errorgap
210
220
  def notify_transaction(transaction, sync: false)
211
221
  return unless configuration.apm_enabled
212
222
  return if configuration.ignored_environment?
223
+ return unless remote_settings.enabled?(:apm)
213
224
  return unless sample_apm?
214
225
 
215
226
  if sync || !configuration.async
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: errorgap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Errorgap
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-13 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -69,7 +69,10 @@ files:
69
69
  - lib/errorgap/configuration.rb
70
70
  - lib/errorgap/delivery.rb
71
71
  - lib/errorgap/integrations/active_job.rb
72
+ - lib/errorgap/integrations/delayed_job.rb
72
73
  - lib/errorgap/integrations/net_http.rb
74
+ - lib/errorgap/integrations/rake.rb
75
+ - lib/errorgap/integrations/resque.rb
73
76
  - lib/errorgap/integrations/sidekiq.rb
74
77
  - lib/errorgap/log_delivery.rb
75
78
  - lib/errorgap/notice.rb
@@ -77,6 +80,7 @@ files:
77
80
  - lib/errorgap/rack_middleware.rb
78
81
  - lib/errorgap/rails/railtie.rb
79
82
  - lib/errorgap/release_detector.rb
83
+ - lib/errorgap/remote_settings.rb
80
84
  - lib/errorgap/sanitizer.rb
81
85
  - lib/errorgap/sign_ins.rb
82
86
  - lib/errorgap/span_collector.rb