Skip to content

Commit

Permalink
Adds Tracks Analytics on backend (#155)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mehmoodak authored Nov 15, 2024
1 parent fa91a56 commit 9c1c659
Show file tree
Hide file tree
Showing 17 changed files with 891 additions and 18 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
43 changes: 42 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions inc/Analytics/EnvironmentConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

namespace RemoteDataBlocks\Analytics;

defined( 'ABSPATH' ) || exit();

use Automattic\VIP\Telemetry\Tracks;
use function Automattic\VIP\Telemetry\Tracks\get_tracks_core_properties;

/**
* Class for environment configuration.
*
* This class abstracts WordPress-specific functions for easy mocking.
*/
class EnvironmentConfig {
/**
* Tracks analytics core properties.
*
* This is set by the Tracks library available in MU Plugins.
*
* @var array<string, mixed>
*/
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;
}
}
170 changes: 170 additions & 0 deletions inc/Analytics/TracksAnalytics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php declare(strict_types = 1);

namespace RemoteDataBlocks\Analytics;

defined( 'ABSPATH' ) || exit();

use RemoteDataBlocks\Editor\BlockManagement\ConfigStore;

/**
* Class to implement Tracks Analytics.
*/
class TracksAnalytics {
/**
* The tracks instance (not using Tracks as type because it is present in MU Plugins codebase).
*/
private static object|null $instance = null;

/**
* Environment configuration.
*/
private static ?EnvironmentConfig $env_config = null;

/**
* Initialize Tracks Analytics based on the environment configuration.
*
* @param EnvironmentConfig $env_config Environment configuration.
*/
public static function init( EnvironmentConfig $env_config ): void {
self::$env_config = $env_config;

// Do not track on local environment.
if ( self::$env_config->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 = '/<!--\s{1}wp:remote-data-blocks\/([^\s]+)\s/';
preg_match_all( $reg_exp, $post->post_content, $matches );
if ( count( $matches[1] ) === 0 ) {
return;
}

// Get data source and track usage.
$track_props = [
'post_status' => $post_status,
'post_type' => $post->post_type,
];
foreach ( $matches[1] as $match ) {
$data_source_type = ConfigStore::get_data_source_type( 'remote-data-blocks/' . $match );
if ( ! $data_source_type ) {
continue;
}

// Calculate stats of each remote data source.
$key = $data_source_type . '_data_source_count';
$track_props[ $key ] = ( $track_props[ $key ] ?? 0 ) + 1;
$track_props['remote_data_blocks_total_count'] = ( $track_props['remote_data_blocks_total_count'] ?? 0 ) + 1;
}

self::record_event( 'remotedatablocks_blocks_usage_stats', $track_props );
}

/**
* Record an event with Tracks.
*
* @param string $event_name The name of the event.
* @param array $props The properties to send with the event.
*
* @return bool True if the event was recorded, false otherwise.
*/
public static function record_event( string $event_name, array $props ): bool {
if ( ! isset( self::$instance ) ) {
return false;
}

self::$instance->record_event( $event_name, $props );

return true;
}

/**
* Get the instance of Tracks.
*/
public static function get_instance(): ?object {
return self::$instance;
}

public static function get_env_config(): ?EnvironmentConfig {
return self::$env_config;
}

/**
* Reset class properties.
*/
public static function reset(): void {
self::$instance = null;
self::$env_config = null;
}
}
9 changes: 8 additions & 1 deletion inc/Config/DataSource/HttpDataSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,17 @@ public function get_image_url(): ?string {
return null;
}

/**
* Get the service name.
*/
public function get_service(): ?string {
return $this->config['service'] ?? null;
}

public function get_slug(): string {
return $this->config['slug'];
}

/**
* @inheritDoc
*/
Expand Down
32 changes: 31 additions & 1 deletion inc/Editor/BlockManagement/ConfigStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public static function init( ?LoggerInterface $logger = null ): void {
* Convert a block title to a block name. Mainly this is to reduce the burden
* of configuration and to ensure that block names are unique (since block
* titles must be unique).
*
*/
public static function get_block_name( string $block_title ): string {
return 'remote-data-blocks/' . sanitize_title( $block_title );
Expand Down Expand Up @@ -69,6 +68,37 @@ public static function is_registered_block( string $block_name ): bool {
return isset( self::$configurations[ $block_name ] );
}

/**
* Get data source type from block name.
*
* @param string $block_name Name of the block.
*/
public static function get_data_source_type( string $block_name ): ?string {
$config = self::get_configuration( $block_name );
if ( ! $config ) {
return null;
}

$queries = $config['queries'];
if ( count( $queries ) === 0 ) {
return null;
}

$data_source_type = null;
foreach ( $queries as $query ) {
if ( ! $query instanceof HttpQueryContext ) {
continue;
}

$data_source_type = $query->get_data_source()->get_service();
if ( $data_source_type ) {
break;
}
}

return $data_source_type;
}

/**
* Return an unprivileged representation of the data sources that can be
* displayed in settings screens.
Expand Down
Loading

0 comments on commit 9c1c659

Please sign in to comment.