From 7a0539d2169a1850edb300684c10da6256dbbdad Mon Sep 17 00:00:00 2001 From: Rinat K Date: Wed, 25 Sep 2024 13:13:45 -0500 Subject: [PATCH 01/11] Simplify Parse.ly loader (#5863) Remove the ability to load by option and fix the reporting discrepancy where versions would be reported as `UNKNOWN` Improve the code readability by switching to `switch` rather than multiple `if/else/elseif` statements. --------- Co-authored-by: Rebecca Hum --- .github/workflows/parsely.yml | 8 - tests/parsely/test-mu-parsely-integration.php | 57 ------ wp-parsely.php | 164 ++++++------------ 3 files changed, 53 insertions(+), 176 deletions(-) diff --git a/.github/workflows/parsely.yml b/.github/workflows/parsely.yml index 776c7b473e..69219d24cb 100644 --- a/.github/workflows/parsely.yml +++ b/.github/workflows/parsely.yml @@ -25,18 +25,10 @@ jobs: # Oldest version of the parsely plugin - { wp: latest, parsely: '3.5', mode: 'filter_enabled', php: '8.1' } - { wp: latest, parsely: '3.5', mode: 'filter_disabled', php: '8.1' } - - { wp: latest, parsely: '3.5', mode: 'option_enabled', php: '8.1' } - - { wp: latest, parsely: '3.5', mode: 'option_disabled', php: '8.1' } - - { wp: latest, parsely: '3.5', mode: 'filter_and_option_enabled', php: '8.1' } - - { wp: latest, parsely: '3.5', mode: 'filter_and_option_disabled', php: '8.1' } # Latest version of the parsely plugin - { wp: latest, mode: 'filter_enabled', php: '8.1' } - { wp: latest, mode: 'filter_disabled', php: '8.1' } - - { wp: latest, mode: 'option_enabled', php: '8.1' } - - { wp: latest, mode: 'option_disabled', php: '8.1' } - - { wp: latest, mode: 'filter_and_option_enabled', php: '8.1' } - - { wp: latest, mode: 'filter_and_option_disabled', php: '8.1' } services: mysql: image: mysql:8 diff --git a/tests/parsely/test-mu-parsely-integration.php b/tests/parsely/test-mu-parsely-integration.php index 4576b9bc1f..a155065e9b 100644 --- a/tests/parsely/test-mu-parsely-integration.php +++ b/tests/parsely/test-mu-parsely-integration.php @@ -139,24 +139,6 @@ public function test_bootstrap_modes_enabled_without_constant() { ); $this->assertEquals( Parsely_Integration_Type::DISABLED_MUPLUGINS_FILTER, Parsely_Loader_Info::get_integration_type() ); break; - case 'option_enabled': - $this->assertFalse( has_filter( 'wpvip_parsely_load_mu' ) ); - $this->assertSame( '1', get_option( '_wpvip_parsely_mu' ) ); - $this->assertTrue( - Parsely_Loader_Info::is_active(), - 'Expecting wp-parsely plugin to be enabled by the option.' - ); - $this->assertEquals( Parsely_Integration_Type::ENABLED_MUPLUGINS_SILENT_OPTION, Parsely_Loader_Info::get_integration_type() ); - break; - case 'option_disabled': - $this->assertFalse( has_filter( 'wpvip_parsely_load_mu' ) ); - $this->assertSame( '0', get_option( '_wpvip_parsely_mu' ) ); - $this->assertFalse( - Parsely_Loader_Info::is_active(), - 'Expecting wp-parsely plugin to be disabled by the option.' - ); - $this->assertEquals( Parsely_Integration_Type::DISABLED_MUPLUGINS_SILENT_OPTION, Parsely_Loader_Info::get_integration_type() ); - break; case 'filter_and_option_enabled': $this->assertTrue( has_filter( 'wpvip_parsely_load_mu' ) ); $this->assertSame( '1', get_option( '_wpvip_parsely_mu' ) ); @@ -238,22 +220,6 @@ public function test_bootstrap_modes_disabled_via_constant() { $this->assertTrue( has_filter( 'wpvip_parsely_load_mu' ) ); $this->assertFalse( get_option( '_wpvip_parsely_mu' ) ); break; - case 'option_enabled': - $this->assertFalse( has_filter( 'wpvip_parsely_load_mu' ) ); - $this->assertSame( '1', get_option( '_wpvip_parsely_mu' ) ); - break; - case 'option_disabled': - $this->assertFalse( has_filter( 'wpvip_parsely_load_mu' ) ); - $this->assertSame( '0', get_option( '_wpvip_parsely_mu' ) ); - break; - case 'filter_and_option_enabled': - $this->assertTrue( has_filter( 'wpvip_parsely_load_mu' ) ); - $this->assertSame( '1', get_option( '_wpvip_parsely_mu' ) ); - break; - case 'filter_and_option_disabled': - $this->assertTrue( has_filter( 'wpvip_parsely_load_mu' ) ); - $this->assertSame( '0', get_option( '_wpvip_parsely_mu' ) ); - break; default: $this->fail( 'Invalid test mode specified: ' . self::$test_mode ); } @@ -262,29 +228,6 @@ public function test_bootstrap_modes_disabled_via_constant() { $this->assertEquals( Parsely_Integration_Type::DISABLED_CONSTANT, Parsely_Loader_Info::get_integration_type() ); } - public function test_parsely_ui_hooks() { - maybe_load_plugin(); - - $this->assertFalse( has_action( 'option_parsely', __NAMESPACE__ . '\alter_option_use_repeated_metas' ) ); - - if ( is_parsely_disabled() ) { - return; - } - - \Parsely\parsely_initialize_plugin(); - maybe_disable_some_features(); - - $repeated_metas_expected = 'option_enabled' === self::$test_mode ? 10 : false; - $this->assertSame( $repeated_metas_expected, has_action( 'option_parsely', __NAMESPACE__ . '\alter_option_use_repeated_metas' ) ); - - $row_actions = new Row_Actions( $GLOBALS['parsely'] ); - $row_actions->run(); - - $row_actions_expected = in_array( self::$test_mode, [ 'filter_enabled', 'filter_and_option_enabled' ] ) ? 10 : false; - $this->assertSame( $row_actions_expected, has_filter( 'page_row_actions', array( $row_actions, 'row_actions_add_parsely_link' ) ) ); - $this->assertSame( $row_actions_expected, has_filter( 'post_row_actions', array( $row_actions, 'row_actions_add_parsely_link' ) ) ); - } - public function test_default_parsely_configs() { maybe_load_plugin(); diff --git a/wp-parsely.php b/wp-parsely.php index 75f1d69ef3..2a2beb28a8 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -17,6 +17,8 @@ namespace Automattic\VIP\WP_Parsely_Integration; +use Parsely\Parsely; + /** * The default version is the first entry in the SUPPORTED_VERSIONS list. */ @@ -174,14 +176,14 @@ public static function get_parsely_options(): array { } /** - * Parse.ly options. + * Parse.ly options, plugin may be not loaded at this moment in the runtime, but we want to check the options anyway. * * @var array */ - $parsely_options = array(); - if ( isset( $GLOBALS['parsely'] ) && is_a( $GLOBALS['parsely'], 'Parsely\Parsely' ) ) { $parsely_options = $GLOBALS['parsely']->get_options(); + } else { + $parsely_options = get_option( 'parsely', [] ); } return $parsely_options; @@ -240,7 +242,6 @@ function is_queued_for_activation() { * To enable it on your site, add this line: * add_filter( 'wpvip_parsely_load_mu', '__return_true' ); * - * We enable it for some sites via the `_wpvip_parsely_mu` blog option. * To prevent it from loading even when this condition is met, add this line: * add_filter( 'wpvip_parsely_load_mu', '__return_false' ); */ @@ -250,61 +251,46 @@ function maybe_load_plugin() { return; } - // Self-managed integration: The plugin exists on the site and is being loaded already. - $plugin_class_exists = class_exists( 'Parsely' ) || class_exists( 'Parsely\Parsely' ); - if ( $plugin_class_exists ) { - Parsely_Loader_Info::set_active( true ); - Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::SELF_MANAGED ); - - $parsely_options = Parsely_Loader_Info::get_parsely_options(); - if ( array_key_exists( 'plugin_version', $parsely_options ) ) { - Parsely_Loader_Info::set_version( $parsely_options['plugin_version'] ); - } - - return; - } - - $parsely_enabled_constant = null; // Represents that the site doesn't have parsely enabled / blocked. - - if ( defined( 'VIP_PARSELY_ENABLED' ) ) { - $parsely_enabled_constant = constant( 'VIP_PARSELY_ENABLED' ); - - // Opt out if constant value isn't true. - if ( true !== $parsely_enabled_constant ) { - Parsely_Loader_Info::set_active( false ); - Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::DISABLED_CONSTANT ); - - return; - } - - Parsely_Loader_Info::set_active( true ); - Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::ENABLED_CONSTANT ); - } - - $option_load_status = get_option( '_wpvip_parsely_mu', null ); $filtered_load_status = apply_filters( 'wpvip_parsely_load_mu', null ); - // If plugin isn't enabled via constant then check for filter and option status. - if ( true !== $parsely_enabled_constant ) { - $should_load = true === $filtered_load_status || '1' === $option_load_status; - $should_prevent_loading = false === $filtered_load_status || '0' === $option_load_status; - - // No integration: The site has not enabled parsely. - if ( ! $should_load || $should_prevent_loading ) { - Parsely_Loader_Info::set_active( false ); - - if ( false === $filtered_load_status ) { - Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::DISABLED_MUPLUGINS_FILTER ); - } elseif ( '0' === $option_load_status ) { - Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::DISABLED_MUPLUGINS_SILENT_OPTION ); + switch ( true ) { + // Self-managed + case class_exists( 'Parsely' ) || class_exists( 'Parsely\Parsely' ): + Parsely_Loader_Info::set_active( true ); + Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::SELF_MANAGED ); + $parsely_options = Parsely_Loader_Info::get_parsely_options(); + if ( array_key_exists( 'plugin_version', $parsely_options ) ) { + Parsely_Loader_Info::set_version( $parsely_options['plugin_version'] ); } + break; + // Integrations-managed + case defined( 'VIP_PARSELY_ENABLED' ): + Parsely_Loader_Info::set_active( true === constant( 'VIP_PARSELY_ENABLED' ) ); + Parsely_Loader_Info::set_integration_type( + Parsely_Loader_Info::is_active() + ? Parsely_Integration_Type::ENABLED_CONSTANT + : Parsely_Integration_Type::DISABLED_CONSTANT + ); + break; + // Filter-managed - enabled + case $filtered_load_status: + Parsely_Loader_Info::set_active( true ); + Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::ENABLED_MUPLUGINS_FILTER ); + break; + // Filter-managed - disabled + case false === $filtered_load_status: + Parsely_Loader_Info::set_active( false ); + Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::DISABLED_MUPLUGINS_FILTER ); + break; + // Not configured in any way + default: + Parsely_Loader_Info::set_active( false ); + Parsely_Loader_Info::set_integration_type( Parsely_Integration_Type::NONE ); + break; + } - return; - } - - // Enqueuing the disabling of Parse.ly features when the plugin is loaded (after the `plugins_loaded` hook) - // We need priority 0, so it's executed before `widgets_init`. - add_action( 'init', __NAMESPACE__ . '\maybe_disable_some_features', 0 ); + if ( ! Parsely_Loader_Info::is_active() || Parsely_Integration_Type::SELF_MANAGED === Parsely_Loader_Info::get_integration_type() ) { + return; } $versions_to_try = SUPPORTED_VERSIONS; @@ -344,23 +330,16 @@ function maybe_load_plugin() { // Require the actual wp-parsely plugin. if ( ! is_readable( $entry_file ) ) { + Parsely_Loader_Info::set_active( false ); return; } - require_once $entry_file; - // If plugin isn't enabled via constant then set filter or option integration_type. - if ( true !== $parsely_enabled_constant ) { - $integration_type = Parsely_Integration_Type::ENABLED_MUPLUGINS_FILTER; - if ( '1' === $option_load_status && true !== $filtered_load_status ) { - $integration_type = Parsely_Integration_Type::ENABLED_MUPLUGINS_SILENT_OPTION; - } + require_once $entry_file; - Parsely_Loader_Info::set_integration_type( $integration_type ); + if ( defined( '\Parsely\PARSELY_VERSION' ) ) { + Parsely_Loader_Info::set_version( constant( '\Parsely\PARSELY_VERSION' ) ); } - Parsely_Loader_Info::set_active( true ); - Parsely_Loader_Info::set_version( $version ); - // Require VIP's customizations over wp-parsely. $vip_parsely_plugin = __DIR__ . '/vip-parsely/vip-parsely.php'; if ( is_readable( $vip_parsely_plugin ) ) { @@ -369,54 +348,17 @@ function maybe_load_plugin() { } add_action( 'plugins_loaded', __NAMESPACE__ . '\maybe_load_plugin', 1 ); -/** - * Hides the UI if the plugin is loaded via silent option. - */ -function maybe_disable_some_features() { - if ( ! isset( $GLOBALS['parsely'] ) || ! is_a( $GLOBALS['parsely'], 'Parsely\Parsely' ) ) { - return; - } - - $filtered_load_status = apply_filters( 'wpvip_parsely_load_mu', null ); - $should_disable_features = apply_filters( 'wpvip_parsely_hide_ui_for_mu', true !== $filtered_load_status ); - - // If the plugin was not loaded via the filter, hide the UI by default. - if ( $should_disable_features ) { - remove_action( 'init', 'Parsely\parsely_wp_admin_early_register' ); - remove_action( 'init', 'Parsely\init_recommendations_block' ); - remove_action( 'enqueue_block_editor_assets', 'Parsely\init_content_helper' ); - remove_action( 'admin_init', 'Parsely\parsely_admin_init_register' ); - remove_action( 'widgets_init', 'Parsely\parsely_recommended_widget_register' ); - - // Don't show the row action links. - add_filter( 'wp_parsely_enable_row_action_links', '__return_false' ); - add_filter( 'wp_parsely_enable_rest_api_support', '__return_false' ); - add_filter( 'wp_parsely_enable_related_api_proxy', '__return_false' ); - - // Default to "repeated metas". - add_filter( 'option_parsely', __NAMESPACE__ . '\alter_option_use_repeated_metas' ); - - // Remove the Parse.ly Recommended Widget. - unregister_widget( 'Parsely_Recommended_Widget' ); - } -} - /** * Enum which represent all options to integrate `wp-parsely`. */ abstract class Parsely_Integration_Type { // phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace - // When parsely is active. - const ENABLED_MUPLUGINS_FILTER = 'ENABLED_MUPLUGINS_FILTER'; - const ENABLED_MUPLUGINS_SILENT_OPTION = 'ENABLED_MUPLUGINS_SILENT_OPTION'; - const ENABLED_CONSTANT = 'ENABLED_CONSTANT'; - - const SELF_MANAGED = 'SELF_MANAGED'; - - // When parsely is not active. - const DISABLED_MUPLUGINS_FILTER = 'DISABLED_MUPLUGINS_FILTER'; - const DISABLED_MUPLUGINS_SILENT_OPTION = 'DISABLED_MUPLUGINS_SILENT_OPTION'; - const DISABLED_CONSTANT = 'DISABLED_CONSTANT'; // Prevent loading of plugin based on integration meta attribute or customers can also define it. - + // When Parse.ly is active. + const ENABLED_MUPLUGINS_FILTER = 'ENABLED_MUPLUGINS_FILTER'; + const ENABLED_CONSTANT = 'ENABLED_CONSTANT'; + const SELF_MANAGED = 'SELF_MANAGED'; + // When Parse.ly is not active. + const DISABLED_MUPLUGINS_FILTER = 'DISABLED_MUPLUGINS_FILTER'; + const DISABLED_CONSTANT = 'DISABLED_CONSTANT'; // Prevent loading of plugin based on integration meta attribute or customers can also define it. + // When Parse.ly is not configured in any way. const NONE = 'NONE'; - const NULL = 'NULL'; } From 406c53d89bd9877eac0b174d0967bc3af3ed98ec Mon Sep 17 00:00:00 2001 From: Alexey Kopytko Date: Fri, 27 Sep 2024 08:25:09 +0900 Subject: [PATCH 02/11] Add base telemetry library (take 2) (#5869) This PR adds a new generic Telemetry library consisting of base classes for Automattic's Tracks system integration. This is based heavily on the existing Telemetry package under the vip-parsely directory but is meant to be more generic and can be used by other plugins, and extended with other telemetry providers. There are no singletons, and most classes have fixed responsibilities. --------- Co-authored-by: Hanif Norman Co-authored-by: Hanif Norman <1227524+hanifn@users.noreply.github.com> Co-authored-by: Alec Geatches --- 000-vip-init.php | 25 ++ telemetry/README.md | 67 ++++ telemetry/class-telemetry-client.php | 26 ++ telemetry/class-telemetry-event-queue.php | 74 ++++ telemetry/class-telemetry-event.php | 27 ++ telemetry/class-telemetry-system.php | 32 ++ telemetry/class-tracks.php | 80 ++++ telemetry/tracks/class-tracks-client.php | 86 +++++ telemetry/tracks/class-tracks-event-dto.php | 45 +++ telemetry/tracks/class-tracks-event.php | 359 ++++++++++++++++++ tests/mock-constants.php | 16 + .../test-class-tracks-event-queue.php | 39 ++ tests/telemetry/test-class-tracks.php | 81 ++++ .../tracks/test-class-tracks-client.php | 90 +++++ .../tracks/test-class-tracks-event-dto.php | 16 + .../tracks/test-class-tracks-event.php | 192 ++++++++++ 16 files changed, 1255 insertions(+) create mode 100644 telemetry/README.md create mode 100644 telemetry/class-telemetry-client.php create mode 100644 telemetry/class-telemetry-event-queue.php create mode 100644 telemetry/class-telemetry-event.php create mode 100644 telemetry/class-telemetry-system.php create mode 100644 telemetry/class-tracks.php create mode 100644 telemetry/tracks/class-tracks-client.php create mode 100644 telemetry/tracks/class-tracks-event-dto.php create mode 100644 telemetry/tracks/class-tracks-event.php create mode 100644 tests/telemetry/test-class-tracks-event-queue.php create mode 100644 tests/telemetry/test-class-tracks.php create mode 100644 tests/telemetry/tracks/test-class-tracks-client.php create mode 100644 tests/telemetry/tracks/test-class-tracks-event-dto.php create mode 100644 tests/telemetry/tracks/test-class-tracks-event.php diff --git a/000-vip-init.php b/000-vip-init.php index d30b588c16..a5710ad1e8 100644 --- a/000-vip-init.php +++ b/000-vip-init.php @@ -233,6 +233,31 @@ require_once __DIR__ . '/vip-helpers/class-user-cleanup.php'; require_once __DIR__ . '/vip-helpers/class-wpcomvip-restrictions.php'; +// Load the Telemetry files +// TODO: switch to plain require_once like the above once the telemetry is fully deployed (all files are present) +$require_telemetry_files = [ + __DIR__ . '/telemetry/class-telemetry-system.php', + __DIR__ . '/telemetry/class-tracks.php', + __DIR__ . '/telemetry/class-telemetry-client.php', + __DIR__ . '/telemetry/class-telemetry-event-queue.php', + __DIR__ . '/telemetry/class-telemetry-event.php', + __DIR__ . '/telemetry/tracks/class-tracks-event-dto.php', + __DIR__ . '/telemetry/tracks/class-tracks-event.php', + __DIR__ . '/telemetry/tracks/class-tracks-client.php', +]; + +// If there is a missing file, the loop will break and the telemetry files will not be loaded at all +do { + foreach ( $require_telemetry_files as $file ) { + if ( ! file_exists( $file ) ) { + break; + } + } + foreach ( $require_telemetry_files as $file ) { + require_once $file; + } +} while ( false ); + add_action( 'init', [ WPComVIP_Restrictions::class, 'instance' ] ); //enabled on selected sites for now diff --git a/telemetry/README.md b/telemetry/README.md new file mode 100644 index 0000000000..9156160325 --- /dev/null +++ b/telemetry/README.md @@ -0,0 +1,67 @@ +# VIP Telemetry Library + +## Tracks + +Tracks is an event tracking tool used to understand user behaviour within Automattic. This library provides a way for plugins to interact with the Tracks system and start recording events. + +### How to use + +Example: + +```php +use Automattic\VIP\Telemetry\Tracks; + +function track_post_status( $new_status, $old_status, $post ) { + $tracks = new Tracks( 'myplugin_' ); + + $tracks->record_event( 'post_status_changed', [ + 'new_status' => $new_status, + 'old_status' => $old_status, + 'post_id' => $post->ID, + ] ); +} +add_action( 'transition_post_status', 'track_post_status', 10, 3 ); +``` + +The example above is the most basic way to use this Tracks library. The client plugin would need a function to hook into the WordPress action they want to track and that function has to instantiate and call the `record_event` method from the `Tracks` class. This can be abstracted further to reduce code duplication by wrapping the functions in a class for example: + +```php +namespace MyPlugin\Telemetry; + +use Automattic\VIP\Telemetry\Tracks; + +class MyPluginTracker { + protected $tracks; + + public function __construct() { + $this->tracks = new Tracks( 'myplugin_' ); + } + + public function register_events() { + add_action( 'transition_post_status', [ $this, 'track_post_status' ], 10, 3 ); + } + + public function track_post_status( $new_status, $old_status, $post ) { + $this->tracks->record_event( 'post_status_changed', [ + 'new_status' => $new_status, + 'old_status' => $old_status, + 'post' => (array) $post, + ] ); + } +} +``` + +With the class above, you can then initiate event tracking in the main plugin file with these lines: + +```php +$tracker = new MyPluginTracker(); +$tracker->register_events(); +``` + +If necessary to provide global properties to all events, you can pass an array of properties to the `Tracks` constructor: + +```php +$this->tracks = new Tracks( 'myplugin_', [ + 'plugin_version' => '1.2.3', +] ); +``` \ No newline at end of file diff --git a/telemetry/class-telemetry-client.php b/telemetry/class-telemetry-client.php new file mode 100644 index 0000000000..51c9d9eacd --- /dev/null +++ b/telemetry/class-telemetry-client.php @@ -0,0 +1,26 @@ + + */ + protected array $events = []; + + /** + * Constructor. Registers the shutdown hook to record any and all events. + */ + public function __construct( Telemetry_Client $client ) { + $this->client = $client; + + // Register the shutdown hook to record any and all events + add_action( 'shutdown', array( $this, 'record_events' ) ); + } + + /** + * Enqueues an event to be recorded asynchronously. + * + * @param Telemetry_Event $event The event to record. + * @return bool|WP_Error True if the event was enqueued for recording. + * False if the event is not recordable. + * WP_Error if the event is generating an error. + */ + public function record_event_asynchronously( Telemetry_Event $event ): bool|WP_Error { + $is_event_recordable = $event->is_recordable(); + + if ( true !== $is_event_recordable ) { + return $is_event_recordable; + } + + $this->events[] = $event; + + return true; + } + + /** + * Records all queued events synchronously. + */ + public function record_events(): void { + if ( [] === $this->events ) { + return; + } + + // No back-off mechanism is implemented here, given the low cost of missing a few events. + // We also need to ensure that there's minimal disruption to a site's operations. + $this->client->batch_record_events( $this->events ); + $this->events = []; + } +} diff --git a/telemetry/class-telemetry-event.php b/telemetry/class-telemetry-event.php new file mode 100644 index 0000000000..108e28c5cf --- /dev/null +++ b/telemetry/class-telemetry-event.php @@ -0,0 +1,27 @@ + $event_properties Any additional properties + * to include with the event. + * @return bool|WP_Error True if recording the event succeeded. + * False if telemetry is disabled. + * WP_Error if recording the event failed. + */ + abstract public function record_event( + string $event_name, + array $event_properties = [] + ): bool|WP_Error; +} diff --git a/telemetry/class-tracks.php b/telemetry/class-tracks.php new file mode 100644 index 0000000000..46c16d75c4 --- /dev/null +++ b/telemetry/class-tracks.php @@ -0,0 +1,80 @@ + The global event properties to be included with every event. + */ + private array $global_event_properties = []; + + /** + * Tracks constructor. + * + * @param string $event_prefix The prefix for all event names. Defaults to 'vip_'. + * @param array $global_event_properties The global event properties to be included with every event. + * @param Telemetry_Event_Queue|null $queue The event queue to use. Falls back to the default queue when none provided. + * @param Tracks_Client|null $client The client instance to use. Falls back to the default client when none provided. + */ + public function __construct( string $event_prefix = 'vip_', array $global_event_properties = [], Telemetry_Event_Queue $queue = null, Tracks_Client $client = null ) { + $this->event_prefix = $event_prefix; + $this->global_event_properties = $global_event_properties; + $client ??= new Tracks_Client(); + $this->queue = $queue ?? new Telemetry_Event_Queue( $client ); + } + + /** + * Records an event to Tracks by using the Tracks API. + * + * If the event doesn't pass validation, it gets silently discarded. + * + * @param string $event_name The event name. Must be snake_case. + * @param array|array $event_properties Any additional properties to include with the event. + * Key names must be lowercase and snake_case. + * @return bool|WP_Error True if recording the event succeeded. + * False if telemetry is disabled. + * WP_Error if recording the event failed. + */ + public function record_event( + string $event_name, + array $event_properties = [] + ): bool|WP_Error { + if ( [] !== $this->global_event_properties ) { + $event_properties = array_merge( $this->global_event_properties, $event_properties ); + } + + $event = new Tracks_Event( $this->event_prefix, $event_name, $event_properties ); + + return $this->queue->record_event_asynchronously( $event ); + } +} diff --git a/telemetry/tracks/class-tracks-client.php b/telemetry/tracks/class-tracks-client.php new file mode 100644 index 0000000000..7774172721 --- /dev/null +++ b/telemetry/tracks/class-tracks-client.php @@ -0,0 +1,86 @@ +http = $http ?? _wp_http_get_object(); + } + + /** + * Record a batch of events using the Tracks REST API + * + * @param Tracks_Event[] $events Array of Tracks_Event objects to record + * @return bool|WP_Error True if batch recording succeeded. + * WP_Error is any error occured. + */ + public function batch_record_events( array $events, array $common_props = [] ): bool|WP_Error { + // filter out invalid events + $valid_events = array_filter( $events, function ( $event ) { + return $event instanceof Tracks_Event && $event->is_recordable() === true; + } ); + + // no events - nothing to do + if ( [] === $valid_events ) { + return true; + } + + $body = [ + 'events' => $valid_events, + 'commonProps' => $common_props, + ]; + + $response = $this->http->post( + static::TRACKS_ENDPOINT, + array( + 'body' => wp_json_encode( $body ), + 'user-agent' => 'viptelemetry', + 'headers' => array( + 'Content-Type' => 'application/json', + ), + ) + ); + + if ( is_wp_error( $response ) ) { + log2logstash( [ + 'severity' => 'error', + 'feature' => 'telemetry', + 'message' => 'error batch recording events to Tracks', + 'extra' => [ + 'error' => $response->get_error_messages(), + ], + ] ); + return $response; + } + + return true; + } +} diff --git a/telemetry/tracks/class-tracks-event-dto.php b/telemetry/tracks/class-tracks-event-dto.php new file mode 100644 index 0000000000..e8e56df211 --- /dev/null +++ b/telemetry/tracks/class-tracks-event-dto.php @@ -0,0 +1,45 @@ +|array $event_properties Any properties included in the event. + */ + public function __construct( string $prefix, string $event_name, array $event_properties = [] ) { + $this->prefix = $prefix; + $this->event_name = $event_name; + $this->event_properties = $event_properties; + $this->event_timestamp = microtime( true ); + } + + /** + * Returns the event's data. + * + * @return Tracks_Event_DTO|WP_Error Event object if the event was created successfully, WP_Error otherwise. + */ + public function get_data(): Tracks_Event_DTO|WP_Error { + if ( ! isset( $this->data ) ) { + $event_data = $this->process_properties( $this->prefix, $this->event_name, $this->event_properties ); + $validation_result = $this->get_event_validation_result( $event_data ); + + $this->data = $validation_result ?? $event_data; + } + + return $this->data; + } + + /** + * Returns the event's data for JSON representation. + */ + public function jsonSerialize(): mixed { + $data = $this->get_data(); + + if ( is_wp_error( $data ) ) { + return (object) []; + } + + return $data; + } + + /** + * Returns whether the event can be recorded. + * + * @return bool|WP_Error True if the event is recordable. + */ + public function is_recordable(): bool|WP_Error { + $data = $this->get_data(); + + if ( is_wp_error( $data ) ) { + return $data; + } + + return true; + } + + /** + * Processes the event's properties to get them ready for validation. + * + * @param string $event_prefix The event's prefix. + * @param string $event_name The event's name. + * @param array|array $event_properties Any event properties to be processed. + * @return Tracks_Event_DTO The resulting event object with processed properties. + */ + protected function process_properties( + string $event_prefix, + string $event_name, + array $event_properties + ): Tracks_Event_DTO { + $event = static::encode_properties( $event_properties ); + $event = static::set_user_properties( $event ); + + // Set event name. If the event name doesn't have the prefix, add it. + $event->_en = preg_replace( + '/^(?:' . $event_prefix . ')?(.+)/', + $event_prefix . '\1', + $event_name + ) ?? ''; + + // Set event timestamp. + if ( ! isset( $event->_ts ) ) { + $event->_ts = static::milliseconds_since_epoch( $this->event_timestamp ); + } + + // Remove non-routable IPs to prevent record from being discarded. + if ( isset( $event->_via_ip ) && + 1 === preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) { + unset( $event->_via_ip ); + } + + // Set VIP environment if it exists. + if ( defined( 'VIP_GO_APP_ENVIRONMENT' ) ) { + $app_environment = constant( 'VIP_GO_APP_ENVIRONMENT' ); + if ( is_string( $app_environment ) && '' !== $app_environment ) { + $event->vipgo_env = $app_environment; + } + } + + // Set VIP organization if it exists. + if ( defined( 'VIP_ORG_ID' ) ) { + $org_id = constant( 'VIP_ORG_ID' ); + if ( is_string( $org_id ) && '' !== $org_id ) { + $event->vipgo_org = $org_id; + } + } + + // Check if the user is a VIP user. + $event->is_vip_user = Support_User::user_has_vip_support_role( get_current_user_id() ); + + return $event; + } + + /** + * Sets the Tracks User ID and User ID Type depending on the current + * environment. + * + * @param Tracks_Event_DTO $event The event to annotate with identity information. + * @return Tracks_Event_DTO The new event object including identity information. + */ + protected static function set_user_properties( Tracks_Event_DTO $event ): Tracks_Event_DTO { + $wp_user = wp_get_current_user(); + + // Only track logged-in users. + if ( 0 === $wp_user->ID ) { + return $event; + } + + // Set anonymized event user ID; it should be consistent across environments. + // VIP_TELEMETRY_SALT is a private constant shared across the platform. + if ( defined( 'VIP_TELEMETRY_SALT' ) ) { + $salt = constant( 'VIP_TELEMETRY_SALT' ); + $tracks_user_id = hash_hmac( 'sha256', $wp_user->user_email, $salt ); + + $event->_ui = $tracks_user_id; + $event->_ut = 'vip:user_email'; + + return $event; + } + + // Users in the VIP environment. + if ( defined( 'VIP_GO_APP_ID' ) ) { + $app_id = constant( 'VIP_GO_APP_ID' ); + if ( is_integer( $app_id ) && $app_id > 0 ) { + $event->_ui = sprintf( '%s_%s', $app_id, $wp_user->ID ); + $event->_ut = 'vip_go_app_wp_user'; + + return $event; + } + } + + // All other environments. + $event->_ui = wp_hash( sprintf( '%s|%s', get_option( 'home' ), $wp_user->ID ) ); + + /** + * @see \Automattic\VIP\Parsely\Telemetry\Tracks_Event::annotate_with_id_and_type() + */ + $event->_ut = 'anon'; // Same as the default value in the original code. + + return $event; + } + + /** + * Validates the event object. + * + * @param Tracks_Event_DTO $event Event object to validate. + * @return ?WP_Error null if validation passed, error otherwise. + */ + protected function get_event_validation_result( Tracks_Event_DTO $event ): ?WP_Error { + // Check that required fields are defined. + if ( ! $event->_en ) { + $msg = __( 'The _en property must be specified to non-empty value', 'vip-telemetry' ); + log2logstash( [ + 'severity' => 'error', + 'feature' => 'telemetry', + 'message' => $msg, + 'extra' => [ + 'event' => (array) $event, + ], + ] ); + return new WP_Error( + 'invalid_event', + $msg + ); + } + + // Validate Event Name (_en). + if ( ! static::event_name_is_valid( $event->_en ) ) { + $msg = __( 'A valid event name must be specified', 'vip-telemetry' ); + log2logstash( [ + 'severity' => 'error', + 'feature' => 'telemetry', + 'message' => $msg, + 'extra' => [ + 'event' => (array) $event, + ], + ] ); + return new WP_Error( + 'invalid_event_name', + $msg + ); + } + + + // Validate property names format. + foreach ( get_object_vars( $event ) as $key => $_ ) { + if ( ! static::property_name_is_valid( $key ) ) { + $msg = __( 'A valid property name must be specified', 'vip-telemetry' ); + log2logstash( [ + 'severity' => 'error', + 'feature' => 'telemetry', + 'message' => $msg, + 'extra' => [ + 'event' => (array) $event, + ], + ] ); + return new WP_Error( + 'invalid_property_name', + $msg + ); + } + } + + // Validate User ID (_ui) and User ID Type (_ut). + if ( ! isset( $event->_ui ) && ! isset( $event->_ut ) ) { + $msg = __( 'Could not determine user identity and type', 'vip-telemetry' ); + log2logstash( [ + 'severity' => 'error', + 'feature' => 'telemetry', + 'message' => $msg, + 'extra' => [ + 'event' => (array) $event, + ], + ] ); + return new WP_Error( + 'empty_user_information', + $msg + ); + } + + return null; + } + + /** + * Checks if the passed event name is valid. + * + * @param string $event_name The event's name. + * @return bool Whether the event name is valid. + */ + protected static function event_name_is_valid( string $event_name ): bool { + return 1 === preg_match( static::EVENT_NAME_REGEX, $event_name ); + } + + /** + * Checks if the passed property name is valid. + * + * @param string $property_name The property's name. + * @return bool Whether the property name is valid. + */ + protected static function property_name_is_valid( string $property_name ): bool { + return 1 === preg_match( static::PROPERTY_NAME_REGEX, $property_name ); + } + + /** + * Sanitizes the passed properties array, JSON-encoding non-string values. + * + * @param array|array $event_properties The array to be sanitized. + * @return Tracks_Event_DTO The sanitized object. + */ + protected static function encode_properties( array $event_properties ): Tracks_Event_DTO { + $result = new Tracks_Event_DTO(); + + foreach ( $event_properties as $key => $value ) { + if ( is_string( $value ) ) { + $result->$key = $value; + continue; + } + + $result->$key = wp_json_encode( $value ); + } + + return $result; + } + + /** + * Builds a JS compatible timestamp for the event (integer number of milliseconds since the Unix Epoch). + * + * @return string + */ + protected static function milliseconds_since_epoch( float $microtime ): string { + $timestamp = round( $microtime * 1000 ); + + return number_format( $timestamp, 0, '', '' ); + } +} diff --git a/tests/mock-constants.php b/tests/mock-constants.php index 7bf061d438..2e4da4526d 100644 --- a/tests/mock-constants.php +++ b/tests/mock-constants.php @@ -248,3 +248,19 @@ function constant( $constant ) { return Constant_Mocker::constant( $constant ); } } + +namespace Automattic\VIP\Telemetry\Tracks { + use Automattic\Test\Constant_Mocker; + + function define( $constant, $value ) { + Constant_Mocker::define( $constant, $value ); + } + + function defined( $constant ) { + return Constant_Mocker::defined( $constant ); + } + + function constant( $constant ) { + return Constant_Mocker::constant( $constant ); + } +} diff --git a/tests/telemetry/test-class-tracks-event-queue.php b/tests/telemetry/test-class-tracks-event-queue.php new file mode 100644 index 0000000000..cc48192c3f --- /dev/null +++ b/tests/telemetry/test-class-tracks-event-queue.php @@ -0,0 +1,39 @@ +getMockBuilder( Telemetry_Client::class ) + ->disableOriginalConstructor() + ->getMock(); + + $event = $this->getMockBuilder( Telemetry_Event::class ) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects( $this->once() )->method( 'is_recordable' )->willReturn( true ); + + $bad_event = $this->getMockBuilder( Telemetry_Event::class ) + ->disableOriginalConstructor() + ->getMock(); + + $bad_event->expects( $this->once() )->method( 'is_recordable' )->willReturn( false ); + + $client->expects( $this->once() ) + ->method( 'batch_record_events' ) + ->with( [ $event ] ) + ->willReturn( true ); + + $queue = new Telemetry_Event_Queue( $client ); + $queue->record_event_asynchronously( $event ); + $queue->record_event_asynchronously( $bad_event ); + $queue->record_events(); + $queue->record_events(); + } +} diff --git a/tests/telemetry/test-class-tracks.php b/tests/telemetry/test-class-tracks.php new file mode 100644 index 0000000000..8fe36475da --- /dev/null +++ b/tests/telemetry/test-class-tracks.php @@ -0,0 +1,81 @@ +factory()->user->create_and_get(); + wp_set_current_user( $user->ID ); + + $queue = $this->getMockBuilder( Telemetry_Event_Queue::class ) + ->disableOriginalConstructor() + ->getMock(); + + $queue->expects( $this->once() ) + ->method( 'record_event_asynchronously' ) + ->with($this->callback(function ( Tracks_Event $event ) { + $this->assertSame( 'test_cool_event', $event->get_data()->_en ); + $this->assertSame( 'bar', $event->get_data()->foo ); + $this->assertFalse( isset( $event->get_data()->global_baz ) ); + + return true; + })) + ->willReturn( true ); + + $tracks = new Tracks( 'test_', [], $queue ); + $this->assertTrue( $tracks->record_event( 'cool_event', [ 'foo' => 'bar' ] ) ); + } + + public function test_event_queued_with_global_properies() { + $user = $this->factory()->user->create_and_get(); + wp_set_current_user( $user->ID ); + + $queue = $this->getMockBuilder( Telemetry_Event_Queue::class ) + ->disableOriginalConstructor() + ->getMock(); + + $queue->expects( $this->once() ) + ->method( 'record_event_asynchronously' ) + ->with($this->callback(function ( Tracks_Event $event ) { + $this->assertSame( 'nice_fuzzy_event', $event->get_data()->_en ); + $this->assertSame( 'bar', $event->get_data()->foo ); + $this->assertSame( 'qux', $event->get_data()->global_baz ); + + return true; + })) + ->willReturn( true ); + + $tracks = new Tracks( 'nice_', [ + 'global_baz' => 'qux', + 'foo' => 'default_foo', + ], $queue ); + $this->assertTrue( $tracks->record_event( 'fuzzy_event', [ 'foo' => 'bar' ] ) ); + } + + public function test_event_prefix() { + $tracks = new Tracks(); + $event_prefix = self::get_property( 'event_prefix' )->getValue( $tracks ); + $this->assertEquals( 'vip_', $event_prefix ); + } + + public function test_custom_event_prefix() { + $tracks = new Tracks( 'test_' ); + $event_prefix = self::get_property( 'event_prefix' )->getValue( $tracks ); + $this->assertEquals( 'test_', $event_prefix ); + } + + /** + * Helper function for accessing protected properties. + */ + protected static function get_property( $name ) { + $class = new \ReflectionClass( Tracks::class ); + $property = $class->getProperty( $name ); + $property->setAccessible( true ); + return $property; + } +} diff --git a/tests/telemetry/tracks/test-class-tracks-client.php b/tests/telemetry/tracks/test-class-tracks-client.php new file mode 100644 index 0000000000..8413874012 --- /dev/null +++ b/tests/telemetry/tracks/test-class-tracks-client.php @@ -0,0 +1,90 @@ +getMockBuilder( WP_Http::class ) + ->disableOriginalConstructor() + ->getMock(); + + $event = $this->getMockBuilder( Tracks_Event::class ) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects( $this->once() )->method( 'is_recordable' )->willReturn( true ); + $event->expects( $this->once() )->method( 'jsonSerialize' )->willReturn( [ 'test_event' => true ] ); + + $bad_event = $this->getMockBuilder( Tracks_Event::class ) + ->disableOriginalConstructor() + ->getMock(); + + $bad_event->expects( $this->once() )->method( 'is_recordable' )->willReturn( false ); + + $http->expects( $this->once() ) + ->method( 'post' ) + ->with( $this->stringContains( 'tracks/record' ), [ + 'body' => wp_json_encode([ + 'events' => [ [ 'test_event' => true ] ], + 'commonProps' => [ 'foo' => 'bar' ], + ]), + 'user-agent' => 'viptelemetry', + 'headers' => array( + 'Content-Type' => 'application/json', + ), + + ] ) + ->willReturn( true ); + + $client = new Tracks_Client( $http ); + $this->assertTrue( $client->batch_record_events( [ $event, $bad_event ], [ 'foo' => 'bar' ] ) ); + } + + public function test_should_handle_failed_requests() { + $http = $this->getMockBuilder( WP_Http::class ) + ->disableOriginalConstructor() + ->getMock(); + + $event = $this->getMockBuilder( Tracks_Event::class ) + ->disableOriginalConstructor() + ->getMock(); + + $event->expects( $this->once() )->method( 'is_recordable' )->willReturn( true ); + $event->expects( $this->once() )->method( 'jsonSerialize' )->willReturn( [ 'test_event' => true ] ); + + $error = new WP_Error( 'http_request_failed', 'This is a failure' ); + + $http->expects( $this->once() ) + ->method( 'post' ) + ->with( $this->stringContains( 'tracks/record' ) ) + ->willReturn( $error ); + + $client = new Tracks_Client( $http ); + $this->assertSame( $error, $client->batch_record_events( [ $event ], [ 'foo' => 'bar' ] ) ); + } + + public function test_should_not_make_requests_for_no_events() { + $http = $this->getMockBuilder( WP_Http::class ) + ->disableOriginalConstructor() + ->getMock(); + + $bad_event = $this->getMockBuilder( Tracks_Event::class ) + ->disableOriginalConstructor() + ->getMock(); + + $bad_event->expects( $this->once() )->method( 'is_recordable' )->willReturn( false ); + + $http->expects( $this->never() ) + ->method( 'post' ); + + $client = new Tracks_Client( $http ); + $this->assertTrue( $client->batch_record_events( [ $bad_event ], [ 'foo' => 'bar' ] ) ); + } +} diff --git a/tests/telemetry/tracks/test-class-tracks-event-dto.php b/tests/telemetry/tracks/test-class-tracks-event-dto.php new file mode 100644 index 0000000000..27e7aeb421 --- /dev/null +++ b/tests/telemetry/tracks/test-class-tracks-event-dto.php @@ -0,0 +1,16 @@ +assertInstanceOf( Tracks_Event_DTO::class, $event ); + } +} diff --git a/tests/telemetry/tracks/test-class-tracks-event.php b/tests/telemetry/tracks/test-class-tracks-event.php new file mode 100644 index 0000000000..d957b77bc2 --- /dev/null +++ b/tests/telemetry/tracks/test-class-tracks-event.php @@ -0,0 +1,192 @@ +user = $this->factory()->user->create_and_get(); + wp_set_current_user( $this->user->ID ); + + parent::setUp(); + } + + public function tearDown(): void { + Constant_Mocker::clear(); + parent::tearDown(); + } + + public function test_should_create_event() { + $event = new Tracks_Event( 'prefix_', 'test_event', [ 'property1' => 'value1' ] ); + + $this->assertInstanceOf( Tracks_Event::class, $event ); + } + + public function test_should_return_event_data() { + Constant_Mocker::define( 'VIP_TELEMETRY_SALT', self::VIP_TELEMETRY_SALT ); + Constant_Mocker::define( 'VIP_GO_APP_ENVIRONMENT', self::VIP_GO_APP_ENVIRONMENT ); + Constant_Mocker::define( 'VIP_ORG_ID', self::VIP_ORG_ID ); + + $event = new Tracks_Event( 'prefix_', 'test_event', [ + 'property1' => 'value1', + '_via_ip' => '1.2.3.4', + ] ); + + if ( $event->get_data() instanceof WP_Error ) { + $this->fail( sprintf( '%s: %s', $event->get_data()->get_error_code(), $event->get_data()->get_error_message() ) ); + } + + $this->assertInstanceOf( Tracks_Event_DTO::class, $event->get_data() ); + $this->assertIsString( $event->get_data()->_ts ); + $this->assertGreaterThan( ( time() - 10 ) * 1000, (int) $event->get_data()->_ts ); + $this->assertSame( 'prefix_test_event', $event->get_data()->_en ); + $this->assertSame( 'value1', $event->get_data()->property1 ); + $this->assertSame( '1.2.3.4', $event->get_data()->_via_ip ); + $this->assertSame( hash_hmac( 'sha256', $this->user->user_email, self::VIP_TELEMETRY_SALT ), $event->get_data()->_ui ); + $this->assertSame( 'vip:user_email', $event->get_data()->_ut ); + $this->assertSame( self::VIP_GO_APP_ENVIRONMENT, $event->get_data()->vipgo_env ); + $this->assertSame( self::VIP_ORG_ID, $event->get_data()->vipgo_org ); + $this->assertFalse( $event->get_data()->is_vip_user ); + $this->assertTrue( $event->is_recordable() ); + } + + public function test_should_not_add_prefix_twice() { + $event = new Tracks_Event( 'prefixed_', 'prefixed_event_name' ); + + $this->assertNotInstanceOf( WP_Error::class, $event->get_data() ); + + $this->assertSame( 'prefixed_event_name', $event->get_data()->_en ); + } + + public function test_should_not_override_timestamp() { + $ts = 1234567890; + $event = new Tracks_Event( 'prefixed_', 'example', [ + '_ts' => $ts, + ] ); + + $this->assertSame( (string) $ts, $event->get_data()->_ts ); + } + + public function test_should_encode_complex_properties() { + $event = new Tracks_Event( 'prefix_', 'event_name', [ 'example' => [ 'a' => 'b' ] ] ); + + $this->assertNotInstanceOf( WP_Error::class, $event->get_data() ); + + $this->assertSame( '{"a":"b"}', $event->get_data()->example ); + } + + public function test_should_not_encode_errors_to_json() { + $event = new Tracks_Event( 'prefix_', 'bogus name' ); + + $this->assertInstanceOf( WP_Error::class, $event->get_data() ); + + $this->assertSame( '{}', wp_json_encode( $event ) ); + } + + public function test_should_fallback_to_vip_go_app_wp_user() { + Constant_Mocker::define( 'VIP_GO_APP_ID', 1234 ); + + $event = new Tracks_Event( 'prefix_', 'test_event' ); + + $this->assertNotInstanceOf( WP_Error::class, $event->get_data() ); + $this->assertSame( 'vip_go_app_wp_user', $event->get_data()->_ut ); + $this->assertSame( '1234_' . $this->user->ID, $event->get_data()->_ui ); + } + + public function test_should_fallback_to_anon_wp_hash() { + $event = new Tracks_Event( 'prefix_', 'test_event' ); + + $this->assertNotInstanceOf( WP_Error::class, $event->get_data() ); + $this->assertSame( 'anon', $event->get_data()->_ut ); + $this->assertMatchesRegularExpression( '/^[0-9a-f]+$/', $event->get_data()->_ui ); + } + + public function test_should_not_record_events_for_logged_out_users() { + wp_set_current_user( 0 ); + + $event = new Tracks_Event( 'prefix_', 'test_event' ); + + $this->assertInstanceOf( WP_Error::class, $event->get_data() ); + $this->assertSame( 'empty_user_information', $event->get_data()->get_error_code() ); + } + + public static function provide_non_routable_ips() { + yield [ '192.168.10.1' ]; + yield [ '10.11.10.11' ]; + } + + /** + * @dataProvider provide_non_routable_ips + */ + public function test_should_remove_non_routable_ips( string $_via_ip ) { + $event = new Tracks_Event( 'prefix_', 'example', [ '_via_ip' => $_via_ip ] ); + + $this->assertNotInstanceOf( WP_Error::class, $event->get_data() ); + $this->assertFalse( isset( $event->get_data()->_via_ip ) ); + $this->assertStringNotContainsString( 'via_ip', wp_json_encode( $event ) ); + } + + public function test_should_return_error_on_missing_event_name() { + $event = new Tracks_Event( 'prefix_', '', [ 'property1' => 'value1' ] ); + + $this->assertInstanceOf( WP_Error::class, $event->get_data() ); + $this->assertInstanceOf( WP_Error::class, $event->is_recordable() ); + $this->assertSame( $event->is_recordable(), $event->get_data() ); + + $this->assertSame( 'invalid_event', $event->get_data()->get_error_code() ); + } + + public static function provide_invalid_event_names() { + yield 'spaces' => [ 'cool page viewed' ]; + yield 'dashes' => [ 'cool-page-viewed' ]; + yield 'mixed-case' => [ 'cool_page_Viewed' ]; + } + + /** + * @dataProvider provide_invalid_event_names + */ + public function test_should_return_error_on_invalid_event_name( string $event_name ) { + $event = new Tracks_Event( 'prefix_', $event_name, [ 'property1' => 'value1' ] ); + + $this->assertInstanceOf( WP_Error::class, $event->get_data() ); + $this->assertInstanceOf( WP_Error::class, $event->is_recordable() ); + $this->assertSame( $event->is_recordable(), $event->get_data() ); + + $this->assertSame( 'invalid_event_name', $event->get_data()->get_error_code() ); + } + + public static function provide_invalid_property_names() { + yield 'empty' => [ '' ]; + yield 'spaces' => [ 'cool property' ]; + yield 'mixed-case' => [ 'cool_Property' ]; + yield 'camelCase' => [ 'compressedSize' ]; + yield 'dashes' => [ 'cool-property' ]; + } + + /** + * @dataProvider provide_invalid_property_names + */ + public function test_should_return_error_on_invalid_property_name( string $property_name ) { + $event = new Tracks_Event( 'prefix_', 'test_event', [ $property_name => 'value1' ] ); + + $this->assertInstanceOf( WP_Error::class, $event->get_data() ); + $this->assertInstanceOf( WP_Error::class, $event->is_recordable() ); + $this->assertSame( $event->is_recordable(), $event->get_data() ); + $this->assertSame( 'invalid_property_name', $event->get_data()->get_error_code() ); + } +} From 09f10aafb280de39282dbe60094d1c32fa3ea894 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:18:08 -0500 Subject: [PATCH 03/11] chore(deps): Bump actions/checkout from 4.1.7 to 4.2.0 (#5897) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.7...v4.2.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/changelog-summary-prod.yml | 4 ++-- .github/workflows/changelog-summary-staging.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/core-tests.yml | 4 ++-- .github/workflows/coverage-develop.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/deploy.yml | 6 +++--- .github/workflows/e2e.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/parsely.yml | 2 +- .github/workflows/search-dev-tools.yml | 2 +- .github/workflows/search-e2e.yml | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/changelog-summary-prod.yml b/.github/workflows/changelog-summary-prod.yml index 54bd5e13f1..059be3ccb7 100644 --- a/.github/workflows/changelog-summary-prod.yml +++ b/.github/workflows/changelog-summary-prod.yml @@ -25,7 +25,7 @@ jobs: egress-policy: audit - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Retrieve tags run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* @@ -61,7 +61,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Setup PHP uses: shivammathur/setup-php@2.30.5 diff --git a/.github/workflows/changelog-summary-staging.yml b/.github/workflows/changelog-summary-staging.yml index 78d3068fef..9299f451dd 100644 --- a/.github/workflows/changelog-summary-staging.yml +++ b/.github/workflows/changelog-summary-staging.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Setup PHP uses: shivammathur/setup-php@2.30.5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd23de8109..a643f5de21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: MYSQL_DATABASE: wordpress_test steps: - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: submodules: recursive diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 934ecbc14c..8e1b27ac28 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: - javascript steps: - name: Checkout repository - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Initialize CodeQL uses: github/codeql-action/init@v3.26.8 diff --git a/.github/workflows/core-tests.yml b/.github/workflows/core-tests.yml index 555401de0c..4684f66ee9 100644 --- a/.github/workflows/core-tests.yml +++ b/.github/workflows/core-tests.yml @@ -42,14 +42,14 @@ jobs: echo "PHP_FPM_GID=$(id -g)" >> "${GITHUB_ENV}" - name: Checkout WordPress - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: repository: wordpress/wordpress-develop path: wordpress ref: ${{ steps.version.outputs.latest }} - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: submodules: recursive path: wordpress/src/wp-content/mu-plugins diff --git a/.github/workflows/coverage-develop.yml b/.github/workflows/coverage-develop.yml index 334dbf7c30..5e243fc560 100644 --- a/.github/workflows/coverage-develop.yml +++ b/.github/workflows/coverage-develop.yml @@ -36,7 +36,7 @@ jobs: MYSQL_DATABASE: wordpress_test steps: - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: submodules: recursive diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index f82faf6c34..b0bd9c6b99 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,7 +22,7 @@ jobs: github.com:443 - name: Check out the source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Review dependencies uses: actions/dependency-review-action@v4.3.4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e092baa4a9..02cbebf42d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,19 +22,19 @@ jobs: contents: write steps: - name: Check out the source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: submodules: recursive path: ${{ env.SOURCE_REPO_PATH }} - name: Check out Automattic/vip-go-mu-plugins-ext - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: repository: Automattic/vip-go-mu-plugins-ext path: ${{ env.EXT_REPO_PATH }} - name: Check out Automattic/vip-go-mu-plugins-built - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: repository: Automattic/vip-go-mu-plugins-built path: ${{ env.TARGET_REPO_PATH }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6e26adf975..514c505afd 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -68,7 +68,7 @@ jobs: wordpress.org:443 - name: Check out repository code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: submodules: true @@ -131,7 +131,7 @@ jobs: egress-policy: audit - name: Check out repository code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Setup Node uses: actions/setup-node@v4.0.4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8d4818baa8..52e42e7788 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Set up PHP uses: shivammathur/setup-php@2.30.5 @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Setup Node uses: actions/setup-node@v4.0.4 diff --git a/.github/workflows/parsely.yml b/.github/workflows/parsely.yml index 69219d24cb..5c41a6a3bd 100644 --- a/.github/workflows/parsely.yml +++ b/.github/workflows/parsely.yml @@ -42,7 +42,7 @@ jobs: MYSQL_DATABASE: wordpress_test steps: - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: submodules: recursive diff --git a/.github/workflows/search-dev-tools.yml b/.github/workflows/search-dev-tools.yml index 7509e98bef..efdd6503da 100644 --- a/.github/workflows/search-dev-tools.yml +++ b/.github/workflows/search-dev-tools.yml @@ -22,7 +22,7 @@ jobs: contents: write steps: - name: Check out source code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: token: ${{ secrets.WPCOM_VIP_BOT_TOKEN }} diff --git a/.github/workflows/search-e2e.yml b/.github/workflows/search-e2e.yml index 463d0ea892..1ba4f80948 100644 --- a/.github/workflows/search-e2e.yml +++ b/.github/workflows/search-e2e.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: submodules: recursive From 4bd6b4d35e5858890d30746c9ecc8de3d1cb45ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:18:47 -0500 Subject: [PATCH 04/11] chore(deps-dev): Bump 10up-toolkit from 6.2.2 to 6.3.0 (#5892) Bumps [10up-toolkit](https://github.com/10up/10up-toolkit/tree/HEAD/packages/toolkit) from 6.2.2 to 6.3.0. - [Release notes](https://github.com/10up/10up-toolkit/releases) - [Changelog](https://github.com/10up/10up-toolkit/blob/develop/packages/toolkit/CHANGELOG.md) - [Commits](https://github.com/10up/10up-toolkit/commits/10up-toolkit@6.3.0/packages/toolkit) --- updated-dependencies: - dependency-name: 10up-toolkit dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c7a6260d3..8a9d5f5907 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7238,9 +7238,9 @@ "dev": true }, "node_modules/10up-toolkit": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/10up-toolkit/-/10up-toolkit-6.2.2.tgz", - "integrity": "sha512-4dfiIWmWF0M+rHINV14+IZxmQLfonDPKuU3YmxCCAjAsRw79GQIW+YNzMeasaxI760N+zWxPGgizv+GwP2jKOA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/10up-toolkit/-/10up-toolkit-6.3.0.tgz", + "integrity": "sha512-ubRo+y+amPVPw0CYE1sPW3GJicQYUiHUORzTRJO8/nCepLNjCwT3lB7HDGLBvKeSgG9+rF6QK+D5NLe0Ekp5gw==", "dev": true, "dependencies": { "@babel/eslint-parser": "^7.23.3", From a79f78eb03bf02c7a49afba3b7434111efefd87b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:20:16 -0500 Subject: [PATCH 05/11] chore(deps): Bump github/codeql-action from 3.26.8 to 3.26.10 (#5896) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.8 to 3.26.10. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3.26.8...v3.26.10) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8e1b27ac28..484c87f567 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,10 +32,10 @@ jobs: uses: actions/checkout@v4.2.0 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.26.8 + uses: github/codeql-action/init@v3.26.10 with: languages: ${{ matrix.language }} config-file: ./.github/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.26.8 + uses: github/codeql-action/analyze@v3.26.10 From eeac80d2bdcc93d49f3a6bce7ae00d34f72926f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:59:17 +0300 Subject: [PATCH 06/11] chore(deps): Bump actions/checkout in /.github/actions/prepare-source (#5899) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.7...v4.2.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/prepare-source/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/prepare-source/action.yml b/.github/actions/prepare-source/action.yml index 7f55e2b8ae..86bcbc005d 100644 --- a/.github/actions/prepare-source/action.yml +++ b/.github/actions/prepare-source/action.yml @@ -4,7 +4,7 @@ runs: using: composite steps: - name: Check out mu-plugins-ext - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 with: repository: 'Automattic/vip-go-mu-plugins-ext' path: 'vip-go-mu-plugins-ext' From 16ac4e7d14867b04431807605907ec20b9e5d517 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:18:33 +0300 Subject: [PATCH 07/11] chore(deps-dev): Bump webpack in /search/search-dev-tools (#5893) Bumps [webpack](https://github.com/webpack/webpack) from 5.94.0 to 5.95.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.94.0...v5.95.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- search/search-dev-tools/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/search/search-dev-tools/package-lock.json b/search/search-dev-tools/package-lock.json index 7775247a1c..c630ca3c71 100644 --- a/search/search-dev-tools/package-lock.json +++ b/search/search-dev-tools/package-lock.json @@ -11980,9 +11980,9 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "dependencies": { "@types/estree": "^1.0.5", From 78044627d05c379c2a7706558662913c0a68f90a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:18:40 +0300 Subject: [PATCH 08/11] chore(deps-dev): Bump sass in /search/search-dev-tools (#5894) Bumps [sass](https://github.com/sass/dart-sass) from 1.79.3 to 1.79.4. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.79.3...1.79.4) --- updated-dependencies: - dependency-name: sass dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- search/search-dev-tools/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/search/search-dev-tools/package-lock.json b/search/search-dev-tools/package-lock.json index c630ca3c71..3745b152ba 100644 --- a/search/search-dev-tools/package-lock.json +++ b/search/search-dev-tools/package-lock.json @@ -10436,9 +10436,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.79.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.3.tgz", - "integrity": "sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==", + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", + "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", "dev": true, "dependencies": { "chokidar": "^4.0.0", From 8d546bdb4c1ffb7d3c064742341afb22b2d5159e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:18:47 +0300 Subject: [PATCH 09/11] chore(deps): Bump preact in /search/search-dev-tools (#5895) Bumps [preact](https://github.com/preactjs/preact) from 10.24.0 to 10.24.1. - [Release notes](https://github.com/preactjs/preact/releases) - [Commits](https://github.com/preactjs/preact/compare/10.24.0...10.24.1) --- updated-dependencies: - dependency-name: preact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- search/search-dev-tools/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/search/search-dev-tools/package-lock.json b/search/search-dev-tools/package-lock.json index 3745b152ba..18884c704d 100644 --- a/search/search-dev-tools/package-lock.json +++ b/search/search-dev-tools/package-lock.json @@ -9774,9 +9774,9 @@ "license": "MIT" }, "node_modules/preact": { - "version": "10.24.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.0.tgz", - "integrity": "sha512-aK8Cf+jkfyuZ0ZZRG9FbYqwmEiGQ4y/PUO4SuTWoyWL244nZZh7bd5h2APd4rSNDYTBNghg1L+5iJN3Skxtbsw==", + "version": "10.24.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.1.tgz", + "integrity": "sha512-PnBAwFI3Yjxxcxw75n6VId/5TFxNW/81zexzWD9jn1+eSrOP84NdsS38H5IkF/UH3frqRPT+MvuCoVHjTDTnDw==", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" From 3a9b9699f063c4b8182bf28897eb58a2814c3ccc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:18:53 +0300 Subject: [PATCH 10/11] chore(deps-dev): Bump @types/node in /__tests__/e2e (#5898) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.6.0 to 22.7.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- __tests__/e2e/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/e2e/package-lock.json b/__tests__/e2e/package-lock.json index e03710fa9d..c9218f02b7 100644 --- a/__tests__/e2e/package-lock.json +++ b/__tests__/e2e/package-lock.json @@ -716,9 +716,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.0.tgz", - "integrity": "sha512-QyR8d5bmq+eR72TwQDfujwShHMcIrWIYsaQFtXRE58MHPTEKUNxjxvl0yS0qPMds5xbSDWtp7ZpvGFtd7dfMdQ==", + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "dev": true, "dependencies": { "undici-types": "~6.19.2" From 8436324b55abb77a40dfcd1380c7efb2fb134217 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Tue, 1 Oct 2024 21:54:48 +0300 Subject: [PATCH 11/11] fix(two-factor): move `load_plugin_textdomain()` to `init` (#5902) --- shared-plugins/two-factor/class-two-factor-core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-plugins/two-factor/class-two-factor-core.php b/shared-plugins/two-factor/class-two-factor-core.php index 6a990c87fd..4d798993c4 100644 --- a/shared-plugins/two-factor/class-two-factor-core.php +++ b/shared-plugins/two-factor/class-two-factor-core.php @@ -93,7 +93,7 @@ class Two_Factor_Core { * @since 0.1-dev */ public static function add_hooks( $compat ) { - add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) ); + add_action( 'init', array( __CLASS__, 'load_textdomain' ) ); add_action( 'init', array( __CLASS__, 'get_providers' ) ); add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 ); add_filter( 'wp_login_errors', array( __CLASS__, 'maybe_show_reset_password_notice' ) );