Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Metrics/BlockLength:
- "customerio.gemspec"

Metrics/ClassLength:
Max: 200
Max: 275

Metrics/MethodLength:
Max: 25
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.markdown
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Customerio 5.7.0 - Unreleased
### Added
- 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.
- Added `DELIVERY_*` constants and `VALID_DELIVERY_METRICS` to `Customerio::Client`.

## Customerio 5.4.0 - June 13, 2025
### Changed
- Added `send_sms` to `APIClient` and `SendSMSRequest` to support sending transactional push notifications.
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,26 @@ Deleting a device token will remove it from the associated customer to stop furt
$customerio.delete_device(5, "my_device_token")
```

### Tracking delivery metrics

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`.

```ruby
$customerio.track_delivery_metric("opened", delivery_id: "RPILAgUBcRillFPDbQQ=")

$customerio.track_delivery_metric("clicked", {
delivery_id: "RPILAgUBcRillFPDbQQ=",
timestamp: 1561231234,
href: "https://example.com/link",
recipient: "user@example.com"
})

$customerio.track_delivery_metric("bounced", {
delivery_id: "RPILAgUBcRillFPDbQQ=",
reason: "mailbox full"
})
```

### Suppress a user

Deletes the customer with the provided id if it exists and suppresses all future events and identifies for that customer.
Expand Down
49 changes: 44 additions & 5 deletions lib/customerio/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,27 @@ class IdentifierType
end

class Client
PUSH_OPENED = "opened"
PUSH_CONVERTED = "converted"
PUSH_DELIVERED = "delivered"

VALID_PUSH_EVENTS = [PUSH_OPENED, PUSH_CONVERTED, PUSH_DELIVERED].freeze
DELIVERY_OPENED = "opened"
DELIVERY_CLICKED = "clicked"
DELIVERY_CONVERTED = "converted"
DELIVERY_DELIVERED = "delivered"
DELIVERY_BOUNCED = "bounced"
DELIVERY_DEFERRED = "deferred"
DELIVERY_DROPPED = "dropped"
DELIVERY_SPAMMED = "spammed"

VALID_DELIVERY_METRICS = [
DELIVERY_OPENED, DELIVERY_CLICKED, DELIVERY_CONVERTED,
DELIVERY_DELIVERED, DELIVERY_BOUNCED, DELIVERY_DEFERRED,
DELIVERY_DROPPED, DELIVERY_SPAMMED
].freeze

VALID_PUSH_EVENTS = [DELIVERY_OPENED, DELIVERY_CONVERTED, DELIVERY_DELIVERED].freeze
Comment thread
cursor[bot] marked this conversation as resolved.

# @deprecated Use DELIVERY_OPENED, DELIVERY_CONVERTED, DELIVERY_DELIVERED instead.
PUSH_OPENED = DELIVERY_OPENED
PUSH_CONVERTED = DELIVERY_CONVERTED
PUSH_DELIVERED = DELIVERY_DELIVERED

class MissingIdAttributeError < StandardError; end
class ParamError < StandardError; end
Expand Down Expand Up @@ -114,6 +130,25 @@ def track_push_notification_event(event_name, attributes = {})
)
end

def track_delivery_metric(metric_name, attributes = {})
keys = %i[delivery_id timestamp recipient reason href]
attributes = symbolize_keys(attributes).slice(*keys)

unless VALID_DELIVERY_METRICS.include?(metric_name)
raise ParamError, "metric_name must be one of: #{VALID_DELIVERY_METRICS.join(', ')}"
end

raise ParamError, "delivery_id must be a non-empty string" if empty?(attributes[:delivery_id])

body = { delivery_id: attributes[:delivery_id], metric: metric_name }
body[:timestamp] = attributes[:timestamp] if valid_timestamp?(attributes[:timestamp])
body[:recipient] = attributes[:recipient] unless empty?(attributes[:recipient])
body[:reason] = attributes[:reason] unless empty?(attributes[:reason])
body[:href] = attributes[:href] unless empty?(attributes[:href])

@client.request_and_verify_response(:post, delivery_metrics_path, body)
end

def merge_customers(primary_id_type, primary_id, secondary_id_type, secondary_id)
raise ParamError, "invalid primary_id_type" unless valid_id_type?(primary_id_type)
raise ParamError, "primary_id must be a non-empty string" if empty?(primary_id)
Expand Down Expand Up @@ -156,6 +191,10 @@ def track_push_notification_event_path
"/push/events"
end

def delivery_metrics_path
"/api/v1/metrics"
end

def merge_customers_path
"/api/v1/merge_customers"
end
Expand Down
164 changes: 164 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,170 @@ def json(data)
end
end

describe "#track_delivery_metric" do
attr_accessor :client, :attributes

before(:each) do
@client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)
@attributes = {
:delivery_id => "abc123"
}
end

it "sends a POST request to customer.io's /api/v1/metrics endpoint" do
stub_request(:post, api_uri("/api/v1/metrics")).
with(
:body => json({
:delivery_id => "abc123",
:metric => "opened"
}),
:headers => {
"Content-Type" => "application/json"
}).
to_return(:status => 200, :body => "", :headers => {})

client.track_delivery_metric("opened", attributes)
end

it "sends optional attributes when provided" do
time = Time.now.to_i

stub_request(:post, api_uri("/api/v1/metrics")).
with(
:body => json({
:delivery_id => "abc123",
:metric => "clicked",
:timestamp => time,
:recipient => "user@example.com",
:href => "https://example.com/page"
}),
:headers => {
"Content-Type" => "application/json"
}).
to_return(:status => 200, :body => "", :headers => {})

client.track_delivery_metric("clicked", {
:delivery_id => "abc123",
:timestamp => time,
:recipient => "user@example.com",
:href => "https://example.com/page"
})
end

it "sends reason attribute for bounced metrics" do
stub_request(:post, api_uri("/api/v1/metrics")).
with(
:body => json({
:delivery_id => "abc123",
:metric => "bounced",
:reason => "mailbox full"
}),
:headers => {
"Content-Type" => "application/json"
}).
to_return(:status => 200, :body => "", :headers => {})

client.track_delivery_metric("bounced", {
:delivery_id => "abc123",
:reason => "mailbox full"
})
end

it "ignores attributes not in the allowed list" do
stub_request(:post, api_uri("/api/v1/metrics")).
with(
:body => json({
:delivery_id => "abc123",
:metric => "opened"
}),
:headers => {
"Content-Type" => "application/json"
}).
to_return(:status => 200, :body => "", :headers => {})

client.track_delivery_metric("opened", {
:delivery_id => "abc123",
:device_id => "should_be_ignored",
:extra => "also_ignored"
})
end

it "omits timestamp when not a valid integer" do
stub_request(:post, api_uri("/api/v1/metrics")).
with(
:body => json({
:delivery_id => "abc123",
:metric => "delivered"
}),
:headers => {
"Content-Type" => "application/json"
}).
to_return(:status => 200, :body => "", :headers => {})

client.track_delivery_metric("delivered", {
:delivery_id => "abc123",
:timestamp => "not-a-timestamp"
})
end

it "should raise if metric_name is invalid" do
expect {
client.track_delivery_metric("closed", attributes)
}.to raise_error(Customerio::Client::ParamError, /metric_name must be one of/)
end

it "should raise if delivery_id is missing" do
expect {
client.track_delivery_metric("opened", { :delivery_id => nil })
}.to raise_error(Customerio::Client::ParamError, "delivery_id must be a non-empty string")

expect {
client.track_delivery_metric("opened", { :delivery_id => "" })
}.to raise_error(Customerio::Client::ParamError, "delivery_id must be a non-empty string")
end

it "should raise if delivery_id is whitespace" do
expect {
client.track_delivery_metric("opened", { :delivery_id => " " })
}.to raise_error(Customerio::Client::ParamError, "delivery_id must be a non-empty string")
end

%w[opened clicked converted delivered bounced deferred dropped spammed].each do |metric|
it "accepts '#{metric}' as a valid metric" do
stub_request(:post, api_uri("/api/v1/metrics")).
to_return(:status => 200, :body => "", :headers => {})

client.track_delivery_metric(metric, attributes)
end
end

it "works with string keys in the attributes hash" do
stub_request(:post, api_uri("/api/v1/metrics")).
with(
:body => json({
:delivery_id => "abc123",
:metric => "opened"
}),
:headers => {
"Content-Type" => "application/json"
}).
to_return(:status => 200, :body => "", :headers => {})

client.track_delivery_metric("opened", {
"delivery_id" => "abc123"
})
end

it "raises an error if POST doesn't return a 2xx response code" do
stub_request(:post, api_uri("/api/v1/metrics")).
to_return(:status => 500, :body => "Server Error", :headers => {})

expect {
client.track_delivery_metric("opened", attributes)
}.to raise_error(Customerio::InvalidResponse)
end
end

describe "#merge_customers" do
before(:each) do
@client = Customerio::Client.new("SITE_ID", "API_KEY", :json => true)
Expand Down