Skip to content

ENGL-663: Catch up the Java SDK with the public API - #24

Open
kavanpancholi wants to merge 9 commits into
mainfrom
feature/engl-663-catch-up-the-ml-sdks-with-the-public-api-subscribers-groups
Open

kavanpancholi wants to merge 9 commits into
mainfrom
feature/engl-663-catch-up-the-ml-sdks-with-the-public-api-subscribers-groups

Conversation

@kavanpancholi

Copy link
Copy Markdown

Why?

The Java SDK was behind the public API. It could read automations but not build, change or control them. It also had the known bugs from ENGL-663: segment subscribers sent limig and after, the list builders had no page(), and the automation parser returned null for several trigger, step and action types. This catches it up with every subscribers, groups, segments, fields and automations endpoint on the card.

Changes

  1. Known bugs fixed:
    • Segment subscribers send limit (it was limig) and take cursor(). The old after() is kept and still sends after.
    • Groups, segments, fields and automations lists have page().
    • The automation parser never returns null. Unknown trigger and step types parse as GenericAutomationTrigger and GenericAutomationStep, with the full JSON in data. That covers subscriber_joins_segment, manual, abandoned_checkout, purchase_frequency, webhook steps and the send_internal_email_notification action. Steps and triggers parse to the same typed classes (EmailStep, DelayStep and so on) on every endpoint.
  2. Other bugs fixed on the way:
    • Query values were not URL-encoded, so a + in an email reached the API as a space.
    • The parser read every trigger from the automation object, so each trigger got the automation's id and fields.
    • completedSubscribersCount and subscribersInQueueCount were always 0 (missing @SerializedName).
    • A/B steps threw on split_percentages, which the API sends as objects.
    • Group, segment, field, form, webhook and import lists never parsed their dates, because postDeserialise was misspelled and never called. Each class now overrides postDeserialize(), and the old name stays as a deprecated delegate. ISO dates are parsed too.
  3. New documented params on existing calls: subscriber list (cursor, sort, include, dates, with_fields, with_groups), subscriber create (resubscribe, double_optin, notes), group subscribers (include), segment list (sort, with_filter), segment update (filter), automation list (sort, with_steps), automation activity (sort, with_subscribers).
  4. New calls: subscribers().forget(), subscribers().activityLog(), groups().importBuilder(), and automation builder().create()/update(), delete, copy, enable, disable, test, addSubscribers, readd and the three activity actions. New automations().triggers(id), steps(id), emails(id), templates(), selectTemplate() and reports(id).
  5. New general response fields: subscriber deleted_at, forget_at, groups, location, timezone, field is_default, segment filter, automation template and build fields, import status counts.
  6. Request tests (AutomationsRequestTest, SubscribersRequestTest, GroupsRequestTest, SegmentsRequestTest, FieldsRequestTest) use a mock HTTP client and assert the HTTP method, path, query and body against the OpenAPI document. Parser tests cover every typed and unknown trigger and step type.

No existing method signature changed and nothing was removed. MailerLite.java is untouched.

One behaviour change to note for the release: query values are now URL-encoded. A caller who encoded values by hand to work around the old bug now sends them encoded twice.

Endpoint checklist

"Live call" means the call ran once against a local API on a test account, and the effect was read back with a separate request.

Subscribers

Endpoint SDK call How checked
POST /subscribers subscribers().builder().email().addNote().resubscribe().create() unit test + live call
PUT /subscribers/{subscriber} subscribers().builder().addField().update(id) unit test + live call
GET /subscribers/{subscriber} subscribers().retriever().getSingle(id) unit test + live call
GET /subscribers subscribers().retriever().filter().include().sort().limit().cursor().withFields().withGroups().get() unit test + live call
GET /subscribers (with_groups) subscribers().retriever().withGroups(true).get() unit test; the API ignores with_groups in the current version (use include=groups)
GET /subscribers/{subscriber}/activity-log subscribers().activityLog(id).logName().limit().page().get() unit test + live call
GET /subscribers/import/{import} subscribers().imports().getSingle(id) unit test + live call
POST /subscribers/{subscriber}/forget subscribers().forget(id) unit test + live call
DELETE /subscribers/{subscriber} subscribers().delete(id) unit test + live call

Groups

Endpoint SDK call How checked
POST /groups groups().builder().create(name) unit test + live call
GET /groups groups().retriever().nameFilter().page().limit().sort().get() unit test + live call
PUT /groups/{group} groups().builder().update(id, name) unit test + live call
POST /subscribers/{subscriber}/groups/{group} groups().subscribers(group).assign(sub) unit test + live call
DELETE /subscribers/{subscriber}/groups/{group} groups().subscribers(group).unassign(sub) unit test + live call
POST /groups/{group}/import-subscribers groups().importBuilder(group).addSubscriber().resubscribe().triggerAutomations().start() unit test + live call
GET /groups/{group}/subscribers groups().subscribers(group).include().limit().cursor().statusFilter().get() unit test + live call
DELETE /groups/{group} groups().delete(id) unit test + live call

Segments

Endpoint SDK call How checked
GET /segments segments().retriever().sort().page().limit().withFilter().get() unit test + live call
PUT /segments/{segment} segments().update(id, name, filter) unit test + live call
GET /segments/{segment}/subscribers segments().subscribers(id).limit(1).cursor(next).statusFilter().get() unit test + live call
GET /segments/{segment}/subscribers (old after param) segments().subscribers(id).limit(1).after(id).get() unit test + live call
DELETE /segments/{segment} segments().delete(id) unit test + live call

Fields

Endpoint SDK call How checked
POST /fields fields().builder().create(name, type) unit test + live call
GET /fields fields().retriever().filter(keyword).filter(type).page().limit().sort().get() unit test + live call
PUT /fields/{field} fields().builder().update(id, name) unit test + live call
DELETE /fields/{field} fields().delete(id) unit test + live call

Automations

Endpoint SDK call How checked
POST /automations automations().builder().name().create() unit test + live call
PUT /automations/{automation} automations().builder().name().triggerData().update(a) unit test + live call
POST /automations/{automation}/test automations().test(a, email) unit test + live call
POST /automations/{automation}/enable automations().enable(a) unit test + live call
POST /automations/{automation}/add-subscribers automations().addSubscribers(a, emails, groups, stepId) unit test + live call
GET /automations/{automation}/activity automations().subscriberActivityRetriever().filter(status).filter(search).sort().limit().page().get(a) unit test + live call
POST /automations/{automation}/activity/add-to-group automations().addActivityToGroups(a, groups, filter) unit test + live call
POST /automations/{automation}/activity/skip-delay automations().skipActivityDelay(a, filter) unit test + live call
POST /automations/{automation}/activity/cancel-automation automations().cancelActivity(a, filter) unit test + live call
POST /automations/{automation}/readd automations().readd(a, to, stepId, filter) unit test + live call
POST /automations/{automation}/copy automations().copy(a) unit test + live call
POST /automations/{automation}/disable automations().disable(a) unit test + live call
GET /automations/{automation} (with an A/B step) automations().retriever().getSingle(a) unit test + live call
DELETE /automations/{automation} automations().delete(a) unit test + live call
GET /automations automations().retriever().filter().page().limit().sort().withSteps().get() unit test + live call

Automation triggers

Endpoint SDK call How checked
POST /automations/{automation}/triggers automations().triggers(a).create(type, data) unit test + live call
GET /automations/{automation}/triggers automations().triggers(a).get() unit test + live call
GET /automations/{automation}/triggers/{trigger} automations().triggers(a).getSingle(t) unit test + live call
PUT /automations/{automation}/triggers/{trigger} automations().triggers(a).update(t, data) unit test + live call
DELETE /automations/{automation}/triggers/{trigger} automations().triggers(a).delete(t) unit test + live call

Automation steps

Endpoint SDK call How checked
POST /automations/{automation}/steps automations().steps(a).builder().type().parentId().data().create() unit test + live call
GET /automations/{automation}/steps/index automations().steps(a).get() unit test + live call
GET /automations/{automation}/steps/{step} automations().steps(a).getSingle(s) unit test + live call
PUT /automations/{automation}/steps/{step} automations().steps(a).builder().data().rescheduleWaitingSubscribers().update(s) unit test + live call
POST /automations/{automation}/steps/{step}/copy automations().steps(a).builder().parentId().copy(s) unit test + live call
POST /automations/{automation}/steps/reorder automations().steps(a).reorder(dragged, newParent, branch, branchForChild) unit test + live call
DELETE /automations/{automation}/steps/{step} automations().steps(a).delete(s) unit test + live call
PUT /automations/{automation}/steps/{step}/select-ab-test-winner automations().steps(a).selectAbTestWinner(s, winner) unit test + live call

Automation emails

Endpoint SDK call How checked
PUT /automations/{automation}/emails/{email}/content automations().emails(a).updateContent(e, html) unit test + live call
GET /automations/{automation}/emails/{email}/content automations().emails(a).getContent(e) unit test + live call
GET /automations/{automation}/emails/{email}/report-stats automations().emails(a).startDate().endDate().reportStats(e) unit test + live call (200, matches a direct request); local stats stay 0, so the date range is not shown
GET /automations/{automation}/emails/{email}/unsubscribe-reasons automations().emails(a).unsubscribeReasons(e) unit test + live call

Automation templates

Endpoint SDK call How checked
GET /automation-templates automations().templates().filter(type).limit().page().get() unit test + live call
POST /automations/select-template/{automationTemplate} automations().selectTemplate(t) unit test + live call

Automation reports

Endpoint SDK call How checked
GET /automations/{automation}/steps/run-counts automations().reports(a).stepRunCounts() unit test + live call (200, matches a direct request); local stats stay 0, so the date range is not shown
GET /automations/{automation}/steps/run-counts/{step} automations().reports(a).stepRunCount(s) unit test + live call
GET /automations/{automation}/triggers/run-counts automations().reports(a).triggerRunCounts() unit test + live call
GET /automations/{automation}/triggers/run-counts/{trigger} automations().reports(a).triggerRunCount(t) unit test + live call
GET /automations/{automation}/ab-steps-stats automations().reports(a).abStepsStats() unit test + live call
GET /automations/{automation}/activity/steps/{step}/subscribers automations().reports(a).stepSubscribers(s).status().sort().filter().limit().page().get() unit test + live call
GET /automations/{automation}/activity/triggers/{trigger}/subscribers automations().reports(a).triggerSubscribers(t).status().get() unit test + live call
GET /automations/{automation}/monthly-totals/email automations().reports(a).startDate().endDate().monthlyEmailTotals() unit test + live call (200, matches a direct request); local stats stay 0, so the date range is not shown
GET /automations/{automation}/monthly-totals/ecommerce automations().reports(a).monthlyEcommerceTotals() unit test + live call (200, matches a direct request); the test account has no shop, so no data to show
GET /automations/{automation}/order-activity automations().reports(a).orderActivity().timeFrame().limit().page().get() unit test + live call (200, matches a direct request); the test account has no shop, so no data to show
GET /automations/{automation}/emails/{email}/ecommerce-overview-stats automations().emails(a).ecommerceOverviewStats(e) unit test + live call
GET /automations/{automation}/ab-steps-stats/{step} automations().reports(a).abStepStats(s) unit test + live call

Risks

Low to medium risk. Most of the change is new code. The parts that change existing behaviour:

  • Query values are URL-encoded (see Changes).
  • The list date fields are now set, where they used to be null.
  • Automation triggers are now read from the trigger object, so their id and fields change from the automation's values to the trigger's real values.

Performance impact

None worth noting. Gson instances are shared, not built per request.

Security impact

No security impact. No auth or transport change.

How to QA

No QA needed on the app. This is a library change, and nothing in the MailerLite app changes. Reviewers can run mvn test. pom.xml sets testFailureIgnore, so read the surefire reports: 112 tests, 0 failures, 0 errors.

Verified before review: 66 of 72 live calls passed. The other 6:

  • with_groups: the SDK sends it, but the current API version ignores it. include=groups works and passes.
  • Run counts, report stats and monthly email totals return 200 and match a direct request. Local stats stay 0, so the date range cannot be shown.
  • The e-commerce reports return 200 with no data, because the test account has no shop.

How to release

Standard release. No hand-written CHANGELOG in this repo. The open simple-editor PR #23 and e-commerce PR #22 both touch MailerLite.java. This PR does not, so they should not conflict there.

Rollback strategy

Primary rollback method: revert

Code changes

  • Safe to revert. SDK code only.

Database migrations

  • None.

Data modifications

  • None.

External dependencies

  • None.

Configuration changes:

  • None.

Screenshots, recordings

N/A, library change.

I used AI to generate parts of this PR

Yes

Docs

The PR adheres to Pull Request guidelines.

See mandatory MailerLite PR rules and flow for more information.

@kavanpancholi kavanpancholi self-assigned this Oct 30, 2026
@kavanpancholi
kavanpancholi marked this pull request as ready for review October 30, 2026 11:41
@mailerlite-kritika

mailerlite-kritika Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Re-runViewConfidence Score: 5/5

[High risk] The risk rests on the query-value URL encoding, which changes every query parameter across the SDK and breaks callers who pre-encoded values, and on the automation parser change, which alters trigger ids and fields that existing callers read. Both are disclosed in the description and the review's account, and a revert undoes them.

Findings

Earlier findings (3 dismissed)

Summary

Brings the Java SDK in line with the public API

The PR adds builders and endpoints for subscribers, groups, segments, fields and automations, plus automation triggers, steps, emails, templates and reports. It also fixes the segment limig typo, adds page() to the list builders, and makes the automation parser fall back to GenericAutomationTrigger and GenericAutomationStep instead of returning null. The latest commit makes AutomationStepDeserializer tolerate a missing or null type, action_type or condition type, and adds TestListStepsWithoutType for it. Query values are now URL-encoded, so a caller who encoded values by hand will send them encoded twice.

What's good

  • The request tests use a mock HTTP client and assert method, path, query and body for each endpoint.
  • Shared Gson instances and the JsonBody helper replace the per-request Gson builders.
  • RawDataAdapterFactory keeps the full JSON for unknown trigger and step types, so nothing is lost on the fallback.
Review context · 0/5 commands
  • Commands run: none

Reviews (4) · Last reviewed commit: "Fall back to generic steps and condition..." · claude-sonnet-5-5

@kavanpancholi

Copy link
Copy Markdown
Author

@mailerlite-kritika review again

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