trigv 1.0.0 → 1.1.1

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: db623af95b6d63ba76f6aadc73ad1fe6783aae9d60a7af01e15be026aa7840a3
4
- data.tar.gz: 72082f5c52bf1e92d207932e646e7478cbc69ff9db4dd62fa348518debdc6e3c
3
+ metadata.gz: bec937b624e4c89520252f25849322469e32f51a74dadb4fe98d5aff5bce3756
4
+ data.tar.gz: 0cd521c1fffedd723fc607e23a865749a41e9c167f4e7804e867b48fb0d52427
5
5
  SHA512:
6
- metadata.gz: bd42a53191af4b90172976e1c0adbb1c0b8a0499f4eae0781676df2793bab349b837efe189fa2d35773893dac7516658c725076ceec27ee56e2c7acfc99e992f
7
- data.tar.gz: 942bb3b48a9f4ddd77b6544f0a7d50a442a19c06135da3ff5e4a87178f88014b5c1bc3989444f2dca8d3161cb5d72e431447eedaca0141e47c2e0ef9effa2b7a
6
+ metadata.gz: 5c48be29cd4ca018bc3a475ddd7113fc43558e0629dbd1a3088898f54f737c61a356eff543654ad9917e4b9376cf35bedf2c2d9e95f3018172a752a8a72505b0
7
+ data.tar.gz: 1853fdeac0f0b339133f666db263af0abc40e6a60a8e665526646cd5ea09e5ebf312b27e1ebee17ceccf5c74dc789d15e5c6b856b40a99196ec2e6c21ab9a732
data/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.1] - 2026-11-01
9
+
10
+ ### Changed
11
+
12
+ - Changed: README and package metadata link to trigv.com docs
13
+
14
+ ## [1.1.0] - 2026-11-01
15
+
16
+ ### Changed
17
+
18
+ - Default `base_url` is now `https://api.trigv.com`; paths are `/v1/events` and `/v1/connection`. A base URL ending in `/api` keeps working.
19
+ - `public_id` values are 26 character ULIDs (no `evt_` prefix).
20
+ - Rate limit is 60 requests per minute per workspace.
21
+ - Invalid JSON bodies return 422 and map to `Trigv::ValidationError`.
22
+ - A `Retry-After` longer than the client `timeout` is not waited for: the 409, 429 or 503 error is raised at once.
23
+
24
+ ### Added
25
+
26
+ - `Trigv::ConflictError` (409) and `Trigv::ServiceUnavailableError` (503), retried with `Retry-After` when an `idempotency_key` is set.
27
+ - `request_id` on every API error (from the `X-Request-ID` header).
28
+ - `request_id` on `Trigv::SendEventResult` and `Trigv::ConnectionResult` (`nil` if the header is missing).
29
+
30
+ ### Deprecated
31
+
32
+ - `Trigv::RateLimitError#retryable` (always `true`), the `retryable:` argument (ignored) and `Trigv::Errors.monthly_limit_message?` (always `false`) are kept for compatibility.
33
+
34
+ ### Removed
35
+
36
+ - Monthly event cap handling.
37
+
8
38
  ## [1.0.0] - 2026-07-07
9
39
 
10
40
  ### Added
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # trigv
2
2
 
3
+ [Docs](https://trigv.com/docs/sdks/ruby/) | [API reference](https://trigv.com/docs/api/) | [Free tools](https://trigv.com/tools/) | [Get an API key](https://app.trigv.com/register)
4
+
3
5
  Official Trigv SDK for Ruby. Send notification events from scripts, servers, and CI pipelines.
4
6
 
5
7
  ## What Trigv is
@@ -11,7 +13,7 @@ Trigv delivers developer notifications to your team's devices. Your backend send
11
13
  Add to your Gemfile:
12
14
 
13
15
  ```ruby
14
- gem 'trigv', '~> 0.1'
16
+ gem 'trigv', '~> 1.1'
15
17
  ```
16
18
 
17
19
  Then run:
@@ -57,21 +59,22 @@ result = client.send_event(
57
59
  )
58
60
 
59
61
  puts result.event.public_id
62
+ puts result.request_id # X-Request-ID header, or nil if missing
60
63
  ```
61
64
 
62
65
  ## Event options
63
66
 
64
67
  | Field | Required | Description |
65
68
  |-------|:--------:|-------------|
66
- | `channel` | Yes | Channel slug (e.g. `general`) |
67
- | `title` | Yes | Notification title |
68
- | `description` | No | Body text |
69
- | `image_url` | No | HTTPS image URL (not stored server-side) |
69
+ | `channel` | Yes | Channel slug (e.g. `general`, max 120 characters) |
70
+ | `title` | Yes | Notification title (max 255 characters) |
71
+ | `description` | No | Body text (max 1000 characters) |
72
+ | `image_url` | No | Image URL (max 2048 characters; not stored server-side) |
70
73
  | `url` | No | Destination URL for the notification (max 2048 characters; not stored server-side) |
71
74
  | `level` | No | `info`, `success`, `warning`, `error` (default: `info`) |
72
75
  | `delivery_urgency` | No | `standard` or `time_sensitive` (default: `standard`) |
73
- | `event_type` | No | Free-form label (e.g. `deploy.completed`) |
74
- | `idempotency_key` | No | Dedup key per workspace |
76
+ | `event_type` | No | Free-form label (e.g. `deploy.completed`, max 120 characters) |
77
+ | `idempotency_key` | No | Dedup key per workspace (max 190 characters) |
75
78
 
76
79
  ## Levels
77
80
 
@@ -87,7 +90,7 @@ puts result.event.public_id
87
90
 
88
91
  ## Idempotency
89
92
 
90
- When you set `idempotency_key`, retries with the same key return the existing event (`duplicate: true`, HTTP 200) without billing again.
93
+ When you set `idempotency_key`, retries with the same key return the existing event (`duplicate: true`, HTTP 200, message `already stored`) without billing again. A 409 or 503 is retried with the same key, honouring `Retry-After`.
91
94
 
92
95
  ```ruby
93
96
  result = client.send_event(
@@ -108,17 +111,29 @@ rescue Trigv::ValidationError => e
108
111
  puts e.errors
109
112
  rescue Trigv::NotFoundError
110
113
  puts 'Channel not found'
111
- rescue Trigv::RateLimitError => e
112
- puts e.retryable ? 'Retry later' : 'Monthly limit reached'
114
+ rescue Trigv::RateLimitError, Trigv::ConflictError, Trigv::ServiceUnavailableError => e
115
+ puts "Retry in #{e.retry_after_seconds}s (request id #{e.request_id})"
113
116
  end
114
117
  ```
115
118
 
119
+ | Error | HTTP | Retried (with `idempotency_key`) |
120
+ |-------|------|:---:|
121
+ | `Trigv::AuthenticationError` | 401 | No |
122
+ | `Trigv::AuthorizationError` | 403 | No |
123
+ | `Trigv::NotFoundError` | 404 | No |
124
+ | `Trigv::ConflictError` | 409, same key being delivered | Yes |
125
+ | `Trigv::ValidationError` | 422 | No |
126
+ | `Trigv::RateLimitError` | 429, 60 requests per minute per workspace | Yes |
127
+ | `Trigv::ServiceUnavailableError` | 503, push unavailable | Yes |
128
+
129
+ Retries wait for `Retry-After`. If `Retry-After` is longer than the client `timeout`, the error is raised at once. 502 and 504 responses, network errors and timeouts are also retried (with an `idempotency_key`), and so is `verify_connection`. Every error has `request_id`, the `X-Request-ID` header. Successful results have `request_id` too (`nil` if missing). Quote it to support.
130
+
116
131
  ## Configuration
117
132
 
118
133
  | Option | Default | Description |
119
134
  |--------|---------|-------------|
120
135
  | `api_key` | `TRIGV_API_KEY` env | Workspace API key |
121
- | `base_url` | `https://api.trigv.com/api` | API base URL |
136
+ | `base_url` | `https://api.trigv.com` | API base URL. A URL ending in `/api` still works |
122
137
  | `timeout` | `30` | Request timeout (seconds) |
123
138
  | `max_retries` | `2` | Retries for retryable errors |
124
139
 
@@ -127,6 +142,7 @@ end
127
142
  ```ruby
128
143
  connection = client.verify_connection
129
144
  puts connection.workspace.name
145
+ puts connection.request_id # X-Request-ID header, or nil if missing
130
146
  ```
131
147
 
132
148
  ## Examples
data/lib/trigv/client.rb CHANGED
@@ -12,11 +12,11 @@ module Trigv
12
12
  keyword_init: true
13
13
  )
14
14
 
15
- SendEventResult = Struct.new(:event, :duplicate, keyword_init: true)
15
+ SendEventResult = Struct.new(:event, :duplicate, :request_id, keyword_init: true)
16
16
 
17
17
  ConnectionWorkspace = Struct.new(:public_id, :name, keyword_init: true)
18
18
  ConnectionApiKey = Struct.new(:public_id, :name, :prefix, keyword_init: true)
19
- ConnectionResult = Struct.new(:workspace, :api_key, keyword_init: true)
19
+ ConnectionResult = Struct.new(:workspace, :api_key, :request_id, keyword_init: true)
20
20
 
21
21
  class Client
22
22
  attr_reader :api_key, :base_url, :timeout, :max_retries
@@ -43,6 +43,11 @@ module Trigv
43
43
  "trigv-ruby/#{VERSION}"
44
44
  end
45
45
 
46
+ # Keep the API key out of logs, consoles and exception reports that call inspect.
47
+ def inspect
48
+ "#<#{self.class.name} base_url=#{base_url.inspect} timeout=#{timeout} max_retries=#{max_retries}>"
49
+ end
50
+
46
51
  def send_event(**params)
47
52
  body = Validation.validate_send_event_params(params)
48
53
  has_idempotency = body.key?('idempotency_key')
@@ -50,7 +55,7 @@ module Trigv
50
55
  request_with_retry(:post, '/v1/events', body, allow_retry: has_idempotency) do |status, parsed, headers|
51
56
  if [200, 202].include?(status) && parsed&.dig('event')
52
57
  event = parse_event(parsed['event'])
53
- SendEventResult.new(event: event, duplicate: status == 200)
58
+ SendEventResult.new(event: event, duplicate: status == 200, request_id: headers['x-request-id'])
54
59
  else
55
60
  error_from_response(status, parsed, headers)
56
61
  end
@@ -60,7 +65,7 @@ module Trigv
60
65
  def verify_connection
61
66
  request_with_retry(:get, '/v1/connection', nil, allow_retry: true) do |status, parsed, headers|
62
67
  if status == 200 && parsed&.dig('workspace') && parsed['api_key']
63
- parse_connection_result(parsed)
68
+ parse_connection_result(parsed, headers['x-request-id'])
64
69
  else
65
70
  error_from_response(status, parsed, headers)
66
71
  end
@@ -81,6 +86,9 @@ module Trigv
81
86
  raise e unless retryable_error?(e)
82
87
 
83
88
  wait = retry_wait_seconds(e, attempt)
89
+ # A wait longer than the timeout is not worth sleeping for: raise the real error.
90
+ raise e if wait > timeout && e.respond_to?(:retry_after_seconds) && e.retry_after_seconds
91
+
84
92
  sleep([wait, timeout].min)
85
93
  end
86
94
 
@@ -88,7 +96,7 @@ module Trigv
88
96
  end
89
97
 
90
98
  def retry_wait_seconds(error, attempt)
91
- return error.retry_after_seconds if error.is_a?(RateLimitError) && error.retry_after_seconds
99
+ return error.retry_after_seconds if error.respond_to?(:retry_after_seconds) && error.retry_after_seconds
92
100
 
93
101
  (0.5 * (2**attempt)) + (rand * 0.25)
94
102
  end
@@ -97,8 +105,8 @@ module Trigv
97
105
  case error
98
106
  when ValidationError, AuthenticationError, AuthorizationError, NotFoundError
99
107
  false
100
- when RateLimitError
101
- error.retryable
108
+ when RateLimitError, ConflictError, ServiceUnavailableError
109
+ true
102
110
  when ApiError
103
111
  retryable_status?(error.status_code)
104
112
  when NetworkError, TimeoutError
@@ -109,7 +117,7 @@ module Trigv
109
117
  end
110
118
 
111
119
  def retryable_status?(status)
112
- [429, 502, 503, 504].include?(status)
120
+ [502, 504].include?(status)
113
121
  end
114
122
 
115
123
  def request_once(method, path, body)
@@ -123,8 +131,7 @@ module Trigv
123
131
  [response.code.to_i, parsed, response_headers]
124
132
  rescue Net::OpenTimeout, Net::ReadTimeout, Timeout::Error
125
133
  raise TimeoutError
126
- rescue ValidationError, AuthenticationError, AuthorizationError, NotFoundError,
127
- RateLimitError, ApiError, TimeoutError
134
+ rescue Error
128
135
  raise
129
136
  rescue StandardError => e
130
137
  raise NetworkError.new(e.message, e)
@@ -166,7 +173,8 @@ module Trigv
166
173
  def parse_json(text)
167
174
  return nil if text.nil? || text.empty?
168
175
 
169
- JSON.parse(text)
176
+ parsed = JSON.parse(text)
177
+ parsed.is_a?(Hash) ? parsed : nil
170
178
  rescue JSON::ParserError
171
179
  nil
172
180
  end
@@ -183,7 +191,7 @@ module Trigv
183
191
  )
184
192
  end
185
193
 
186
- def parse_connection_result(parsed)
194
+ def parse_connection_result(parsed, request_id = nil)
187
195
  workspace_data = parsed['workspace']
188
196
  api_key_data = parsed['api_key']
189
197
 
@@ -202,7 +210,8 @@ module Trigv
202
210
 
203
211
  ConnectionResult.new(
204
212
  workspace: ConnectionWorkspace.new(**symbolize_keys(workspace_data)),
205
- api_key: ConnectionApiKey.new(**symbolize_keys(api_key_data))
213
+ api_key: ConnectionApiKey.new(**symbolize_keys(api_key_data)),
214
+ request_id: request_id
206
215
  )
207
216
  rescue ArgumentError, KeyError, TypeError
208
217
  raise ValidationError.new('Invalid connection response payload', {})
@@ -214,26 +223,22 @@ module Trigv
214
223
 
215
224
  def error_from_response(status, body, headers = {})
216
225
  message = body&.fetch('message', nil) || "Trigv API returned HTTP #{status}"
226
+ retry_after = Retry.parse_retry_after(headers['retry-after'])
227
+
228
+ error =
229
+ case status
230
+ when 401 then AuthenticationError.new(message)
231
+ when 403 then AuthorizationError.new(message)
232
+ when 404 then NotFoundError.new(message)
233
+ when 409 then ConflictError.new(message, retry_after_seconds: retry_after)
234
+ when 422 then ValidationError.new(message, body&.fetch('errors', {}) || {})
235
+ when 429 then RateLimitError.new(message, retry_after_seconds: retry_after)
236
+ when 503 then ServiceUnavailableError.new(message, retry_after_seconds: retry_after)
237
+ else ApiError.new(message, status, body)
238
+ end
217
239
 
218
- case status
219
- when 401
220
- raise AuthenticationError, message
221
- when 403
222
- raise AuthorizationError, message
223
- when 404
224
- raise NotFoundError, message
225
- when 422
226
- raise ValidationError.new(message, body&.fetch('errors', {}) || {})
227
- when 429
228
- retry_after = Retry.parse_retry_after(headers['retry-after'])
229
- raise RateLimitError.new(
230
- message,
231
- retryable: !Errors.monthly_limit_message?(message),
232
- retry_after_seconds: retry_after
233
- )
234
- else
235
- raise ApiError.new(message, status, body)
236
- end
240
+ error.request_id = headers['x-request-id']
241
+ raise error
237
242
  end
238
243
  end
239
244
  end
data/lib/trigv/errors.rb CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Trigv
4
4
  class Error < StandardError
5
+ # Value of the X-Request-ID response header, when the API answered.
6
+ attr_accessor :request_id
7
+
5
8
  def initialize(message = nil)
6
9
  super
7
10
  end
@@ -44,12 +47,42 @@ module Trigv
44
47
  end
45
48
 
46
49
  class RateLimitError < Error
47
- attr_reader :status_code, :retryable, :retry_after_seconds
50
+ attr_reader :status_code, :retry_after_seconds
48
51
 
49
- def initialize(message, retryable: true, retry_after_seconds: nil)
52
+ # +retryable:+ is accepted and ignored so code written for 1.0 keeps working.
53
+ def initialize(message, retry_after_seconds: nil, retryable: true) # rubocop:disable Lint/UnusedMethodArgument
50
54
  super(message)
51
55
  @status_code = 429
52
- @retryable = retryable
56
+ @retry_after_seconds = retry_after_seconds
57
+ end
58
+
59
+ # Always true.
60
+ #
61
+ # @deprecated Kept so code written for 1.0 still works. Every 429 is a rate limit:
62
+ # wait +retry_after_seconds+ and try again. There is no monthly cap any more.
63
+ def retryable # rubocop:disable Naming/PredicateMethod
64
+ true
65
+ end
66
+ end
67
+
68
+ # HTTP 409: another request is delivering the same idempotency_key.
69
+ class ConflictError < Error
70
+ attr_reader :status_code, :retry_after_seconds
71
+
72
+ def initialize(message, retry_after_seconds: nil)
73
+ super(message)
74
+ @status_code = 409
75
+ @retry_after_seconds = retry_after_seconds
76
+ end
77
+ end
78
+
79
+ # HTTP 503: push delivery is unavailable. Retry with the same idempotency_key.
80
+ class ServiceUnavailableError < Error
81
+ attr_reader :status_code, :retry_after_seconds
82
+
83
+ def initialize(message, retry_after_seconds: nil)
84
+ super(message)
85
+ @status_code = 503
53
86
  @retry_after_seconds = retry_after_seconds
54
87
  end
55
88
  end
@@ -82,8 +115,11 @@ module Trigv
82
115
  module Errors
83
116
  module_function
84
117
 
85
- def monthly_limit_message?(message)
86
- message.include?('Monthly event limit')
118
+ # Always false.
119
+ #
120
+ # @deprecated Kept so code written for 1.0 still works. The monthly event cap was removed.
121
+ def monthly_limit_message?(_message)
122
+ false
87
123
  end
88
124
  end
89
125
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Trigv
4
4
  module Validation
5
- DEFAULT_BASE_URL = 'https://api.trigv.com/api'
5
+ DEFAULT_BASE_URL = 'https://api.trigv.com'
6
6
  DEFAULT_TIMEOUT = 30
7
7
  MAX_RETRIES = 2
8
8
 
data/lib/trigv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trigv
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trigv
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Trigv
@@ -26,12 +26,15 @@ files:
26
26
  - lib/trigv/retry.rb
27
27
  - lib/trigv/validation.rb
28
28
  - lib/trigv/version.rb
29
- homepage: https://github.com/Trigv/trigv-ruby
29
+ homepage: https://trigv.com
30
30
  licenses:
31
31
  - MIT
32
32
  metadata:
33
- homepage_uri: https://github.com/Trigv/trigv-ruby
33
+ homepage_uri: https://trigv.com
34
+ documentation_uri: https://trigv.com/docs/sdks/ruby/
34
35
  source_code_uri: https://github.com/Trigv/trigv-ruby
36
+ bug_tracker_uri: https://github.com/Trigv/trigv-ruby/issues
37
+ changelog_uri: https://github.com/Trigv/trigv-ruby/blob/main/CHANGELOG.md
35
38
  rubygems_mfa_required: 'true'
36
39
  rdoc_options: []
37
40
  require_paths: