condux 0.1.7 → 0.2.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 +4 -4
- data/README.md +17 -2
- data/lib/condux/client.rb +6 -1
- data/lib/condux/modules.rb +70 -0
- data/lib/condux/rack.rb +34 -4
- data/lib/condux.rb +7 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 985adaf0c8807f4c03e4c83b2d4969bf37d505f7abba46d06cbb6de95bb83ff5
|
|
4
|
+
data.tar.gz: 466b2a9ddcdc2056a6bf38db174fabad21c3a920f6e3bea090c1a4b0bda493e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a638998e2b15a3f7c90c1c8c632ee8c400892ad848aeba80a4361eec7d178e2b5add4a00057c393042dded2b32d5244f2ae55958efad1c5c6aab7d3eb9256081
|
|
7
|
+
data.tar.gz: 6513420fa59559a2e5cee3237d6c91ed254342d45110348a18a15b4feab59924214c87c21823e84551803a73f85c752c0c000706cd1e0f7e33ecf4cedcff1ef4
|
data/README.md
CHANGED
|
@@ -92,12 +92,27 @@ Condux.capture_exception(error, request: { "url" => "/api/sync" }, tags: { "job"
|
|
|
92
92
|
require "condux/rack"
|
|
93
93
|
use Condux::Rack::CaptureExceptions
|
|
94
94
|
|
|
95
|
-
# Rails (config/application.rb)
|
|
96
|
-
|
|
95
|
+
# Rails (config/application.rb), the require at the top of the file
|
|
96
|
+
require "condux/rack"
|
|
97
|
+
config.middleware.use Condux::Rack::CaptureExceptions
|
|
97
98
|
```
|
|
98
99
|
|
|
99
100
|
Uncaught exceptions are reported as unhandled and re-raised, so the app's own error handling still runs.
|
|
100
101
|
|
|
102
|
+
**On Rails, caller-caused exceptions are re-raised but not reported.** Rails already classifies them:
|
|
103
|
+
`ActionDispatch::ExceptionWrapper.rescue_responses` maps an exception class to a status, and anything it
|
|
104
|
+
answers with a 4xx describes what the client sent rather than a defect in your app. A malformed JSON
|
|
105
|
+
body and a bad percent-encoded query both land there, and anyone can send those at will, so filing them
|
|
106
|
+
would let a stranger bury your real errors. Add your own with
|
|
107
|
+
`config.action_dispatch.rescue_responses`, which Condux reads too, so one setting governs your error
|
|
108
|
+
pages and your reporting together. Anything Rails does not classify still reports, since the registry
|
|
109
|
+
defaults to 500. Under bare Rack there is no such registry, so nothing is filtered.
|
|
110
|
+
|
|
111
|
+
**Pass the class, not its name as a string.** Rails builds each middleware with `klass.new(app)`, so a
|
|
112
|
+
string aborts boot with `undefined method 'new' for an instance of String`. Rails deprecated string
|
|
113
|
+
middleware in 5.0 and removed it in 5.1, so no supported version accepts it. The `require` matters too:
|
|
114
|
+
Bundler loads `condux`, which does not define `Condux::Rack`.
|
|
115
|
+
|
|
101
116
|
**On Rails use `config.middleware.use`, and nothing else.** `use` appends, which puts the middleware at
|
|
102
117
|
the bottom of the stack, inside `ActionDispatch::ShowExceptions`. That position is why it works:
|
|
103
118
|
`ShowExceptions` catches a controller exception and turns it into a 500, so anything above it never sees
|
data/lib/condux/client.rb
CHANGED
|
@@ -13,12 +13,14 @@ module Condux
|
|
|
13
13
|
class Client
|
|
14
14
|
DEFAULT_MAX_RETRIES = 3
|
|
15
15
|
|
|
16
|
-
def initialize(dsn:, environment:, release:, max_retries:, transport:, sleep:, clock
|
|
16
|
+
def initialize(dsn:, environment:, release:, max_retries:, transport:, sleep:, clock:,
|
|
17
|
+
send_modules: true)
|
|
17
18
|
parsed = Dsn.parse(dsn)
|
|
18
19
|
@transport = EventTransport.new(parsed.store_url, parsed.public_key, max_retries, transport, sleep)
|
|
19
20
|
@environment = environment
|
|
20
21
|
@release = release
|
|
21
22
|
@clock = clock || -> { Time.now }
|
|
23
|
+
@send_modules = send_modules
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
# +request+ (url/method/query_string) and +tags+ describe this one event. They are passed here
|
|
@@ -43,6 +45,9 @@ module Condux
|
|
|
43
45
|
"platform" => "ruby",
|
|
44
46
|
"level" => level,
|
|
45
47
|
}.merge(Scope.fields)
|
|
48
|
+
# The runtime dependency inventory (ADR-0041). Collected HERE rather than at init because
|
|
49
|
+
# Gem.loaded_specs reports activated gems, and at boot almost nothing is activated yet.
|
|
50
|
+
event.merge!(Modules.fields(event["timestamp"])) if @send_modules
|
|
46
51
|
event["request"] = request if request && !request.empty?
|
|
47
52
|
# Merged over the ambient tags rather than replacing them, so a per-event tag cannot silently drop
|
|
48
53
|
# the deployment-wide ones.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Condux
|
|
4
|
+
# The runtime dependency inventory that rides events (ADR-0041): which gem versions are actually
|
|
5
|
+
# loaded, as opposed to which ones a Gemfile declares. The relay indexes this so a security advisory
|
|
6
|
+
# can be answered with "and you are running 2.3.0 in production" rather than only "your lockfile
|
|
7
|
+
# says so".
|
|
8
|
+
#
|
|
9
|
+
# <b>This one is collected lazily, unlike the other SDKs, and that is not an inconsistency.</b>
|
|
10
|
+
# Gem.loaded_specs reports the gems that have been ACTIVATED, not the ones installed. Measured in a
|
|
11
|
+
# bare interpreter it holds a single entry, because nothing has been required yet. Collecting during
|
|
12
|
+
# Condux.init, which an application typically calls early in boot, would therefore report almost
|
|
13
|
+
# nothing. Collecting at capture time reports what was genuinely loaded by the moment the error
|
|
14
|
+
# happened, which is both correct and the truest reading of what this feature claims to measure. It
|
|
15
|
+
# costs nothing: loaded_specs is an in-memory hash, with no filesystem behind it.
|
|
16
|
+
module Modules
|
|
17
|
+
# The most entries carried on one event. The cap applies after sorting, so which entries survive is
|
|
18
|
+
# stable across events rather than varying with hash order: the server sees one consistent set
|
|
19
|
+
# instead of a shifting sample.
|
|
20
|
+
MAX_MODULES = 1000
|
|
21
|
+
|
|
22
|
+
# How long to wait before repeating the inventory on another event.
|
|
23
|
+
#
|
|
24
|
+
# This is what makes the feature affordable. The server deduplicates a release's inventory down to
|
|
25
|
+
# one row per package per day, so attaching the whole map to every event would spend bytes for
|
|
26
|
+
# nothing. Repeating on an interval rather than sending once keeps the robustness that every-event
|
|
27
|
+
# buys: the event carrying the inventory can be dropped by a rate limit or a quota rejection
|
|
28
|
+
# before anything parses it, so a single attempt per process would lose that day's inventory.
|
|
29
|
+
MODULES_INTERVAL_SECONDS = 15 * 60
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# The loaded gems as a name to version hash, empty when they cannot be read.
|
|
34
|
+
#
|
|
35
|
+
# Never raises. This runs inside capture, which is already handling somebody's error, so a broken
|
|
36
|
+
# spec costs that entry and nothing else.
|
|
37
|
+
def collect
|
|
38
|
+
found = {}
|
|
39
|
+
Gem.loaded_specs.each do |name, spec|
|
|
40
|
+
version = spec.version.to_s
|
|
41
|
+
found[name.to_s] = version if name && !name.to_s.empty? && !version.empty?
|
|
42
|
+
rescue StandardError
|
|
43
|
+
# One unreadable spec is not a reason to report nothing about the rest.
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
found
|
|
47
|
+
rescue StandardError, NameError
|
|
48
|
+
# No RubyGems at all (a packaged binary, a trimmed runtime). Unknown is the honest answer.
|
|
49
|
+
{}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The inventory's contribution to an event: the full map on the first event and then at most once
|
|
53
|
+
# per MODULES_INTERVAL_SECONDS, and an empty hash otherwise, so an event that carries nothing keeps
|
|
54
|
+
# its exact previous wire shape.
|
|
55
|
+
def fields(now)
|
|
56
|
+
return {} if @last_attached_at && (now - @last_attached_at) < MODULES_INTERVAL_SECONDS
|
|
57
|
+
|
|
58
|
+
loaded = collect
|
|
59
|
+
return {} if loaded.empty?
|
|
60
|
+
|
|
61
|
+
@last_attached_at = now
|
|
62
|
+
{ "modules" => loaded.sort.first(MAX_MODULES).to_h }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Reset the interval. Tests only; a process loads its gems once.
|
|
66
|
+
def reset
|
|
67
|
+
@last_attached_at = nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/condux/rack.rb
CHANGED
|
@@ -11,8 +11,15 @@ module Condux
|
|
|
11
11
|
# require "condux/rack"
|
|
12
12
|
# use Condux::Rack::CaptureExceptions
|
|
13
13
|
#
|
|
14
|
-
# # Rails (config/application.rb)
|
|
15
|
-
#
|
|
14
|
+
# # Rails (config/application.rb), the require at the top of the file
|
|
15
|
+
# require "condux/rack"
|
|
16
|
+
# config.middleware.use Condux::Rack::CaptureExceptions
|
|
17
|
+
#
|
|
18
|
+
# PASS THE CLASS, NOT ITS NAME AS A STRING. Rails builds each entry with `klass.new(app)`, so a
|
|
19
|
+
# String argument aborts boot with `undefined method 'new' for an instance of String`. Rails
|
|
20
|
+
# deprecated string middleware in 5.0 and removed the constantize in 5.1, so there is no supported
|
|
21
|
+
# version where the string form works. The require is needed too: Bundler loads `condux`, which
|
|
22
|
+
# does not define `Condux::Rack`, and without it boot fails on an uninitialized constant.
|
|
16
23
|
#
|
|
17
24
|
# ON RAILS, USE `config.middleware.use` AND NOTHING ELSE.
|
|
18
25
|
#
|
|
@@ -36,12 +43,35 @@ module Condux
|
|
|
36
43
|
# left to process state.
|
|
37
44
|
Condux.request_scope do
|
|
38
45
|
@app.call(env)
|
|
39
|
-
rescue StandardError => e # report
|
|
40
|
-
|
|
46
|
+
rescue StandardError => e # report what the app got wrong, then re-raise whatever it was
|
|
47
|
+
unless self.class.caller_caused?(e)
|
|
48
|
+
Condux.capture_exception(e, handled: false, request: self.class.request_fields(env))
|
|
49
|
+
end
|
|
41
50
|
raise
|
|
42
51
|
end
|
|
43
52
|
end
|
|
44
53
|
|
|
54
|
+
# True when the framework classifies this exception as the CALLER's mistake, not the app's.
|
|
55
|
+
#
|
|
56
|
+
# Rails keeps that classification in ActionDispatch::ExceptionWrapper.rescue_responses, a public
|
|
57
|
+
# registry of exception class name to status. Asking it means there is no list of our own to keep
|
|
58
|
+
# extending, and `config.action_dispatch.rescue_responses` merges into the same registry, so an
|
|
59
|
+
# app's own classifications are honoured with no API from us.
|
|
60
|
+
#
|
|
61
|
+
# The registry defaults to :internal_server_error, so anything Rails does not recognise still
|
|
62
|
+
# reports: the rule fails toward reporting rather than toward silence. Measured against Rails
|
|
63
|
+
# 8.1.3.1, it answers 500 for an unknown class, an empty string and nil, and raises for none of
|
|
64
|
+
# them, which is why there is no defensive rescue here to go stale.
|
|
65
|
+
#
|
|
66
|
+
# Bare Rack has no equivalent registry, so with ActionDispatch absent nothing is filtered and the
|
|
67
|
+
# behaviour is exactly as before. See ADR-0044.
|
|
68
|
+
def self.caller_caused?(error)
|
|
69
|
+
return false unless defined?(ActionDispatch::ExceptionWrapper)
|
|
70
|
+
|
|
71
|
+
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
|
|
72
|
+
status.is_a?(Integer) && status >= 400 && status < 500
|
|
73
|
+
end
|
|
74
|
+
|
|
45
75
|
# The request, in the Sentry store shape the relay parses.
|
|
46
76
|
#
|
|
47
77
|
# Deliberately only the path, method and query string. Rack puts headers in HTTP_* keys, including
|
data/lib/condux.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "condux/level"
|
|
4
4
|
require_relative "condux/send_result"
|
|
5
|
+
require_relative "condux/modules"
|
|
5
6
|
require_relative "condux/scope"
|
|
6
7
|
require_relative "condux/client"
|
|
7
8
|
|
|
@@ -17,9 +18,13 @@ module Condux
|
|
|
17
18
|
# Configure the SDK with a project DSN (and optional testing hooks). Raises on a malformed DSN:
|
|
18
19
|
# setup runs once at developer time, so a typo is worth failing loudly for.
|
|
19
20
|
def init(dsn:, environment: nil, release: nil, max_retries: Client::DEFAULT_MAX_RETRIES,
|
|
20
|
-
transport: nil, sleep: nil, clock: nil)
|
|
21
|
+
transport: nil, sleep: nil, clock: nil, send_modules: true)
|
|
21
22
|
@client = Client.new(dsn: dsn, environment: environment, release: release,
|
|
22
|
-
max_retries: max_retries, transport: transport, sleep: sleep, clock: clock
|
|
23
|
+
max_retries: max_retries, transport: transport, sleep: sleep, clock: clock,
|
|
24
|
+
send_modules: send_modules)
|
|
25
|
+
# A re-init starts the interval afresh, so a new client does not inherit the previous one's
|
|
26
|
+
# "already sent recently" state and skip its first event.
|
|
27
|
+
Modules.reset
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
# Report an exception as an error-level event, with its stack trace. Never raises on delivery failure.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: condux
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Condux
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'The Condux SDK for Ruby: report errors to a Condux relay with a resilient,
|
|
14
14
|
never-raising transport. Zero runtime dependencies (standard library only).'
|
|
@@ -27,6 +27,7 @@ files:
|
|
|
27
27
|
- lib/condux/event_payload.rb
|
|
28
28
|
- lib/condux/event_transport.rb
|
|
29
29
|
- lib/condux/level.rb
|
|
30
|
+
- lib/condux/modules.rb
|
|
30
31
|
- lib/condux/rack.rb
|
|
31
32
|
- lib/condux/scope.rb
|
|
32
33
|
- lib/condux/send_result.rb
|