Skip to content

Add Stats API#61

Merged
piobeny merged 1 commit intomainfrom
stats-api
Mar 18, 2026
Merged

Add Stats API#61
piobeny merged 1 commit intomainfrom
stats-api

Conversation

@piobeny
Copy link
Copy Markdown
Contributor

@piobeny piobeny commented Mar 6, 2026

Motivation

  • Add support for the Email Sending Stats API (/api/accounts/{account_id}/stats) to the PHP SDK, enabling users to retrieve aggregated email sending statistics.

Changes

  • Add Stats class extending AbstractApi with 5 methods: get, byDomain, byCategory, byEmailServiceProvider, byDate
  • Add query param handling for array filters (sending_domain_ids, sending_streams, categories, email_service_providers)
  • Add usage example in examples/sending/stats.php
  • Update README with Stats API reference
  • Update CHANGELOG with v3.10.0 entry

How to test

  • $stats->get($startDate, $endDate) with different parameters (sendingDomainIds, sendingStreams, categories, emailServiceProviders)
  • Test grouped endpoints (byDomain, byCategory, byEmailServiceProvider, byDate) with filters

Examples

use Mailtrap\Config;
use Mailtrap\Helper\ResponseHelper;
use Mailtrap\MailtrapSendingClient;

$config = new Config('api_key');
$stats = (new MailtrapSendingClient($config))->stats($accountId);

// Get aggregated stats
$response = $stats->get('2026-01-01', '2026-01-31');
var_dump(ResponseHelper::toArray($response));

// Get stats with optional filters
$response = $stats->get(
    '2026-01-01',
    '2026-01-31',
    sendingDomainIds: [1, 2],
    sendingStreams: ['transactional'],
    categories: ['Welcome email'],
    emailServiceProviders: ['Gmail', 'Yahoo']
);

// Get stats grouped by date
$response = $stats->byDate('2026-01-01', '2026-01-02');

// Get stats grouped by categories
$response = $stats->byCategory('2026-01-01', '2026-01-02');

// Get stats grouped by email service providers with filters
$response = $stats->byEmailServiceProvider(
    '2026-01-01',
    '2026-01-02',
    categories: ['Welcome email']
);

// Get stats grouped by domains with filters
$response = $stats->byDomain(
    '2026-01-01',
    '2026-01-02',
    categories: ['Welcome email'],
    emailServiceProviders: ['Google']
);

Summary by CodeRabbit

  • New Features

    • Added Stats API for aggregated sending statistics (overall, by domain, category, email service provider, and date) with optional filters across date ranges; exposed via the client.
  • Documentation

    • Added a PHP example demonstrating Stats API usage and filtering.
  • Tests

    • Added a test suite validating Stats API behaviors and error handling.
  • Chore

    • Updated changelog with Unreleased Stats API entry.

@piobeny piobeny changed the title Stats api Add Stats API Mar 6, 2026
@piobeny piobeny requested review from IgorDobryn and mklocek March 16, 2026 07:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

Adds a new Sending Stats API with endpoints for aggregated metrics and groupings (by domain, category, email service provider, and date); integrates it into the client, provides an example, and adds tests.

Changes

Cohort / File(s) Summary
Core Stats API
src/Api/Sending/Stats.php
New Stats class implementing five public methods (get, byDomain, byCategory, byEmailServiceProvider, byDate) that build query params and perform HTTP GETs for aggregated sending statistics.
Client Integration
src/MailtrapSendingClient.php
Added stats mapping to API_MAPPING and docblock method annotation to expose the Stats API via MailtrapSendingClient.
Examples
examples/sending/stats.php
New example demonstrating construction and usage of the Stats client methods with try/catch and ResponseHelper conversion.
Tests
tests/Api/Sending/StatsTest.php, tests/MailtrapSendingClientTest.php
New test suite covering all Stats methods, filtering, error handling, and grouped responses; client test updated to include stats mapping.
Changelog
CHANGELOG.md
Added Unreleased section documenting the new Stats API endpoints.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as MailtrapSendingClient
    participant Stats as Stats API
    participant HTTP as HTTP Layer
    participant Server as API Server

    User->>Client: stats(accountId) -> get Stats instance
    Client->>Stats: __construct(config, accountId)
    Stats-->>Client: Stats instance

    User->>Stats: get(startDate, endDate, filters)
    Stats->>Stats: buildQueryParams(filters)
    Stats->>HTTP: httpGet(url, query)
    HTTP->>Server: GET /sending/{accountId}/stats?params
    Server-->>HTTP: JSON response
    HTTP-->>Stats: PSR-7 Response
    Stats->>Stats: handleResponse(response)
    Stats-->>User: ResponseInterface
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • IgorDobryn
  • VladimirTaytor
  • i7an
  • mklocek

Poem

🐇 Hop, hop, metrics now brighten my day,
Five little endpoints to count and display,
By domain, by date, provider, and category too,
I nibble the numbers and share them with you,
A rabbit's small cheer for the stats that just grew!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Stats API' is clear and directly related to the main change in the changeset, which introduces a new Stats API to the PHP SDK.
Description check ✅ Passed The description includes Motivation, Changes, and How to test sections with practical examples. While the optional 'Images and GIFs' section is missing, the core required information is comprehensive and complete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stats-api
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can disable poems in the walkthrough.

Disable the reviews.poem setting to disable the poems in the walkthrough.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/Api/Sending/StatsTest.php (1)

55-55: Assert the PSR-7 contract instead of Nyholm’s concrete class.

Stats returns ResponseInterface; these checks will start failing on any valid PSR-7 implementation swap even though the behavior is unchanged. Checking the interface keeps the tests aligned with the public contract.

🔧 Contract-level assertion
+use Psr\Http\Message\ResponseInterface;
...
-        $this->assertInstanceOf(Response::class, $response);
+        $this->assertInstanceOf(ResponseInterface::class, $response);

Repeat the same change for the other response assertions in this file.

Also applies to: 92-92, 130-130, 164-164, 195-195, 226-226

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Api/Sending/StatsTest.php` at line 55, Replace concrete Nyholm response
assertions with the PSR-7 contract: change assertions like
$this->assertInstanceOf(Response::class, $response) to assert the interface
Psr\Http\Message\ResponseInterface instead, and apply the same replacement for
the other occurrences in this test (the assertions at the other mentioned
locations). Ensure the tests import or reference
Psr\Http\Message\ResponseInterface so the assertions check the public contract
rather than Nyholm's concrete Response class.
src/Api/Sending/Stats.php (1)

24-29: Add concrete element types to these filter docblocks.

All five public methods expose multiple plain array params, so IDEs and static analysis cannot tell that sendingDomainIds is an int[] while the other filters are string[]. Tightening the PHPDoc will make this new SDK surface easier to call correctly.

✍️ Example of the docblock tightening
-     * `@param` array  $sendingDomainIds
-     * `@param` array  $sendingStreams
-     * `@param` array  $categories
-     * `@param` array  $emailServiceProviders
+     * `@param` int[]    $sendingDomainIds
+     * `@param` string[] $sendingStreams
+     * `@param` string[] $categories
+     * `@param` string[] $emailServiceProviders

Also applies to: 50-55, 76-81, 102-107, 128-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Api/Sending/Stats.php` around lines 24 - 29, Update the PHPDoc for the
public methods in src/Api/Sending/Stats.php to use concrete array element types
so static analysis and IDEs know the expected types; change `@param` array
$sendingDomainIds to `@param` int[] $sendingDomainIds and change `@param` array
$sendingStreams, `@param` array $categories, `@param` array $emailServiceProviders
(and any other plain array filters in the same methods such as at lines
referenced: 50-55, 76-81, 102-107, 128-133) to `@param` string[] $sendingStreams,
`@param` string[] $categories, `@param` string[] $emailServiceProviders
respectively, keeping existing parameter names and descriptions intact in each
method's docblock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/sending/stats.php`:
- Around line 7-10: The example's bootstrap uses the wrong vendor path and
unreliable $_ENV; update the require of the autoloader (the existing require
__DIR__ ... line) to point to the repository-root vendor/autoload.php, replace
$_ENV usage with getenv() for MAILTRAP_ACCOUNT_ID and MAILTRAP_API_KEY, add a
guard that exits or throws a clear error when either value is missing, and
ensure $accountId is cast to int before constructing Config (reference variables
$accountId and the Config constructor to locate the fix).

---

Nitpick comments:
In `@src/Api/Sending/Stats.php`:
- Around line 24-29: Update the PHPDoc for the public methods in
src/Api/Sending/Stats.php to use concrete array element types so static analysis
and IDEs know the expected types; change `@param` array $sendingDomainIds to
`@param` int[] $sendingDomainIds and change `@param` array $sendingStreams, `@param`
array $categories, `@param` array $emailServiceProviders (and any other plain
array filters in the same methods such as at lines referenced: 50-55, 76-81,
102-107, 128-133) to `@param` string[] $sendingStreams, `@param` string[]
$categories, `@param` string[] $emailServiceProviders respectively, keeping
existing parameter names and descriptions intact in each method's docblock.

In `@tests/Api/Sending/StatsTest.php`:
- Line 55: Replace concrete Nyholm response assertions with the PSR-7 contract:
change assertions like $this->assertInstanceOf(Response::class, $response) to
assert the interface Psr\Http\Message\ResponseInterface instead, and apply the
same replacement for the other occurrences in this test (the assertions at the
other mentioned locations). Ensure the tests import or reference
Psr\Http\Message\ResponseInterface so the assertions check the public contract
rather than Nyholm's concrete Response class.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c51cbc7-ce19-4272-958d-5bf6da40b03c

📥 Commits

Reviewing files that changed from the base of the PR and between 306d117 and 258e697.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • examples/sending/stats.php
  • src/Api/Sending/Stats.php
  • src/MailtrapSendingClient.php
  • tests/Api/Sending/StatsTest.php
  • tests/MailtrapSendingClientTest.php

Comment on lines +7 to +10
require __DIR__ . '/../vendor/autoload.php';

$accountId = (int) $_ENV['MAILTRAP_ACCOUNT_ID'];
$config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the overall repository structure
git ls-files | head -30

Repository: mailtrap/mailtrap-php

Length of output: 813


🏁 Script executed:

# Specifically look for vendor directory and examples structure
find . -maxdepth 3 -type d -name vendor -o -type d -name examples | head -20

Repository: mailtrap/mailtrap-php

Length of output: 75


🏁 Script executed:

# Check where vendor/autoload.php actually exists
find . -name autoload.php -type f 2>/dev/null | head -10

Repository: mailtrap/mailtrap-php

Length of output: 47


🏁 Script executed:

# List the examples directory structure
fd --max-depth 3 . examples/ 2>/dev/null | head -40

Repository: mailtrap/mailtrap-php

Length of output: 1132


🏁 Script executed:

# Check if there are other example files that bootstrap, to see the pattern used
rg "require.*autoload" examples/ --type php -B2 -A2

Repository: mailtrap/mailtrap-php

Length of output: 7832


Fix the example bootstrap before shipping it.

__DIR__ . '/../vendor/autoload.php' resolves to examples/vendor/autoload.php, but the vendor directory is at the repository root. The same block also assumes $_ENV is populated, which is unreliable in CLI; getenv() plus an explicit guard is safer for runnable examples.

🛠️ Minimal bootstrap fix
-require __DIR__ . '/../vendor/autoload.php';
+require dirname(__DIR__, 2) . '/vendor/autoload.php';

-$accountId = (int) $_ENV['MAILTRAP_ACCOUNT_ID'];
-$config = new Config($_ENV['MAILTRAP_API_KEY']); `#your` API token from here https://mailtrap.io/api-tokens
+$accountId = filter_var(getenv('MAILTRAP_ACCOUNT_ID'), FILTER_VALIDATE_INT);
+$apiKey = getenv('MAILTRAP_API_KEY');
+
+if (false === $accountId || false === $apiKey) {
+    throw new \RuntimeException('Set MAILTRAP_ACCOUNT_ID and MAILTRAP_API_KEY before running this example.');
+}
+
+$config = new Config($apiKey); // your API token from https://mailtrap.io/api-tokens
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
require __DIR__ . '/../vendor/autoload.php';
$accountId = (int) $_ENV['MAILTRAP_ACCOUNT_ID'];
$config = new Config($_ENV['MAILTRAP_API_KEY']); #your API token from here https://mailtrap.io/api-tokens
require dirname(__DIR__, 2) . '/vendor/autoload.php';
$accountId = filter_var(getenv('MAILTRAP_ACCOUNT_ID'), FILTER_VALIDATE_INT);
$apiKey = getenv('MAILTRAP_API_KEY');
if (false === $accountId || false === $apiKey) {
throw new \RuntimeException('Set MAILTRAP_ACCOUNT_ID and MAILTRAP_API_KEY before running this example.');
}
$config = new Config($apiKey); // your API token from https://mailtrap.io/api-tokens
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/sending/stats.php` around lines 7 - 10, The example's bootstrap uses
the wrong vendor path and unreliable $_ENV; update the require of the autoloader
(the existing require __DIR__ ... line) to point to the repository-root
vendor/autoload.php, replace $_ENV usage with getenv() for MAILTRAP_ACCOUNT_ID
and MAILTRAP_API_KEY, add a guard that exits or throws a clear error when either
value is missing, and ensure $accountId is cast to int before constructing
Config (reference variables $accountId and the Config constructor to locate the
fix).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/Api/Sending/Stats.php (1)

59-149: Consider extracting common GET endpoint plumbing.

byDomain, byCategory, byEmailServiceProvider, and byDate repeat the same call pattern with only a path suffix change. A small private helper would reduce maintenance overhead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Api/Sending/Stats.php` around lines 59 - 149, The four methods byDomain,
byCategory, byEmailServiceProvider and byDate duplicate identical GET plumbing;
add a small private helper (e.g., private function getStatsBy(string $suffix,
string $startDate, string $endDate, array $sendingDomainIds = [], array
$sendingStreams = [], array $categories = [], array $emailServiceProviders =
[]): ResponseInterface) that calls
$this->handleResponse($this->httpGet($this->getBasePath() . $suffix,
$this->buildQueryParams(...))) and then update each public method to call that
helper with '/domains', '/categories', '/email_service_providers', and '/date'
respectively, reusing buildQueryParams, getBasePath, httpGet and handleResponse.
tests/Api/Sending/StatsTest.php (1)

115-229: Add filter-forwarding tests for grouped endpoints.

Only testGetWithFilters() verifies optional filter arrays are passed to httpGet. Add filtered variants for grouped methods too, so query mapping regressions are caught early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Api/Sending/StatsTest.php` around lines 115 - 229, Add new test methods
that mirror the existing grouped tests but include an additional filter array
parameter and assert it is passed to httpGet; e.g. create
testByDomainWithFilters, testByCategoryWithFilters,
testByEmailServiceProviderWithFilters, and testByDateWithFilters that call
Stats::byDomain/byCategory/byEmailServiceProvider/byDate with a third $filters
argument, set the mock expectation on httpGet (same AbstractApi::DEFAULT_HOST
paths) to receive
['start_date'=>'2026-01-01','end_date'=>'2026-01-31','filters'=> $filters] (or
however the client maps filters into the query), return the same JSON Response,
convert with ResponseHelper::toArray and assert the response shape/values as in
the existing tests; follow the existing pattern of
$this->stats->expects($this->once())->method('httpGet')->with(...)->willReturn(...).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Api/Sending/Stats.php`:
- Around line 16-19: Validate the injected accountId in the Stats::__construct
and throw an InvalidArgumentException if it's not a positive integer (<= 0) so
misuse is caught immediately; add the check at the start of the constructor
(before proceeding with parent::__construct) and use a clear error message
mentioning the invalid $accountId and the Stats class.

---

Nitpick comments:
In `@src/Api/Sending/Stats.php`:
- Around line 59-149: The four methods byDomain, byCategory,
byEmailServiceProvider and byDate duplicate identical GET plumbing; add a small
private helper (e.g., private function getStatsBy(string $suffix, string
$startDate, string $endDate, array $sendingDomainIds = [], array $sendingStreams
= [], array $categories = [], array $emailServiceProviders = []):
ResponseInterface) that calls
$this->handleResponse($this->httpGet($this->getBasePath() . $suffix,
$this->buildQueryParams(...))) and then update each public method to call that
helper with '/domains', '/categories', '/email_service_providers', and '/date'
respectively, reusing buildQueryParams, getBasePath, httpGet and handleResponse.

In `@tests/Api/Sending/StatsTest.php`:
- Around line 115-229: Add new test methods that mirror the existing grouped
tests but include an additional filter array parameter and assert it is passed
to httpGet; e.g. create testByDomainWithFilters, testByCategoryWithFilters,
testByEmailServiceProviderWithFilters, and testByDateWithFilters that call
Stats::byDomain/byCategory/byEmailServiceProvider/byDate with a third $filters
argument, set the mock expectation on httpGet (same AbstractApi::DEFAULT_HOST
paths) to receive
['start_date'=>'2026-01-01','end_date'=>'2026-01-31','filters'=> $filters] (or
however the client maps filters into the query), return the same JSON Response,
convert with ResponseHelper::toArray and assert the response shape/values as in
the existing tests; follow the existing pattern of
$this->stats->expects($this->once())->method('httpGet')->with(...)->willReturn(...).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2766544d-76af-47b9-bfc8-c9ae086b5632

📥 Commits

Reviewing files that changed from the base of the PR and between 258e697 and b3c83f8.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • examples/sending/stats.php
  • src/Api/Sending/Stats.php
  • src/MailtrapSendingClient.php
  • tests/Api/Sending/StatsTest.php
  • tests/MailtrapSendingClientTest.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/MailtrapSendingClient.php
  • CHANGELOG.md
  • tests/MailtrapSendingClientTest.php
  • examples/sending/stats.php

Comment on lines +16 to +19
public function __construct(ConfigInterface $config, private int $accountId)
{
parent::__construct($config);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fail fast for invalid account IDs.

A non-positive account ID currently passes through and only fails at request time. Validate it in the constructor to catch misuse earlier.

Proposed fix
 public function __construct(ConfigInterface $config, private int $accountId)
 {
+    if ($accountId <= 0) {
+        throw new \InvalidArgumentException('Account ID must be greater than 0.');
+    }
+
     parent::__construct($config);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function __construct(ConfigInterface $config, private int $accountId)
{
parent::__construct($config);
}
public function __construct(ConfigInterface $config, private int $accountId)
{
if ($accountId <= 0) {
throw new \InvalidArgumentException('Account ID must be greater than 0.');
}
parent::__construct($config);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Api/Sending/Stats.php` around lines 16 - 19, Validate the injected
accountId in the Stats::__construct and throw an InvalidArgumentException if
it's not a positive integer (<= 0) so misuse is caught immediately; add the
check at the start of the constructor (before proceeding with
parent::__construct) and use a clear error message mentioning the invalid
$accountId and the Stats class.

@piobeny piobeny merged commit bd06885 into main Mar 18, 2026
22 checks passed
@piobeny piobeny deleted the stats-api branch March 18, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants