tinymonrb 0.3.0 → 0.4.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: 8df6c08b32ccae44e241b41fbd2a98f2033043c37ce6292a2f529869838cb8aa
4
- data.tar.gz: 8e924831169fbbbbd9c409c49c10a71428eef4dbd48e9b01345f606230e75eb5
3
+ metadata.gz: 721bdc7f477b8281a78914201aadf4367fe02206248735b91dc6c564459c4050
4
+ data.tar.gz: 5b119d9cd4b65d8cef7648b779be6a84f93ffca1c78c7fd0560e63529ca31560
5
5
  SHA512:
6
- metadata.gz: 27265f4b71e845ffba1f87f3679f6979e7ec5a0175cdbc3399130a2140a8896178554ce72504159f1469674e288ef6a959f0dc84c293ff20a8a4f6b89cf55345
7
- data.tar.gz: 0ff34d5f4fa892fb1dc00076ba17746836fd5d2205a4b804ca3cf16ec333e81fb307cdcabbbf0a1b778e1ae8cd8a8ff8392e1affaae381def405c72953af09d1
6
+ metadata.gz: 911fac08481f040b354966c3baf7d14db96d9a5deab26b86bd077b432c430eca3dcda7c9dc84e0fc600efd344c4c8354c31137651e3b22cada233e976b142a47
7
+ data.tar.gz: b3cf2caffb694ba16700b9f69d758d2f025d87bae3a4c1849bc10d0899118905a096082b79a29aa87f23b786b69ed48f82fb7231c419c3b2d02bf40e8ef032a2
@@ -37,7 +37,7 @@ module Tinymon
37
37
  return if rand > @sample_rate
38
38
  event = _prepare(exception)
39
39
  return if event.nil?
40
- @transport.enqueue(event)
40
+ @transport.send_event(event)
41
41
  rescue StandardError
42
42
  # SWALLOW. The SDK must never throw into the host app.
43
43
  nil
@@ -48,7 +48,7 @@ module Tinymon
48
48
  return if event.nil?
49
49
  event["level"] = level
50
50
  event["exception"]["type"] = "Message"
51
- @transport.enqueue(event)
51
+ @transport.send_event(event)
52
52
  rescue StandardError
53
53
  nil
54
54
  end
@@ -60,7 +60,15 @@ module Tinymon
60
60
  return if rand > @sample_rate
61
61
  event = _prepare(exception, ctx: { request: request, user: user, tags: tags })
62
62
  return if event.nil?
63
- @transport.enqueue(event)
63
+ @transport.send_event(event)
64
+ rescue StandardError
65
+ nil
66
+ end
67
+
68
+ # Block until queued events are delivered (or timeout). Call before a
69
+ # short-lived process exits so events aren't dropped.
70
+ def flush(timeout = 2.0)
71
+ @transport.flush(timeout)
64
72
  rescue StandardError
65
73
  nil
66
74
  end
@@ -5,46 +5,55 @@ require "net/http"
5
5
  require "uri"
6
6
 
7
7
  module Tinymon
8
- # Batched HTTP transport. stdlib-only. A background thread flushes every
9
- # 5 seconds; at_exit drains the queue on shutdown.
8
+ # HTTP transport. stdlib-only. Sends each event immediately on a background
9
+ # worker thread; events are only queued when the network fails, and that
10
+ # retry queue is drained with exponential backoff. This matches how
11
+ # production error SDKs behave — the error lands in the dashboard now, not
12
+ # on a 5-second timer. at_exit drains anything still pending on shutdown.
10
13
  class Transport
11
- FLUSH_INTERVAL = 5.0 # seconds
12
- MAX_BATCH = 10
13
- MAX_QUEUE = 30
14
- REQUEST_TIMEOUT = 5 # seconds
14
+ MAX_QUEUE = 30 # cap on retry buffer (dead-network memory guard)
15
+ REQUEST_TIMEOUT = 5 # seconds
16
+ BACKOFF_START = 1.0 # seconds
17
+ BACKOFF_MAX = 30.0 # seconds
15
18
 
16
19
  def initialize(endpoint, dsn)
17
20
  @endpoint = endpoint
18
21
  @dsn = dsn
19
- @queue = []
22
+ @pending = [] # fresh events — sent immediately
23
+ @retry = [] # failed events — retried with backoff
20
24
  @mutex = Mutex.new
25
+ @cond = ConditionVariable.new
26
+ @backoff = BACKOFF_START
21
27
  @stop = false
22
28
  @thread = Thread.new { run_loop }
23
29
  @thread.name = "tinymon-transport" if @thread.respond_to?(:name=)
24
30
  at_exit { shutdown }
25
31
  end
26
32
 
27
- def enqueue(event)
28
- should_flush = false
33
+ # Queue for immediate send on the worker thread (non-blocking).
34
+ def send_event(event)
29
35
  @mutex.synchronize do
30
- @queue.shift while @queue.length >= MAX_QUEUE
31
- @queue.push(event)
32
- should_flush = @queue.length >= MAX_BATCH
36
+ @pending.push(event)
37
+ @cond.signal
33
38
  end
34
- flush if should_flush
35
39
  end
36
40
 
37
- def flush
38
- batch = nil
39
- @mutex.synchronize do
40
- batch = @queue.shift(MAX_BATCH)
41
+ # Block until pending + retry events are sent, or timeout elapses.
42
+ def flush(timeout = 2.0)
43
+ deadline = monotonic + timeout
44
+ loop do
45
+ empty = @mutex.synchronize { @pending.empty? && @retry.empty? }
46
+ return if empty
47
+ break if monotonic >= deadline
48
+ @mutex.synchronize { @cond.signal }
49
+ sleep(0.02)
41
50
  end
42
- return if batch.nil? || batch.empty?
43
- batch.each { |event| send_one(event) }
44
51
  end
45
52
 
46
53
  private
47
54
 
55
+ # Returns true on success OR an unrecoverable 4xx (retrying a client bug
56
+ # won't help); false on 5xx / network failure.
48
57
  def send_one(event)
49
58
  uri = URI.parse(@endpoint)
50
59
  http = Net::HTTP.new(uri.host, uri.port)
@@ -57,33 +66,89 @@ module Tinymon
57
66
  })
58
67
  req.body = JSON.dump(event)
59
68
  begin
60
- http.request(req)
69
+ res = http.request(req)
70
+ code = res.code.to_i
71
+ return true if code >= 200 && code < 300
72
+ return true if code >= 400 && code < 500 # client bug — drop, don't retry
73
+ false
61
74
  rescue StandardError
62
- # Network failed — re-enqueue if there's room.
63
- @mutex.synchronize do
64
- @queue.unshift(event) if @queue.length < MAX_QUEUE
65
- end
75
+ false
66
76
  end
67
77
  end
68
78
 
69
79
  def run_loop
70
80
  until @stop
71
- sleep(FLUSH_INTERVAL)
72
- begin
73
- flush
74
- rescue StandardError
75
- # swallow — never crash the host app
81
+ event = next_pending
82
+ if event
83
+ queue_retry(event) unless send_one(event)
84
+ next
85
+ end
86
+
87
+ next if drain_retry
88
+
89
+ # Idle — wait for new work or the next retry window.
90
+ @mutex.synchronize do
91
+ wait = @retry.empty? ? nil : @backoff
92
+ @cond.wait(@mutex, wait) unless @stop
93
+ end
94
+ end
95
+ end
96
+
97
+ def next_pending
98
+ @mutex.synchronize { @pending.shift }
99
+ end
100
+
101
+ def queue_retry(event)
102
+ @mutex.synchronize do
103
+ @retry.shift while @retry.length >= MAX_QUEUE
104
+ @retry.push(event)
105
+ end
106
+ end
107
+
108
+ def drain_retry
109
+ batch = @mutex.synchronize do
110
+ next nil if @retry.empty?
111
+ b = @retry.dup
112
+ @retry.clear
113
+ b
114
+ end
115
+ return false if batch.nil?
116
+
117
+ any_failed = false
118
+ batch.each do |event|
119
+ unless send_one(event)
120
+ any_failed = true
121
+ queue_retry(event)
76
122
  end
77
123
  end
124
+ if any_failed
125
+ @backoff = [@backoff * 2, BACKOFF_MAX].min
126
+ sleep(@backoff)
127
+ else
128
+ @backoff = BACKOFF_START
129
+ end
130
+ true
78
131
  end
79
132
 
80
133
  def shutdown
81
- @stop = true
82
- begin
83
- flush
84
- rescue StandardError
85
- nil
134
+ # Best-effort: deliver anything still pending before exit.
135
+ deadline = monotonic + 2.0
136
+ while monotonic < deadline
137
+ event = next_pending
138
+ event ||= @mutex.synchronize { @retry.shift }
139
+ break if event.nil?
140
+ send_one(event)
141
+ end
142
+ @mutex.synchronize do
143
+ @stop = true
144
+ @cond.broadcast
86
145
  end
146
+ rescue StandardError
147
+ nil
148
+ end
149
+
150
+ def monotonic
151
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
87
152
  end
88
153
  end
89
154
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tinymon
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  SDK_NAME = "tinymon.ruby"
6
6
  end
data/lib/tinymon.rb CHANGED
@@ -1,64 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "tinymon/version"
4
- require_relative "tinymon/stacktrace"
5
- require_relative "tinymon/event_builder"
6
- require_relative "tinymon/scope"
7
- require_relative "tinymon/transport"
8
- require_relative "tinymon/client"
9
- require_relative "tinymon/integrations"
10
-
11
- # Public API:
12
- #
13
- # require "tinymon"
14
- # Tinymon.init(dsn: "tm_pub_xxx", environment: "production", release: "1.0.0")
15
- #
16
- # begin
17
- # risky_thing
18
- # rescue => e
19
- # Tinymon.capture_exception(e)
20
- # end
21
- #
22
- module Tinymon
23
- @client = nil
24
-
25
- class << self
26
- def init(dsn:, endpoint: nil, environment: nil, release: nil, sample_rate: 1.0, before_send: nil)
27
- @client = Client.new(
28
- dsn: dsn,
29
- endpoint: endpoint,
30
- environment: environment,
31
- release: release,
32
- sample_rate: sample_rate,
33
- before_send: before_send,
34
- )
35
- Integrations.install(@client)
36
- @client
37
- end
38
-
39
- def capture_exception(exception)
40
- @client&.capture_exception(exception)
41
- end
42
-
43
- def capture_message(message, level: "info")
44
- @client&.capture_message(message, level: level)
45
- end
46
-
47
- def set_user(user)
48
- SCOPE.set_user(user)
49
- end
50
-
51
- def set_tag(key, value)
52
- SCOPE.set_tag(key, value)
53
- end
54
-
55
- def add_breadcrumb(crumb)
56
- SCOPE.add_breadcrumb(crumb)
57
- end
58
-
59
- # Exposed for the cross-language contract test in test/test_contract.rb.
60
- def _build_event(exception, **kwargs)
61
- EventBuilder.build(exception, **kwargs)
62
- end
63
- end
64
- end
3
+ # Back-compat shim. The gem is published as `tinymonrb`, so the canonical
4
+ # entry point is `require "tinymonrb"`. This file only exists so that older
5
+ # code doing `require "tinymon"` keeps working — it loads the full SDK
6
+ # (context capture, Rack middleware, Rails railtie) rather than a partial copy.
7
+ require_relative "tinymonrb"
data/lib/tinymonrb.rb CHANGED
@@ -66,6 +66,12 @@ module Tinymon
66
66
  @client&.capture_message(message, level: level)
67
67
  end
68
68
 
69
+ # Block until queued events are delivered (or timeout). Call before a
70
+ # short-lived process exits so events aren't dropped.
71
+ def flush(timeout = 2.0)
72
+ @client&.flush(timeout)
73
+ end
74
+
69
75
  def set_user(user)
70
76
  SCOPE.set_user(user)
71
77
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tinymonrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tinymon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-10 00:00:00.000000000 Z
11
+ date: 2026-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_schemer