fulfil_api 0.7.1 → 0.7.2

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: 2971bc621921930210bce77c057d6da8ce549cfe476fb1fc29d72f10c4447303
4
- data.tar.gz: 97658bf767d8909e359ad66441de0f81108982c6383070778eb04d3386c2fb3d
3
+ metadata.gz: 9a5ff8f7f8d9afe6a5130c2cf042875f2d79eabcdb53f61e7f229d03b2dc0c6e
4
+ data.tar.gz: 017f33c16b4562586e696724b8428f76bc27daeeaf61b0cd2d783f79a952b7e4
5
5
  SHA512:
6
- metadata.gz: 020e57b3a676a3c56f2d2b76e5b269010764d3a95f41ef19848f0477e4f77ac9db9871252c68efe77fe35af1fbd6f319d8283f43b578caf7e09e8de4651120a9
7
- data.tar.gz: 51af4a10179668acf8977fca9dccfc133bbb25516c7464cb87bd05f9abf0a5926b1631620cf9265a9637b7b0ffa8c7b9a6618bf9ee7b75912005eb3f1722e1a3
6
+ metadata.gz: 387283adc42ad86ed86cea9c20f0c65728fb942b7ef7db70286a2dac2fcc544b0bf49a78feb520ff918faec916dabadf94067f71003bab2adb4087219fe63437
7
+ data.tar.gz: 8a69f8a11d5152825c8648babbe9535e533d394e13f8da819759eca069aab66bc8656517b35b5d4275ad62ffcabe4e022e93952e09f2780508bca8d90ba6830e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Publish an `ActiveSupport::Notifications` event named `error.fulfil_api` whenever a `FulfilApi::Error` is raised, so an application can report failures of the Fulfil API to its APM without rescuing every call into the gem. Subscribe with `FulfilApi.on_error { |error| ... }`, or through `ActiveSupport::Notifications` directly to also reach the status code, response body and response headers of a `FulfilApi::HttpError`.
4
+
3
5
  - Raise a `FulfilApi::HttpError` instead of a generic `FulfilApi::Error` when a request to Fulfil fails, with a dedicated subclass per HTTP status code (e.g. `FulfilApi::HttpError::TooManyRequests` for a 429). The exception message is now the description reported by Fulfil rather than its serialized response body, and the status code, body and headers are available through `#status_code`, `#response_body` and `#response_headers`. `FulfilApi::HttpError` inherits from `FulfilApi::Error`, so existing rescues keep working.
4
6
 
5
7
  - Re-enable Ruby's built-in retry for idempotent requests on the persistent connection, which the `net_http_persistent` adapter disables by forcing `max_retries` to `0`. This recovers stale keep-alive sockets transparently instead of surfacing them as read timeouts.
data/README.md CHANGED
@@ -178,6 +178,50 @@ A request that never reached Fulfil — a connection reset, a DNS failure, a tim
178
178
 
179
179
  `FulfilApi::HttpError` inherits from `FulfilApi::Error`, the base class of every error in this gem, so code that already rescues `FulfilApi::Error` keeps working unchanged.
180
180
 
181
+ ### Subscribing to Errors
182
+
183
+ Rescuing tells you about the one call you wrapped. To report *every* failure of the Fulfil API — to count rate limit hits in your APM, to page on authentication failures, to log what Fulfil actually said — subscribe once instead.
184
+
185
+ Every `FulfilApi::Error` publishes an `ActiveSupport::Notifications` event named `error.fulfil_api` when it is raised. `FulfilApi.on_error` is the shorthand for listening to it:
186
+
187
+ ```ruby
188
+ # config/initializers/fulfil_api.rb
189
+
190
+ FulfilApi.on_error do |error|
191
+ Appsignal.increment_counter("fulfil_api_errors", 1, error: error.class.name)
192
+ end
193
+ ```
194
+
195
+ Because every error carries its own class, you can report only the failures you care about:
196
+
197
+ ```ruby
198
+ FulfilApi.on_error do |error|
199
+ next unless error.is_a?(FulfilApi::HttpError::TooManyRequests)
200
+
201
+ Appsignal.increment_counter("fulfil_api_rate_limit_exceeded")
202
+ end
203
+ ```
204
+
205
+ Subscribing through `ActiveSupport::Notifications` directly gives you the full payload, which carries the HTTP details of a `FulfilApi::HttpError` as separate keys — handy as metric dimensions:
206
+
207
+ ```ruby
208
+ ActiveSupport::Notifications.subscribe(FulfilApi::Error::EVENT_NAME) do |event|
209
+ event.payload[:exception] # => ["FulfilApi::HttpError::TooManyRequests", "Try again later."]
210
+ event.payload[:exception_object] # => the FulfilApi::HttpError::TooManyRequests instance
211
+ event.payload[:status_code] # => 429
212
+ event.payload[:response_headers] # => { "retry-after" => "5", ... }
213
+ event.payload[:response_body] # => the raw response of Fulfil
214
+ end
215
+ ```
216
+
217
+ Both methods return the subscriber, so you can stop listening again with `ActiveSupport::Notifications.unsubscribe(subscriber)`.
218
+
219
+ A few things worth knowing:
220
+
221
+ - The event is published when an error is **raised**, not when it is built. Rescuing it afterwards does not suppress the notification, and retrying a request publishes one event per attempt — which is exactly what you want when counting rate limit hits.
222
+ - The subscriber runs on the thread that raised the error, while the error travels up the stack. Keep it cheap and hand anything slow to a background job.
223
+ - A subscriber cannot change the behaviour of your application. If it raises, the exception is swallowed and reported on `$stderr` rather than replacing the `FulfilApi::Error` on its way up.
224
+
181
225
  ### Using the 3PL (TPL) Client
182
226
 
183
227
  The gem also includes a client for Fulfil's [3PL Integration API](https://fulfil-3pl-integration-api.readme.io/reference/getting-started-with-your-api). This is a separate API that allows third-party logistics providers to interact with Fulfil on behalf of a merchant.
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Error
5
+ # The {FulfilApi::Error::Notifiable} publishes an {ActiveSupport::Notifications}
6
+ # event every time a {FulfilApi::Error} is raised. It gives an application a
7
+ # single place to report a failure of the Fulfil API to its APM — a counter of
8
+ # rate limit hits, an error tracker, a log line — without having to wrap every
9
+ # call into the gem in a `begin`/`rescue`.
10
+ #
11
+ # Ruby routes every form of `raise` through {Exception.exception} (when raising a
12
+ # class) or {Exception#exception} (when raising an instance), which makes those
13
+ # two methods the hook for "an error is on its way up the stack". Building an
14
+ # error without raising it — {FulfilApi::HttpError.from_faraday_error}, for
15
+ # example — publishes nothing.
16
+ #
17
+ # @example counting the errors of Fulfil in an APM
18
+ # # config/initializers/fulfil_api.rb
19
+ # FulfilApi.on_error do |error|
20
+ # Appsignal.increment_counter("fulfil_api_errors", 1, error: error.class.name)
21
+ # end
22
+ #
23
+ # @example subscribing through ActiveSupport directly, for the full payload
24
+ # ActiveSupport::Notifications.subscribe(FulfilApi::Error::EVENT_NAME) do |event|
25
+ # Rails.logger.warn("Fulfil responded with #{event.payload[:status_code]}")
26
+ # end
27
+ #
28
+ # @note A subscriber must not change the behaviour of the application it observes.
29
+ # An exception raised by a subscriber is therefore swallowed and reported on
30
+ # `$stderr` rather than allowed to replace the {FulfilApi::Error} on its way up.
31
+ module Notifiable
32
+ extend ActiveSupport::Concern
33
+
34
+ # The name of the published event. It follows the `<action>.<library>` naming
35
+ # convention of Rails, so an APM that subscribes to a whole library through a
36
+ # `/\.fulfil_api\z/` regexp picks it up along with everything the gem may
37
+ # instrument in the future.
38
+ EVENT_NAME = "error.fulfil_api"
39
+
40
+ # The key of the fiber local flag guarding against infinite recursion. A
41
+ # subscriber that itself raises a {FulfilApi::Error} — one calling back into
42
+ # Fulfil, for instance — would otherwise publish an event from within the
43
+ # publication of an event, without end.
44
+ REENTRANCY_KEY = :fulfil_api_notifying_error
45
+
46
+ class_methods do
47
+ # Publishes {EVENT_NAME} for `raise SomeError` and `raise SomeError, "message"`.
48
+ #
49
+ # @return [FulfilApi::Error] The error being raised.
50
+ def exception(*, **)
51
+ super.tap(&:notify)
52
+ end
53
+ end
54
+
55
+ # Publishes {EVENT_NAME} for `raise error` and `raise error, "message"`.
56
+ #
57
+ # @return [FulfilApi::Error] The error being raised.
58
+ def exception(*)
59
+ super.tap(&:notify)
60
+ end
61
+
62
+ # Publishes {EVENT_NAME} for the receiver.
63
+ #
64
+ # @return [void]
65
+ def notify
66
+ return if Thread.current[REENTRANCY_KEY]
67
+
68
+ Thread.current[REENTRANCY_KEY] = true
69
+ ActiveSupport::Notifications.instrument(EVENT_NAME, notification_payload)
70
+ rescue StandardError => e
71
+ warn "[FulfilApi] a subscriber of #{EVENT_NAME} raised #{e.class}: #{e.message}"
72
+ ensure
73
+ Thread.current[REENTRANCY_KEY] = nil
74
+ end
75
+
76
+ private
77
+
78
+ # The payload published alongside {EVENT_NAME}.
79
+ #
80
+ # `:exception` and `:exception_object` follow the convention Rails uses for its
81
+ # own instrumentation, which is what an APM looks for to attribute an event to
82
+ # a failure.
83
+ #
84
+ # @return [Hash]
85
+ def notification_payload
86
+ { exception: [self.class.name, message], exception_object: self }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -3,7 +3,13 @@
3
3
  module FulfilApi
4
4
  # The {FulfilApi::Error} is the base class for all FulfilApi errors, also used
5
5
  # for generic or unexpected errors.
6
+ #
7
+ # Raising any of them publishes an {ActiveSupport::Notifications} event, so an
8
+ # application can report the failure to its APM without rescuing every call into
9
+ # the gem. See {FulfilApi::Error::Notifiable} and {FulfilApi.on_error}.
6
10
  class Error < StandardError
11
+ include Notifiable
12
+
7
13
  attr_reader :details
8
14
 
9
15
  # @param message [String] The displayable error message for the receiver.
@@ -32,4 +38,36 @@ module FulfilApi
32
38
  nil
33
39
  end
34
40
  end
41
+
42
+ # Subscribes to every {FulfilApi::Error} raised by the gem.
43
+ #
44
+ # This is the shorthand for the common case: reporting the failure somewhere. To
45
+ # reach the HTTP status code, response body and response headers of a
46
+ # {FulfilApi::HttpError} as well, subscribe to {FulfilApi::Error::EVENT_NAME}
47
+ # through {ActiveSupport::Notifications} directly.
48
+ #
49
+ # @example incrementing a counter of an APM
50
+ # # config/initializers/fulfil_api.rb
51
+ # FulfilApi.on_error do |error|
52
+ # Appsignal.increment_counter("fulfil_api_errors", 1, error: error.class.name)
53
+ # end
54
+ #
55
+ # @example reporting only the rate limit hits
56
+ # FulfilApi.on_error do |error|
57
+ # next unless error.is_a?(FulfilApi::HttpError::TooManyRequests)
58
+ #
59
+ # Appsignal.increment_counter("fulfil_api_rate_limit_exceeded")
60
+ # end
61
+ #
62
+ # @note The block runs while the error travels up the stack, on the thread that
63
+ # raised it. Keep it cheap, and hand anything slow to a background job.
64
+ #
65
+ # @yieldparam error [FulfilApi::Error] The error being raised.
66
+ # @return [Object] The subscriber, to hand back to
67
+ # `ActiveSupport::Notifications.unsubscribe` to stop listening.
68
+ def self.on_error
69
+ ActiveSupport::Notifications.subscribe(Error::EVENT_NAME) do |event|
70
+ yield(event.payload[:exception_object])
71
+ end
72
+ end
35
73
  end
@@ -174,5 +174,20 @@ module FulfilApi
174
174
  def status_code
175
175
  details&.dig(:response_status)
176
176
  end
177
+
178
+ private
179
+
180
+ # Enriches the payload published by {FulfilApi::Error::Notifiable} with what
181
+ # Fulfil reported, so a subscriber can tag its metrics by status code without
182
+ # having to unpack the exception itself.
183
+ #
184
+ # @return [Hash]
185
+ def notification_payload
186
+ super.merge(
187
+ response_body: response_body,
188
+ response_headers: response_headers,
189
+ status_code: status_code
190
+ )
191
+ end
177
192
  end
178
193
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FulfilApi
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.2"
5
5
  end
data/lib/fulfil_api.rb CHANGED
@@ -12,6 +12,7 @@ require "active_support/core_ext/hash/deep_merge"
12
12
  require "active_support/core_ext/hash/indifferent_access"
13
13
  require "active_support/core_ext/module/delegation"
14
14
  require "active_support/core_ext/object/blank"
15
+ require "active_support/notifications"
15
16
 
16
17
  module FulfilApi
17
18
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fulfil_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Vermaas
@@ -86,6 +86,7 @@ files:
86
86
  - lib/fulfil_api/configuration.rb
87
87
  - lib/fulfil_api/customer_shipment.rb
88
88
  - lib/fulfil_api/error.rb
89
+ - lib/fulfil_api/error/notifiable.rb
89
90
  - lib/fulfil_api/http_error.rb
90
91
  - lib/fulfil_api/relation.rb
91
92
  - lib/fulfil_api/relation/batchable.rb