condux 0.1.4 → 0.1.5
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 +44 -2
- data/exe/condux +6 -0
- data/lib/condux/client.rb +2 -1
- data/lib/condux/dsn.rb +3 -2
- data/lib/condux/scope.rb +85 -0
- data/lib/condux/test_event.rb +56 -0
- data/lib/condux.rb +52 -6
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72bffbeb8549e50c7defe4c9fb3d18c5eeecd3ca9d02704e05a8bc3733e5eed9
|
|
4
|
+
data.tar.gz: 41df087b35b4f1861384e0826e43b8adc7fe42d8c921c59df35339f54ed64ec6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97dd3d4ce440838f0e67fed077def7476d2fcd8a61c20e869dc8fc90372a1137575d30ac450c555f074f30133f428deecfa81da2dd370e8f4bedca13f9f2be46
|
|
7
|
+
data.tar.gz: 9fd4ce9a8f850b83bdec18d1d70f6539eab33b42aceb636db01689d354ce59fa9d74afa5ef391156df1ad7ae1d03a6b16a09d78611d95f27df437eeb6d656490
|
data/README.md
CHANGED
|
@@ -4,7 +4,10 @@ Report errors from a Ruby app to a Condux relay. Emits the Sentry "store" wire s
|
|
|
4
4
|
normalizes it exactly like an official Sentry SDK — point it at a project DSN and it works.
|
|
5
5
|
|
|
6
6
|
Delivery is resilient (429 / 5xx / network failures retry with backoff, honoring `Retry-After`) and
|
|
7
|
-
**never raises** — a failed send returns a `SendResult`, it does not crash the caller.
|
|
7
|
+
**never raises** — a failed send returns a `SendResult`, it does not crash the caller. That holds even
|
|
8
|
+
if `init` was never called: capture warns once and drops the event, because the Rack middleware below
|
|
9
|
+
reports from inside a `rescue` and an SDK that raised there would replace your application's exception
|
|
10
|
+
with its own. A malformed DSN is refused by `init` instead, where it is a developer-time mistake.
|
|
8
11
|
|
|
9
12
|
## Usage
|
|
10
13
|
|
|
@@ -28,10 +31,49 @@ end
|
|
|
28
31
|
Condux.capture_message("cache miss storm", level: Condux::Level::WARNING)
|
|
29
32
|
```
|
|
30
33
|
|
|
34
|
+
## Verify your setup
|
|
35
|
+
|
|
36
|
+
Silence is what a broken error monitor and a healthy app look like from the outside, so prove the
|
|
37
|
+
pipeline once:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
CONDUX_DSN="https://<key>@ingest.condux.ai/<projectId>" bundle exec condux test-event
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Exit code 0 means delivered (the message appears as an info-level issue), 1 means delivery failed and
|
|
44
|
+
prints why, 2 means the DSN was missing or malformed.
|
|
45
|
+
|
|
46
|
+
## Enrichment
|
|
47
|
+
|
|
48
|
+
Attach the ambient facts triage always needs. Every subsequent event carries them, so nothing has to be
|
|
49
|
+
threaded through capture calls:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
Condux.set_user({ "id" => "1042", "email" => "dev@example.com" }) # nil clears it (sign-out)
|
|
53
|
+
Condux.set_tag("plan", "team") # nil removes the tag
|
|
54
|
+
Condux.set_context("job", { "queue" => "billing", "attempt" => 3 })
|
|
55
|
+
Condux.add_breadcrumb("charge.started", category: "billing")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The breadcrumb trail keeps the most recent 30 entries. `Condux.clear_scope` resets everything.
|
|
59
|
+
|
|
60
|
+
## Rack and Rails
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# config.ru
|
|
64
|
+
require "condux/rack"
|
|
65
|
+
use Condux::Rack::CaptureExceptions
|
|
66
|
+
|
|
67
|
+
# Rails (config/application.rb)
|
|
68
|
+
config.middleware.use "Condux::Rack::CaptureExceptions"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Uncaught exceptions are reported as unhandled and re-raised, so the app's own error handling still runs.
|
|
72
|
+
|
|
31
73
|
## Develop
|
|
32
74
|
|
|
33
75
|
```bash
|
|
34
|
-
ruby -Ilib -Itest
|
|
76
|
+
for f in test/*.rb; do ruby -Ilib -Itest "$f"; done
|
|
35
77
|
```
|
|
36
78
|
|
|
37
79
|
Zero runtime dependencies (standard library only). The transport, sleep, and clock are injectable
|
data/exe/condux
ADDED
data/lib/condux/client.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "level"
|
|
|
6
6
|
require_relative "dsn"
|
|
7
7
|
require_relative "event_payload"
|
|
8
8
|
require_relative "event_transport"
|
|
9
|
+
require_relative "scope"
|
|
9
10
|
|
|
10
11
|
module Condux
|
|
11
12
|
# A configured reporter. Cheap to hold for the process lifetime; safe for concurrent use.
|
|
@@ -36,7 +37,7 @@ module Condux
|
|
|
36
37
|
"timestamp" => @clock.call.to_f, # epoch seconds, the store convention
|
|
37
38
|
"platform" => "ruby",
|
|
38
39
|
"level" => level,
|
|
39
|
-
}
|
|
40
|
+
}.merge(Scope.fields)
|
|
40
41
|
event["environment"] = @environment if @environment
|
|
41
42
|
event["release"] = @release if @release
|
|
42
43
|
event["message"] = message if message
|
data/lib/condux/dsn.rb
CHANGED
|
@@ -9,13 +9,14 @@ module Condux
|
|
|
9
9
|
|
|
10
10
|
def self.parse(dsn)
|
|
11
11
|
uri = URI(dsn)
|
|
12
|
-
|
|
12
|
+
project_id = uri.path.to_s.sub(%r{\A/}, "")
|
|
13
|
+
if uri.user.nil? || uri.user.empty? || uri.host.nil? || project_id.empty?
|
|
13
14
|
raise ArgumentError, "Condux: DSN must be scheme://<key>@<host>/<projectId>"
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
endpoint = "#{uri.scheme}://#{uri.host}"
|
|
17
18
|
endpoint += ":#{uri.port}" if uri.port && ![80, 443].include?(uri.port)
|
|
18
|
-
new(endpoint,
|
|
19
|
+
new(endpoint, project_id, uri.user)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def initialize(endpoint, project_id, public_key)
|
data/lib/condux/scope.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Condux
|
|
4
|
+
# Ambient event enrichment: who the user is, which tags and contexts apply, and the breadcrumb trail
|
|
5
|
+
# leading up to an error. Set once (or as the app's state changes) and every subsequent event carries
|
|
6
|
+
# it — the first triage questions ("which customer, which plan, what did they do last") answered
|
|
7
|
+
# without threading anything through capture calls. The relay already scrubs all of these at ingest
|
|
8
|
+
# and derives the pseudonymous users-affected key from the user fields.
|
|
9
|
+
module Scope
|
|
10
|
+
# Newest trail wins: a long-lived process drops the oldest crumbs rather than growing without bound.
|
|
11
|
+
MAX_BREADCRUMBS = 30
|
|
12
|
+
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@user = nil
|
|
15
|
+
@tags = {}
|
|
16
|
+
@contexts = {}
|
|
17
|
+
@breadcrumbs = []
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Attach the signed-in user (id/email/username) to subsequent events; nil clears.
|
|
21
|
+
def user=(user)
|
|
22
|
+
@mutex.synchronize { @user = user&.transform_keys(&:to_s) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Attach a tag to subsequent events; a nil value removes it.
|
|
26
|
+
def set_tag(key, value)
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
if value.nil?
|
|
29
|
+
@tags.delete(key.to_s)
|
|
30
|
+
else
|
|
31
|
+
@tags[key.to_s] = value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Attach a named context object to subsequent events; nil removes it.
|
|
37
|
+
def set_context(name, context)
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
if context.nil?
|
|
40
|
+
@contexts.delete(name.to_s)
|
|
41
|
+
else
|
|
42
|
+
@contexts[name.to_s] = context.transform_keys(&:to_s)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Record a breadcrumb; the trail (newest last, capped) rides every subsequent event.
|
|
48
|
+
def add_breadcrumb(message, category: nil, level: nil, type: nil, data: nil, timestamp: nil)
|
|
49
|
+
crumb = { "message" => message, "timestamp" => timestamp || Time.now.to_f }
|
|
50
|
+
crumb["category"] = category if category
|
|
51
|
+
crumb["level"] = level if level
|
|
52
|
+
crumb["type"] = type if type
|
|
53
|
+
crumb["data"] = data if data
|
|
54
|
+
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
@breadcrumbs << crumb
|
|
57
|
+
@breadcrumbs.shift while @breadcrumbs.length > MAX_BREADCRUMBS
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reset all ambient state (tests, or a full sign-out).
|
|
62
|
+
def clear
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
@user = nil
|
|
65
|
+
@tags = {}
|
|
66
|
+
@contexts = {}
|
|
67
|
+
@breadcrumbs = []
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The scope's contribution to an event, holding only the keys that are actually set so an
|
|
72
|
+
# unenriched event keeps its exact wire shape. Breadcrumbs use the Sentry {"values" => []} envelope.
|
|
73
|
+
def fields
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
fields = {}
|
|
76
|
+
fields["user"] = @user.dup if @user
|
|
77
|
+
fields["tags"] = @tags.dup unless @tags.empty?
|
|
78
|
+
fields["contexts"] = @contexts.dup unless @contexts.empty?
|
|
79
|
+
fields["breadcrumbs"] = { "values" => @breadcrumbs.dup } unless @breadcrumbs.empty?
|
|
80
|
+
fields
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../condux"
|
|
4
|
+
|
|
5
|
+
module Condux
|
|
6
|
+
# `condux test-event` — prove the pipeline end to end.
|
|
7
|
+
#
|
|
8
|
+
# An error monitor's failure mode is silence, and silence looks exactly like health. This sends one
|
|
9
|
+
# info-level message through the real client and transport and reports the delivery outcome, so "did my
|
|
10
|
+
# DSN / network / relay work" is one command instead of waiting for a production error.
|
|
11
|
+
module TestEvent
|
|
12
|
+
USAGE = "Usage: condux test-event [--dsn <dsn>] [--message <text>]"
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Runs the command; returns the process exit code (0 delivered, 1 failed, 2 usage).
|
|
17
|
+
def run(argv, out: $stdout, err: $stderr)
|
|
18
|
+
command = argv.first
|
|
19
|
+
return usage(err, "unknown command '#{command}'") unless command == "test-event"
|
|
20
|
+
|
|
21
|
+
dsn = argument(argv, "--dsn") || ENV.fetch("CONDUX_DSN", nil)
|
|
22
|
+
return usage(err, "no DSN. Pass --dsn <dsn> or set CONDUX_DSN") if dsn.nil? || dsn.empty?
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
Condux.init(dsn: dsn, environment: "condux-test")
|
|
26
|
+
rescue ArgumentError, URI::InvalidURIError => e
|
|
27
|
+
return usage(err, e.message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
report(Condux.capture_message(argument(argv, "--message") || "Condux test event"), out, err)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def report(result, out, err)
|
|
34
|
+
message_count = "#{result.attempts} attempt#{result.attempts == 1 ? "" : "s"}"
|
|
35
|
+
if result.ok
|
|
36
|
+
out.puts "Delivered (#{message_count}). Check your project's issues list; a test message " \
|
|
37
|
+
"appears as an info-level issue."
|
|
38
|
+
return 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
err.puts "Delivery FAILED after #{message_count}: #{result.error || "relay answered #{result.status}"}. " \
|
|
42
|
+
"Check the DSN (Project settings -> DSN keys) and that the ingest host is reachable."
|
|
43
|
+
1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def argument(argv, name)
|
|
47
|
+
index = argv.index(name)
|
|
48
|
+
index && argv[index + 1]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def usage(err, reason)
|
|
52
|
+
err.puts "condux: #{reason}. #{USAGE}"
|
|
53
|
+
2
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
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/scope"
|
|
5
6
|
require_relative "condux/client"
|
|
6
7
|
|
|
7
8
|
# Condux SDK for Ruby — report errors to a Condux relay.
|
|
@@ -13,7 +14,8 @@ require_relative "condux/client"
|
|
|
13
14
|
# no real network or timers. Inspired by common SDK transports, implemented fresh.
|
|
14
15
|
module Condux
|
|
15
16
|
class << self
|
|
16
|
-
# Configure the SDK with a project DSN (and optional testing hooks).
|
|
17
|
+
# Configure the SDK with a project DSN (and optional testing hooks). Raises on a malformed DSN:
|
|
18
|
+
# setup runs once at developer time, so a typo is worth failing loudly for.
|
|
17
19
|
def init(dsn:, environment: nil, release: nil, max_retries: Client::DEFAULT_MAX_RETRIES,
|
|
18
20
|
transport: nil, sleep: nil, clock: nil)
|
|
19
21
|
@client = Client.new(dsn: dsn, environment: environment, release: release,
|
|
@@ -23,20 +25,64 @@ module Condux
|
|
|
23
25
|
# Report an exception as an error-level event, with its stack trace. Never raises on delivery failure.
|
|
24
26
|
# Pass handled: false for an uncaught exception (a framework integration does this).
|
|
25
27
|
def capture_exception(error, handled: true)
|
|
26
|
-
|
|
28
|
+
client = active_client
|
|
29
|
+
return not_initialized unless client
|
|
30
|
+
|
|
31
|
+
client.capture_exception(error, handled: handled)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
# Report a bare message event at the given level (default info).
|
|
30
35
|
def capture_message(message, level: Level::INFO)
|
|
31
|
-
|
|
36
|
+
client = active_client
|
|
37
|
+
return not_initialized unless client
|
|
38
|
+
|
|
39
|
+
client.capture_message(message, level)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Attach the signed-in user (id/email/username) to subsequent events; nil clears.
|
|
43
|
+
def set_user(user)
|
|
44
|
+
Scope.user = user
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Attach a tag to subsequent events; a nil value removes it.
|
|
48
|
+
def set_tag(key, value)
|
|
49
|
+
Scope.set_tag(key, value)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Attach a named context object to subsequent events; nil removes it.
|
|
53
|
+
def set_context(name, context)
|
|
54
|
+
Scope.set_context(name, context)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Record a breadcrumb; the trail (newest last, capped) rides every subsequent event.
|
|
58
|
+
def add_breadcrumb(message, category: nil, level: nil, type: nil, data: nil, timestamp: nil)
|
|
59
|
+
Scope.add_breadcrumb(message, category: category, level: level, type: type, data: data,
|
|
60
|
+
timestamp: timestamp)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Reset all ambient enrichment (tests, or a full sign-out).
|
|
64
|
+
def clear_scope
|
|
65
|
+
Scope.clear
|
|
32
66
|
end
|
|
33
67
|
|
|
34
68
|
private
|
|
35
69
|
|
|
36
|
-
|
|
37
|
-
|
|
70
|
+
# Reporting never raises — an error monitor that raises turns a handled error into an unhandled one in
|
|
71
|
+
# exactly the code path where someone is already dealing with a failure. The Rack middleware reports
|
|
72
|
+
# from inside a rescue, so raising here would replace the application's own exception with this one.
|
|
73
|
+
# Warn once and drop the event instead.
|
|
74
|
+
def active_client
|
|
75
|
+
return @client if @client
|
|
76
|
+
|
|
77
|
+
unless @warned_uninitialized
|
|
78
|
+
@warned_uninitialized = true
|
|
79
|
+
warn "Condux: capture called before Condux.init(dsn:); events are being dropped."
|
|
80
|
+
end
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
38
83
|
|
|
39
|
-
|
|
84
|
+
def not_initialized
|
|
85
|
+
SendResult.new(ok: false, attempts: 0, error: "not_initialized")
|
|
40
86
|
end
|
|
41
87
|
end
|
|
42
88
|
end
|
metadata
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: condux
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Condux
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-08-14 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).'
|
|
15
15
|
email:
|
|
16
|
-
executables:
|
|
16
|
+
executables:
|
|
17
|
+
- condux
|
|
17
18
|
extensions: []
|
|
18
19
|
extra_rdoc_files: []
|
|
19
20
|
files:
|
|
20
21
|
- LICENSE
|
|
21
22
|
- README.md
|
|
23
|
+
- exe/condux
|
|
22
24
|
- lib/condux.rb
|
|
23
25
|
- lib/condux/client.rb
|
|
24
26
|
- lib/condux/dsn.rb
|
|
@@ -26,7 +28,9 @@ files:
|
|
|
26
28
|
- lib/condux/event_transport.rb
|
|
27
29
|
- lib/condux/level.rb
|
|
28
30
|
- lib/condux/rack.rb
|
|
31
|
+
- lib/condux/scope.rb
|
|
29
32
|
- lib/condux/send_result.rb
|
|
33
|
+
- lib/condux/test_event.rb
|
|
30
34
|
homepage: https://condux.ai
|
|
31
35
|
licenses:
|
|
32
36
|
- Apache-2.0
|