Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sync): strip unsuffixed UTM meta keys from normalized contact data #3623

Open
wants to merge 3 commits into
base: release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions includes/cli/class-initializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ public static function register_comands() {
[ 'Newspack\CLI\RAS_ESP_Sync', 'cli_sync_contacts' ]
);

WP_CLI::add_command(
'newspack esp merge-fields list',
[ 'Newspack\CLI\RAS_ESP_Sync', 'cli_mailchimp_list_merge_fields' ]
);

WP_CLI::add_command(
'newspack esp merge-fields delete',
[ 'Newspack\CLI\RAS_ESP_Sync', 'cli_mailchimp_delete_merge_fields' ]
);

WP_CLI::add_command( 'newspack migrate-co-authors-guest-authors', [ 'Newspack\CLI\Co_Authors_Plus', 'migrate_guest_authors' ] );
WP_CLI::add_command( 'newspack backfill-non-editing-contributors', [ 'Newspack\CLI\Co_Authors_Plus', 'backfill_non_editing_contributor' ] );
WP_CLI::add_command(
Expand Down
166 changes: 166 additions & 0 deletions includes/cli/class-ras-esp-sync.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

use WP_CLI;
use Newspack\Reader_Activation;
use Newspack\Reader_Activation\Sync\Metadata;
use Newspack\Mailchimp_API;

use Newspack_Subscription_Migrations\CSV_Importers\CSV_Importer;
use Newspack_Subscription_Migrations\Stripe_Sync;
Expand Down Expand Up @@ -409,4 +411,168 @@ public static function cli_sync_contacts( $args, $assoc_args ) {
)
);
}

/**
* List all or matching merge fields in the connected Mailchimp audience.
*
* ## OPTIONS
*
* [--fields=<field1,field2,etc>]
* : Field slugs to match. These should match raw field slugs as defined in Newspack\Reader_Activation\Sync\Metadata. If specified, only merge fields matching these slugs will be shown.
*
* [--prefix=<prefix>]
* : If specified, only fields with a matching prefix will be shown.
*
* @param array $args Positional args.
* @param array $assoc_args Associative args.
*/
public static function cli_mailchimp_list_merge_fields( $args, $assoc_args ) {
$is_dry_run = ! empty( $assoc_args['dry-run'] );
$fields_to_show = ! empty( $assoc_args['fields'] ) ? explode( ',', $assoc_args['fields'] ) : false;
$prefix = $assoc_args['prefix'] ?? '';

$all_fields = Metadata::get_all_fields();
if ( $fields_to_show ) {
$fields_to_show = array_reduce(
$fields_to_show,
function( $acc, $field_slug ) use ( $prefix, $all_fields ) {
if ( ! empty( $all_fields[ $field_slug ] ) ) {
$acc[] = trim( $prefix . $all_fields[ $field_slug ] );
} elseif ( ! empty( Metadata::get_utm_key( $field_slug ) ) && ( ! $prefix || 0 === strpos( Metadata::get_utm_key( $field_slug ), $prefix ) ) ) {
$acc[] = trim( $prefix . str_replace( Metadata::PREFIX, '', Metadata::get_utm_key( $field_slug ) ) );
} else {
\WP_CLI::warning( sprintf( 'Field %s not recognized.', $field_slug ) );
}
return $acc;
},
[]
);
}

$audience_id = Reader_Activation::get_setting( 'mailchimp_audience_id' );
if ( empty( $audience_id ) ) {
\WP_CLI::error( __( 'Mailchimp audience ID not set.', 'newspack-plugin' ) );
}

$result = Mailchimp_API::get( "lists/$audience_id/merge-fields?count=1000" );
if ( is_wp_error( $result ) || empty( $result['merge_fields'] || ! is_array( $result['merge_fields'] ) ) ) {
\WP_CLI::error( __( 'Could not connect to Mailchimp API. Is the site connected to Mailchimp?', 'newspack-subscription-migrations' ) );
}
$fields = $result['merge_fields'];

$matching = 0;
$results = [];
foreach ( $fields as $field ) {
$name_parts = explode( '_', $field['name'] );
$field_name = $prefix ? $field['name'] : end( $name_parts );
if ( ( ! $fields_to_show || in_array( $field_name, $fields_to_show, true ) ) && ( ! $prefix || 0 === strpos( $field['name'], $prefix ) ) ) {
$results[] = [
'id' => $field['merge_id'],
'tag' => $field['tag'],
'name' => $field['name'],
'type' => $field['type'],
];
$matching++;
}
}

\WP_CLI\Utils\format_items(
'table',
$results,
[
'id',
'tag',
'name',
'type',
]
);
\WP_CLI::success(
sprintf(
'Found %d merge fields.',
$matching
)
);
}

/**
* Delete the specified merge fields in the connected Mailchimp audience. WARNING: Any data in the deleted fields will be lost.
*
* ## OPTIONS
*
* [--dry-run]
* : If passed, output results but do not modify any fields.
*
* [--fields=<field1,field2,etc>]
* : (required) Field slugs to delete, comma-separated. These should match raw field slugs as defined in Newspack\Reader_Activation\Sync\Metadata.
*
* [--prefix=<prefix>]
* : If specified, only fields with a matching prefix will be deleted.
*
* @param array $args Positional args.
* @param array $assoc_args Associative args.
*/
public static function cli_mailchimp_delete_merge_fields( $args, $assoc_args ) {
$is_dry_run = ! empty( $assoc_args['dry-run'] );
$fields_to_delete = ! empty( $assoc_args['fields'] ) ? explode( ',', $assoc_args['fields'] ) : false;
$prefix = $assoc_args['prefix'] ?? '';

if ( empty( $fields_to_delete ) ) {
\WP_CLI::error( __( 'Please specify at least one field to delete.', 'newspack-subscription-migrations' ) );
}

$all_fields = Metadata::get_all_fields();
$fields_to_delete = array_reduce(
$fields_to_delete,
function( $acc, $field_slug ) use ( $prefix, $all_fields ) {
if ( ! empty( $all_fields[ $field_slug ] ) ) {
$acc[] = trim( $prefix . $all_fields[ $field_slug ] );
} elseif ( ! empty( Metadata::get_utm_key( $field_slug ) ) && ( ! $prefix || 0 === strpos( Metadata::get_utm_key( $field_slug ), $prefix ) ) ) {
$acc[] = trim( $prefix . str_replace( Metadata::PREFIX, '', Metadata::get_utm_key( $field_slug ) ) );
} else {
\WP_CLI::warning( sprintf( 'Field %s not recognized.', $field_slug ) );
}
return $acc;
},
[]
);

$audience_id = Reader_Activation::get_setting( 'mailchimp_audience_id' );
if ( empty( $audience_id ) ) {
\WP_CLI::error( __( 'Mailchimp audience ID not set.', 'newspack-plugin' ) );
}

$result = Mailchimp_API::get( "lists/$audience_id/merge-fields?count=1000" );
if ( is_wp_error( $result ) || empty( $result['merge_fields'] || ! is_array( $result['merge_fields'] ) ) ) {
\WP_CLI::error( __( 'Could not connect to Mailchimp API. Is the site connected to Mailchimp?', 'newspack-subscription-migrations' ) );
}
$fields = $result['merge_fields'];

$deleted = 0;
foreach ( $fields as $field ) {
$name_parts = explode( '_', $field['name'] );
$field_name = $prefix ? $field['name'] : end( $name_parts );

if ( in_array( $field_name, $fields_to_delete, true ) ) {
if ( ! $is_dry_run ) {
Mailchimp_API::delete( "lists/$audience_id/merge-fields/" . $field['merge_id'] );
}
\WP_CLI::log(
sprintf(
'%s merge field %s.',
$is_dry_run ? 'Would delete' : 'Deleted',
$field['name']
)
);
$deleted++;
}
}

\WP_CLI::success(
sprintf(
'%s %d merge fields.',
$is_dry_run ? 'Would delete' : 'Deleted',
$deleted
)
);
}
}
24 changes: 18 additions & 6 deletions includes/reader-activation/sync/class-metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,23 +290,25 @@ private static function get_key_value( $key, $metadata ) {

/**
* Get the UTM key from a raw or prefixed key.
* The returned key must have a suffix (source, medium, campaign, content).
*
* @param string $key Key to check.
*
* @return string|false Formatted key if it is a UTM key, false otherwise.
*/
private static function get_utm_key( $key ) {
public static function get_utm_key( $key ) {
$keys = [ 'signup_page_utm', 'payment_page_utm' ];
$raw_keys = self::get_raw_keys();
foreach ( $keys as $utm_key ) {
if ( ! in_array( $utm_key, $raw_keys, true ) ) { // Skip if the UTM key is not in the list of fields to sync.
continue;
}
$prefixed_key = self::get_key( $utm_key );
if ( 0 === strpos( $key, $utm_key ) ) {
$suffix = str_replace( $utm_key . '_', '', $key );
return self::get_key( $utm_key ) . $suffix;
return ! empty( trim( $suffix ) ) && $suffix !== $key ? $prefixed_key . $suffix : false;
}
if ( 0 === strpos( $key, self::get_key( $utm_key ) ) ) {
if ( 0 === strpos( $key, $prefixed_key ) && $key !== $prefixed_key ) {
return $key;
}
}
Expand Down Expand Up @@ -349,7 +351,7 @@ private static function add_registration_data( $metadata ) {
*
* @return array Metadata with UTM fields added.
*/
private static function add_utm_data( $metadata ) {
public static function add_utm_data( $metadata ) {
// Capture UTM params and signup/payment page URLs as meta for registration or payment.
if ( self::has_key( 'current_page_url', $metadata ) || self::has_key( 'registration_page', $metadata ) || self::has_key( 'payment_page', $metadata ) ) {
$is_payment = self::has_key( 'payment_page', $metadata );
Expand Down Expand Up @@ -406,10 +408,20 @@ public static function normalize_contact_data( $contact ) {
// Keys allowed to pass through without prefixing.
$allowed_keys = [ 'status', 'status_if_new' ];

// UTM keys must be suffixed.
$disallowed_keys = [
'payment_page_utm',
'payment_page_utm_',
'signup_page_utm',
'signup_page_utm_',
self::get_key( 'payment_page_utm' ),
self::get_key( 'signup_page_utm' ),
];

foreach ( $metadata as $meta_key => $meta_value ) {
if ( in_array( $meta_key, $raw_keys, true ) ) { // Handle raw keys.
if ( in_array( $meta_key, $raw_keys, true ) && ! in_array( $meta_key, $disallowed_keys, true ) ) { // Handle raw keys.
$normalized_metadata[ self::get_key( $meta_key ) ] = $meta_value;
} elseif ( in_array( $meta_key, $prefixed_keys, true ) ) { // Handle prefixed keys.
} elseif ( in_array( $meta_key, $prefixed_keys, true ) && ! in_array( $meta_key, $disallowed_keys, true ) ) { // Handle prefixed keys.
$normalized_metadata[ $meta_key ] = $meta_value;
} elseif ( self::get_utm_key( $meta_key ) ) { // Handle UTM keys.
$normalized_metadata[ self::get_utm_key( $meta_key ) ] = $meta_value;
Expand Down
47 changes: 36 additions & 11 deletions tests/unit-tests/reader-activation-sync.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,36 +124,56 @@ public function test_sync_contact_data() {
],
];

// Raw metadata keys should be converted to prefixed keys.
$this->assertEquals(
$contact_data_with_prefixed_keys,
Sync\Metadata::normalize_contact_data( $contact_data_with_raw_keys )
Sync\Metadata::normalize_contact_data( $contact_data_with_raw_keys ),
'Raw metadata keys should be converted to prefixed keys.'
);

Sync\Metadata::update_prefix( 'CU_' );

// Metadata keys should be prefixed with the custom prefix, if set.
$this->assertEquals(
$contact_data_with_custom_prefix,
Sync\Metadata::normalize_contact_data( $contact_data_with_raw_keys )
Sync\Metadata::normalize_contact_data( $contact_data_with_raw_keys ),
'Metadata keys should be prefixed with the custom prefix, if set.'
);

// Clear from last test.
\delete_option( Sync\Metadata::PREFIX_OPTION );

// Most keys should be exact.
$contact_data_with_prefixed_keys['metadata']['NP_Invalid_Key'] = 'Invalid data';
$this->assertEquals(
array_diff( $contact_data_with_prefixed_keys['metadata'], Sync\Metadata::normalize_contact_data( $contact_data_with_prefixed_keys )['metadata'] ),
[ 'NP_Invalid_Key' => 'Invalid data' ]
[ 'NP_Invalid_Key' => 'Invalid data' ],
'Most keys should be exact.'
);

// But UTM keys can have arbitrary suffixes.
unset( $contact_data_with_prefixed_keys['metadata']['NP_Invalid_Key'] );
$contact_data_with_prefixed_keys['metadata']['NP_Signup UTM: foo'] = 'bar';
$this->assertEquals(
$contact_data_with_prefixed_keys,
Sync\Metadata::normalize_contact_data( $contact_data_with_prefixed_keys )
$this->assertArrayHasKey(
'NP_Signup UTM: foo',
Sync\Metadata::normalize_contact_data( $contact_data_with_prefixed_keys )['metadata'],
'But UTM keys can have arbitrary suffixes.'
);

// And UTM keys MUST have a suffix.
$contact_data_with_prefixed_keys['metadata']['NP_Signup UTM: '] = 'foo';
$contact_data_with_prefixed_keys['metadata']['signup_page_utm'] = 'bar';
$contact_data_with_prefixed_keys['metadata']['signup_page_utm_'] = 'baz';
$this->assertArrayNotHasKey(
'NP_Signup UTM: ',
Sync\Metadata::normalize_contact_data( $contact_data_with_prefixed_keys )['metadata'],
'Prefixed UTM keys must have a suffix.'
);
$this->assertArrayNotHasKey(
'signup_page_utm',
Sync\Metadata::normalize_contact_data( $contact_data_with_prefixed_keys )['metadata'],
'Raw UTM keys must have a suffix.'
);
$this->assertArrayNotHasKey(
'NP_Signup UTM: ',
Sync\Metadata::normalize_contact_data( $contact_data_with_prefixed_keys )['metadata'],
'Raw UTM keys must have a suffix.'
);
}

Expand All @@ -163,7 +183,12 @@ public function test_sync_contact_data() {
public function test_with_default_option() {
$contact = $this->get_sample_contact();
$normalized = Sync\Metadata::normalize_contact_data( $contact );
$this->assertSame( $contact, $normalized );

// Strip unsuffixed UTM keys.
unset( $contact['metadata'][ Sync\Metadata::get_key( 'signup_page_utm' ) ] );
unset( $contact['metadata'][ Sync\Metadata::get_key( 'payment_page_utm' ) ] );

$this->assertSame( $contact, $normalized, 'All default keys pass normalization except for unsuffixed UTM keys.' );
}

/**
Expand Down