Skip to content

Commit 507f6c0

Browse files
Add track_delivery_metric for /api/v1/metrics endpoint (#139)
* Add track_delivery_metric for /api/v1/metrics endpoint Adds support for reporting delivery metrics (opened, clicked, converted, delivered, bounced, deferred, dropped, spammed) via the new /api/v1/metrics endpoint, replacing the deprecated /push/events endpoint. Based on community PR #112 by @eserra with fixes for: - valid_url? bug that passed boolean instead of URL string - .present? usage (Rails-only, unavailable in plain Ruby) - Breaking constant renames (PUSH_* constants preserved) - Inconsistent attribute handling (now uses symbolize_keys/slice) * Remove PUSH_* constants, use DELIVERY_* for VALID_PUSH_EVENTS PUSH_OPENED/PUSH_CONVERTED/PUSH_DELIVERED were duplicates of DELIVERY_OPENED/DELIVERY_CONVERTED/DELIVERY_DELIVERED. Consolidate to a single set of constants. * Fix rubocop: use single quotes inside string interpolation * Add back PUSH_* constants as deprecated aliases Preserves backwards compatibility for anyone referencing PUSH_OPENED, PUSH_CONVERTED, or PUSH_DELIVERED directly.
1 parent 6d08ff2 commit 507f6c0

5 files changed

Lines changed: 234 additions & 6 deletions

File tree

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Metrics/BlockLength:
2020
- "customerio.gemspec"
2121

2222
Metrics/ClassLength:
23-
Max: 200
23+
Max: 275
2424

2525
Metrics/MethodLength:
2626
Max: 25

CHANGELOG.markdown

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
16
## Customerio 5.4.0 - June 13, 2025
27
### Changed
38
- Added `send_sms` to `APIClient` and `SendSMSRequest` to support sending transactional push notifications.

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,26 @@ Deleting a device token will remove it from the associated customer to stop furt
287287
$customerio.delete_device(5, "my_device_token")
288288
```
289289

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+
290310
### Suppress a user
291311

292312
Deletes the customer with the provided id if it exists and suppresses all future events and identifies for that customer.

lib/customerio/client.rb

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,27 @@ class IdentifierType
1010
end
1111

1212
class Client
13-
PUSH_OPENED = "opened"
14-
PUSH_CONVERTED = "converted"
15-
PUSH_DELIVERED = "delivered"
16-
17-
VALID_PUSH_EVENTS = [PUSH_OPENED, PUSH_CONVERTED, PUSH_DELIVERED].freeze
13+
DELIVERY_OPENED = "opened"
14+
DELIVERY_CLICKED = "clicked"
15+
DELIVERY_CONVERTED = "converted"
16+
DELIVERY_DELIVERED = "delivered"
17+
DELIVERY_BOUNCED = "bounced"
18+
DELIVERY_DEFERRED = "deferred"
19+
DELIVERY_DROPPED = "dropped"
20+
DELIVERY_SPAMMED = "spammed"
21+
22+
VALID_DELIVERY_METRICS = [
23+
DELIVERY_OPENED, DELIVERY_CLICKED, DELIVERY_CONVERTED,
24+
DELIVERY_DELIVERED, DELIVERY_BOUNCED, DELIVERY_DEFERRED,
25+
DELIVERY_DROPPED, DELIVERY_SPAMMED
26+
].freeze
27+
28+
VALID_PUSH_EVENTS = [DELIVERY_OPENED, DELIVERY_CONVERTED, DELIVERY_DELIVERED].freeze
29+
30+
# @deprecated Use DELIVERY_OPENED, DELIVERY_CONVERTED, DELIVERY_DELIVERED instead.
31+
PUSH_OPENED = DELIVERY_OPENED
32+
PUSH_CONVERTED = DELIVERY_CONVERTED
33+
PUSH_DELIVERED = DELIVERY_DELIVERED
1834

1935
class MissingIdAttributeError < StandardError; end
2036
class ParamError < StandardError; end
@@ -114,6 +130,25 @@ def track_push_notification_event(event_name, attributes = {})
114130
)
115131
end
116132

133+
def track_delivery_metric(metric_name, attributes = {})
134+
keys = %i[delivery_id timestamp recipient reason href]
135+
attributes = symbolize_keys(attributes).slice(*keys)
136+
137+
unless VALID_DELIVERY_METRICS.include?(metric_name)
138+
raise ParamError, "metric_name must be one of: #{VALID_DELIVERY_METRICS.join(', ')}"
139+
end
140+
141+
raise ParamError, "delivery_id must be a non-empty string" if empty?(attributes[:delivery_id])
142+
143+
body = { delivery_id: attributes[:delivery_id], metric: metric_name }
144+
body[:timestamp] = attributes[:timestamp] if valid_timestamp?(attributes[:timestamp])
145+
body[:recipient] = attributes[:recipient] unless empty?(attributes[:recipient])
146+
body[:reason] = attributes[:reason] unless empty?(attributes[:reason])
147+
body[:href] = attributes[:href] unless empty?(attributes[:href])
148+
149+
@client.request_and_verify_response(:post, delivery_metrics_path, body)
150+
end
151+
117152
def merge_customers(primary_id_type, primary_id, secondary_id_type, secondary_id)
118153
raise ParamError, "invalid primary_id_type" unless valid_id_type?(primary_id_type)
119154
raise ParamError, "primary_id must be a non-empty string" if empty?(primary_id)
@@ -162,6 +197,10 @@ def track_push_notification_event_path
162197
"/push/events"
163198
end
164199

200+
def delivery_metrics_path
201+
"/api/v1/metrics"
202+
end
203+
165204
def merge_customers_path
166205
"/api/v1/merge_customers"
167206
end

spec/client_spec.rb

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,170 @@ def json(data)
757757
end
758758
end
759759

760+
describe "#track_delivery_metric" do
761+
attr_accessor :client, :attributes
762+
763+
before(:each) do
764+
@client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)
765+
@attributes = {
766+
:delivery_id => "abc123"
767+
}
768+
end
769+
770+
it "sends a POST request to customer.io's /api/v1/metrics endpoint" do
771+
stub_request(:post, api_uri("/api/v1/metrics")).
772+
with(
773+
:body => json({
774+
:delivery_id => "abc123",
775+
:metric => "opened"
776+
}),
777+
:headers => {
778+
"Content-Type" => "application/json"
779+
}).
780+
to_return(:status => 200, :body => "", :headers => {})
781+
782+
client.track_delivery_metric("opened", attributes)
783+
end
784+
785+
it "sends optional attributes when provided" do
786+
time = Time.now.to_i
787+
788+
stub_request(:post, api_uri("/api/v1/metrics")).
789+
with(
790+
:body => json({
791+
:delivery_id => "abc123",
792+
:metric => "clicked",
793+
:timestamp => time,
794+
:recipient => "user@example.com",
795+
:href => "https://example.com/page"
796+
}),
797+
:headers => {
798+
"Content-Type" => "application/json"
799+
}).
800+
to_return(:status => 200, :body => "", :headers => {})
801+
802+
client.track_delivery_metric("clicked", {
803+
:delivery_id => "abc123",
804+
:timestamp => time,
805+
:recipient => "user@example.com",
806+
:href => "https://example.com/page"
807+
})
808+
end
809+
810+
it "sends reason attribute for bounced metrics" do
811+
stub_request(:post, api_uri("/api/v1/metrics")).
812+
with(
813+
:body => json({
814+
:delivery_id => "abc123",
815+
:metric => "bounced",
816+
:reason => "mailbox full"
817+
}),
818+
:headers => {
819+
"Content-Type" => "application/json"
820+
}).
821+
to_return(:status => 200, :body => "", :headers => {})
822+
823+
client.track_delivery_metric("bounced", {
824+
:delivery_id => "abc123",
825+
:reason => "mailbox full"
826+
})
827+
end
828+
829+
it "ignores attributes not in the allowed list" do
830+
stub_request(:post, api_uri("/api/v1/metrics")).
831+
with(
832+
:body => json({
833+
:delivery_id => "abc123",
834+
:metric => "opened"
835+
}),
836+
:headers => {
837+
"Content-Type" => "application/json"
838+
}).
839+
to_return(:status => 200, :body => "", :headers => {})
840+
841+
client.track_delivery_metric("opened", {
842+
:delivery_id => "abc123",
843+
:device_id => "should_be_ignored",
844+
:extra => "also_ignored"
845+
})
846+
end
847+
848+
it "omits timestamp when not a valid integer" do
849+
stub_request(:post, api_uri("/api/v1/metrics")).
850+
with(
851+
:body => json({
852+
:delivery_id => "abc123",
853+
:metric => "delivered"
854+
}),
855+
:headers => {
856+
"Content-Type" => "application/json"
857+
}).
858+
to_return(:status => 200, :body => "", :headers => {})
859+
860+
client.track_delivery_metric("delivered", {
861+
:delivery_id => "abc123",
862+
:timestamp => "not-a-timestamp"
863+
})
864+
end
865+
866+
it "should raise if metric_name is invalid" do
867+
expect {
868+
client.track_delivery_metric("closed", attributes)
869+
}.to raise_error(Customerio::Client::ParamError, /metric_name must be one of/)
870+
end
871+
872+
it "should raise if delivery_id is missing" do
873+
expect {
874+
client.track_delivery_metric("opened", { :delivery_id => nil })
875+
}.to raise_error(Customerio::Client::ParamError, "delivery_id must be a non-empty string")
876+
877+
expect {
878+
client.track_delivery_metric("opened", { :delivery_id => "" })
879+
}.to raise_error(Customerio::Client::ParamError, "delivery_id must be a non-empty string")
880+
end
881+
882+
it "should raise if delivery_id is whitespace" do
883+
expect {
884+
client.track_delivery_metric("opened", { :delivery_id => " " })
885+
}.to raise_error(Customerio::Client::ParamError, "delivery_id must be a non-empty string")
886+
end
887+
888+
%w[opened clicked converted delivered bounced deferred dropped spammed].each do |metric|
889+
it "accepts '#{metric}' as a valid metric" do
890+
stub_request(:post, api_uri("/api/v1/metrics")).
891+
to_return(:status => 200, :body => "", :headers => {})
892+
893+
client.track_delivery_metric(metric, attributes)
894+
end
895+
end
896+
897+
it "works with string keys in the attributes hash" do
898+
stub_request(:post, api_uri("/api/v1/metrics")).
899+
with(
900+
:body => json({
901+
:delivery_id => "abc123",
902+
:metric => "opened"
903+
}),
904+
:headers => {
905+
"Content-Type" => "application/json"
906+
}).
907+
to_return(:status => 200, :body => "", :headers => {})
908+
909+
client.track_delivery_metric("opened", {
910+
"delivery_id" => "abc123"
911+
})
912+
end
913+
914+
it "raises an error if POST doesn't return a 2xx response code" do
915+
stub_request(:post, api_uri("/api/v1/metrics")).
916+
to_return(:status => 500, :body => "Server Error", :headers => {})
917+
918+
expect {
919+
client.track_delivery_metric("opened", attributes)
920+
}.to raise_error(Customerio::InvalidResponse)
921+
end
922+
end
923+
760924
describe "#merge_customers" do
761925
before(:each) do
762926
@client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)

0 commit comments

Comments
 (0)