Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Contact management:
Email marketing:

- Email Campaigns CRUD, Lifecycle Actions & Stats – [`email_campaigns_api.rb`](examples/email_campaigns_api.rb)
- Email Marketing Stats API – [`marketing_stats_api.rb`](examples/marketing_stats_api.rb)

General:

Expand Down
28 changes: 28 additions & 0 deletions examples/marketing_stats_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'mailtrap'
require 'date'

client = Mailtrap::Client.new(api_key: 'your-api-key')
marketing_stats = Mailtrap::MarketingStatsAPI.new(client)

today = Date.today

# Get aggregated Email Marketing stats (campaigns and automations) for the last 30 days
marketing_stats.get(start_date: today - 29, end_date: today)
# => #<struct Mailtrap::MarketingStats
# delivery_count=1200, delivery_rate=0.96,
# bounce_count=50, bounce_rate=0.04,
# open_count=600, open_rate=0.5,
# click_count=240, click_rate=0.2,
# spam_count=3, spam_rate=0.0025,
# unsubscription_count=12, unsubscription_rate=0.01,
# sent_count=1250>

# Narrow to campaigns only (or automations only), for some sending domains and email service providers
marketing_stats.get(
start_date: today - 29,
end_date: today,
sending_domain_ids: [3938, 3939],
sources: ['campaigns'],
email_service_providers: %w[Google Yahoo]
)
# => #<struct Mailtrap::MarketingStats delivery_count=900, delivery_rate=0.97, ...>
1 change: 1 addition & 0 deletions lib/mailtrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require_relative 'mailtrap/suppressions_api'
require_relative 'mailtrap/tracking_opt_outs_api'
require_relative 'mailtrap/email_campaigns_api'
require_relative 'mailtrap/marketing_stats_api'
require_relative 'mailtrap/sending_domains_api'
require_relative 'mailtrap/company_info_api'
require_relative 'mailtrap/email_logs_api'
Expand Down
39 changes: 39 additions & 0 deletions lib/mailtrap/marketing_stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module Mailtrap
# Data Transfer Object for Email Marketing Stats
#
# Aggregated metrics of the account's Email Marketing traffic (campaigns and automations) for
# the requested period and filters. Rates are fractions from 0 to 1. Every count and rate is +0+
# when nothing matched.
# @see https://docs.mailtrap.io/developers/email-marketing/marketing-stats
# @attr_reader delivery_count [Integer] Number of delivered messages
# @attr_reader delivery_rate [Float, Integer] Share of sent messages that were delivered (0–1)
# @attr_reader bounce_count [Integer] Number of bounced messages
# @attr_reader bounce_rate [Float, Integer] Share of sent messages that bounced (0–1)
# @attr_reader open_count [Integer] Number of opened messages
# @attr_reader open_rate [Float, Integer] Share of delivered messages that were opened (0–1)
# @attr_reader click_count [Integer] Number of clicked messages
# @attr_reader click_rate [Float, Integer] Share of delivered messages that were clicked (0–1)
# @attr_reader spam_count [Integer] Number of spam complaints
# @attr_reader spam_rate [Float, Integer] Share of delivered messages marked as spam (0–1)
# @attr_reader unsubscription_count [Integer] Number of unsubscriptions
# @attr_reader unsubscription_rate [Float, Integer] Share of delivered messages that unsubscribed (0–1)
# @attr_reader sent_count [Integer] Number of sent messages (delivered plus bounced)
MarketingStats = Struct.new(
:delivery_count,
:delivery_rate,
:bounce_count,
:bounce_rate,
:open_count,
:open_rate,
:click_count,
:click_rate,
:spam_count,
:spam_rate,
:unsubscription_count,
:unsubscription_rate,
:sent_count,
keyword_init: true
)
end
43 changes: 43 additions & 0 deletions lib/mailtrap/marketing_stats_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require_relative 'base_api'
require_relative 'stats_query_params'
require_relative 'marketing_stats'

module Mailtrap
class MarketingStatsAPI
include BaseAPI
include StatsQueryParams

# @param client [Mailtrap::Client] The client instance
def initialize(client = Mailtrap::Client.new)
@client = client
end

# Get aggregated Email Marketing stats: campaigns and automations together, the same numbers
# as Email Marketing > Stats Overview in the app. Email API/SMTP traffic is reported by {StatsAPI}.
# @param start_date [String, Date, Time] Start of the period, inclusive (required)
# @param end_date [String, Date, Time] End of the period, inclusive (required)
# @param sending_domain_ids [Array<Integer>] Filter by sending domain IDs
# @param sources [Array<String>] Filter by source: +campaigns+ or +automations+;
# omit it, or pass both, to include campaigns and automations together
# @param email_service_providers [Array<String>] Filter by recipient email service providers
# @return [MarketingStats] Aggregated Email Marketing stats
# @!macro api_errors
# @raise [ArgumentError] If a date is not a Date, Time, or String in +YYYY-MM-DD+ format
def get(start_date:, end_date:, sending_domain_ids: nil, sources: nil, email_service_providers: nil)
query_params = build_query_params(
start_date, end_date,
{ sending_domain_ids:, sources:, email_service_providers: }
)
response = client.get(base_path, query_params)
build_entity(response[:data], MarketingStats)
end

private

def base_path
'/api/marketing/stats'
end
end
end
36 changes: 2 additions & 34 deletions lib/mailtrap/stats_api.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# frozen_string_literal: true

require 'date'
require_relative 'base_api'
require_relative 'stats_query_params'
require_relative 'sending_stats'
require_relative 'sending_stat_group'

module Mailtrap
class StatsAPI
include BaseAPI
include StatsQueryParams

ARRAY_FILTERS = %i[sending_domain_ids sending_streams categories email_service_providers].freeze
GROUP_KEYS = {
'domains' => :sending_domain_id,
'categories' => :category,
Expand Down Expand Up @@ -112,38 +112,6 @@ def grouped_stats(group, start_date, end_date, filters)
end
end

def build_query_params(start_date, end_date, filters)
params = { start_date: normalize_date(start_date), end_date: normalize_date(end_date) }

ARRAY_FILTERS.each do |filter_key|
values = filters[filter_key]
params["#{filter_key}[]"] = values if values
end

params
end

def normalize_date(value)
case value
when Date
value.iso8601
when Time
value.strftime('%F')
when String
unless /\A\d{4}-\d{2}-\d{2}\z/.match?(value)
raise ArgumentError,
"Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format."
end

Date.iso8601(value).iso8601
else
raise ArgumentError,
"Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format."
end
rescue Date::Error
raise ArgumentError, "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format."
end

def base_path
"/api/accounts/#{account_id}/stats"
end
Expand Down
29 changes: 29 additions & 0 deletions lib/mailtrap/stats_query_params.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

require 'date'

module Mailtrap
# Query parameters of the sending stats and Email Marketing stats endpoints: the period as
# +YYYY-MM-DD+ dates and every array filter as a repeated +key[]+ parameter. A +nil+ or empty
# filter is left out.
module StatsQueryParams
private

def build_query_params(start_date, end_date, filters)
params = { start_date: normalize_date(start_date), end_date: normalize_date(end_date) }
filters.each { |filter_key, values| params["#{filter_key}[]"] = values unless Array(values).empty? }
params
end

def normalize_date(value)
case value
when Date, Time # DateTime is a Date, but its #iso8601 includes the time of day
return value.strftime('%F')
when String
return value if /\A\d{4}-\d{2}-\d{2}\z/.match?(value) && Date.valid_date?(*value.split('-').map(&:to_i))
end

raise ArgumentError, "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format."
end
end
end

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

132 changes: 132 additions & 0 deletions spec/mailtrap/marketing_stats_api_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true

RSpec.describe Mailtrap::MarketingStatsAPI do
subject(:marketing_stats_api) { described_class.new(Mailtrap::Client.new(api_key: 'correct-api-key')) }

let(:base_url) { 'https://mailtrap.io/api/marketing/stats' }
let(:stats_attributes) do
{
delivery_count: 1200,
delivery_rate: 0.96,
bounce_count: 50,
bounce_rate: 0.04,
open_count: 600,
open_rate: 0.5,
click_count: 240,
click_rate: 0.2,
spam_count: 3,
spam_rate: 0.0025,
unsubscription_count: 12,
unsubscription_rate: 0.01,
sent_count: 1250
}
end
let(:success_response) do
{ status: 200, body: { 'data' => stats_attributes }.to_json, headers: { 'Content-Type' => 'application/json' } }
end

describe '#get' do
subject(:stats) { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-05-31') }

it 'returns a MarketingStats object' do
stub = stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31")
.to_return(success_response)

expect(stats).to be_a(Mailtrap::MarketingStats)
expect(stats).to match_struct(stats_attributes)
expect(stub).to have_been_requested
end

context 'with optional filters' do
subject(:stats) do
marketing_stats_api.get(
start_date: '2026-05-01',
end_date: '2026-05-31',
sending_domain_ids: [3938, 3939],
sources: ['campaigns'],
email_service_providers: %w[Google Yahoo]
)
end

it 'sends every filter as an array query parameter' do
stub = stub_request(
:get,
"#{base_url}?start_date=2026-05-01&end_date=2026-05-31" \
'&sending_domain_ids[]=3938&sending_domain_ids[]=3939&sources[]=campaigns' \
'&email_service_providers[]=Google&email_service_providers[]=Yahoo'
).to_return(success_response)

expect(stats).to have_attributes(delivery_count: 1200, sent_count: 1250)
expect(stub).to have_been_requested
end
end

it 'sends DateTime and Time arguments as plain dates' do
stub = stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31")
.to_return(success_response)

marketing_stats_api.get(start_date: DateTime.new(2026, 5, 1, 13, 45), end_date: Time.new(2026, 5, 31, 23, 59))

expect(stub).to have_been_requested
end

it 'leaves out empty filters' do
allow(marketing_stats_api.client).to receive(:get).and_return(data: stats_attributes)

marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-05-31', sending_domain_ids: [],
sources: ['campaigns'])

expect(marketing_stats_api.client).to have_received(:get).with(
'/api/marketing/stats',
{ start_date: '2026-05-01', end_date: '2026-05-31', 'sources[]' => ['campaigns'] }
)
end

it 'raises ArgumentError for an invalid date' do
expect { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-13-01') }
.to raise_error(ArgumentError, /Invalid date: "2026-13-01"/)
end

it 'raises error when a filter is rejected' do
stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31&sources[]=newsletters")
.to_return(
status: 400,
body: { 'errors' => "Invalid source: 'newsletters'" }.to_json,
headers: { 'Content-Type' => 'application/json' }
)

expect { marketing_stats_api.get(start_date: '2026-05-01', end_date: '2026-05-31', sources: ['newsletters']) }
.to raise_error(Mailtrap::Error, "Invalid source: 'newsletters'")
end

it 'raises authorization error when the API key is incorrect' do
stub_request(:get, "#{base_url}?start_date=2026-05-01&end_date=2026-05-31")
.to_return(
status: 401,
body: { 'error' => 'Incorrect API token' }.to_json,
headers: { 'Content-Type' => 'application/json' }
)

expect { stats }.to raise_error(Mailtrap::AuthorizationError, 'Incorrect API token')
end
end

describe 'vcr#get', :vcr do
subject(:stats) do
described_class.new(client).get(start_date: '2026-09-01', end_date: '2026-09-30', sources: ['campaigns'])
end

let(:client) { Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY', 'local-api-key')) }

it 'maps response data to a MarketingStats object' do
expect(stats).to be_a(Mailtrap::MarketingStats)
expect(stats).to have_attributes(
delivery_count: be_a(Integer),
delivery_rate: be_a(Numeric),
open_count: be_a(Integer),
open_rate: be_a(Numeric),
sent_count: be_a(Integer)
)
end
end
end
Loading
Loading