From 9c1c6590e89e2ea4ca388c90b79e3018905fac79 Mon Sep 17 00:00:00 2001 From: Mehmood Ahmad <31419912+mehmoodak@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:42:34 +0500 Subject: [PATCH] Adds Tracks Analytics on backend (#155) Adds Tracks Analytics by using the Telemetry library available in MU Plugins which will only work if site is hosted on VIP Platform or if the analytics is enabled via filter. --- composer.json | 3 +- composer.lock | 43 ++- inc/Analytics/EnvironmentConfig.php | 81 +++++ inc/Analytics/TracksAnalytics.php | 170 +++++++++++ inc/Config/DataSource/HttpDataSource.php | 9 +- inc/Editor/BlockManagement/ConfigStore.php | 32 +- inc/REST/DataSourceController.php | 61 +++- psalm.xml | 2 + remote-data-blocks.php | 3 + src/data-sources/hooks/useDataSources.ts | 12 +- tests/inc/Analytics/EnvironmentConfigTest.php | 77 +++++ tests/inc/Analytics/TracksAnalyticsTest.php | 277 ++++++++++++++++++ tests/inc/Config/HttpDataSourceTest.php | 23 ++ tests/inc/Editor/ConfigStoreTest.php | 32 ++ tests/inc/bootstrap.php | 1 + tests/inc/stubs.php | 22 +- tests/inc/test-utils.php | 61 ++++ 17 files changed, 891 insertions(+), 18 deletions(-) create mode 100644 inc/Analytics/EnvironmentConfig.php create mode 100644 inc/Analytics/TracksAnalytics.php create mode 100644 tests/inc/Analytics/EnvironmentConfigTest.php create mode 100644 tests/inc/Analytics/TracksAnalyticsTest.php create mode 100644 tests/inc/Config/HttpDataSourceTest.php create mode 100644 tests/inc/Editor/ConfigStoreTest.php create mode 100644 tests/inc/test-utils.php diff --git a/composer.json b/composer.json index 06279533..65ef6b5f 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ "slevomat/coding-standard": "^8.15", "php-stubs/wordpress-stubs": "^6.6", "psalm/phar": "^5.26", - "mockery/mockery": "^1.6" + "mockery/mockery": "^1.6", + "php-stubs/wordpress-globals": "^0.2.0" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 51eb7b72..78bcf23e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "829c7d99f898737644ae2dd3d45313e0", + "content-hash": "5e0d80162087cfd0477746d29ec55e89", "packages": [ { "name": "galbar/jsonpath", @@ -1292,6 +1292,47 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-stubs/wordpress-globals", + "version": "v0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-globals.git", + "reference": "748a1fb2ae8fda94844bd0545935095dbf404b32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-globals/zipball/748a1fb2ae8fda94844bd0545935095dbf404b32", + "reference": "748a1fb2ae8fda94844bd0545935095dbf404b32", + "shasum": "" + }, + "require-dev": { + "php": "~7.1" + }, + "suggest": { + "php-stubs/wordpress-stubs": "Up-to-date WordPress function and class declaration stubs", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Global variables and global constants from WordPress core.", + "homepage": "https://github.com/php-stubs/wordpress-globals", + "keywords": [ + "PHPStan", + "constants", + "globals", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-globals/issues", + "source": "https://github.com/php-stubs/wordpress-globals/tree/master" + }, + "time": "2020-01-13T06:12:59+00:00" + }, { "name": "php-stubs/wordpress-stubs", "version": "v6.6.2", diff --git a/inc/Analytics/EnvironmentConfig.php b/inc/Analytics/EnvironmentConfig.php new file mode 100644 index 00000000..fe52b93e --- /dev/null +++ b/inc/Analytics/EnvironmentConfig.php @@ -0,0 +1,81 @@ + + */ + private array $tracks_core_props = []; + + public function __construct() { + if ( function_exists( 'Automattic\VIP\Telemetry\Tracks\get_tracks_core_properties' ) ) { + add_action( 'init', function (): void { + /** @psalm-suppress UndefinedFunction */ + $this->tracks_core_props = get_tracks_core_properties(); + } ); + } + } + + public function is_enabled_via_filter(): bool { + return apply_filters( 'remote_data_blocks_enable_tracks_analytics', false ) ?? false; + } + + public function get_tracks_lib_class(): ?string { + if ( ! class_exists( 'Automattic\VIP\Telemetry\Tracks' ) ) { + return null; + } + + return Tracks::class; + } + + public function is_wpvip_site(): bool { + if ( ! isset( $this->tracks_core_props['hosting_provider'] ) ) { + return false; + } + + return 'wpvip' === $this->tracks_core_props['hosting_provider']; + } + + public function is_local_env(): bool { + if ( ! isset( $this->tracks_core_props['vipgo_env'] ) ) { + return false; + } + + return 'local' === $this->tracks_core_props['vipgo_env']; + } + + public function is_remote_data_blocks_plugin( string|null $plugin_path ): bool { + return 'remote-data-blocks/remote-data-blocks.php' === $plugin_path; + } + + public function should_track_post_having_remote_data_blocks( int $post_id ): bool { + // Ensure this is not an auto-save or revision. + if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { + return false; + } + + return true; + } + + /** + * Get the core properties to be sent with each event. + */ + public function get_tracks_core_properties(): array { + return $this->tracks_core_props; + } +} diff --git a/inc/Analytics/TracksAnalytics.php b/inc/Analytics/TracksAnalytics.php new file mode 100644 index 00000000..f6fa05e2 --- /dev/null +++ b/inc/Analytics/TracksAnalytics.php @@ -0,0 +1,170 @@ +is_local_env() ) { + return; + } + + $tracks_class = self::$env_config->get_tracks_lib_class(); + if ( ! $tracks_class ) { + return; + } + + if ( self::$env_config->is_wpvip_site() || self::$env_config->is_enabled_via_filter() ) { + self::$instance = new $tracks_class( '', self::get_global_properties() ); + self::setup_tracking_via_hooks(); + } + } + + /** + * Get `Tracks` global properties to be sent with each event. + */ + public static function get_global_properties(): array { + $rdb_specific_props = [ + 'plugin_version' => defined( 'REMOTE_DATA_BLOCKS__PLUGIN_VERSION' ) ? constant( 'REMOTE_DATA_BLOCKS__PLUGIN_VERSION' ) : '', + ]; + + return array_merge( $rdb_specific_props, self::$env_config->get_tracks_core_properties() ); + } + + private static function setup_tracking_via_hooks(): void { + // WordPress Dashboard Hooks. + add_action( 'activated_plugin', [ __CLASS__, 'track_plugin_activation' ] ); + add_action( 'deactivated_plugin', [ __CLASS__, 'track_plugin_deactivation' ] ); + add_action( 'save_post', [ __CLASS__, 'track_remote_data_blocks_usage' ], 10, 2 ); + } + + /** + * Activation hook. + * + * @param string $plugin_path Path of the plugin that was activated. + */ + public static function track_plugin_activation( string $plugin_path ): void { + if ( ! self::$env_config->is_remote_data_blocks_plugin( $plugin_path ) ) { + return; + } + + self::record_event( 'remotedatablocks_plugin_toggle', [ 'action' => 'activate' ] ); + } + + /** + * Deactivation hook. + * + * @param string $plugin_path Path of the plugin that was deactivated. + */ + public static function track_plugin_deactivation( string $plugin_path ): void { + if ( ! self::$env_config->is_remote_data_blocks_plugin( $plugin_path ) ) { + return; + } + + self::record_event( 'remotedatablocks_plugin_toggle', [ 'action' => 'deactivate' ] ); + } + + /** + * Track usage of Remote Data Blocks. + * + * @param int $post_id Post ID. + * @param \WP_Post $post Post object. + */ + public static function track_remote_data_blocks_usage( int $post_id, object $post ): void { + if ( ! self::$env_config->should_track_post_having_remote_data_blocks( $post_id ) ) { + return; + } + + $post_status = $post->post_status; + if ( 'publish' !== $post_status ) { + return; + } + + // Regular expression to match all remote data blocks present in the post content. + $reg_exp = '/

Tonal Accessories Shelf (Coffee Oak)

Our floating shelf is the perfect way to store all your Tonal accessories. Its sleek, versatile design makes this an easy fit with any style room. Available in Coffee Oak (seen here), as well as Matte Black and Light Aged Ash. Made in the U.S.

$272.99

T-Locks (Pack of 4)

No detail is too small. Tonal’s proprietary T-Locks let you swap out Tonal accessories with a quick push and twist to lock everything in place.

$42.99

Break

Pearl room

Panel

', + ] ); + } + + public function testRecordEventDoesNothingIfInstanceIsNotSet(): void { + /** @var TracksAnalytics|MockObject */ + $obj = new TracksAnalytics(); + + $result = $obj->record_event( 'name', [] ); + + $this->assertEquals( false, $result ); + } + + public function testRecordEventTracksTheEventIfInstanceIsSet(): void { + $mock_tracks = $this->getMockBuilder( MockTracks::class )->onlyMethods( [ 'record_event' ] )->getMock(); + $mock_tracks->expects( $this->exactly( 1 ) )->method( 'record_event' )->with( 'event_name', [ 'event_props' ] ); + /** @var TracksAnalytics|MockObject */ + $obj = new TracksAnalytics(); + set_private_property( TracksAnalytics::class, $obj, 'instance', $mock_tracks ); + + $result = $obj->record_event( 'event_name', [ 'event_props' ] ); + + $this->assertEquals( true, $result ); + } + + public function testResetMethod(): void { + $obj = new TracksAnalytics(); + set_private_property( TracksAnalytics::class, $obj, 'instance', new MockTracks() ); + TracksAnalytics::init( new EnvironmentConfig() ); + + $this->assertInstanceOf( MockTracks::class, TracksAnalytics::get_instance() ); + $this->assertInstanceOf( EnvironmentConfig::class, TracksAnalytics::get_env_config() ); + + TracksAnalytics::reset(); + + $this->assertEquals( null, TracksAnalytics::get_instance() ); + $this->assertEquals( null, TracksAnalytics::get_env_config() ); + } +} diff --git a/tests/inc/Config/HttpDataSourceTest.php b/tests/inc/Config/HttpDataSourceTest.php new file mode 100644 index 00000000..bf51c0a4 --- /dev/null +++ b/tests/inc/Config/HttpDataSourceTest.php @@ -0,0 +1,23 @@ +http_data_source = MockDataSource::from_array( [], new MockValidator() ); + + $this->assertNull( $this->http_data_source->get_service() ); + } + + public function testGetServiceMethodReturnsCorrectValue(): void { + $this->http_data_source = MockDataSource::from_array( MockDataSource::MOCK_CONFIG, new MockValidator() ); + + $this->assertEquals( 'mock', $this->http_data_source->get_service() ); + } +} diff --git a/tests/inc/Editor/ConfigStoreTest.php b/tests/inc/Editor/ConfigStoreTest.php new file mode 100644 index 00000000..76a7e739 --- /dev/null +++ b/tests/inc/Editor/ConfigStoreTest.php @@ -0,0 +1,32 @@ +assertNull( ConfigStore::get_data_source_type( 'block_name' ) ); + } + + public function testGetDataSourceReturnsNullIfThereAreNoQueries(): void { + ConfigStore::init(); + ConfigStore::set_configuration( 'block_name', [ 'queries' => [] ] ); + + $this->assertNull( ConfigStore::get_data_source_type( 'block_name' ) ); + } + + public function testGetDataSourceReturnsDataSource(): void { + ConfigStore::init(); + ConfigStore::set_configuration( 'airtable_remote_blocks', [ + 'queries' => [ new HttpQueryContext( AirtableDataSource::create( 'access_token', 'base_id', [], 'Name' ) ) ], + ] ); + + $this->assertEquals( 'airtable', ConfigStore::get_data_source_type( 'airtable_remote_blocks' ) ); + } +} diff --git a/tests/inc/bootstrap.php b/tests/inc/bootstrap.php index b6fdaa02..967645bd 100644 --- a/tests/inc/bootstrap.php +++ b/tests/inc/bootstrap.php @@ -10,3 +10,4 @@ require_once __DIR__ . '/../../inc/Integrations/constants.php'; require_once __DIR__ . '/../../functions.php'; require_once __DIR__ . '/stubs.php'; +require_once __DIR__ . '/test-utils.php'; diff --git a/tests/inc/stubs.php b/tests/inc/stubs.php index e0f08c43..64ae441e 100644 --- a/tests/inc/stubs.php +++ b/tests/inc/stubs.php @@ -1,11 +1,17 @@ getProperty( $property_name ); + + /** + * @psalm-suppress UnusedMethodCall + */ + $property->setAccessible( true ); + + return $property; +} + +/** + * Overrides the value of a private property on a given object. This is + * useful when mocking the internals of a class. + * + * Note that the property will no longer be private after setAccessible is + * called. + * + * @param class-string $class_name The fully qualified class name, including namespace. + * @param object $object_instance The object instance on which to set the value. + * @param string $property_name The name of the private property to override. + * @param mixed $value The value to set. + */ +function set_private_property( + string $class_name, + $object_instance, + string $property_name, + $value +): void { + $property = get_private_property( $class_name, $property_name ); + $property->setValue( $object_instance, $value ); +} + +/** + * Gets private method of a class. + * + * @param class-string $class_name Name of the class. + * @param string $method Name of the method. + * @return ReflectionMethod + */ +function get_private_method( string $class_name, string $method ): ReflectionMethod { + $reflector = new ReflectionClass( $class_name ); + $method = $reflector->getMethod( $method ); + + /** + * @psalm-suppress UnusedMethodCall + */ + $method->setAccessible( true ); + + return $method; +}