fix(active-campaign): keep signups working after field tag edits#573
fix(active-campaign): keep signups working after field tag edits#573wil-gerken wants to merge 4 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLfKx43ZWQnJYM8fcyaPpN
There was a problem hiding this comment.
Pull request overview
Improves the ActiveCampaign provider’s add_contact() metadata-field registration so newsletter signups continue to succeed even when an ActiveCampaign admin edits a custom field’s personalization tag (perstag), which previously caused duplicate-title errors that aborted the entire signup.
Changes:
- Match existing ActiveCampaign fields by perstag first, then fall back to case-insensitive title matching; when matched by title, write using the field’s actual perstag.
- If a field cannot be created, log the error and continue syncing the contact instead of failing the signup.
- Add PHPUnit coverage for perstag match, title fallback (including malformed field rows), and non-blocking behavior on field-create failure.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| plugins/newspack-newsletters/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign.php | Adds resilient field matching (perstag → title) and makes field-create failures non-fatal with logging. |
| plugins/newspack-newsletters/tests/test-active-campaign-field-matching.php | Introduces tests covering renamed perstag fallback, malformed field rows, and ensuring signup proceeds when field creation fails. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLfKx43ZWQnJYM8fcyaPpN
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLfKx43ZWQnJYM8fcyaPpN
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLfKx43ZWQnJYM8fcyaPpN
dkoo
left a comment
There was a problem hiding this comment.
Tested end-to-end with a live ActiveCampaign account. Approve with comments. The core logic is a clear improvement over the previous array_search/array_column approach and the tests genuinely cover the claimed behaviors, including malformed rows. No blockers. Two low-severity suggestions before merge: harden the field-match guards against an empty-string perstag (defensive; no data-leak risk), and make the field-create failure log unconditional so a silently-dropped RAS metadata field is diagnosable on a default production site.
Making a failed field-create non-fatal is the correct direction — a single unregisterable field must never block a reader signup. The common real failure (a duplicate-title 422) is now caught by the new title match before any create is attempted, so this silent-drop path is only reached for genuinely-new fields that fail to create for some other reason: a narrow, self-healing window rather than a standing state.
| if ( false === $existing_index ) { | ||
| foreach ( $existing_fields as $index => $existing_field ) { | ||
| if ( | ||
| isset( $existing_field['title'], $existing_field['perstag'] ) && |
There was a problem hiding this comment.
Suggestion: Field-match guards check isset() not !empty() — an empty perstag can match — The perstag and title-match guards check isset( $existing_field['perstag'] ) rather than ! empty( ... ), so a field row whose perstag is an empty string can still match. When it does, $field_perstag is set to '' (line 2034) and the payload emits a malformed field[%%,0] key — and two such fields would collide on it. There's no cross-field data-leak risk: a value is never routed into a different real named field, and in practice ActiveCampaign auto-assigns perstags, so an empty one is effectively unreachable. Still, tightening both guards to ! empty( $existing_field['perstag'] ) (and skipping the field, or falling through to create, when the generated perstag is empty) is cheap defense and would round out the existing malformed-row test coverage — the current test_title_match_survives_field_rows_without_title covers a missing perstag key but not an empty-string one.
| if ( \is_wp_error( $field_res ) ) { | ||
| return $field_res; | ||
| /** A field that can't be registered must never block the signup — sync the contact without it. */ | ||
| Newspack_Newsletters_Logger::log( 'Error creating ActiveCampaign field "' . $field_title . '": ' . $field_res->get_error_message() ); |
There was a problem hiding this comment.
Suggestion: Field-create failure log is level-gated, so a dropped RAS field is silent on a default site — Making a failed field-create non-fatal is the right call — one unregisterable field should never abort a reader's whole signup. Worth weighing, though: the metadata synced here is Reader Activation contact data (registration, engagement, subscription, donation, content-gate status) that ActiveCampaign segments and automations key on, so a silently dropped field isn't purely cosmetic. The common real failure — the duplicate-title 422 — is now caught by the new title match before any create is attempted, and the remaining path is self-healing (retried on the next signup), which keeps this low-risk. The catch: Newspack_Newsletters_Logger::log() no-ops entirely unless NEWSPACK_LOG_LEVEL is defined (class-newspack-newsletters-logger.php:20), which it isn't on a default production site — so a field that keeps failing to create leaves no trace at all. Consider making just this one failure surface unconditionally (e.g. a direct error_log) so an under-populated segment is diagnosable.
Risk if not addressed: If a field-create keeps failing for a non-title reason on a site that has not defined NEWSPACK_LOG_LEVEL, a load-bearing Reader Activation field (donation, subscription, or membership status) is silently dropped on every signup with no log trace, so readers who should enter an AC segment or automation keyed on that field are never added to it — invisible until a publisher notices the segment is under-populated.
All Submissions:
Changes proposed in this Pull Request:
Reference: NPPM-283
When a custom field's personalization tag (perstag) is renamed in ActiveCampaign, newsletter signups that sync reader data start failing with "There is already a field with this title". Because Newspack attaches signup metadata to every signup, on an affected site that means all signups fail.
What this does
Signup field-matching is now resilient to field edits made on the ActiveCampaign side:
How to test the changes in this Pull Request:
Prerequisite: a site with ActiveCampaign connected. Run at least one signup first so the Newspack-generated contact fields exist.
Reproduce the problem (on
releasewithout this fix):perstag) of a Newspack-generated field (for exampleNEWSLETTER_SUBSCRIPTION_METHOD→NEWSLETTERSSUBSCRIPTIONMETHOD).Verify the fix:
Other information:
Notes for reviewers
add_contact()in the AC provider; the method's signature and everything downstream are untouched.array_searchoverarray_column— AC field rows can be missing keys, andarray_columnreindexes around them, which could map a match to the wrong field's perstag. A title match also requires the row to carry a usable perstag; otherwise it falls through to field creation.Newspack_Newsletters_Loggerand continues; previously the rawWP_Erroraborted the signup before any contact sync ran.pre_http_requestmock pattern: renamed perstag matches by title and uses the actual perstag; a failed create doesn't block the signup; the perstag fast path is unchanged; malformed rows (missing title or perstag) and re-cased, whitespace-padded titles are handled. Full newsletters suite green (486 tests).Release risk
Medium. The change sits on the reader-signup path, but it only executes for publishers using ActiveCampaign as their ESP, and the common case — tag matches, no admin edits — is verified unchanged by tests. One deliberate trade-off: a field that can't be registered is now skipped with a log entry instead of failing the whole signup loudly; keeping signups working outweighs one metadata field. Rollback is a plain revert, which restores the previous hard-fail behavior.