Skip to content

Commit

Permalink
Add base telemetry library (take 2) (#5869)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Hanif Norman <[email protected]>
Co-authored-by: Alec Geatches <[email protected]>
  • Loading branch information
4 people authored Sep 26, 2024
1 parent 7a0539d commit 406c53d
Show file tree
Hide file tree
Showing 16 changed files with 1,255 additions and 0 deletions.
25 changes: 25 additions & 0 deletions 000-vip-init.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions telemetry/README.md
Original file line number Diff line number Diff line change
@@ -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',
] );
```
26 changes: 26 additions & 0 deletions telemetry/class-telemetry-client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Telemetry: Telemetry client abstract class
*
* @package Automattic\VIP\Telemetry
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry;

use WP_Error;

/**
* Base class for all telemetry client implementations.
*/
abstract class Telemetry_Client {
/**
* Record a batch of events using the telemetry API
*
* @param Telemetry_Event[] $events Array of Tracks_Event objects to record
* @return bool|WP_Error True if batch recording succeeded.
* WP_Error is any error occurred.
*/
abstract public function batch_record_events( array $events, array $common_props = [] );
}
74 changes: 74 additions & 0 deletions telemetry/class-telemetry-event-queue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
/**
* Telemetry: event queue.
*
* @package Automattic\VIP\Telemetry
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry;

use WP_Error;

/**
* Handles queued events to be sent to a telemetry service.
*/
class Telemetry_Event_Queue {

/**
* @var Telemetry_Client The client to use to record events.
*/
private Telemetry_Client $client;

/**
* Events queued to be sent to the telemetry service.
*
* @var array<Telemetry_Event>
*/
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 = [];
}
}
27 changes: 27 additions & 0 deletions telemetry/class-telemetry-event.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
* Telemetry: Telemetry client abstract class
*
* @package Automattic\VIP\Telemetry
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry;

use JsonSerializable;
use WP_Error;

/**
* Base class for all telemetry event implementations.
*/
abstract class Telemetry_Event implements JsonSerializable {

/**
* Returns whether the event can be recorded.
*
* @return bool|WP_Error True if the event is recordable.
* WP_Error is any error occurred.
*/
abstract public function is_recordable();
}
32 changes: 32 additions & 0 deletions telemetry/class-telemetry-system.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* Telemetry: Telemetry System abstract class
*
* @package Automattic\VIP\Telemetry
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry;

use WP_Error;

/**
* Base class for all telemetry system implementations.
*/
abstract class Telemetry_System {
/**
* Records the passed event.
*
* @param string $event_name The event's name.
* @param array<string, mixed> $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;
}
80 changes: 80 additions & 0 deletions telemetry/class-tracks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
/**
* Telemetry: Tracks class
*
* @package Automattic\VIP\Telemetry
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry;

use Automattic\VIP\Telemetry\Tracks\Tracks_Client;
use Automattic\VIP\Telemetry\Tracks\Tracks_Event;
use WP_Error;

/**
* This class comprises the mechanics of sending events to the Automattic
* Tracks system.
*/
class Tracks extends Telemetry_System {

/**
* The prefix for all event names.
*
* @var string
*/
protected string $event_prefix;

/**
* Event queue.
*
* @var Telemetry_Event_Queue
*/
private Telemetry_Event_Queue $queue;

/**
* @param array<string, mixed> 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<string, mixed> $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<string, mixed>|array<empty> $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 );
}
}
Loading

0 comments on commit 406c53d

Please sign in to comment.