raises 0.2.0 → 0.3.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: cd2351828d8eeb04e0af96c88322c369dcd2ecd158384d7e80543cede4153646
4
- data.tar.gz: 85e43d1fa7d393159c8bb54fc99898863fc5afd7369f680486e8947d74412c1a
3
+ metadata.gz: 826d161b194d0bb9f3d3b1e00f3326dc986da4efb420e9ae1c8ff0dccf59708e
4
+ data.tar.gz: b0b9b1dbe0e03571e0585c6982e59091495337e14118765a0ff1274427a1b956
5
5
  SHA512:
6
- metadata.gz: f10b1aa6748ccf6848f9b2ceccb479c1d4a6182b44350318cffd93bab4e70f152613fe66520c1c6cc9d600e912b36eb0abc4ba4b5e0072cca6356e6cdf8676ba
7
- data.tar.gz: 5ba3f6610cce1de92879f3599180d630e38b8183a5ea357ad1e1b9e8d918d7199fdf48bda4fedf9152f93492fec65c576db6404779160d85d2e8cba1413bb532
6
+ metadata.gz: ee447f476885abbda2516cbe5b92600b824b879507954f23c8f276225adb718f78652f2cc53bf79e1bf55fe11a6203bad9519b4adffbbcb64e4eae20ca9b653a
7
+ data.tar.gz: 15d056b1826db28175a104359215f1a3eebd7497358a8c260057b0811e5a3758bef330c9412372393f7de077a81ea7741bcd8a82a20a9e0f2a0e0898c14c6098
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add `Raises.notify` for informational, warning, and error-level events.
6
+ - Preserve event and exception routes in the optional disk spool.
7
+ - Keep informational events separate from exception grouping and GitHub issues.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  - Add opt-in, bounded disk spooling through `RAISES_SPOOL_DIR`.
data/README.md CHANGED
@@ -8,8 +8,21 @@ gem "raises"
8
8
 
9
9
  Set `RAISES_TOKEN` to the ingestion credential created for the project. Production errors are reported to `https://raises.dev` automatically through `Rails.error`. Set `RAISES_REPORT=1` to exercise the integration outside production.
10
10
 
11
+ Send a one-off operational notice without raising an exception:
12
+
13
+ ```ruby
14
+ Raises.notify(
15
+ "Import finished",
16
+ level: :info,
17
+ source: "nightly-import",
18
+ context: { imported: 412, skipped: 3 }
19
+ )
20
+ ```
21
+
22
+ Valid levels are `:info`, `:warning`, and `:error`. Notices are retained and delivered to configured outbound integrations, but they do not create error groups, acknowledgements, or GitHub issues.
23
+
11
24
  Optional settings are `RAISES_URL`, `RAISES_ENV`, `RAISES_REVISION`, `RAISES_OPEN_TIMEOUT`, and `RAISES_READ_TIMEOUT`.
12
25
 
13
- Set `RAISES_SPOOL_DIR` to add restart-safe, disk-backed delivery retries. The directory must be on persistent storage if notices should survive a deploy. Raises stores notice JSON there with private permissions, never stores the ingestion token, retries network errors, HTTP 408/429, and 5xx responses, and bounds the spool at 1,000 notices or 100 MB. Leave it unset to preserve synchronous best-effort delivery.
26
+ Set `RAISES_SPOOL_DIR` to add restart-safe, disk-backed delivery retries. The directory must be on persistent storage if payloads should survive a deploy. Raises stores event or exception JSON there with private permissions, never stores the ingestion token, retries network errors, HTTP 408/429, and 5xx responses, and bounds the spool at 1,000 payloads or 100 MB. Leave it unset to preserve synchronous best-effort delivery.
14
27
 
15
28
  The reporter fails open: network or serialization failures never replace the application exception. Rails filtered parameters are included; headers, raw bodies, query strings, and the request object are not.
@@ -10,33 +10,36 @@ module Raises
10
10
  @warn = warn
11
11
  end
12
12
 
13
- def call(notice)
14
- response = @post.call(notice)
15
- return response unless response.respond_to?(:code)
16
- return response if response.is_a?(Net::HTTPSuccess)
13
+ def call(payload, path: "v1/notices")
14
+ item = { "path" => path, "payload" => payload }
15
+ response = @post.call(path, payload)
16
+ return true unless response.respond_to?(:code)
17
+ return true if response.is_a?(Net::HTTPSuccess) || response.code.to_i.between?(200, 299)
17
18
 
18
- handle_response(notice, response.code.to_i)
19
- response
19
+ response_accepted?(item, response.code.to_i)
20
20
  rescue StandardError => e
21
- handle_exception(notice, e)
22
- nil
21
+ exception_accepted?(item || { "path" => path, "payload" => payload }, e)
23
22
  end
24
23
 
25
24
  private
26
25
 
27
- def handle_response(notice, status)
28
- if retryable_status?(status) && @spool&.enqueue(notice)
26
+ def response_accepted?(item, status)
27
+ if retryable_status?(status) && @spool&.enqueue(item)
29
28
  @warn.call("raises queued notice after HTTP #{status}")
29
+ true
30
30
  else
31
31
  @warn.call("raises rejected notice: HTTP #{status}")
32
+ false
32
33
  end
33
34
  end
34
35
 
35
- def handle_exception(notice, error)
36
- if @spool&.enqueue(notice)
36
+ def exception_accepted?(item, error)
37
+ if @spool&.enqueue(item)
37
38
  @warn.call("raises queued notice after #{error.class}")
39
+ true
38
40
  else
39
41
  @warn.call("raises subscriber failed: #{error.class}: #{error.message}")
42
+ false
40
43
  end
41
44
  end
42
45
 
@@ -3,7 +3,7 @@
3
3
  module Raises
4
4
  class Railtie < Rails::Railtie
5
5
  initializer "raises.subscribe" do
6
- Rails.error.subscribe(Raises::Subscriber.new.start)
6
+ Rails.error.subscribe(Raises.subscriber)
7
7
  end
8
8
  end
9
9
  end
data/lib/raises/spool.rb CHANGED
@@ -38,8 +38,8 @@ module Raises
38
38
  thread&.join(1)
39
39
  end
40
40
 
41
- def enqueue(notice)
42
- accepted = @storage.store(notice) == :stored
41
+ def enqueue(item)
42
+ accepted = @storage.store(item) == :stored
43
43
  if accepted
44
44
  start if @start_on_enqueue
45
45
  wake if @start_on_enqueue
@@ -69,7 +69,7 @@ module Raises
69
69
  end
70
70
 
71
71
  def deliver(path, envelope)
72
- response = @deliver.call(envelope.fetch("notice"))
72
+ response = @deliver.call(envelope.fetch("path"), envelope.fetch("payload"))
73
73
  status = response_code(response)
74
74
  if status.between?(200, 299)
75
75
  @storage.delete(path)
@@ -16,9 +16,15 @@ module Raises
16
16
  prepare
17
17
  end
18
18
 
19
- def store(notice)
19
+ def store(item)
20
20
  prepare
21
- raw = JSON.generate("version" => 1, "attempts" => 0, "next_attempt_at" => @now.call.to_f, "notice" => notice)
21
+ raw = JSON.generate(
22
+ "version" => 2,
23
+ "attempts" => 0,
24
+ "next_attempt_at" => @now.call.to_f,
25
+ "path" => item.fetch("path"),
26
+ "payload" => item.fetch("payload")
27
+ )
22
28
  with_directory_lock do
23
29
  return :full unless capacity_for?(raw)
24
30
 
@@ -107,7 +113,14 @@ module Raises
107
113
 
108
114
  def parse(raw, path)
109
115
  envelope = JSON.parse(raw)
110
- return envelope if envelope.is_a?(Hash) && envelope["notice"].is_a?(Hash)
116
+ return envelope if envelope.is_a?(Hash) && envelope["path"].is_a?(String) && envelope["payload"].is_a?(Hash)
117
+
118
+ if envelope.is_a?(Hash) && envelope["notice"].is_a?(Hash)
119
+ envelope["version"] = 2
120
+ envelope["path"] = "v1/notices"
121
+ envelope["payload"] = envelope.delete("notice")
122
+ return envelope
123
+ end
111
124
 
112
125
  raise JSON::ParserError, "invalid envelope"
113
126
  rescue JSON::ParserError, TypeError => e
@@ -5,6 +5,8 @@ require "net/http"
5
5
  require "uri"
6
6
 
7
7
  module Raises
8
+ # Rails.error integration and explicit application notices share one delivery path.
9
+ # rubocop:disable Metrics/ClassLength
8
10
  class Subscriber
9
11
  def initialize
10
12
  @spool = build_spool
@@ -31,13 +33,45 @@ module Raises
31
33
  request: request_from(context)
32
34
  }
33
35
 
34
- @delivery.call(notice)
36
+ @delivery.call(notice, path: "v1/notices")
35
37
  rescue StandardError => e
36
38
  warn("raises subscriber failed: #{e.class}: #{e.message}")
39
+ false
40
+ end
41
+
42
+ def notify(message, level: :info, context: {}, source: nil)
43
+ message = message.to_s.strip
44
+ level = level.to_s
45
+ validate_event!(message, level, context, source)
46
+ return false unless report?
47
+
48
+ start
49
+ event = {
50
+ env: env_name,
51
+ revision: revision,
52
+ level: level,
53
+ message: message,
54
+ source: source,
55
+ context: json_safe(context)
56
+ }
57
+ @delivery.call(event, path: "v1/events")
58
+ rescue ArgumentError
59
+ raise
60
+ rescue StandardError => e
61
+ warn("raises notify failed: #{e.class}: #{e.message}")
62
+ false
37
63
  end
38
64
 
39
65
  private
40
66
 
67
+ def validate_event!(message, level, context, source)
68
+ raise ArgumentError, "message is required" if message.empty?
69
+ raise ArgumentError, "message is too long" if message.length > 2_000
70
+ raise ArgumentError, "level must be info, warning, or error" unless %w[info warning error].include?(level)
71
+ raise ArgumentError, "source is too long" if source.to_s.length > 120
72
+ raise ArgumentError, "context must be a hash" unless context.respond_to?(:each_pair)
73
+ end
74
+
41
75
  def build_spool
42
76
  directory = ENV["RAISES_SPOOL_DIR"].to_s
43
77
  return if directory.empty?
@@ -132,8 +166,8 @@ module Raises
132
166
  end
133
167
  end
134
168
 
135
- def post(notice)
136
- uri = URI.join(url.end_with?("/") ? url : "#{url}/", "v1/notices")
169
+ def post(path, payload)
170
+ uri = URI.join(url.end_with?("/") ? url : "#{url}/", path)
137
171
  http = Net::HTTP.new(uri.host, uri.port)
138
172
  http.use_ssl = uri.scheme == "https"
139
173
  http.open_timeout = timeout("RAISES_OPEN_TIMEOUT", 1)
@@ -142,7 +176,7 @@ module Raises
142
176
  request["Authorization"] = "Bearer #{token}"
143
177
  request["Content-Type"] = "application/json"
144
178
  request["User-Agent"] = "raises-ruby/#{Raises::VERSION}"
145
- request.body = JSON.generate(notice)
179
+ request.body = JSON.generate(payload)
146
180
  http.request(request)
147
181
  end
148
182
 
@@ -153,4 +187,5 @@ module Raises
153
187
  fallback
154
188
  end
155
189
  end
190
+ # rubocop:enable Metrics/ClassLength
156
191
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raises
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/raises.rb CHANGED
@@ -8,4 +8,13 @@ require "raises/subscriber"
8
8
  require "raises/railtie" if defined?(Rails::Railtie)
9
9
 
10
10
  module Raises
11
+ class << self
12
+ def notify(message, level: :info, context: {}, source: nil)
13
+ subscriber.notify(message, level: level, context: context, source: source)
14
+ end
15
+
16
+ def subscriber
17
+ @subscriber ||= Subscriber.new.start
18
+ end
19
+ end
11
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raises
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clayton Lengel-Zigich