diff --git a/includes/Core/User/Audience_Settings.php b/includes/Core/User/Audience_Settings.php index 56bf47bd390..e201943af1f 100644 --- a/includes/Core/User/Audience_Settings.php +++ b/includes/Core/User/Audience_Settings.php @@ -89,6 +89,17 @@ function ( $value ) { return $this->set( array_merge( $settings, $updated ) ); } + /** + * Reset the setting to their default values. + * + * @since n.e.x.t + * + * @return bool True on success, false on failure. + */ + public function reset() { + return $this->set( $this->get_default() ); + } + /** * Gets the callback for sanitizing the setting's value before saving. * diff --git a/includes/Modules/Analytics_4.php b/includes/Modules/Analytics_4.php index 309a02fc300..988db9f9daf 100644 --- a/includes/Modules/Analytics_4.php +++ b/includes/Modules/Analytics_4.php @@ -93,6 +93,7 @@ use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Cron; use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Events_Sync; use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Provider; +use Google\Site_Kit\Modules\Analytics_4\Reset_Audiences; use stdClass; use WP_Error; @@ -232,6 +233,8 @@ public function register() { ( new Advanced_Tracking( $this->context ) )->register(); + ( new Reset_Audiences( $this->user_options, $this ) )->register(); + add_action( 'admin_init', array( $synchronize_property, 'maybe_schedule_synchronize_property' ) ); add_action( 'admin_init', array( $synchronize_adsense_linked, 'maybe_schedule_synchronize_adsense_linked' ) ); add_action( 'admin_init', array( $synchronize_ads_linked, 'maybe_schedule_synchronize_ads_linked' ) ); @@ -298,7 +301,6 @@ function ( $audience ) { 'adsLinked' => false, 'adsLinkedLastSyncedAt' => 0, 'detectedEvents' => array(), - 'availableAudiencesLastSyncedAt' => 0, ) ); diff --git a/includes/Modules/Analytics_4/Reset_Audiences.php b/includes/Modules/Analytics_4/Reset_Audiences.php new file mode 100644 index 00000000000..cd76ec2175b --- /dev/null +++ b/includes/Modules/Analytics_4/Reset_Audiences.php @@ -0,0 +1,175 @@ +user_options = $user_options; + $this->dismissed_prompts = new Dismissed_Prompts( $this->user_options ); + $this->dismissed_items = new Dismissed_Items( $this->user_options ); + $this->audience_settings = new Audience_Settings( $this->user_options ); + $this->analytics = $analytics; + } + + /** + * Register on change actions. + * + * @since n.e.x.t + */ + public function register() { + $this->analytics->get_settings()->on_change( + function ( $old_value, $new_value ) { + // Reset Audience specific settings, only when the Analytics propertyID changes. + if ( $old_value['propertyID'] !== $new_value['propertyID'] ) { + $this->reset_audience_data(); + } + } + ); + } + + /** + * Reset Audience specific settings for all SK users when propertyID changes. + * + * @since n.e.x.t + */ + public function reset_audience_data() { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $users = $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT user_id + FROM $wpdb->usermeta + WHERE meta_key IN (%s, %s) + LIMIT 100 -- Arbitrary limit to avoid unbounded user iteration.", + $this->user_options->get_meta_key( Dismissed_Items::OPTION ), + $this->user_options->get_meta_key( Dismissed_Prompts::OPTION ), + ) + ); + + if ( $users ) { + $backup_user_id = $this->user_options->get_user_id(); + + foreach ( $users as $user_id ) { + $this->user_options->switch_user( $user_id ); + + // Remove Audience Segmentation specific dismissed prompts. + foreach ( self::AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS as $prompt ) { + $this->dismissed_prompts->remove( $prompt ); + } + + // Remove Audience Segmentation specific dismissed items. + foreach ( self::AUDIENCE_SEGMENTATION_DISMISSED_ITEMS as $item ) { + // Support wildcard matches, in order to delete all dismissed items prefixed with audience-tile-*. + if ( strpos( $item, '*' ) !== false ) { + $dismissed_items = $this->dismissed_items->get(); + + foreach ( array_keys( $dismissed_items ) as $existing_item ) { + if ( str_starts_with( $existing_item, rtrim( $item, '*' ) ) ) { + $this->dismissed_items->remove( $existing_item ); + } + } + } else { + // For non-wildcard items, remove them directly. + $this->dismissed_items->remove( $item ); + } + } + + // Reset the users Audience Settings, such as configured audiences. + $this->audience_settings->reset(); + } + + // Restore original user. + $this->user_options->switch_user( $backup_user_id ); + } + + // Reset the main Analytics Module, Audience Segmentation settings. + $this->analytics->get_settings()->merge( + array( + 'availableAudiences' => null, + 'availableAudiencesLastSyncedAt' => 0, + 'audienceSegmentationSetupComplete' => false, + ) + ); + } +} diff --git a/tests/phpunit/integration/Modules/Analytics_4/Reset_AudiencesTest.php b/tests/phpunit/integration/Modules/Analytics_4/Reset_AudiencesTest.php new file mode 100644 index 00000000000..69bfc5bedbb --- /dev/null +++ b/tests/phpunit/integration/Modules/Analytics_4/Reset_AudiencesTest.php @@ -0,0 +1,311 @@ +context = new Context( GOOGLESITEKIT_PLUGIN_MAIN_FILE ); + $this->options = new Options( $this->context ); + $this->users = array( $this->factory()->user->create_and_get( array( 'role' => 'administrator' ) ), $this->factory()->user->create_and_get( array( 'role' => 'editor' ) ) ); + $this->user_options = new User_Options( $this->context, $this->users[0]->ID ); + $this->dismissed_prompts = new Dismissed_Prompts( $this->user_options ); + $this->dismissed_items = new Dismissed_Items( $this->user_options ); + $this->audience_settings = new Audience_Settings( $this->user_options ); + $this->authentication = new Authentication( $this->context, $this->options, $this->user_options ); + $this->analytics = new Analytics_4( $this->context, $this->options, $this->user_options, $this->authentication ); + + $this->analytics->register(); + wp_set_current_user( $this->users[0]->ID ); + + $this->analytics->get_settings()->merge( + array( + 'propertyID' => 'UA-111111', + ), + ); + + // Give each user a Dismissed Prompt so the reset code will run for each user. + $backup_user_id = $this->user_options->get_user_id(); + foreach ( $this->users as $user ) { + $this->user_options->switch_user( $user->ID ); + $this->dismissed_prompts->add( Reset_Audiences::AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS[0] ); + } + $this->user_options->switch_user( $backup_user_id ); + } + + public function test_reset_audience_segmentation_dismissed_prompts_across_multiple_users() { + + $backup_user_id = $this->user_options->get_user_id(); + foreach ( $this->users as $user ) { + $this->user_options->switch_user( $user->ID ); + foreach ( Reset_Audiences::AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS as $dismissed_prompt ) { + $this->dismissed_prompts->add( $dismissed_prompt ); + } + + $user_dismissed_prompts = $this->dismissed_prompts->get(); + foreach ( Reset_Audiences::AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS as $dismissed_prompt ) { + $this->assertTrue( array_key_exists( $dismissed_prompt, $user_dismissed_prompts ) ); + } + } + $this->user_options->switch_user( $backup_user_id ); + + // Update the propertyID to trigger reset. + $this->analytics->get_settings()->merge( + array( + 'propertyID' => 'UA-222222', + ) + ); + + // Confirm the users dismissed items have been reset. + foreach ( $this->users as $user ) { + $this->user_options->switch_user( $user->ID ); + + $user_dismissed_prompts = $this->dismissed_prompts->get(); + foreach ( Reset_Audiences::AUDIENCE_SEGMENTATION_DISMISSED_PROMPTS as $dismissed_prompt ) { + $this->assertFalse( array_key_exists( $dismissed_prompt, $user_dismissed_prompts ) ); + } + } + $this->user_options->switch_user( $backup_user_id ); + } + + public function test_reset_audience_segmentation_dismissed_items_across_multiple_users() { + + $test_dismissed_items = array_merge( + Reset_Audiences::AUDIENCE_SEGMENTATION_DISMISSED_ITEMS, + array( + 'audience-tile-testAudienceTile1', + 'audience-tile-testAudienceTile2', + ) + ); + // Remove the wildcard key, as we have added test examples above. + unset( $test_dismissed_items[ array_search( 'audience-tile-*', $test_dismissed_items, true ) ] ); + + // Set possible dismissed items for each user. + $backup_user_id = $this->user_options->get_user_id(); + foreach ( $this->users as $user ) { + $this->user_options->switch_user( $user->ID ); + + foreach ( $test_dismissed_items as $dismissed_item ) { + $this->dismissed_items->add( $dismissed_item ); + } + + $user_dismissed_items = $this->dismissed_items->get(); + foreach ( $test_dismissed_items as $dismissed_item ) { + $this->assertTrue( array_key_exists( $dismissed_item, $user_dismissed_items ) ); + } + } + $this->user_options->switch_user( $backup_user_id ); + + // Update the propertyID to trigger reset. + $this->analytics->get_settings()->merge( + array( + 'propertyID' => 'UA-222222', + ) + ); + + // Confirm the users dismissed items have been reset. + foreach ( $this->users as $user ) { + $this->user_options->switch_user( $user->ID ); + + $user_dismissed_items = $this->dismissed_items->get(); + foreach ( $test_dismissed_items as $dismissed_item ) { + $this->assertFalse( array_key_exists( $dismissed_item, $user_dismissed_items ) ); + } + } + $this->user_options->switch_user( $backup_user_id ); + } + + public function test_reset_audience_segmentation_configured_audience_settings_across_multiple_users() { + + $default_user_audience_settings = array( + 'configuredAudiences' => null, + 'isAudienceSegmentationWidgetHidden' => false, + 'didSetAudiences' => false, + ); + + $activated_user_audience_settings = array( + 'configuredAudiences' => array( + 'properties/12345678/audiences/12345', + 'properties/12345678/audiences/67890', + ), + 'isAudienceSegmentationWidgetHidden' => true, + 'didSetAudiences' => true, + ); + + // Set configured audiences for each user. + $backup_user_id = $this->user_options->get_user_id(); + foreach ( $this->users as $user ) { + $this->user_options->switch_user( $user->ID ); + + $this->audience_settings->merge( + array( + 'configuredAudiences' => $activated_user_audience_settings['configuredAudiences'], + 'isAudienceSegmentationWidgetHidden' => $activated_user_audience_settings['isAudienceSegmentationWidgetHidden'], + ) + ); + + $audience_settings = $this->audience_settings->get(); + foreach ( array_keys( $default_user_audience_settings ) as $key ) { + $this->assertEquals( $activated_user_audience_settings[ $key ], $audience_settings[ $key ] ); + } + } + $this->user_options->switch_user( $backup_user_id ); + + // Update the propertyID to trigger reset. + $this->analytics->get_settings()->merge( + array( + 'propertyID' => 'UA-222222', + ) + ); + + // Confirm the users audience settings have been reset. + foreach ( $this->users as $user ) { + $this->user_options->switch_user( $user->ID ); + $audience_settings = $this->audience_settings->get(); + foreach ( array_keys( $default_user_audience_settings ) as $key ) { + // As 'configuredAudiences' is updated to null, this key is removed. + if ( 'configuredAudiences' === $key ) { + $this->assertFalse( array_key_exists( $key, $audience_settings ) ); + } else { + $this->assertEquals( $default_user_audience_settings[ $key ], $audience_settings[ $key ] ); + } + } + } + $this->user_options->switch_user( $backup_user_id ); + } + + public function test_reset_audience_segmentation_module_settings() { + $default_audience_segmentation_settings = array( + 'availableAudiences' => null, + 'availableAudiencesLastSyncedAt' => 0, + 'audienceSegmentationSetupComplete' => false, + ); + + $activated_audience_segmentation_settings = array( + 'availableAudiences' => array( + array( + 'name' => 'properties/12345678/audiences/12345', + ), + array( + 'name' => 'properties/12345678/audiences/67890', + ), + ), + 'availableAudiencesLastSyncedAt' => time(), + 'audienceSegmentationSetupComplete' => true, + ); + + $this->analytics->get_settings()->merge( + $activated_audience_segmentation_settings + ); + + $analytics_settings = $this->analytics->get_settings()->get(); + foreach ( array_keys( $default_audience_segmentation_settings ) as $key ) { + $this->assertEquals( $activated_audience_segmentation_settings[ $key ], $analytics_settings[ $key ] ); + } + + // Update the propertyID to trigger reset. + $this->analytics->get_settings()->merge( + array( + 'propertyID' => 'UA-222222', + ) + ); + + $analytics_settings = $this->analytics->get_settings()->get(); + foreach ( array_keys( $default_audience_segmentation_settings ) as $key ) { + $this->assertEquals( $default_audience_segmentation_settings[ $key ], $analytics_settings[ $key ] ); + } + } +}