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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3db626d815c96cf4115cb51ea7fc2fa076b14b06f90c8b3d4732694810a00aef
4
- data.tar.gz: e819143aff9c172bafea74fc17b2a7a2012657cab8f3fca06f3981d20d45b879
3
+ metadata.gz: 72bffbeb8549e50c7defe4c9fb3d18c5eeecd3ca9d02704e05a8bc3733e5eed9
4
+ data.tar.gz: 41df087b35b4f1861384e0826e43b8adc7fe42d8c921c59df35339f54ed64ec6
5
5
  SHA512:
6
- metadata.gz: b7697052843054cbbdf929fb9cfa6671b0c0489e2dec35c162c62f57fed5249532c4713d36811db506ec9463ca106d90354174dd97818690f25a6d22ea2f0f2c
7
- data.tar.gz: ad5d223a15da543bbcf903b4bbab2b899c3712f9f8859b5f94f84c5fa226f92512d2a15d71d20b7b57a6f1368fb786efcd3365256810914d1d20f13504e9b817
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 test/test_condux.rb
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "condux/test_event"
5
+
6
+ exit Condux::TestEvent.run(ARGV)
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
- if uri.user.nil? || uri.user.empty? || uri.host.nil?
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, uri.path.sub(%r{\A/}, ""), uri.user)
19
+ new(endpoint, project_id, uri.user)
19
20
  end
20
21
 
21
22
  def initialize(endpoint, project_id, public_key)
@@ -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
- require_client.capture_exception(error, handled: handled)
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
- require_client.capture_message(message, level)
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
- def require_client
37
- raise "Condux not initialized call Condux.init(dsn:) first" unless @client
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
- @client
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
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Condux
8
8
  autorequire:
9
- bindir: bin
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