Skip to content

Add Stats API#115

Open
piobeny wants to merge 1 commit intomainfrom
stats-api
Open

Add Stats API#115
piobeny wants to merge 1 commit intomainfrom
stats-api

Conversation

@piobeny
Copy link

@piobeny piobeny commented Mar 6, 2026

Motivation

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

Changes

  • Add SendingStats, SendingStatGroup, and StatsFilterParams types in src/types/api/stats
  • Add StatsApi class 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/general/stats.ts
  • Update README with Stats API reference

How to test

  • statsClient.get() with different parameters (start_date, end_date, sending_domain_ids, sending_streams, categories, email_service_providers)
  • Test grouped endpoints (byDomain, byCategory, byEmailServiceProvider, byDate) with filters

Examples

import { MailtrapClient } from "mailtrap"

const client = new MailtrapClient({ token: "api_key", accountId: accountId })
const statsClient = client.general.stats

// Get aggregated stats
const result = await statsClient.get({
  start_date: "2026-01-01",
  end_date: "2026-01-31",
})
// { delivery_count: 11349, delivery_rate: 0.974, bounce_count: 295, ... }

// Get stats with optional filters
const filtered = await statsClient.get({
  start_date: "2026-01-01",
  end_date: "2026-01-31",
  categories: ["Welcome email"],
})

// Get stats grouped by date
const byDate = await statsClient.byDate({
  start_date: "2026-01-01",
  end_date: "2026-01-02",
})
// [{ name: "date", value: "2026-01-01", stats: { delivery_count: 2220, ... } }, ...]

// Get stats grouped by category
const byCategories = await statsClient.byCategory({
  start_date: "2026-01-01",
  end_date: "2026-01-02",
})
// [{ name: "category", value: "Welcome email", stats: { ... } }, ...]

// Get stats grouped by email service provider with filters
const byEsp = await statsClient.byEmailServiceProvider({
  start_date: "2026-01-01",
  end_date: "2026-01-02",
  categories: ["Welcome email"],
})
// [{ name: "email_service_provider", value: "Google", stats: { ... } }, ...]

// Get stats grouped by domain with filters
const byDomains = await statsClient.byDomain({
  start_date: "2026-01-01",
  end_date: "2026-01-02",
  categories: ["Welcome email"],
  email_service_providers: ["Google"],
})
// [{ name: "sending_domain_id", value: 75581, stats: { ... } }]

Summary by CodeRabbit

  • New Features

    • Added a Stats API to retrieve aggregated and grouped sending statistics (by domain, category, email service provider, and date) with date-range and filter support.
  • Documentation

    • Added an example demonstrating how to call the Stats API.
  • Tests

    • Added a comprehensive test suite covering Stats API endpoints and responses.

@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

Adds a new StatsApi to retrieve aggregated and grouped sending statistics (endpoints: get, byDomain, byCategory, byEmailServiceProvider, byDate). Includes types, API client, GeneralAPI integration, tests, example usage, and a CHANGELOG entry.

Changes

Cohort / File(s) Summary
Type Definitions
src/types/api/stats.ts
Adds SendingStats, SendingStatGroup, and StatsFilterParams exported types for stats responses and query filters.
Stats API Implementation
src/lib/api/resources/Stats.ts
New StatsApi class: constructor(client, accountId), get, byDomain, byCategory, byEmailServiceProvider, byDate, private groupedStats and buildQueryParams, GROUP_KEYS mapping, and response mapping.
API Integration
src/lib/api/General.ts, src/index.ts
Adds stats lazy getter to GeneralAPI and re-exports src/types/api/stats.
Tests
src/__tests__/lib/api/resources/Stats.test.ts
New Jest suite covering URL/base formation, all endpoints, array query serialization (single-bracket params), success mappings, and error handling (401, 404).
Examples & Changelog
examples/general/stats.ts, CHANGELOG.md
New example demonstrating usage of all stats endpoints and a CHANGELOG entry announcing the StatsApi feature.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant General as GeneralAPI
    participant Stats as StatsApi
    participant HTTP as Axios/HTTP Layer
    participant API as Remote API

    Client->>General: access general.stats
    activate General
    General->>General: checkAccountIdPresence()
    General->>Stats: instantiate StatsApi(axios, accountId)
    activate Stats
    deactivate General
    Client->>Stats: stats.get(filters)
    activate Stats
    Stats->>Stats: buildQueryParams(filters)
    Stats->>HTTP: GET /api/accounts/{accountId}/stats?start_date=...&sending_domain_ids[]=...
    activate HTTP
    HTTP->>API: HTTP GET
    activate API
    API-->>HTTP: 200 with payload
    deactivate API
    HTTP-->>Stats: response.data
    deactivate HTTP
    Stats-->>Client: mapped SendingStats / groups
    deactivate Stats
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Billing api #103 — Adds a new account-scoped API resource with a lazy-instantiated getter and related resource/types/tests, similar integration pattern.

Suggested labels

feature request

Suggested reviewers

  • VladimirTaytor

Poem

🐇 I hopped through endpoints, counts in tow,
Domains and dates in tidy rows,
Five new doors to metrics bright,
I mapped each stat by day and site,
A tiny rabbit, cheering stats tonight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Stats API' directly reflects the main objective of the pull request, which is to add support for the Email Sending Stats API to the Node.js SDK.
Description check ✅ Passed The description includes all required sections (Motivation, Changes, How to test) and provides comprehensive examples demonstrating the new functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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
  • Post copyable unit tests in a comment
  • Commit unit tests in branch stats-api

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.

Copy link

@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 (3)
examples/general/stats.ts (1)

87-92: Consider awaiting function calls or using an async IIFE.

The example functions are called without await, meaning errors won't be handled at the top level and execution order isn't guaranteed. For a cleaner example:

Suggested fix
-testGetStats()
-testGetStatsWithFilters()
-testGetStatsByDomains()
-testGetStatsByCategories()
-testGetStatsByEmailServiceProviders()
-testGetStatsByDate()
+(async () => {
+  await testGetStats()
+  await testGetStatsWithFilters()
+  await testGetStatsByDomains()
+  await testGetStatsByCategories()
+  await testGetStatsByEmailServiceProviders()
+  await testGetStatsByDate()
+})()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/general/stats.ts` around lines 87 - 92, The example calls to
testGetStats, testGetStatsWithFilters, testGetStatsByDomains,
testGetStatsByCategories, testGetStatsByEmailServiceProviders, and
testGetStatsByDate are invoked without awaiting, so wrap these calls in an async
IIFE or await them from an async function and handle errors; update the example
to either (1) make the top-level module run an immediately-invoked async
function that sequentially awaits each of the listed functions (testGetStats,
testGetStatsWithFilters, testGetStatsByDomains, testGetStatsByCategories,
testGetStatsByEmailServiceProviders, testGetStatsByDate) with try/catch around
the sequence to surface errors, or (2) export/use an async main() that awaits
each function and catches/logs errors before exiting.
src/__tests__/lib/api/resources/Stats.test.ts (1)

70-91: Consider moving initialization tests after beforeAll.

The describe("class Stats(): ") block (lines 70-80) appears before beforeAll (lines 82-91). While this works because Jest hoists beforeAll, it's unconventional and could confuse readers. Consider moving beforeAll to the top of the outer describe block for clarity.

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

In `@src/__tests__/lib/api/resources/Stats.test.ts` around lines 70 - 91, Move the
test setup so initialization runs before the spec declarations: relocate the
beforeAll block (which initializes axios.interceptors.response.use,
handleSendingError, and creates the AxiosMockAdapter mock) to the top of the
outer describe that contains the "class Stats(): " tests so that beforeAll
executes visually before the describe("class Stats(): ") block and the statsAPI
initialization tests (expect(...).toHaveProperty(...)) appear after the test
setup; keep references to axios.interceptors.response.use, handleSendingError,
AxiosMockAdapter, mock, and statsAPI intact.
src/lib/api/resources/Stats.ts (1)

75-94: Consider adding a guard for unknown group keys.

While groupedStats is private and only called with valid keys, adding a guard would make the code more defensive against future changes:

Optional defensive check
 private async groupedStats(
   group: string,
   params: StatsFilterParams
 ): Promise<SendingStatGroup[]> {
   const url = `${this.statsURL}/${group}`;
   const groupKey = GROUP_KEYS[group];
+
+  if (!groupKey) {
+    throw new Error(`Unknown stats group: ${group}`);
+  }

   const response = await this.client.get<
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/resources/Stats.ts` around lines 75 - 94, Add a defensive guard
in groupedStats: after computing const groupKey = GROUP_KEYS[group], verify
groupKey is defined and handle the unknown case (e.g., throw a clear Error
mentioning the invalid group or return an empty array) before using it to build
the response; update groupedStats (and any callers if needed) so the function
fails fast with a descriptive message referencing groupedStats and GROUP_KEYS
when an invalid group is supplied.
🤖 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/general/stats.ts`:
- Around line 4-5: The example defines ACCOUNT_ID as a string placeholder but
MailtrapClient expects a numeric accountId; change the placeholder to a numeric
literal (e.g., ACCOUNT_ID = 123456) or update usage to coerce/parse it to a
number before passing to MailtrapClient (reference ACCOUNT_ID and the
MailtrapClient constructor/accountId parameter) so the example shows a number
type rather than a string.

---

Nitpick comments:
In `@examples/general/stats.ts`:
- Around line 87-92: The example calls to testGetStats, testGetStatsWithFilters,
testGetStatsByDomains, testGetStatsByCategories,
testGetStatsByEmailServiceProviders, and testGetStatsByDate are invoked without
awaiting, so wrap these calls in an async IIFE or await them from an async
function and handle errors; update the example to either (1) make the top-level
module run an immediately-invoked async function that sequentially awaits each
of the listed functions (testGetStats, testGetStatsWithFilters,
testGetStatsByDomains, testGetStatsByCategories,
testGetStatsByEmailServiceProviders, testGetStatsByDate) with try/catch around
the sequence to surface errors, or (2) export/use an async main() that awaits
each function and catches/logs errors before exiting.

In `@src/__tests__/lib/api/resources/Stats.test.ts`:
- Around line 70-91: Move the test setup so initialization runs before the spec
declarations: relocate the beforeAll block (which initializes
axios.interceptors.response.use, handleSendingError, and creates the
AxiosMockAdapter mock) to the top of the outer describe that contains the "class
Stats(): " tests so that beforeAll executes visually before the describe("class
Stats(): ") block and the statsAPI initialization tests
(expect(...).toHaveProperty(...)) appear after the test setup; keep references
to axios.interceptors.response.use, handleSendingError, AxiosMockAdapter, mock,
and statsAPI intact.

In `@src/lib/api/resources/Stats.ts`:
- Around line 75-94: Add a defensive guard in groupedStats: after computing
const groupKey = GROUP_KEYS[group], verify groupKey is defined and handle the
unknown case (e.g., throw a clear Error mentioning the invalid group or return
an empty array) before using it to build the response; update groupedStats (and
any callers if needed) so the function fails fast with a descriptive message
referencing groupedStats and GROUP_KEYS when an invalid group is supplied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8d29381-21db-4838-bd09-f8a2d0532f47

📥 Commits

Reviewing files that changed from the base of the PR and between 484d833 and 8497f1e.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • examples/general/stats.ts
  • src/__tests__/lib/api/resources/Stats.test.ts
  • src/index.ts
  • src/lib/api/General.ts
  • src/lib/api/resources/Stats.ts
  • src/types/api/stats.ts

Copy link

@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

🤖 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/general/stats.ts`:
- Around line 3-7: TEST_INBOX_ID is declared as a string but should be a number;
change the TEST_INBOX_ID constant to a numeric placeholder (e.g., 123456) and
ensure the MailtrapClient instantiation still passes testInboxId as that numeric
value so the testInboxId argument type matches what MailtrapClient expects in
its constructor.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9dedc9ac-4117-43ab-95ed-0612effa87a5

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6f082 and 21b247b.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • examples/general/stats.ts
  • src/__tests__/lib/api/resources/Stats.test.ts
  • src/index.ts
  • src/lib/api/General.ts
  • src/lib/api/resources/Stats.ts
  • src/types/api/stats.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/api/General.ts

Comment on lines +3 to +7
const TOKEN = "<YOUR-TOKEN-HERE>";
const TEST_INBOX_ID = "<YOUR-TEST-INBOX-ID-HERE>"
const ACCOUNT_ID = 123456

const client = new MailtrapClient({ token: TOKEN, testInboxId: TEST_INBOX_ID, accountId: ACCOUNT_ID });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type mismatch: TEST_INBOX_ID should be a number.

While ACCOUNT_ID was correctly updated to a numeric placeholder, TEST_INBOX_ID remains a string. Although testInboxId is optional and not used by the Stats API, keeping consistency helps users who copy the example.

Suggested fix
 const TOKEN = "<YOUR-TOKEN-HERE>";
-const TEST_INBOX_ID = "<YOUR-TEST-INBOX-ID-HERE>"
+const TEST_INBOX_ID = 12345 // Replace with your test inbox ID
 const ACCOUNT_ID = 123456
📝 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
const TOKEN = "<YOUR-TOKEN-HERE>";
const TEST_INBOX_ID = "<YOUR-TEST-INBOX-ID-HERE>"
const ACCOUNT_ID = 123456
const client = new MailtrapClient({ token: TOKEN, testInboxId: TEST_INBOX_ID, accountId: ACCOUNT_ID });
const TOKEN = "<YOUR-TOKEN-HERE>";
const TEST_INBOX_ID = 12345 // Replace with your test inbox ID
const ACCOUNT_ID = 123456
const client = new MailtrapClient({ token: TOKEN, testInboxId: TEST_INBOX_ID, accountId: ACCOUNT_ID });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/general/stats.ts` around lines 3 - 7, TEST_INBOX_ID is declared as a
string but should be a number; change the TEST_INBOX_ID constant to a numeric
placeholder (e.g., 123456) and ensure the MailtrapClient instantiation still
passes testInboxId as that numeric value so the testInboxId argument type
matches what MailtrapClient expects in its constructor.

@piobeny piobeny force-pushed the stats-api branch 2 times, most recently from ab2c7fe to 6e5276e Compare March 10, 2026 12:32
Copy link

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

🧹 Nitpick comments (1)
src/__tests__/lib/api/resources/Stats.test.ts (1)

70-79: Consider cleaning up interceptors in afterAll.

The response interceptors added to the global axios instance in beforeAll are not removed after tests complete. This could affect other test files if they share the same axios instance and don't expect these interceptors.

♻️ Suggested cleanup
 beforeAll(() => {
   /**
    * Init Axios interceptors for handling response.data, errors.
    */
-  axios.interceptors.response.use(
+  const interceptorId = axios.interceptors.response.use(
     (response) => response.data,
     handleSendingError
   );
   mock = new AxiosMockAdapter(axios);
+  return () => {
+    axios.interceptors.response.eject(interceptorId);
+  };
 });
+
+afterAll(() => {
+  mock.restore();
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/lib/api/resources/Stats.test.ts` around lines 70 - 79, The
response interceptor added with axios.interceptors.response.use in beforeAll
must be removed in afterAll to avoid leaking into other tests: capture the
interceptor id when calling
axios.interceptors.response.use((response)=>response.data, handleSendingError)
and call axios.interceptors.response.eject(id) in afterAll; also restore the
AxiosMockAdapter mock (mock.restore()) in afterAll to fully clean up the test
fixture. Ensure the identifiers referenced are the interceptor id variable,
axios.interceptors.response.use, axios.interceptors.response.eject,
handleSendingError, and mock (AxiosMockAdapter) so the cleanup is applied to the
same objects created in beforeAll.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/lib/api/resources/Stats.test.ts`:
- Around line 70-79: The response interceptor added with
axios.interceptors.response.use in beforeAll must be removed in afterAll to
avoid leaking into other tests: capture the interceptor id when calling
axios.interceptors.response.use((response)=>response.data, handleSendingError)
and call axios.interceptors.response.eject(id) in afterAll; also restore the
AxiosMockAdapter mock (mock.restore()) in afterAll to fully clean up the test
fixture. Ensure the identifiers referenced are the interceptor id variable,
axios.interceptors.response.use, axios.interceptors.response.eject,
handleSendingError, and mock (AxiosMockAdapter) so the cleanup is applied to the
same objects created in beforeAll.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 446f146d-5eb8-4126-9357-874e19a6d778

📥 Commits

Reviewing files that changed from the base of the PR and between 21b247b and 495d27a.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • examples/general/stats.ts
  • src/__tests__/lib/api/resources/Stats.test.ts
  • src/index.ts
  • src/lib/api/General.ts
  • src/lib/api/resources/Stats.ts
  • src/types/api/stats.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/index.ts
  • src/lib/api/General.ts
  • src/types/api/stats.ts

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.

1 participant