feat(audience): date range segmentation for ESP date fields - #767
Draft
miguelpeixe wants to merge 32 commits into
Draft
feat(audience): date range segmentation for ESP date fields#767miguelpeixe wants to merge 32 commits into
miguelpeixe wants to merge 32 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds first-class date range segmentation for ESP-provided date/datetime fields across Newspack Audience, including provider-declared date format metadata to safely normalize stored reader values for reliable matching.
Changes:
- Introduces a new
date_rangeoperator for incoming ESP date fields, plus UI to edit{ start, end }bounds (absolute date or relative days). - Normalizes stored reader date values to ISO-8601 at pull time only when the publisher selected
date_range, using provider-declareddate_format. - Updates popups criteria matching to support
date_rangeand hardens matching-function resolution to fail closed when unknown.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| plugins/newspack-popups/tests/test-schemas.php | Adds schema test cases covering date-range criterion values. |
| plugins/newspack-popups/src/criteria/utils.js | Fails closed when a matching function name can’t be resolved (prevents TypeError sitewide). |
| plugins/newspack-popups/src/criteria/matching-functions.test.js | Adds unit coverage for date_range matching behavior and edge cases. |
| plugins/newspack-popups/src/criteria/matching-functions.js | Implements date_range matcher and date parsing/validation helpers. |
| plugins/newspack-popups/src/criteria/index.test.js | Adds regression test ensuring unresolved matchers never match (and don’t throw). |
| plugins/newspack-popups/includes/class-newspack-segments-model.php | Extends REST meta schema to allow { start, end } date-range criterion values. |
| plugins/newspack-plugin/tests/unit-tests/integrations/class-test-promoted-fields.php | Pins access-rule behavior: date fields still exact-match via access rules. |
| plugins/newspack-plugin/tests/unit-tests/integrations/class-test-integrations.php | Adds coverage for storing date_range operator + date normalization behavior. |
| plugins/newspack-plugin/tests/unit-tests/integrations/class-test-esp.php | Verifies date_format is applied from provider schema and defaults to empty. |
| plugins/newspack-plugin/src/wizards/audience/views/integrations/configure-view.test.js | Updates operator constraints for date fields and adds regression for preserving Text. |
| plugins/newspack-plugin/src/wizards/audience/views/integrations/configure-view.js | Offers Date range + Text operators for date/datetime value types. |
| plugins/newspack-plugin/src/wizards/audience/views/campaigns/segments/single-segment.test.js | Adds rendering + editing tests for the date range segment UI, including regressions. |
| plugins/newspack-plugin/src/wizards/audience/views/campaigns/segments/single-segment.js | Renders DateRangeSetting when a criterion uses date_range. |
| plugins/newspack-plugin/includes/reader-activation/integrations/README.md | Documents new date_range operator and date_format field metadata. |
| plugins/newspack-plugin/includes/reader-activation/integrations/class-integration.php | Allows date_range as a persisted matching function. |
| plugins/newspack-plugin/includes/reader-activation/integrations/class-incoming-field.php | Adds date_format property + accessors to Incoming_Field. |
| plugins/newspack-plugin/includes/reader-activation/integrations/class-esp.php | Applies provider-declared date_format onto Incoming_Field during configuration. |
| plugins/newspack-plugin/includes/reader-activation/integrations/class-contact-pull.php | Normalizes date/datetime values to ISO on pull when date_range is selected. |
| plugins/newspack-newsletters/tests/test-mailchimp-integrations-schema.php | Updates mapping expectations: Mailchimp date → date_range, birthday → string; asserts date_format. |
| plugins/newspack-newsletters/tests/test-active-campaign-integrations-schema.php | Ensures AC date/datetime fields default to date_range. |
| plugins/newspack-newsletters/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php | Maps Mailchimp date_format option to PHP format and emits date_range for date fields. |
| plugins/newspack-newsletters/includes/service-providers/class-newspack-newsletters-service-provider.php | Updates provider schema docs for date_range and date_format. |
| plugins/newspack-newsletters/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign.php | Defaults date/datetime fields to date_range operator. |
| packages/components/src/settings/style.scss | Adds layout styles for the date range bound rows. |
| packages/components/src/settings/index.js | Exports the new DateRangeSetting component. |
| packages/components/src/settings/DateRangeSetting.js | Implements the shared date range control (From/To with Any/Date/Days ago/from now). |
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
plugins/newspack-popups/tests/test-criteria.php:161
- The regex assertion that checks SUPPORTED_MATCHING_FUNCTIONS exist in matching-functions.js is coupled to the file’s exact indentation (
^\t). This makes the test fragile to formatting changes (e.g. if Prettier/ESLint changes indentation to spaces), even though the export would still be correct. Matching optional leading whitespace keeps the test focused on the presence of the key rather than formatting.
public function test_supported_matching_functions_exist_in_js_module() {
$source = file_get_contents( __DIR__ . '/../src/criteria/matching-functions.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
foreach ( Newspack_Popups_Criteria::SUPPORTED_MATCHING_FUNCTIONS as $matching_function ) {
$this->assertMatchesRegularExpression(
'/^\t' . preg_quote( $matching_function, '/' ) . ':\s/m',
$source,
"$matching_function is advertised as supported but not exported by matching-functions.js"
);
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All Submissions:
Changes proposed in this Pull Request:
Phase 2 of NPPD-2047, following #628. That PR gave every incoming ESP field a publisher-chosen segmentation operator, but left
date/datetimefields with Text only — matching a date meant typing it exactly, which is useless for the case that motivated the ticket: "Latest Gift Date: January 1 2026 – Now".A date-typed ESP field can now be segmented as a Date range.
In Audience → Connections → integration, a date field's "Segment as" dropdown offers Date range (the new default when you enable one) and Text. Fields already enabled as Text keep matching exactly as they do today until a publisher deliberately changes them.
In Audience → Campaigns → Segments, that criterion renders as a single dropdown:
Each custom row is independently Any (unbounded on that end), a fixed Date, or Days ago / Days from now. A preset is a shortcut for a range the custom rows could build by hand — "Last 30 days" is exactly
From: Days ago 30,To: Days ago 0— so there is one stored format either way, and a range saved as a preset comes back as that preset rather than as Custom.Relative bounds resolve at match time, so a rolling window stays current without anyone editing the segment each month — an absolute "since Jan 1" silently goes stale. Forward offsets cover expiry and renewal windows.
Matching is day-granular and inclusive at both ends. Reader values are normalized to ISO 8601 at pull time, using a source format each provider declares —
03/04/2026is genuinely ambiguous between Mailchimp's twodate_formatsettings, and the ESP's own metadata is only in hand at that point. The client matcher then validates aYYYY-MM-DDslice and compares lexicographically, reading the date part as written so a value the publisher sees as Jan 15 in ActiveCampaign matches on Jan 15 for every reader regardless of browser timezone.A value that cannot be parsed with confidence is stored untouched, never guessed at. PHP's general parser only ever sees an already ISO-shaped value, and its result is only trusted when the date part survives a round trip — so
2026-02-30stays verbatim rather than silently becoming2026-03-02, and a Mailchimp value that doesn't fit its declared format is not re-read American-first. Both matchers reject anything that isn't a real calendar date, so an unparsable value matches nothing instead of matching wrongly. When that happens the pull logs the field and the source format it used — otherwise the only symptom is a segment that quietly matches nobody.Both ESP providers declare their date metadata. ActiveCampaign's
date/datetimefields and Mailchimp'sdatemerge fields default to the date-range operator, and Mailchimp translates its per-fielddate_format. ActiveCampaign declaresdate_formatexplicitly as''even though it always sends ISO: the key's presence is how a field snapshotted before source formats existed is told apart from one that genuinely has none, and the latter is refreshed from the live provider schema on read. Without that, a Mailchimp field enabled before this branch would have had no format to normalize with and matched nobody. Mailchimpbirthdayis corrected fromdatetostring— it isMM/DDwith no year, so it cannot sit on an absolute timeline and would have offered a range it could not honor.Content-gate access rules have no range UI and still match a single date exactly, but they now compare calendar dates, with both sides put through the same normalizer. Selecting Date range for segmentation rewrites the stored value to ISO, so a gate rule written in the provider's own format (
03/04/2026) would otherwise have silently stopped granting access to every reader it previously admitted — and the natural fix for that (switching the operator back, or disabling the field) unregisters the rule, which the content gate treats as "don't block", swinging the same gate wide open.date_rangeis only emitted to a newspack-popups that can run it. newspack-popups now publishes the matching functions it supports, and newspack-plugin asks before registering the criterion, falling back to exact matching when the answer is no or the method is absent. An older build left an unresolvable name as a raw string and then called it, throwing aTypeErrorthat escaped segment matching and aborted prompt display sitewide — and the fail-closed guard for that ships in newspack-popups, which is the plugin that would be stale, so it cannot be relied on for this skew.No newspack-manager companion PR this time. An unset source format means ISO /
Y-m-d, which is exactly what ActiveCampaign sends, so the managed AC integration needs no change.Also fixed here
Latent problems this feature would otherwise have shipped into:
newspack-popupsleft an unresolvable matching function as a raw string and then called it; it now fails closed.value: {}, which the matcher reads as "no bounds to check" — matching every reader who had any date in that field.Toend, widening the window to everyone.start: [object Object].Out of scope and known limits
date/datetimefields. A date living in a plain text field stays free text.birthdaywould need).MM/DD/YYYYtoDD/MM/YYYYgets confidently wrong dates until they re-save the Integrations screen. Closing this needs a periodic schema refresh, which is a broader change than belongs here.register_meta, not editor save. The wizard's segmentation routes register noargsschema and write criteria throughupdate_term_meta, so a malformed criterion still saves silently. This gap is pre-existing and identical for the shippedmin/maxbranch.Closes NPPD-2047.
How to test the changes in this Pull Request:
Automated — all green locally:
newspack-plugin:n test-php(2357 tests, 6691 assertions) andn test-js(486)newspack-popups:n test-php(261 tests, 637 assertions) andn test-js(84)newspack-newsletters:n test-php(699 tests, 2941 assertions)packages/components: 95 testsManual — with an ActiveCampaign or Mailchimp connection:
datefield. Confirm the operator dropdown offers Date range and Text, and that enabling it selects Date range.Days ago 30/Days ago 0— the preset's range, not a reset.From = Dateand pick a fixed date. Confirm the native date input is usable at its rendered width and lines up with its selector. Save and reload.Days from nowbound, clear the value and retype it. Confirm the selector still reads Days from now and the saved value is a positive offset.range) criterion, a Multiple values (list__in) criterion, and a plain Text criterion all still render, save and match as before.Other information:
Manual verification was done in an isolated environment against a stand-in CRM integration registered through the public
IntegrationAPI, rather than a live Mailchimp or ActiveCampaign account. That exercises the framework end to end — provider schema, operator selection, pull-time normalization, segment editor, segment matching and content-gate rules — including the ambiguous03/04/2026case under both Mailchimpdate_formatsettings, an impossible calendar date, a value that doesn't fit its declared format, and a field stored without a source format. It does not exercise the real provider mappers against live API payloads; those remain covered by unit tests only.