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 +4 -4
- data/CHANGELOG.md +161 -0
- data/README.md +457 -74
- data/docs/clicksend-api-notes.md +49 -5
- data/lib/clicksend/client.rb +16 -0
- data/lib/clicksend/connection.rb +81 -26
- data/lib/clicksend/errors.rb +14 -10
- data/lib/clicksend/instrumentation.rb +6 -0
- data/lib/clicksend/page.rb +6 -1
- data/lib/clicksend/resources/sms.rb +77 -0
- data/lib/clicksend/retry_policy.rb +5 -2
- data/lib/clicksend/testing/failure.rb +15 -4
- data/lib/clicksend/testing/fake_api.rb +87 -18
- data/lib/clicksend/testing/minitest.rb +56 -0
- data/lib/clicksend/testing/payloads.rb +15 -0
- data/lib/clicksend/testing/records.rb +17 -1
- data/lib/clicksend/testing/rspec.rb +124 -0
- data/lib/clicksend/testing/sms_expectations.rb +91 -0
- data/lib/clicksend/testing.rb +6 -0
- data/lib/clicksend/transport.rb +29 -9
- data/lib/clicksend/version.rb +1 -1
- data/lib/clicksend/webhook.rb +22 -16
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b4081eb11dc1575e44a3036a2cd584f0a2308da05cc4c3461c9a67a1dfcaa237
|
|
4
|
+
data.tar.gz: e908b0864b39ed7a437ea5d448daa1101b921f59341bc5c3044aea2af5310bc1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|