clicksend 1.1.0 → 1.2.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: 73ecf8d49a2a4fa5ffceef490e0f6ebaeb3ccb903286f5b80f31ae82a1a22d30
4
- data.tar.gz: 51cde5f40184bcd8b80b9bcf404076b48b17f9904eb26cb8aab781356980666c
3
+ metadata.gz: b4081eb11dc1575e44a3036a2cd584f0a2308da05cc4c3461c9a67a1dfcaa237
4
+ data.tar.gz: e908b0864b39ed7a437ea5d448daa1101b921f59341bc5c3044aea2af5310bc1
5
5
  SHA512:
6
- metadata.gz: 90b530124f465bf299cdc25acf770239f1963c9b87e96cc7a9685d8fbfc1029ec72c050e07e0882d2d7b65bc9244e74dfd36934b30c159f616b2e63151d6f086
7
- data.tar.gz: d14f63a3e5a1ba4ee4f862b82902fcae71f079b5a1d55f06bc2b784c474e19612e37078f88f9382ad3ab48ba3f41568158b57f5fb0bd3757af5b03251395033b
6
+ metadata.gz: a61a3e64fdcb9514e555c680d0f6c9db7f1a2758d1b292f75e7063fbb9361e613b4711c98ae5f3da583ca641ebbc69b15a8748b38c75baa6ad112e20c1de85f6
7
+ data.tar.gz: 4214c6aa5ffdebae106f690d4276b72aacd6b0451dfaa7c81be70b3d929b2dbdf6b39bda832dda7f24d5aad1500e5f38e866335a123ca5a96647932d6ac25c61
data/CHANGELOG.md CHANGED
@@ -4,6 +4,165 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [1.2.0] - 2026-10-06
10
+
11
+ Cancelling and reconciling messages, test-framework helpers, opt-in persistent connections, and
12
+ hardening. Mostly additive. "Fixed" lists behaviour changes an existing application may notice,
13
+ such as `Marshal.dump(client)` and `YAML.dump(client)` now raising and stricter `Retry-After`
14
+ parsing. `sms.cancel` is **experimental** and may change in a minor release: its successful answer
15
+ has not been observed live (see below). The OpenTelemetry instrumenter is a separate gem,
16
+ `clicksend-opentelemetry` (`companions/`), released on its own schedule.
17
+
18
+ ### Added
19
+
20
+ - **`sms.cancel(message_id)` (experimental)** cancels one scheduled SMS
21
+ (`PUT /v3/sms/{message_id}/cancel`) and returns nil. ClickSend documents only the successful
22
+ answer, and no idempotency, so it is not retried after a timeout or 5xx; such a failure is an
23
+ `AmbiguousRequestError` (the message may or may not have been cancelled). Check
24
+ `sms.history(message_id:)` for the status `"Cancelled"` when it matters.
25
+ `PUT /v3/sms/cancel-all` is still deliberately not wrapped.
26
+ The successful 200 `SUCCESS` answer is covered by contract specs but was **not observed live**:
27
+ ClickSend's free test number doesn't hold scheduled messages (they show as `Completed` within
28
+ seconds), so it can't exercise a successful cancel. Live cancels of those test-number messages,
29
+ a repeated cancel and a random ID all answered HTTP 404 `NOT_FOUND`, raised as
30
+ `Clicksend::NotFoundError`. A 404 doesn't say which case applies; it does not mean "already
31
+ sent". The API may change in a minor release once a real cancellation has been observed.
32
+ - `sms.search_history(to:, custom_string:, sent_after:, sent_before: nil)` returns the outbound
33
+ history rows ClickSend shows now for that recipient and exact `custom_string`, possibly none. It
34
+ widens the date window by five minutes on each side, reads every page (100 rows each), and
35
+ requires an E.164 recipient. An empty result is not proof that nothing was sent.
36
+ - Testing: `FakeAPI` cancels messages it holds as scheduled for the future
37
+ (`fake.cancelled_messages`) and raises `StubError` for every cancel ClickSend doesn't document,
38
+ so tests must stub that answer. `fake.stub_history(*messages, status: "Sent")` states what
39
+ history shows; the fake still serves no history by itself.
40
+ - Testing: opt-in RSpec matchers, `require "clicksend/testing/rspec"`:
41
+ `expect(fake).to have_sent_sms(to:, body:, custom_string:, ...)` (any `SentMessage` attribute,
42
+ matched with `===`) with `.once`, `.twice`, `.times(n)` and `.exactly(n).times`;
43
+ `not_to have_sent_sms(...)`; and `have_sent_no_sms`. Without a count exactly one message must
44
+ match, so a duplicate fails, and the negated form means "none matching". Failures list what was
45
+ sent, one line per message (at most ten, long values shortened). The require includes the
46
+ matchers in every example group (`Clicksend::Testing::RSpecMatchers`).
47
+ - Testing: opt-in Minitest assertions, `require "clicksend/testing/minitest"` and
48
+ `include Clicksend::Testing::MinitestAssertions`: `assert_sms_sent(fake, count: 1, **attributes)`
49
+ (returns the matching messages) and `assert_no_sms_sent(fake, **attributes)`, with the same
50
+ matching and failure output. Neither framework is a dependency or loaded by
51
+ `require "clicksend"` or `require "clicksend/testing"`.
52
+ - Testing: `fake.fail_next(:interrupted, processed: true|false)` simulates the job runner stopping
53
+ the worker mid-send (Sidekiq's shutdown, a deploy's SIGTERM), after or before ClickSend processed
54
+ the request. It raises `Clicksend::Testing::SimulatedInterrupt`, an `Exception` that is neither
55
+ a `StandardError` nor an `Interrupt`, so it passes through the client untouched and is never
56
+ retried. Use it to test that the job's re-run doesn't send again. ClickSend itself never does
57
+ this.
58
+
59
+ ### Fixed
60
+
61
+ - **Instrumenters that don't run the block synchronously can no longer send late or return nil.**
62
+ A `request.clicksend` block kept by the instrumenter and called after `#instrument` returned
63
+ now raises `ConfigurationError` without sending (before, the call raised `ConfigurationError`
64
+ and the SMS was sent later anyway). If `#instrument` returns while the block is still running on
65
+ another thread, or after swallowing an exception that escaped the request, the call raises a
66
+ `ConfigurationError` that is also an `AmbiguousRequestError` unless the request is idempotent
67
+ (before, `Client#request` could return nil while the send went ahead).
68
+ - **A `ScriptError` from code outside the gem is handled like any other failure.** A logger or
69
+ instrumenter raising `NotImplementedError` or `LoadError` after a send was accepted escaped as a
70
+ non-Clicksend error (also in 1.1); job runners such as Sidekiq rescue `Exception` and would run
71
+ the job, and the send, again. Loggers, instrumenters, retry policies and custom transports are
72
+ now treated the same for `StandardError` and `ScriptError`: the request's own result or error
73
+ wins, and a custom transport's `ScriptError` is an ambiguous `ConnectionError` for a send.
74
+ `Interrupt`, `SystemExit` and `NoMemoryError` still propagate.
75
+ - **`Clicksend::Client` refuses `Marshal.dump` and `YAML.dump`** (`TypeError`), including inside
76
+ another object such as `client.sms`. A client holds the API key, which both used to write out in
77
+ clear (e.g. into a cache or a job payload). Build a new client instead. Responses and errors can
78
+ still be serialized. Other serializers that walk instance variables (e.g. ActiveSupport's
79
+ `Object#as_json`) are not covered: pass job arguments, not clients.
80
+ - **A retry delay too long to sleep no longer raises `RangeError`.** With
81
+ `RetryPolicy.new(max_retry_after: Float::INFINITY)`, a `Retry-After: 99999999999999999999` made
82
+ `Kernel.sleep` raise `RangeError` instead of the `RateLimitError`. A delay over 2**31 - 1 seconds
83
+ (from any policy) now means "don't retry": the request's own error is raised.
84
+ - **A retry policy answering with an Integer or Rational too large for a Float** no longer makes
85
+ Ruby print "Integer out of Float range" (with `-W`); it means "don't retry", as before.
86
+ - **Pagination never raises a non-Clicksend error for a nonsensical page.** A `current_page` below 1,
87
+ or a negative `last_page`, `total` or `per_page`, is a `MalformedResponseError` with the request
88
+ attached (before, `current_page: -1` made `next_page` raise `ArgumentError`).
89
+ `client.paginate(path, query: nil)` now means no query, like `Client#request`; any other
90
+ non-Hash `query:` raises `ArgumentError` (before, both raised `NoMethodError`).
91
+ - **A response body that isn't valid in its declared charset is classified by its status.** A
92
+ body labelled e.g. `charset=us-ascii`, `shift_jis` or `utf-16le` that holds bytes invalid in that
93
+ charset made JSON raise an `EncodingError`, so every such response became an unreadable
94
+ (`MalformedResponseError`) one: a GET's 503 was not retried, and a send's 429 was reported as
95
+ ambiguous instead of being retried. Such a body is now treated like any other non-JSON body: an
96
+ error status keeps the raw body and its usual error class and retry rule; a 2xx is still a
97
+ `MalformedResponseError` (ambiguous for a send). Bodies labelled `utf-8` were already handled.
98
+ - **`RateLimitError#retry_after` accepts only what RFC 9110 allows**: plain non-negative decimal
99
+ seconds or an HTTP-date. It used Ruby's `Integer()`, so `"0x10"` meant 16 seconds, `"1_0"` 10 and
100
+ `"+5"` 5; those, `"-5"` (before: 0) and non-String values are now nil, and the retry policy backs
101
+ off as for a missing header. It no longer raises for `nil` headers or an Array value from a
102
+ custom transport, so such a 429 is retried with backoff instead of being raised at once.
103
+ - Testing: `FakeAPI#client(max_retries:, retry_policy:)` silently ignored `max_retries:`. It now
104
+ raises `ConfigurationError`, exactly as `Client.new` does for both, and `max_retries: nil` means
105
+ the default, as in `Client.new`.
106
+ - **With `adapter: :net_http_persistent`, failures before the request was written are no longer
107
+ ambiguous.** That adapter reports them differently from the default one, so a refused connection
108
+ (`Net::HTTP::Persistent::Error` "connection refused", caused by `Errno::ECONNREFUSED`), a connect
109
+ or TLS-handshake timeout (`Net::OpenTimeout`, wrapped in `Faraday::TimeoutError`) and a wait for a
110
+ pooled connection longer than connection_pool's 0.5 s (`ConnectionPool::TimeoutError`) were
111
+ classified as possibly sent: a send that never left was an `AmbiguousRequestError` and not
112
+ retried. They are now not sent, so they are retried like the default adapter's (and a pool wait is
113
+ a `TimeoutError`). A downed host and TLS errors still count as possibly sent with either adapter.
114
+
115
+ ### Changed
116
+
117
+ - **Webhook documentation corrected from new evidence** (`Clicksend::Webhook` is still
118
+ experimental; no behaviour changed). Archived ClickSend help articles and ClickSend's own n8n
119
+ and Power Automate integrations list the pushed fields, including legacy duplicates (`message`,
120
+ `sms`, `originalsenderid`, `messageid`, `customstring`, ...) that stay in `#raw`. The README and
121
+ API notes no longer say that no source ever listed IP addresses: an archived article did, but it
122
+ is stale and unpublished, so the gem still offers no allowlist. Archived sources disagree on the
123
+ retry schedule, which is now said. Receipts must be deduplicated on `message_id` and
124
+ `status_code`, not `message_id` alone, because a message may get more than one receipt. New
125
+ receiver advice: secret rotation, `discard_on Clicksend::Webhook::InvalidPayload`, Rails'
126
+ log filtering, voice/email/fax receipts sharing the format, inbound MMS links, and the
127
+ dashboard's "Add Test Reply".
128
+
129
+ ### Documentation
130
+
131
+ - **Background jobs rewritten from new measurements** (ActiveJob 8.1.4, Sidekiq 8.1.7 and
132
+ ActiveRecord 8.1.4 against local stand-ins for ClickSend). The 1.1 recipe stops framework
133
+ retries from repeating an ambiguous send, but a real Sidekiq process stopped mid-send re-ran the
134
+ job and sent the message twice with the default 30s read timeout (once with a 1s timeout). The
135
+ README now covers: a timeout budget for job clients (one attempt must end inside the runner's
136
+ shutdown timeout; Sidekiq's default is 25s); the `retry_on`/`discard_on` declaration order (the
137
+ same two lines in the wrong order sent twice); stacked retry layers (`retry_on` re-raises when
138
+ exhausted and the backend retries again); an in-flight marker committed on the application's own
139
+ row before `deliver` (claiming inside the send's own transaction still sent twice); a
140
+ reconciliation job built on `sms.search_history` that never resends on "not found"; and a
141
+ Sidekiq recipe with `sidekiq_retry_in` returning `:kill`.
142
+ - Observability recipes: `key=value` logs, metrics labelled by `operation` (never by `path`, which
143
+ can hold message IDs), and a Rails 8.1 `Rails.event` bridge. The OpenTelemetry warning now names
144
+ what the stock Faraday and Net::HTTP instrumentations record (the query string, with the
145
+ recipient's number from `sms.history(to:)`) and how to exclude ClickSend from them.
146
+ - Persistent connections: the measured benefit (local benchmark), the pool-size rule, and which
147
+ failures are retried under that adapter (see "Fixed").
148
+ - The companion gem `clicksend-opentelemetry` 0.1.0, in `companions/clicksend-opentelemetry`, is
149
+ versioned and released separately and is not yet on RubyGems: one OpenTelemetry span per
150
+ ClickSend call, built on the `instrumenter:` hook, with no query strings, bodies or phone
151
+ numbers. It changes nothing in this gem, which still depends on Faraday only. See its own
152
+ [CHANGELOG](companions/clicksend-opentelemetry/CHANGELOG.md).
153
+
154
+ ### Development
155
+
156
+ - Webhook replay fixtures (`spec/fixtures/webhooks`, one per published push shape, replayed
157
+ through Rack's request parsing) and `script/webhook_capture.rb`, which captures real pushes
158
+ locally and redacts them into fixtures. `rack` is a new development dependency.
159
+ - `minitest` is now declared as a development dependency (it was only pulled in through
160
+ activesupport); the Minitest assertions are tested inside real `Minitest::Test` cases.
161
+ - `faraday-net_http_persistent` is a new development dependency:
162
+ `spec/integration/persistent_connection_spec.rb` runs that adapter on real sockets (plain and
163
+ TLS) and pins that a reused connection failing after the write never hides a retry of a POST or
164
+ PUT, that timeouts are honoured, and the not-sent classifications above.
165
+
7
166
  ## [1.1.0] - 2026-10-06
8
167
 
9
168
  Failure semantics, observability and testing support for production messaging. Mostly additive;
@@ -192,6 +351,8 @@ A rewrite for ClickSend's REST v3 API and modern Ruby. See [MIGRATING.md](MIGRAT
192
351
  - Last release of the original gem: send SMS, poll replies and delivery reports, and check
193
352
  the balance through ClickSend's v2 API.
194
353
 
354
+ [Unreleased]: https://github.com/prayantr/clicksend/compare/v1.2.0...HEAD
355
+ [1.2.0]: https://github.com/prayantr/clicksend/compare/v1.1.0...v1.2.0
195
356
  [1.1.0]: https://github.com/prayantr/clicksend/compare/v1.0.0...v1.1.0
196
357
  [1.0.0]: https://github.com/prayantr/clicksend/compare/v1.0.0.rc1...v1.0.0
197
358
  [1.0.0.rc1]: https://github.com/prayantr/clicksend/compare/c99edc5...v1.0.0.rc1