Skip to content

CMM-1288: Add endpoints for Subscribers tab#1200

Merged
adalpari merged 8 commits intotrunkfrom
feature/CMM-1288-create-endpoints-for-Subscribers-tab
Mar 16, 2026
Merged

CMM-1288: Add endpoints for Subscribers tab#1200
adalpari merged 8 commits intotrunkfrom
feature/CMM-1288-create-endpoints-for-Subscribers-tab

Conversation

@adalpari
Copy link
Copy Markdown
Contributor

@adalpari adalpari commented Feb 26, 2026

Description

Add three new WP.com API endpoints to support the Subscribers tab feature:

  1. Stats emails summary/rest/v1.1/sites/{site_id}/stats/emails/summary
  2. Stats subscribers/rest/v1.1/sites/{site_id}/stats/subscribers
  3. Subscribers by user type/wpcom/v2/sites/{site_id}/subscribers_by_user_type

Changes

Stats emails summary

  • Add StatsEmailsSummaryParams with period, quantity, sort_field, and sort_order parameters
  • Add StatsEmailsSummaryResponse and StatsEmailsSummaryPost response types
  • Add endpoint definition using WpDerivedRequest macro with WpComNamespace::RestV1_1
  • Wire up the endpoint in WpComApiRequestBuilder and WpComApiClient
  • Add 10 unit tests and 3 JSON test fixtures
  • Add e2e integration test

Stats subscribers

  • Add StatsSubscribersParams with unit, quantity, and stat_fields (comma-joined) parameters
  • Add StatsSubscribersResponse reusing StatsVisitsDataValue for the fields+data array pattern
  • Add helper methods subscribers_data() and subscribers_paid_data() with #[uniffi::export]
  • Add endpoint definition with WpComNamespace::RestV1_1
  • Wire up the endpoint in WpComApiRequestBuilder and WpComApiClient
  • Add 13 unit tests and 5 JSON test fixtures (day/week/month/year/empty)
  • Add e2e integration test

Subscribers by user type

  • Add SubscribersByUserTypeParams with user_type, per_page, page, and sort parameters
  • Add SubscribersByUserTypeUserType and SubscribersByUserTypeSortField enums
  • Reuse existing ListSubscribersResponse type
  • Add ListSubscribersByUserType variant to SubscribersRequest with WpComNamespace::V2
  • Add 4 unit tests and 2 JSON test fixtures

Test plan

  • cargo build succeeds
  • cargo test --lib -p wp_api — all new unit tests pass
  • cargo fmt clean
  • Run e2e tests with WP.com credentials

🤖 Generated with Claude Code

adalpari and others added 4 commits February 26, 2026 13:54
Implements the /rest/v1.1/sites/{site_id}/stats/emails/summary endpoint
with params (period, quantity, sort_field, sort_order), response types,
unit tests, JSON fixtures, and e2e integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the /rest/v1.1/sites/{site_id}/stats/subscribers endpoint
with params (unit, quantity, date, stat_fields), response types with
helper methods for extracting subscribers and paid subscribers data,
unit tests, JSON fixtures for day/week/month/year/empty responses,
and e2e integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the /wpcom/v2/sites/{site_id}/subscribers_by_user_type endpoint
to the existing subscribers module. Includes params (per_page, page,
user_type, sort), reuses the existing ListSubscribersResponse type,
unit tests, and JSON test fixtures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The API can return invalid dates like "-001-11-30T00:00:00+00:00" for
some subscribers. Change date_subscribed to Option<WpGmtDateTime> with
a lenient deserializer that returns None for unparseable dates instead
of failing the entire response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari adalpari changed the title Add stats emails summary endpoint Add endpoints for Subscribers tab Mar 3, 2026
@adalpari adalpari changed the title Add endpoints for Subscribers tab CMM-1288: Add endpoints for Subscribers tab Mar 3, 2026
@adalpari adalpari marked this pull request as ready for review March 3, 2026 15:52
@adalpari adalpari requested a review from jkmassel March 3, 2026 15:52
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel left a comment

Choose a reason for hiding this comment

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

Caught a few things to take a look over (mostly suggestions by the bot)

Comment thread wp_api/src/wp_com/stats_emails_summary.rs Outdated
Comment thread wp_api/src/wp_com/subscribers.rs Outdated
Comment thread wp_api/src/wp_com/subscribers.rs Outdated
Comment thread wp_api/src/wp_com/stats_subscribers.rs Outdated
Comment thread wp_api/src/wp_com/endpoint/subscribers_endpoint.rs
Comment thread wp_api/src/wp_com/stats_emails_summary.rs Outdated
Comment thread wp_api/src/wp_com/subscribers.rs
Comment thread wp_api/src/wp_com/stats_subscribers.rs Outdated
adalpari and others added 2 commits March 4, 2026 21:02
- Rename Alltime to AllTime with serde/strum rename overrides
- Move date deserializer to wp_utc_date_format_option and wp_gmt_date_time_option modules
- Add #[serde(default)] on date_subscribed for missing fields
- Fix hardcoded period index in stats subscribers data extraction
- Replace StatsEmailsSummarySortOrder with shared WpApiParamOrder
- Add #[serde(rename_all = "snake_case")] to SubscribersByUserTypeSortField
- Remove unnecessary Hash derive from StatsSubscribersResponse
- Add e2e test for subscribers by user type endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…onse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
adalpari added a commit that referenced this pull request Mar 5, 2026
…endpoints

Address two issues flagged in PR #1200 review:

1. Fix unsafe row indexing in stats_visits.rs: The get_stats_data() function
   hard-coded row[0] for period and used unwrap() on position(). Now it safely
   looks up both "period" and data field indices, using .get() for bounds-safe
   access instead of panicking on out-of-bounds indices.

2. Remove Hash derive from all stats types: Hash was blindly copied across
   12 stats Period/Unit enums and 8 DataPoint/Response types in stats_visits.rs,
   but none are used as HashMap/HashSet keys. Removed Hash from:
   - StatsVisitsUnit, StatsClicksPeriod, StatsReferrersPeriod, StatsSearchTermsPeriod,
     StatsTopAuthorsPeriod, StatsTopPostsPeriod, StatsCountryViewsPeriod,
     StatsRegionViewsPeriod, StatsCityViewsPeriod, StatsDevicesPeriod,
     StatsFileDownloadsPeriod, StatsVideoPlaysPeriod
   - StatsVisitsResponse, StatsVisitsDataPoint, StatsVisitorsDataPoint,
     StatsLikesDataPoint, StatsReblogsDataPoint, StatsCommentsDataPoint,
     StatsPostsDataPoint, StatsVisitsDataValue

All 1256 unit tests pass.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@adalpari adalpari requested a review from jkmassel March 5, 2026 15:00
@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Mar 5, 2026

@jkmassel can you take another look?

"user_id": 3251794,
"subscription_id": 10660,
"email_address": "olduser@example.com",
"date_subscribed": "-001-11-30T00:00:00+00:00",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note: This format comes from this real error I got while using the app:

ResponseParsingError(reason=Invalid date format: -001-11-30T00:00:00+00:00 at line 1 column 41532, response={"total":88,"pages":1,"page":1,"per_page":100,"subscribers":[{"user_id":33840434,"subscription_id":792200219,"email_address":"email@nikhilchavan.com","date_subscribed":"2024-12-26T10:08:55+00:00","is_email_subscriber":false,"subscription_status":null,"avatar":"https:\/\/0.gravatar.com\/avatar\/fb265c001fcd9384d6dc59d2c47f43196c3a5835e298bcd0ba18b5ae0d82bb96?s=128&amp;d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D128&amp;r=G","display_name":"Nik","url":"https:\/\/nikhilc.dev"},{"user_id":257457202,"subscription_id":790270118,"email_address":"michal.dziewonski@automattic.com","date_subscribed":"2024-12-02T14:34:36+00:00","is_email_subscriber":false,"subscription_status":null,"avatar":"https:\/\/2.gravatar.com\/avatar\/5e3c9de90dc73677b682d85fe23af5f477082ab9c1b65b06067ea6e3fa3017d1?s=128&amp;d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D128&amp;r=G","display_name":"Micha\u0142","url":"https:\/\/michaldziewonski.wpcomstaging.com"},{"user_id":179649005,"subscription_id":786241236,"email_address":"sorin.nunca@automattic.com","date_subscribed":"2024-10-19T10:32:44+00:00","is_email_subscriber":false,"subscription_status":null,"avatar":"h

adalpari added a commit that referenced this pull request Mar 10, 2026
…endpoints (#1218)

Address two issues flagged in PR #1200 review:

1. Fix unsafe row indexing in stats_visits.rs: The get_stats_data() function
   hard-coded row[0] for period and used unwrap() on position(). Now it safely
   looks up both "period" and data field indices, using .get() for bounds-safe
   access instead of panicking on out-of-bounds indices.

2. Remove Hash derive from all stats types: Hash was blindly copied across
   12 stats Period/Unit enums and 8 DataPoint/Response types in stats_visits.rs,
   but none are used as HashMap/HashSet keys. Removed Hash from:
   - StatsVisitsUnit, StatsClicksPeriod, StatsReferrersPeriod, StatsSearchTermsPeriod,
     StatsTopAuthorsPeriod, StatsTopPostsPeriod, StatsCountryViewsPeriod,
     StatsRegionViewsPeriod, StatsCityViewsPeriod, StatsDevicesPeriod,
     StatsFileDownloadsPeriod, StatsVideoPlaysPeriod
   - StatsVisitsResponse, StatsVisitsDataPoint, StatsVisitorsDataPoint,
     StatsLikesDataPoint, StatsReblogsDataPoint, StatsCommentsDataPoint,
     StatsPostsDataPoint, StatsVisitsDataValue

All 1256 unit tests pass.

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Remove optional date handling for date_subscribed. Malformed or missing
dates now produce a parsing error instead of being silently treated as
None. Remove wp_gmt_date_time_option and wp_utc_date_format_option
modules that are no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Mar 10, 2026

@jkmassel changed how the date_subscribed field is fa94c4b. So now, null or malformed entries are treated as a parsing error

adalpari added a commit that referenced this pull request Mar 12, 2026
…endpoints (#1218)

Address two issues flagged in PR #1200 review:

1. Fix unsafe row indexing in stats_visits.rs: The get_stats_data() function
   hard-coded row[0] for period and used unwrap() on position(). Now it safely
   looks up both "period" and data field indices, using .get() for bounds-safe
   access instead of panicking on out-of-bounds indices.

2. Remove Hash derive from all stats types: Hash was blindly copied across
   12 stats Period/Unit enums and 8 DataPoint/Response types in stats_visits.rs,
   but none are used as HashMap/HashSet keys. Removed Hash from:
   - StatsVisitsUnit, StatsClicksPeriod, StatsReferrersPeriod, StatsSearchTermsPeriod,
     StatsTopAuthorsPeriod, StatsTopPostsPeriod, StatsCountryViewsPeriod,
     StatsRegionViewsPeriod, StatsCityViewsPeriod, StatsDevicesPeriod,
     StatsFileDownloadsPeriod, StatsVideoPlaysPeriod
   - StatsVisitsResponse, StatsVisitsDataPoint, StatsVisitorsDataPoint,
     StatsLikesDataPoint, StatsReblogsDataPoint, StatsCommentsDataPoint,
     StatsPostsDataPoint, StatsVisitsDataValue

All 1256 unit tests pass.

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
adalpari added a commit that referenced this pull request Mar 12, 2026
…endpoints (#1218)

Address two issues flagged in PR #1200 review:

1. Fix unsafe row indexing in stats_visits.rs: The get_stats_data() function
   hard-coded row[0] for period and used unwrap() on position(). Now it safely
   looks up both "period" and data field indices, using .get() for bounds-safe
   access instead of panicking on out-of-bounds indices.

2. Remove Hash derive from all stats types: Hash was blindly copied across
   12 stats Period/Unit enums and 8 DataPoint/Response types in stats_visits.rs,
   but none are used as HashMap/HashSet keys. Removed Hash from:
   - StatsVisitsUnit, StatsClicksPeriod, StatsReferrersPeriod, StatsSearchTermsPeriod,
     StatsTopAuthorsPeriod, StatsTopPostsPeriod, StatsCountryViewsPeriod,
     StatsRegionViewsPeriod, StatsCityViewsPeriod, StatsDevicesPeriod,
     StatsFileDownloadsPeriod, StatsVideoPlaysPeriod
   - StatsVisitsResponse, StatsVisitsDataPoint, StatsVisitorsDataPoint,
     StatsLikesDataPoint, StatsReblogsDataPoint, StatsCommentsDataPoint,
     StatsPostsDataPoint, StatsVisitsDataValue

All 1256 unit tests pass.

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Comment thread wp_api/src/wp_com/stats_emails_summary.rs Outdated
Comment thread wp_api/src/wp_com/subscribers.rs Outdated
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel left a comment

Choose a reason for hiding this comment

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

A couple of minor improvements, but this is ready to go otherwise.

…ypeUserType

Remove redundant #[serde(rename)] and #[strum(serialize)] attributes from
StatsEmailsSummarySortField variants since rename_all/serialize_all already
handle the conversion. Rename SubscribersByUserTypeUserType to WPComSubscriberType
for clarity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari adalpari merged commit 7b5d343 into trunk Mar 16, 2026
23 checks passed
@adalpari adalpari deleted the feature/CMM-1288-create-endpoints-for-Subscribers-tab branch March 16, 2026 10:52
adalpari added a commit that referenced this pull request Mar 18, 2026
* Add stats insights endpoint

Add support for the WP.com REST API v1.1 stats/insights endpoint,
which returns posting activity patterns including best hour/day to post,
hourly view breakdowns, and yearly posting summaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle empty array responses in stats insights deserialization

The WP.com API returns `[]` instead of `{}` for empty map fields
(days, hours, hourly_views). Use `deserialize_empty_array_or_hashmap`
to handle both cases gracefully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle integer values in map deserialization for stats insights

The WP.com API returns `0` instead of `{}` for empty map fields
(days, hours, hourly_views) on some sites. Extend
`deserialize_empty_array_or_hashmap` to also accept integers,
treating them as empty HashMaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Make map deserializer handle all non-map placeholder values

The WP.com API can return various non-map values (0, null, false, [])
for map fields on sites with no data. Make deserialize_empty_array_or_hashmap
handle all of them by adding visit_bool, visit_none, visit_unit, and
visit_f64 handlers, returning empty HashMap for any non-map value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rewrite map deserializers to use serde_json::Value intermediate

Replace the visitor-based approach with a simpler Value-based strategy
that first deserializes any JSON value, then only converts JSON objects
into HashMaps. All non-object values (integers, null, false, arrays,
strings, etc.) are treated as empty map / None. This is more robust
against unexpected API response formats.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add stats summary endpoint

Add GET /rest/v1.1/sites/<site_id>/stats endpoint that returns
aggregate site statistics and recent visit time-series data.
Reuses StatsVisitsResponse for the visits field. Supports
locale query parameter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add locale parameter to stats insights endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Bump fastlane-plugin-wpmreleasetoolkit from 14.1.0 to 14.2.0 (#1223)

Bumps [fastlane-plugin-wpmreleasetoolkit](https://github.com/wordpress-mobile/release-toolkit) from 14.1.0 to 14.2.0.
- [Release notes](https://github.com/wordpress-mobile/release-toolkit/releases)
- [Changelog](https://github.com/wordpress-mobile/release-toolkit/blob/trunk/CHANGELOG.md)
- [Commits](wordpress-mobile/release-toolkit@14.1.0...14.2.0)

---
updated-dependencies:
- dependency-name: fastlane-plugin-wpmreleasetoolkit
  dependency-version: 14.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump fluent-templates from 0.13.2 to 0.13.3 (#1222)

Bumps [fluent-templates](https://github.com/XAMPPRocky/fluent-templates) from 0.13.2 to 0.13.3.
- [Release notes](https://github.com/XAMPPRocky/fluent-templates/releases)
- [Changelog](https://github.com/XAMPPRocky/fluent-templates/blob/master/CHANGELOG.md)
- [Commits](XAMPPRocky/fluent-templates@fluent-templates-v0.13.2...fluent-templates-v0.13.3)

---
updated-dependencies:
- dependency-name: fluent-templates
  dependency-version: 0.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix unsafe row indexing and remove unnecessary Hash derives in stats endpoints (#1218)

Address two issues flagged in PR #1200 review:

1. Fix unsafe row indexing in stats_visits.rs: The get_stats_data() function
   hard-coded row[0] for period and used unwrap() on position(). Now it safely
   looks up both "period" and data field indices, using .get() for bounds-safe
   access instead of panicking on out-of-bounds indices.

2. Remove Hash derive from all stats types: Hash was blindly copied across
   12 stats Period/Unit enums and 8 DataPoint/Response types in stats_visits.rs,
   but none are used as HashMap/HashSet keys. Removed Hash from:
   - StatsVisitsUnit, StatsClicksPeriod, StatsReferrersPeriod, StatsSearchTermsPeriod,
     StatsTopAuthorsPeriod, StatsTopPostsPeriod, StatsCountryViewsPeriod,
     StatsRegionViewsPeriod, StatsCityViewsPeriod, StatsDevicesPeriod,
     StatsFileDownloadsPeriod, StatsVideoPlaysPeriod
   - StatsVisitsResponse, StatsVisitsDataPoint, StatsVisitorsDataPoint,
     StatsLikesDataPoint, StatsReblogsDataPoint, StatsCommentsDataPoint,
     StatsPostsDataPoint, StatsVisitsDataValue

All 1256 unit tests pass.

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>

* Bump chrono from 0.4.43 to 0.4.44 (#1221)

Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.43 to 0.4.44.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](chronotope/chrono@v0.4.43...v0.4.44)

---
updated-dependencies:
- dependency-name: chrono
  dependency-version: 0.4.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump proc-macro-crate from 3.4.0 to 3.5.0 (#1225)

Bumps [proc-macro-crate](https://github.com/bkchr/proc-macro-crate) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/bkchr/proc-macro-crate/releases)
- [Commits](bkchr/proc-macro-crate@v3.4.0...v3.5.0)

---
updated-dependencies:
- dependency-name: proc-macro-crate
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump quote from 1.0.44 to 1.0.45 (#1224)

Bumps [quote](https://github.com/dtolnay/quote) from 1.0.44 to 1.0.45.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](dtolnay/quote@1.0.44...1.0.45)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Add a migration script to ensure custom terms are re-fetched (#1217)

* Fix loading trashed post by ids (#1226)

* Add integration tests for fetching trashed posts by id

* Fix loading trashed post by ids

* Add PostStatus::Any

* Support filtering by "any" status (#1210)

* Support filtering by "any" status

* Use PostStatus::Any

* Add stats tags endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert changes to deserialize_empty_array_or_hashmap helpers

Restores the original Visitor-based implementation. These helpers
don't need to be modified for the stats tags endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tony Li <tony.li@automattic.com>
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.

2 participants