customerio 5.6.0 → 6.0.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: e400cd48d515843a8d229b0d395c118f57b989d780220e187695c20cc6b13109
4
- data.tar.gz: dd2c8d4a83fdfb02f6cda4084ed5f54d641b157ecffb6ff93357526fcad6eb73
3
+ metadata.gz: 9563849000bf5d39f2e0f02aebe93072d42f6eb02343d7d444f964fc86ee3a3f
4
+ data.tar.gz: 3f4e966503aa9526546a1767580f141da91f06c3ab539c5ee796bedce550d15d
5
5
  SHA512:
6
- metadata.gz: 8515504c0a52b34fd5aca03393947270f8374c53bc6e7285ef2d49dbd9625602ca16b396189f879bc68ca4b1f8fee0dae5b0d91724638a9bee412cb12cda5092
7
- data.tar.gz: 3b985d7390279186c3eb110ba7a43771ac10b8f764794db640b5daf49e7b876bdcd539772370c757a5f71db7285e6053bd29cf1b9afc5802b29e7e056f8a7488
6
+ metadata.gz: 7230e937323c875bc45e6dbd5a3d499f68ca6df4bf7fc414b0e425f8c0ea1b85bab5315e56887f11f184ee013c33d2d5b0d7d6373069a5a8a17ae397a980a9ff
7
+ data.tar.gz: 6742e5db8afbd888ce0eef0e9667f96578307142564f70b2151663fa4e17dc00df1b09b4db667fb8294a235ed92d1059af08ad84498136fce91739415cba4524
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,8 @@
1
+ ## Customerio 5.7.0 - Unreleased
2
+ ### Added
3
+ - Added `track_delivery_metric` to `Client` for reporting delivery metrics via the `/api/v1/metrics` endpoint, replacing the deprecated `/push/events` endpoint. Supports metrics: opened, clicked, converted, delivered, bounced, deferred, dropped, and spammed.
4
+ - Added `DELIVERY_*` constants and `VALID_DELIVERY_METRICS` to `Customerio::Client`.
5
+
1
6
  ## Customerio 5.4.0 - June 13, 2025
2
7
  ### Changed
3
8
  - Added `send_sms` to `APIClient` and `SendSMSRequest` to support sending transactional push notifications.
data/README.md CHANGED
@@ -7,10 +7,12 @@
7
7
  [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blueviolet?logo=gitpod)](https://gitpod.io/#https://github.com/customerio/customerio-ruby/)
8
8
  [![ci](https://github.com/customerio/customerio-ruby/actions/workflows/main.yml/badge.svg)](https://github.com/customerio/customerio-ruby/actions/workflows/main.yml)
9
9
 
10
- # Customer.io Ruby
10
+ # Customer.io Ruby
11
11
 
12
12
  A ruby client for the [Customer.io Journeys Track API](https://customer.io/docs/api/track/).
13
13
 
14
+ Supported Ruby versions are the actively maintained Ruby lines, starting at Ruby 3.3.
15
+
14
16
  ## Installation
15
17
 
16
18
  Add this line to your application's Gemfile:
@@ -30,7 +32,7 @@ Or install it yourself:
30
32
  ### Before we get started: API client vs. JavaScript snippet
31
33
 
32
34
  It's helpful to know that everything below can also be accomplished
33
- through the [Customer.io JavaScript snippet](https://customer.io/docs/javascript-quick-start/).
35
+ through the [Customer.io JavaScript client](https://docs.customer.io/integrations/data-in/connections/javascript/js-source/).
34
36
 
35
37
  In many cases, using the JavaScript snippet will be easier to integrate with
36
38
  your app, but there are several reasons why using the API client is useful:
@@ -58,6 +60,28 @@ $customerio = Customerio::Client.new("YOUR SITE ID", "YOUR API SECRET KEY", regi
58
60
 
59
61
  `region` is optional and takes one of two values—`US` or `EU`. If you do not specify your region, we assume that your account is based in the US (`US`). If your account is based in the EU and you do not provide the correct region (`EU`), we'll route requests to our EU data centers accordingly, however this may cause data to be logged in the US.
60
62
 
63
+ #### Timeouts
64
+
65
+ Both clients accept a `timeout` option (in seconds) that controls the HTTP connect and read timeouts. The default is 10 seconds.
66
+
67
+ ```ruby
68
+ # Track API client with a 3-second timeout
69
+ $customerio = Customerio::Client.new("YOUR SITE ID", "YOUR API SECRET KEY", timeout: 3)
70
+
71
+ # Transactional API client with a 3-second timeout
72
+ client = Customerio::APIClient.new("your API key", timeout: 3)
73
+ ```
74
+
75
+ When a request exceeds the timeout, Ruby raises `Net::OpenTimeout` (connection phase) or `Net::ReadTimeout` (waiting for response). You can rescue both:
76
+
77
+ ```ruby
78
+ begin
79
+ $customerio.identify(id: 5, email: "person@example.com")
80
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
81
+ # handle timeout
82
+ end
83
+ ```
84
+
61
85
  ### Identify logged in customers
62
86
 
63
87
  Tracking data of logged in customers is a key part of [Customer.io](https://customer.io). In order to
@@ -187,14 +211,32 @@ encourage your customers to perform an action.
187
211
  # event. These attributes can be used in your triggers to control who should
188
212
  # receive the triggered email. You can set any number of data values.
189
213
 
190
- $customerio.track(5, "purchase", :type => "socks", :price => "13.99")
214
+ $customerio.track(5, "purchase", { :type => "socks", :price => "13.99" })
191
215
  ```
192
216
 
193
- **Note:** If you want to track events which occurred in the past, you can include a `timestamp` attribute
217
+ **Note:** If you want to track events which occurred in the past, you can pass a `timestamp` keyword argument
194
218
  (in seconds since the epoch), and we'll use that as the date the event occurred.
195
219
 
196
220
  ```ruby
197
- $customerio.track(5, "purchase", :type => "socks", :price => "13.99", :timestamp => 1365436200)
221
+ $customerio.track(5, "purchase", { :type => "socks", :price => "13.99" }, timestamp: 1365436200)
222
+ ```
223
+
224
+ #### Deduplicating events
225
+
226
+ You can provide a [ULID](https://github.com/ulid/spec) `id` to deduplicate events. If two events have the same `id`, Customer.io won't process the event a second time.
227
+
228
+ ```ruby
229
+ $customerio.track(5, "purchase", { :type => "socks" }, id: "01BX5ZZKBKACTAV9WEVGEMMVRY")
230
+
231
+ $customerio.track_anonymous("anon-id", "purchase", { :type => "socks" }, id: "01BX5ZZKBKACTAV9WEVGEMMVRY")
232
+ ```
233
+
234
+ You can also pass `timestamp` as a keyword argument (epoch seconds) instead of including it in the attributes hash:
235
+
236
+ ```ruby
237
+ $customerio.track(5, "purchase", { :type => "socks" }, timestamp: 1561231234)
238
+
239
+ $customerio.track(5, "purchase", { :type => "socks" }, id: "01BX5ZZKBKACTAV9WEVGEMMVRY", timestamp: 1561231234)
198
240
  ```
199
241
 
200
242
  ### Tracking anonymous events
@@ -209,7 +251,7 @@ Anonymous events cannot trigger campaigns by themselves. To trigger a campaign,
209
251
  # name (required) - the name of the event you want to track.
210
252
  # attributes (optional) - related information you want to attach to the event.
211
253
 
212
- $customerio.track_anonymous(anonymous_id, "product_view", :type => "socks" )
254
+ $customerio.track_anonymous(anonymous_id, "product_view", { :type => "socks" })
213
255
  ```
214
256
 
215
257
  Use the `recipient` attribute to specify the email address to send the messages to. [See our documentation on how to use anonymous events for more details](https://customer.io/docs/invite-emails/).
@@ -219,7 +261,7 @@ Use the `recipient` attribute to specify the email address to send the messages
219
261
  If you previously sent [invite events](https://customer.io/docs/anonymous-invite-emails/), you can achieve the same functionality by sending an anonymous event with `nil` for the anonymous identifier. To send anonymous invites, your event *must* include a `recipient` attribute.
220
262
 
221
263
  ```ruby
222
- $customerio.track_anonymous(nil, "invite", :recipient => "new.person@example.com" )
264
+ $customerio.track_anonymous(nil, "invite", { :recipient => "new.person@example.com" })
223
265
  ```
224
266
 
225
267
  ### Adding a mobile device
@@ -245,6 +287,26 @@ Deleting a device token will remove it from the associated customer to stop furt
245
287
  $customerio.delete_device(5, "my_device_token")
246
288
  ```
247
289
 
290
+ ### Tracking delivery metrics
291
+
292
+ Report delivery metrics using the `track_delivery_metric` method, which calls the `/api/v1/metrics` endpoint. This replaces the deprecated `/push/events` endpoint. The `delivery_id` is required. Valid metrics are: `opened`, `clicked`, `converted`, `delivered`, `bounced`, `deferred`, `dropped`, `spammed`.
293
+
294
+ ```ruby
295
+ $customerio.track_delivery_metric("opened", delivery_id: "RPILAgUBcRillFPDbQQ=")
296
+
297
+ $customerio.track_delivery_metric("clicked", {
298
+ delivery_id: "RPILAgUBcRillFPDbQQ=",
299
+ timestamp: 1561231234,
300
+ href: "https://example.com/link",
301
+ recipient: "user@example.com"
302
+ })
303
+
304
+ $customerio.track_delivery_metric("bounced", {
305
+ delivery_id: "RPILAgUBcRillFPDbQQ=",
306
+ reason: "mailbox full"
307
+ })
308
+ ```
309
+
248
310
  ### Suppress a user
249
311
 
250
312
  Deletes the customer with the provided id if it exists and suppresses all future events and identifies for that customer.
@@ -261,6 +323,35 @@ Start tracking events and identifies again for a previously suppressed customer.
261
323
  $customerio.unsuppress(5)
262
324
  ```
263
325
 
326
+ ### Batch operations
327
+
328
+ You can send up to 500KB of operations in a single request using the [Track v2 batch endpoint](https://customer.io/docs/api/track/#tag/Track-v2/operation/batch). Each operation can identify, track events, or delete people and objects.
329
+
330
+ ```ruby
331
+ $customerio.batch([
332
+ {
333
+ type: "person",
334
+ identifiers: { id: "42" },
335
+ action: "identify",
336
+ attributes: { first_name: "Jane", plan: "premium" },
337
+ },
338
+ {
339
+ type: "person",
340
+ identifiers: { id: "42" },
341
+ action: "event",
342
+ name: "purchase",
343
+ data: { amount: 99 },
344
+ },
345
+ {
346
+ type: "person",
347
+ identifiers: { id: "99" },
348
+ action: "delete",
349
+ },
350
+ ])
351
+ ```
352
+
353
+ See the [Track v2 API docs](https://customer.io/docs/api/track/#tag/Track-v2/operation/batch) for the full list of supported actions and fields.
354
+
264
355
  ### Send Transactional Messages
265
356
 
266
357
  To use the Customer.io [Transactional API](https://customer.io/docs/transactional-api), create an instance of the API client using an [app key](https://customer.io/docs/managing-credentials#app-api-keys) and create a request object of your message type.
@@ -271,7 +362,7 @@ Create a new `SendEmailRequest` object containing:
271
362
 
272
363
  * `transactional_message_id`: the ID of the transactional message you want to send, or the `body`, `from`, and `subject` of a new message.
273
364
  * `to`: the email address of your recipients
274
- * an `identifiers` object containing the `id` of your recipient. If the `id` does not exist, Customer.io creates it.
365
+ * an `identifiers` object containing the `email` and/or `id` of your recipient. If the person you reference by email or ID does not exist, Customer.io creates them.
275
366
  * a `message_data` object containing properties that you want reference in your message using liquid.
276
367
  * You can also send attachments with your message. Use `attach` to encode attachments.
277
368
 
@@ -295,7 +386,7 @@ request = Customerio::SendEmailRequest.new(
295
386
  products: [],
296
387
  },
297
388
  identifiers: {
298
- id: "2",
389
+ email: "person@example.com",
299
390
  },
300
391
  )
301
392
 
@@ -348,11 +439,52 @@ rescue Customerio::InvalidResponse => e
348
439
  end
349
440
  ```
350
441
 
442
+ ### Trigger Broadcasts
443
+
444
+ You can trigger [API-triggered broadcasts](https://customer.io/docs/api-triggered-broadcasts/) using the `APIClient`. Create a `TriggerBroadcastRequest` with the broadcast's numeric ID and optional audience/data parameters.
445
+
446
+ ```ruby
447
+ require "customerio"
448
+
449
+ client = Customerio::APIClient.new("your API key", region: Customerio::Regions::US)
450
+
451
+ request = Customerio::TriggerBroadcastRequest.new(
452
+ broadcast_id: 12,
453
+ emails: ["recipient@example.com"],
454
+ data: {
455
+ headline: "Roadrunner spotted in Albuquerque!",
456
+ date: 1511315635,
457
+ },
458
+ email_add_duplicates: false,
459
+ email_ignore_missing: false,
460
+ id_ignore_missing: false,
461
+ )
462
+
463
+ begin
464
+ response = client.trigger_broadcast(request)
465
+ puts response
466
+ rescue Customerio::InvalidResponse => e
467
+ puts e.code, e.message
468
+ end
469
+ ```
470
+
471
+ You can target the broadcast audience in several ways. Only one audience option can be present per request:
472
+
473
+ - `recipients`: a hash with filter conditions (e.g., `{ segment: { id: 7 } }`)
474
+ - `emails`: an array of email addresses
475
+ - `ids`: an array of customer IDs
476
+ - `per_user_data`: an array of per-user objects
477
+ - `data_file_url`: a URL to a JSON lines file
478
+
479
+ If you omit the audience option, the broadcast uses its default audience configured in the UI.
480
+
351
481
  ## Contributing
352
482
 
353
483
  1. Fork it
354
484
  2. Clone your fork (`git clone git@github.com:MY_USERNAME/customerio-ruby.git && cd customerio-ruby`)
355
485
  3. Create your feature branch (`git checkout -b my-new-feature`)
356
- 4. Commit your changes (`git commit -am 'Added some feature'`)
357
- 5. Push to the branch (`git push origin my-new-feature`)
358
- 6. Create new Pull Request
486
+ 4. Install dependencies (`bundle install`)
487
+ 5. Run the test and lint suite (`bundle exec rake`)
488
+ 6. Commit your changes (`git commit -am 'Added some feature'`)
489
+ 7. Push to the branch (`git push origin my-new-feature`)
490
+ 8. Create new Pull Request
@@ -1,92 +1,78 @@
1
- require 'net/http'
2
- require 'multi_json'
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
3
5
 
4
6
  module Customerio
5
7
  class APIClient
6
8
  def initialize(app_key, options = {})
7
- options[:region] = Customerio::Regions::US if options[:region].nil?
8
- raise "region must be an instance of Customerio::Regions::Region" unless options[:region].is_a?(Customerio::Regions::Region)
9
+ options = options.dup
10
+ options[:region] = Regions::US if options[:region].nil?
11
+ unless options[:region].is_a?(Regions::Region)
12
+ raise ArgumentError, "region must be an instance of Customerio::Regions::Region"
13
+ end
9
14
 
10
15
  options[:url] = options[:region].api_url if options[:url].nil? || options[:url].empty?
11
- @client = Customerio::BaseClient.new({ app_key: app_key }, options)
16
+ @client = BaseClient.new({ app_key: app_key }, options)
12
17
  end
13
18
 
14
19
  def send_email(req)
15
- raise "request must be an instance of Customerio::SendEmailRequest" unless req.is_a?(Customerio::SendEmailRequest)
16
- response = @client.request(:post, send_email_path, req.message)
20
+ validate_request!(req, SendEmailRequest)
17
21
 
18
- case response
19
- when Net::HTTPSuccess then
20
- JSON.parse(response.body)
21
- when Net::HTTPBadRequest then
22
- json = JSON.parse(response.body)
23
- raise Customerio::InvalidResponse.new(response.code, json['meta']['error'], response)
24
- else
25
- raise InvalidResponse.new(response.code, response.body)
26
- end
22
+ deliver(send_email_path, req.message)
27
23
  end
28
24
 
29
25
  def send_push(req)
30
- raise "request must be an instance of Customerio::SendPushRequest" unless req.is_a?(Customerio::SendPushRequest)
31
- response = @client.request(:post, send_push_path, req.message)
26
+ validate_request!(req, SendPushRequest)
32
27
 
33
- case response
34
- when Net::HTTPSuccess then
35
- JSON.parse(response.body)
36
- when Net::HTTPBadRequest then
37
- json = JSON.parse(response.body)
38
- raise Customerio::InvalidResponse.new(response.code, json['meta']['error'], response)
39
- else
40
- raise InvalidResponse.new(response.code, response.body)
41
- end
28
+ deliver(send_push_path, req.message)
42
29
  end
43
30
 
44
31
  def send_sms(req)
45
- raise "request must be an instance of Customerio::SendSMSRequest" unless req.is_a?(Customerio::SendSMSRequest)
46
- response = @client.request(:post, send_sms_path, req.message)
32
+ validate_request!(req, SendSMSRequest)
47
33
 
48
- case response
49
- when Net::HTTPSuccess then
50
- JSON.parse(response.body)
51
- when Net::HTTPBadRequest then
52
- json = JSON.parse(response.body)
53
- raise Customerio::InvalidResponse.new(response.code, json['meta']['error'], response)
54
- else
55
- raise InvalidResponse.new(response.code, response.body)
56
- end
34
+ deliver(send_sms_path, req.message)
57
35
  end
58
36
 
59
37
  def send_inbox_message(req)
60
- raise "request must be an instance of Customerio::SendInboxMessageRequest" unless req.is_a?(Customerio::SendInboxMessageRequest)
61
- response = @client.request(:post, send_inbox_message_path, req.message)
38
+ validate_request!(req, SendInboxMessageRequest)
62
39
 
63
- case response
64
- when Net::HTTPSuccess then
65
- JSON.parse(response.body)
66
- when Net::HTTPBadRequest then
67
- json = JSON.parse(response.body)
68
- raise Customerio::InvalidResponse.new(response.code, json['meta']['error'], response)
69
- else
70
- raise InvalidResponse.new(response.code, response.body)
71
- end
40
+ deliver(send_inbox_message_path, req.message)
72
41
  end
73
42
 
74
43
  def send_in_app(req)
75
- raise "request must be an instance of Customerio::SendInAppRequest" unless req.is_a?(Customerio::SendInAppRequest)
76
- response = @client.request(:post, send_in_app_path, req.message)
44
+ validate_request!(req, SendInAppRequest)
45
+
46
+ deliver(send_in_app_path, req.message)
47
+ end
48
+
49
+ def trigger_broadcast(req)
50
+ validate_request!(req, TriggerBroadcastRequest)
51
+
52
+ deliver(trigger_broadcast_path(req.broadcast_id), req.message)
53
+ end
54
+
55
+ private
56
+
57
+ def deliver(path, message)
58
+ response = @client.request(:post, path, message)
77
59
 
78
60
  case response
79
- when Net::HTTPSuccess then
61
+ when Net::HTTPSuccess
80
62
  JSON.parse(response.body)
81
- when Net::HTTPBadRequest then
82
- json = JSON.parse(response.body)
83
- raise Customerio::InvalidResponse.new(response.code, json['meta']['error'], response)
63
+ when Net::HTTPBadRequest
64
+ error = JSON.parse(response.body).dig("meta", "error")
65
+ raise InvalidResponse.new(response.code, error, response)
84
66
  else
85
- raise InvalidResponse.new(response.code, response.body)
67
+ raise InvalidResponse.new(response.code, response.body, response)
86
68
  end
87
69
  end
88
70
 
89
- private
71
+ def validate_request!(request, request_class)
72
+ return if request.is_a?(request_class)
73
+
74
+ raise ArgumentError, "request must be an instance of #{request_class}"
75
+ end
90
76
 
91
77
  def send_email_path
92
78
  "/v1/send/email"
@@ -107,5 +93,9 @@ module Customerio
107
93
  def send_in_app_path
108
94
  "/v1/send/in_app"
109
95
  end
96
+
97
+ def trigger_broadcast_path(broadcast_id)
98
+ "/v1/campaigns/#{broadcast_id}/triggers"
99
+ end
110
100
  end
111
101
  end
@@ -1,19 +1,22 @@
1
- require 'net/http'
2
- require 'multi_json'
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
3
6
 
4
7
  module Customerio
5
- DEFAULT_TIMEOUT = 10
8
+ DEFAULT_TIMEOUT = 10
9
+
10
+ class InvalidRequest < StandardError; end
6
11
 
7
- class InvalidRequest < RuntimeError; end
8
- class InvalidResponse < RuntimeError
12
+ class InvalidResponse < StandardError
9
13
  attr_reader :code, :response
10
14
 
11
- def initialize(code, body, response=nil)
12
- @message = body
15
+ def initialize(code, body, response = nil)
13
16
  @code = code
14
17
  @response = response
15
18
 
16
- super(@message)
19
+ super(body)
17
20
  end
18
21
  end
19
22
 
@@ -36,27 +39,28 @@ module Customerio
36
39
 
37
40
  def execute(method, path, body = nil, headers = {})
38
41
  uri = URI.join(@base_uri, path)
42
+ request_headers = headers.dup
39
43
 
40
44
  session = Net::HTTP.new(uri.host, uri.port)
41
- session.use_ssl = (uri.scheme == 'https')
45
+ session.use_ssl = uri.scheme == "https"
42
46
  session.open_timeout = @timeout
43
47
  session.read_timeout = @timeout
44
48
 
45
- req = request_class(method).new(uri.path)
49
+ req = request_class(method).new(uri.request_uri)
46
50
 
47
- headers['User-Agent'] = "Customer.io Ruby Client/" + Customerio::VERSION
51
+ request_headers["User-Agent"] = "Customer.io Ruby Client/#{VERSION}"
48
52
 
49
- if @auth.has_key?(:site_id) && @auth.has_key?(:api_key)
50
- req.initialize_http_header(headers)
53
+ if @auth.key?(:site_id) && @auth.key?(:api_key)
54
+ req.initialize_http_header(request_headers)
51
55
  req.basic_auth @auth[:site_id], @auth[:api_key]
52
56
  else
53
- headers['Authorization'] = "Bearer #{@auth[:app_key]}"
54
- req.initialize_http_header(headers)
57
+ request_headers["Authorization"] = "Bearer #{@auth[:app_key]}"
58
+ req.initialize_http_header(request_headers)
55
59
  end
56
60
 
57
- if !body.nil?
58
- req.add_field('Content-Type', 'application/json')
59
- req.body = MultiJson.dump(body)
61
+ unless body.nil?
62
+ req.add_field("Content-Type", "application/json")
63
+ req.body = JSON.generate(body)
60
64
  end
61
65
 
62
66
  session.start do |http|
@@ -72,14 +76,16 @@ module Customerio
72
76
  Net::HTTP::Put
73
77
  when :delete
74
78
  Net::HTTP::Delete
79
+ when :get
80
+ Net::HTTP::Get
75
81
  else
76
- raise InvalidRequest.new("Invalid request method #{method.inspect}")
82
+ raise InvalidRequest, "Invalid request method #{method.inspect}"
77
83
  end
78
84
  end
79
85
 
80
86
  def verify_response(response)
81
87
  case response
82
- when Net::HTTPSuccess then
88
+ when Net::HTTPSuccess
83
89
  response
84
90
  else
85
91
  raise InvalidResponse.new(response.code, response.body, response)