letterapp 0.2.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +43 -0
- data/lib/letterapp/client.rb +29 -14
- data/lib/letterapp/delivery_method.rb +4 -5
- data/lib/letterapp/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a28e33d3a0a3f740b0d45a1a53c22983dbd781fba93c87dc8a167ae51075bc6
|
|
4
|
+
data.tar.gz: 643ca3a372bf627e1cea174989880196ce66c722da3ebfdd605e35d4bd8373b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 334a7ce7651803672d8d46bc37c97924bf18a556513a66d879ab0dfb6cf8f5559db138f3501bccc1e23ca4836071c4512a8c3886ff69eb8759b9273a13865080
|
|
7
|
+
data.tar.gz: 25819b5ad570ceec7b853f19bc2629a6b5a2c5b27e10849a5994e0d6e616c4335b616fd5c0be7e88f33419d60a2f7b73ee61e0d809db52aeb350ab0c32edce82
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.2 - 2026-10-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Sending mail through the `:letter` ActionMailer delivery method no longer
|
|
8
|
+
leaks a thread per message. `Client.new` started the batching thread, and
|
|
9
|
+
registered an `at_exit` hook holding the client, even for a client that
|
|
10
|
+
only calls `send_email`; Mail builds a new delivery method, and so a new
|
|
11
|
+
client, for every message. A long-running process kept one idle thread and
|
|
12
|
+
one client per email sent. The thread now starts with the first `identify`,
|
|
13
|
+
`group` or `track` call.
|
|
14
|
+
- A client created before a fork (a Rails initializer under Puma cluster mode
|
|
15
|
+
or Solid Queue) now sends from the forked process as usual. The child
|
|
16
|
+
inherited a dead copy of the parent's thread, so its `identify`, `group` and
|
|
17
|
+
`track` calls waited in the queue until the process exited. The client now
|
|
18
|
+
starts a new thread when the old one is dead.
|
|
19
|
+
|
|
20
|
+
## 0.2.1 - 2026-10-25
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `Letterapp::Client` no longer breaks Active Support's `present?`, `blank?`
|
|
25
|
+
and `presence`. 0.2.0 had a private `blank?(value)` helper that shadowed
|
|
26
|
+
`Object#blank?`, so in a Rails app `client.present?` and `client.presence`
|
|
27
|
+
raised `ArgumentError: wrong number of arguments (given 0, expected 1)`, and
|
|
28
|
+
`client.blank?` raised `NoMethodError`. The helper is now named `missing?`.
|
|
29
|
+
|
|
30
|
+
## 0.2.0 - 2026-08-20
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `send_email` for transactional email (`POST /v1/send`), with
|
|
35
|
+
`idempotency_key:` for replay-safe retries.
|
|
36
|
+
- `Letterapp::DeliveryMethod`, registered as `:letter` with ActionMailer in a
|
|
37
|
+
Rails app.
|
|
38
|
+
- `Letterapp::Error#code` and `#reason`, from the API's error body.
|
|
39
|
+
|
|
40
|
+
## 0.1.0 - 2026-05-29
|
|
41
|
+
|
|
42
|
+
- First release: `identify`, `group` and `track`, auto-batched in the
|
|
43
|
+
background, with retries and idempotent message IDs.
|
data/lib/letterapp/client.rb
CHANGED
|
@@ -34,8 +34,9 @@ module Letterapp
|
|
|
34
34
|
# A Letter client.
|
|
35
35
|
#
|
|
36
36
|
# Long-running server (default): identify / group / track enqueue calls that a
|
|
37
|
-
# background thread auto-batches and flushes every 100ms or 50 events.
|
|
38
|
-
# +close+ before the process
|
|
37
|
+
# background thread auto-batches and flushes every 100ms or 50 events. The
|
|
38
|
+
# thread starts with the first queued call. Call +close+ before the process
|
|
39
|
+
# exits.
|
|
39
40
|
#
|
|
40
41
|
# Serverless: pass flush_at: 1 and call +flush+ at the end of each invocation.
|
|
41
42
|
#
|
|
@@ -69,9 +70,7 @@ module Letterapp
|
|
|
69
70
|
@cond = ConditionVariable.new
|
|
70
71
|
@send_mutex = Mutex.new
|
|
71
72
|
@closed = false
|
|
72
|
-
|
|
73
|
-
@thread = Thread.new { loop_run }
|
|
74
|
-
at_exit { close }
|
|
73
|
+
@thread = nil # see start_worker
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
# Queue an identify call (creates or updates a contact).
|
|
@@ -109,9 +108,9 @@ module Letterapp
|
|
|
109
108
|
def send_email(to:, subject:, html: nil, text: nil, from: nil, from_name: nil,
|
|
110
109
|
reply_to: nil, headers: nil, tag: nil, metadata: nil,
|
|
111
110
|
idempotency_key: nil)
|
|
112
|
-
raise Error, "send_email: to is required" if
|
|
113
|
-
raise Error, "send_email: subject is required" if
|
|
114
|
-
raise Error, "send_email: provide html, text, or both" if
|
|
111
|
+
raise Error, "send_email: to is required" if missing?(to)
|
|
112
|
+
raise Error, "send_email: subject is required" if missing?(subject)
|
|
113
|
+
raise Error, "send_email: provide html, text, or both" if missing?(html) && missing?(text)
|
|
115
114
|
|
|
116
115
|
body = { "to" => to, "subject" => subject }
|
|
117
116
|
{
|
|
@@ -154,10 +153,23 @@ module Letterapp
|
|
|
154
153
|
return
|
|
155
154
|
end
|
|
156
155
|
@queue << item
|
|
156
|
+
start_worker
|
|
157
157
|
@cond.signal
|
|
158
158
|
end
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
+
# Starts the batching thread (and the at_exit hook that drains it) on the
|
|
162
|
+
# first queued call rather than in +initialize+. Mail builds a new delivery
|
|
163
|
+
# method, and so a new client, for every message; each one used to leave
|
|
164
|
+
# an idle thread behind. Also restarts a dead thread, which is what a
|
|
165
|
+
# forked child inherits from its parent. Called with @mutex held.
|
|
166
|
+
def start_worker
|
|
167
|
+
return if @thread&.alive?
|
|
168
|
+
|
|
169
|
+
at_exit { close } if @thread.nil?
|
|
170
|
+
@thread = Thread.new { loop_run }
|
|
171
|
+
end
|
|
172
|
+
|
|
161
173
|
def take_batch
|
|
162
174
|
@mutex.synchronize do
|
|
163
175
|
return nil if @queue.empty?
|
|
@@ -248,7 +260,7 @@ module Letterapp
|
|
|
248
260
|
end
|
|
249
261
|
|
|
250
262
|
def serialize_identify(user_id, email, traits, timezone, timestamp, message_id)
|
|
251
|
-
raise Error, "identify: user_id is required" if
|
|
263
|
+
raise Error, "identify: user_id is required" if missing?(user_id)
|
|
252
264
|
|
|
253
265
|
item = {
|
|
254
266
|
"type" => "identify",
|
|
@@ -264,8 +276,8 @@ module Letterapp
|
|
|
264
276
|
end
|
|
265
277
|
|
|
266
278
|
def serialize_group(user_id, account_id, name, traits, timestamp, message_id)
|
|
267
|
-
raise Error, "group: user_id is required" if
|
|
268
|
-
raise Error, "group: account_id is required" if
|
|
279
|
+
raise Error, "group: user_id is required" if missing?(user_id)
|
|
280
|
+
raise Error, "group: account_id is required" if missing?(account_id)
|
|
269
281
|
|
|
270
282
|
item = {
|
|
271
283
|
"type" => "group",
|
|
@@ -281,8 +293,8 @@ module Letterapp
|
|
|
281
293
|
end
|
|
282
294
|
|
|
283
295
|
def serialize_track(user_id, event, properties, timestamp, message_id)
|
|
284
|
-
raise Error, "track: user_id is required" if
|
|
285
|
-
raise Error, "track: event is required" if
|
|
296
|
+
raise Error, "track: user_id is required" if missing?(user_id)
|
|
297
|
+
raise Error, "track: event is required" if missing?(event)
|
|
286
298
|
|
|
287
299
|
item = {
|
|
288
300
|
"type" => "track",
|
|
@@ -303,7 +315,10 @@ module Letterapp
|
|
|
303
315
|
value.to_s
|
|
304
316
|
end
|
|
305
317
|
|
|
306
|
-
|
|
318
|
+
# Not named +blank?+: Active Support defines Object#blank?, and its
|
|
319
|
+
# +present?+ and +presence+ call it, so a private blank?(value) here would
|
|
320
|
+
# shadow it and make +client.present?+ raise ArgumentError in a Rails app.
|
|
321
|
+
def missing?(value)
|
|
307
322
|
value.nil? || value.to_s.empty?
|
|
308
323
|
end
|
|
309
324
|
|
|
@@ -82,14 +82,13 @@ module Letterapp
|
|
|
82
82
|
|
|
83
83
|
private
|
|
84
84
|
|
|
85
|
+
# Mail builds a new delivery method for every message, so this client
|
|
86
|
+
# lives for one message. That's cheap: it only calls send_email, which
|
|
87
|
+
# bypasses the queue, so the client never starts its batching thread.
|
|
85
88
|
def client
|
|
86
89
|
@client ||= Client.new(
|
|
87
90
|
api_key: settings.fetch(:api_key) { ENV.fetch("LETTER_API_KEY") },
|
|
88
|
-
base_url: settings[:base_url] || ENV["LETTER_BASE_URL"] || DEFAULT_BASE_URL
|
|
89
|
-
# This client only ever calls send_email, which bypasses the queue, so
|
|
90
|
-
# the batching thread would sit idle forever. Setting flush_at to 1
|
|
91
|
-
# keeps it from being started for nothing.
|
|
92
|
-
flush_at: 1
|
|
91
|
+
base_url: settings[:base_url] || ENV["LETTER_BASE_URL"] || DEFAULT_BASE_URL
|
|
93
92
|
)
|
|
94
93
|
end
|
|
95
94
|
|
data/lib/letterapp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: letterapp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- letter.app
|
|
@@ -16,6 +16,7 @@ executables: []
|
|
|
16
16
|
extensions: []
|
|
17
17
|
extra_rdoc_files: []
|
|
18
18
|
files:
|
|
19
|
+
- CHANGELOG.md
|
|
19
20
|
- LICENSE
|
|
20
21
|
- README.md
|
|
21
22
|
- lib/letterapp.rb
|
|
@@ -29,6 +30,7 @@ licenses:
|
|
|
29
30
|
metadata:
|
|
30
31
|
homepage_uri: https://letter.app/docs/ruby-sdk
|
|
31
32
|
source_code_uri: https://github.com/vincenzor/letter-ruby
|
|
33
|
+
changelog_uri: https://github.com/vincenzor/letter-ruby/blob/main/CHANGELOG.md
|
|
32
34
|
bug_tracker_uri: https://github.com/vincenzor/letter-ruby/issues
|
|
33
35
|
rubygems_mfa_required: 'true'
|
|
34
36
|
rdoc_options: []
|