Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
resend (1.7.0)
resend (1.10.0)
base64
httparty (>= 0.22.0)

Expand Down
52 changes: 52 additions & 0 deletions lib/resend/emails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,58 @@ def list(options = {})

Resend::Request.new(path, query_params, "get").perform
end

# Retrieve email metrics. (beta)
# see more: https://resend.com/docs/api-reference/emails/metrics
#
# @param options [Hash] Optional parameters for filtering and shaping the metrics response
# @option options [String] :start_date ISO 8601 date/datetime. Defaults to 6 days before :end_date
# @option options [String] :end_date ISO 8601 date/datetime. Defaults to now
# @option options [String] :timezone IANA timezone, e.g. "America/New_York". Defaults to "UTC"
# @option options [String] :granularity Bucket size used when :period is in :dimensions.
# One of "hourly", "daily", "weekly", "monthly". Defaults to "daily"
# @option options [Array<String>] :metrics Metrics to include. Defaults to all metrics.
# @option options [Array<String>] :dimensions Dimensions to break down by: "period", "domain",
# "email", "broadcast". Defaults to none, which returns totals only.
# @option options [Array<String>] :domain_id Restrict to these sending domain IDs (max 100)
# @option options [Array<String>] :email_id Restrict to these email IDs (max 100)
# @option options [Array<String>] :broadcast_id Restrict to these broadcast IDs (max 100)
def metrics(options = {})
path = "emails/metrics"
validate_metrics_options(options)
Resend::Request.new(path, build_metrics_query(options), "get").perform
end

private

# The `email` and `broadcast` dimensions/filters are mutually exclusive.
def validate_metrics_options(options)
dimensions = Array(options[:dimensions])
has_broadcast = dimensions.include?("broadcast") || !Array(options[:broadcast_id]).empty?
has_email = dimensions.include?("email") || !Array(options[:email_id]).empty?

return unless has_broadcast && has_email

raise ArgumentError,
"`broadcast`/`broadcast_id` cannot be combined with `email`/`email_id`"
end

# Builds the metrics query hash, filtering out nil values and joining
# list-style params (metrics, dimensions, domain_id, email_id, broadcast_id)
# into comma-separated strings as expected by the API.
def build_metrics_query(options)
query_params = {}

%i[start_date end_date timezone granularity].each do |key|
query_params[key] = options[key] if options[key]
end

%i[metrics dimensions domain_id email_id broadcast_id].each do |key|
query_params[key] = Array(options[key]).join(",") if options[key]
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
end

query_params
end
end
end
end
268 changes: 268 additions & 0 deletions spec/emails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,274 @@
expect(result[:has_more]).to eql(false)
end

it "retrieves metrics without parameters" do
resp = {
"object" => "metrics",
"start_date" => "2026-07-01T00:00:00.000Z",
"end_date" => "2026-07-08T00:00:00.000Z",
"metrics" => ["delivered"],
"dimensions" => [],
"granularity" => "daily",
"totals" => { "delivered" => 100 }
}
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

result = Resend::Emails.metrics

expect(HTTParty).to have_received(:send).with(:get, "#{Resend::Request::BASE_URL}emails/metrics", anything)
expect(result[:object]).to eql("metrics")
expect(result[:totals]).to eql({ "delivered" => 100 })
end

it "retrieves metrics with the period dimension" do
resp = {
"object" => "metrics",
"dimensions" => ["period"],
"totals" => { "delivered" => 100 },
"data" => [{ "period" => "2026-07-01", "delivered" => 10 }]
}
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

result = Resend::Emails.metrics(dimensions: ["period"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { dimensions: "period" })
)
expect(result[:data].first["period"]).to eql("2026-07-01")
end

it "retrieves metrics with the domain dimension" do
resp = {
"object" => "metrics",
"dimensions" => ["domain"],
"totals" => { "delivered" => 100 },
"data" => [{ "domain_id" => "d91cd9bd-f5ab-4bbe-89c8-c890a4caced4", "domain_name" => "example.com", "delivered" => 10 }]
}
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

result = Resend::Emails.metrics(dimensions: ["domain"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { dimensions: "domain" })
)
expect(result[:data].first["domain_name"]).to eql("example.com")
end

it "retrieves metrics with the email dimension" do
resp = {
"object" => "metrics",
"dimensions" => ["email"],
"totals" => { "delivered" => 100 },
"data" => [{ "email_id" => "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", "delivered" => 1 }]
}
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

result = Resend::Emails.metrics(dimensions: ["email"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { dimensions: "email" })
)
expect(result[:data].first["email_id"]).to eql("4ef9a417-02e9-4d39-ad75-9611e0fcc33c")
end

it "retrieves metrics with the broadcast dimension" do
resp = {
"object" => "metrics",
"dimensions" => ["broadcast"],
"totals" => { "delivered" => 100 },
"data" => [{ "broadcast_id" => "b6d24b8e-af0b-4c3c-be0c-359bf9d251d2", "broadcast_name" => "July Newsletter", "delivered" => 10 }]
}
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

result = Resend::Emails.metrics(dimensions: ["broadcast"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { dimensions: "broadcast" })
)
expect(result[:data].first["broadcast_name"]).to eql("July Newsletter")
end

it "retrieves metrics with multiple dimensions combined" do
resp = {
"object" => "metrics",
"dimensions" => ["period", "broadcast"],
"totals" => { "delivered" => 100 }
}
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(dimensions: ["period", "broadcast"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { dimensions: "period,broadcast" })
)
end

it "raises when combining the email and broadcast dimensions" do
expect { Resend::Emails.metrics(dimensions: %w[email broadcast]) }
.to raise_error(ArgumentError, /broadcast.*email/)
end

it "raises when combining the broadcast dimension with email_id" do
expect { Resend::Emails.metrics(dimensions: ["broadcast"], email_id: ["e1"]) }
.to raise_error(ArgumentError, /broadcast.*email/)
end

it "raises when combining the email dimension with broadcast_id" do
expect { Resend::Emails.metrics(dimensions: ["email"], broadcast_id: ["b1"]) }
.to raise_error(ArgumentError, /broadcast.*email/)
end

it "raises when combining email_id and broadcast_id filters" do
expect { Resend::Emails.metrics(email_id: ["e1"], broadcast_id: ["b1"]) }
.to raise_error(ArgumentError, /broadcast.*email/)
end

it "retrieves metrics filtered by a single domain_id" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(domain_id: ["d91cd9bd-f5ab-4bbe-89c8-c890a4caced4"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { domain_id: "d91cd9bd-f5ab-4bbe-89c8-c890a4caced4" })
)
end

it "retrieves metrics filtered by multiple domain_ids" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(domain_id: ["domain_1", "domain_2"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { domain_id: "domain_1,domain_2" })
)
end

it "retrieves metrics filtered by a single email_id" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(email_id: ["4ef9a417-02e9-4d39-ad75-9611e0fcc33c"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c" })
)
end

it "retrieves metrics filtered by multiple email_ids" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(email_id: ["email_1", "email_2"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { email_id: "email_1,email_2" })
)
end

it "retrieves metrics filtered by a single broadcast_id" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(broadcast_id: ["b6d24b8e-af0b-4c3c-be0c-359bf9d251d2"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { broadcast_id: "b6d24b8e-af0b-4c3c-be0c-359bf9d251d2" })
)
end

it "retrieves metrics filtered by multiple broadcast_ids" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(broadcast_id: ["broadcast_1", "broadcast_2"])

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(query: { broadcast_id: "broadcast_1,broadcast_2" })
)
end

it "passes metrics, granularity and timezone through to the query" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(
metrics: ["delivered", "opened"],
granularity: "hourly",
timezone: "America/New_York"
)

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(
query: {
metrics: "delivered,opened",
granularity: "hourly",
timezone: "America/New_York"
}
)
)
end

it "passes start_date and end_date through to the query" do
resp = { "object" => "metrics", "totals" => {} }
allow(resp).to receive(:body).and_return(resp)
allow(HTTParty).to receive(:send).and_return(resp)

Resend::Emails.metrics(
start_date: "2026-07-01",
end_date: "2026-07-08T00:00:00.000Z"
)

expect(HTTParty).to have_received(:send).with(
:get,
"#{Resend::Request::BASE_URL}emails/metrics",
hash_including(
query: {
start_date: "2026-07-01",
end_date: "2026-07-08T00:00:00.000Z"
}
)
)
end

it "sends email with template without variables" do
resp = {"id"=>"49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"}
params = {
Expand Down