Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Pendo Track events and Page/Feature events #6140

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion 000-vip-init.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,20 @@

// Load the Telemetry files
require_once __DIR__ . '/telemetry/class-telemetry-system.php';
require_once __DIR__ . '/telemetry/class-tracks.php';
require_once __DIR__ . '/telemetry/class-telemetry-client.php';
require_once __DIR__ . '/telemetry/class-telemetry-event-queue.php';
require_once __DIR__ . '/telemetry/class-telemetry-event.php';
require_once __DIR__ . '/telemetry/class-telemetry.php';
require_once __DIR__ . '/telemetry/tracks/class-tracks.php';
require_once __DIR__ . '/telemetry/tracks/class-tracks-event-dto.php';
require_once __DIR__ . '/telemetry/tracks/class-tracks-event.php';
require_once __DIR__ . '/telemetry/tracks/class-tracks-client.php';
require_once __DIR__ . '/telemetry/tracks/tracks-utils.php';
require_once __DIR__ . '/telemetry/pendo/class-pendo.php';
require_once __DIR__ . '/telemetry/pendo/class-pendo-track-client.php';
require_once __DIR__ . '/telemetry/pendo/class-pendo-track-event-dto.php';
require_once __DIR__ . '/telemetry/pendo/class-pendo-track-event.php';
require_once __DIR__ . '/telemetry/pendo/pendo-utils.php';

add_action( 'init', [ WPComVIP_Restrictions::class, 'instance' ] );

Expand Down
50 changes: 26 additions & 24 deletions telemetry/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
# VIP Telemetry Library

## Tracks
For most use cases, you should use the `Telemetry` class, which will send events to all configured telemetry systems:

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.
- Tracks is an event tracking tool used to understand user behavior within Automattic.
- Pendo is a product analytics tool used by WPVIP to understand user behavior on WordPress admin screens. Note that Pendo is completely disabled in some environments; see `Pendo::is_pendo_enabled_for_environment()` for details.

### How to use
If you would like to configure and use the telemetry systems individually, see the section below.

Example:
## How to use

In this simple functional example, we track changes to the status of posts. Note that `myplugin_` is passed as the first argument to the `Telemetry` constructor; it will be prepended to all event names.

```php
use Automattic\VIP\Telemetry\Tracks;
use Automattic\VIP\Telemetry\Telemetry;

function track_post_status( $new_status, $old_status, $post ) {
$tracks = new Tracks( 'myplugin_' );
$telemetry = new Telemetry( 'myplugin_' );

$tracks->record_event( 'post_status_changed', [
$telemetry->record_event( 'post_status_changed', [
'new_status' => $new_status,
'old_status' => $old_status,
'post_id' => $post->ID,
Expand All @@ -23,26 +26,22 @@ function track_post_status( $new_status, $old_status, $post ) {
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:
To reduce code duplication, you may wish to create a class to encapsulate tracking logic:

```php
namespace MyPlugin\Telemetry;

use Automattic\VIP\Telemetry\Tracks;

class MyPluginTracker {
protected $tracks;
protected Telemetry $telemetry;

public function __construct() {
$this->tracks = new Tracks( 'myplugin_' );
$this->telemetry = new Telemetry( 'myplugin_' );
}

public function register_events() {
public function init() {
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', [
$this->telemetry->record_event( 'post_status_changed', [
'new_status' => $new_status,
'old_status' => $old_status,
'post' => (array) $post,
Expand All @@ -51,17 +50,20 @@ class MyPluginTracker {
}
```

With the class above, you can then initiate event tracking in the main plugin file with these lines:
If you would like to provide global properties to all events, you can pass an array of properties to the `Telemetry` constructor:

```php
$tracker = new MyPluginTracker();
$tracker->register_events();
new Telemetry( 'myplugin_', [ 'plugin_version' => '1.2.3' ] );
```

If necessary to provide global properties to all events, you can pass an array of properties to the `Tracks` constructor:
## Using Tracks and Pendo individually

If you wish, you can configure and use `Tracks` and `Pendo` classes individually. They have the same API as the `Telemetry` class.

```php
$this->tracks = new Tracks( 'myplugin_', [
'plugin_version' => '1.2.3',
] );
```
use Automattic\VIP\Telemetry\Pendo;
use Automattic\VIP\Telemetry\Tracks;

new Pendo( 'myplugin_', [ /* global properties */ ] );
new Tracks( 'myplugin_', [ /* global properties */ ] );
```
51 changes: 49 additions & 2 deletions telemetry/class-telemetry-event.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,59 @@
* Base class for all telemetry event implementations.
*/
abstract class Telemetry_Event implements JsonSerializable {
/**
* Variable containing the event's data or a WP_Error if an error was
* encountered during the event's creation or validation.
*
* @var object|WP_Error
*/
protected object $data;

/**
* Generate the event data.
*
* @return object|WP_Error A serializable object if the event is valid, otherwise a WP_Error
*/
abstract protected function generate(): object;

/**
* Wraps generate() and stores the result to prevent multiple calls.
*
* @return object|WP_Error A serializable object if the event is valid, otherwise a WP_Error
*/
public function get_data(): object {
if ( ! isset( $this->data ) ) {
$this->data = $this->generate();
}

return $this->data;
}

/**
* 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();
public function is_recordable(): bool|WP_Error {
$data = $this->get_data();

if ( is_wp_error( $data ) ) {
return $data;
}

return true;
}

/**
* 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;
}
}
75 changes: 75 additions & 0 deletions telemetry/class-telemetry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/**
* Telemetry: Telemetry class
*
* @package Automattic\VIP\Telemetry
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry;

use Automattic\VIP\Telemetry\Pendo;
use Automattic\VIP\Telemetry\Tracks;
use WP_Error;

/**
* This class comprises the mechanics of sending events to all configured
* telemetry systems.
*/
class Telemetry extends Telemetry_System {

/**
* Configured telemetry systems.
*
* @var array<Telemetry_Systems>
*/
private array $systems = [];

/**
* Telemetry constructor.
*
* @param string|null $event_prefix The prefix for all event names, or null to use the default prefix.
* @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 Telemetry_Client|null $client The client instance to use. Falls back to the default client when none provided.
*/
public function __construct( string $event_prefix = null, array $global_event_properties, Telemetry_Event_Queue $queue = null, Telemetry_Client $client = null ) {
$this->systems = [
new Pendo( $event_prefix, $global_event_properties, $queue, $client ),
new Tracks( $event_prefix, $global_event_properties, $queue, $client ),
];
}

/**
* Records an event to the configured telemetry systems.
*
* 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 all events succeeded.
* False if any of the telemetry systems are disabled.
* WP_Error if any event recording failed.
*/
public function record_event(
string $event_name,
array $event_properties = []
): bool|WP_Error {
$return_value = true;

foreach ( $this->systems as $system ) {
$result = $system->record_event( $event_name, $event_properties );

// If any system fails to record the event, return the error.
if ( is_wp_error( $result ) ) {
return $result;
}

$return_value = $return_value && $result;
}

return $return_value;
}
}
104 changes: 104 additions & 0 deletions telemetry/pendo/class-pendo-track-client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/**
* Telemetry: Pendo Track Client class
*
* @package Automattic\VIP\Telemetry\Pendo
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry\Pendo;

use WP_Error;
use WP_Http;
use Automattic\VIP\Telemetry\Telemetry_Client;
use function Automattic\VIP\Logstash\log2logstash;

/**
* Handles sending Track events to the Pendo API.
*/
class Pendo_Track_Client extends Telemetry_Client {
/**
* Pendo API endpoint for "Track events"
*
* https://support.pendo.io/hc/en-us/articles/360032294151-Track-Events
*/
protected const PENDO_ENDPOINT = 'https://app.pendo.io/data/track';

/**
* @var string
*/
private string|null $api_key;

/**
* @var WP_Http
*/
private WP_Http $http;

/**
* Constructor.
*/
public function __construct( string|null $api_key = null, WP_Http $http = null ) {
$this->api_key = $api_key;
$this->http = $http ?? _wp_http_get_object();
}

/**
* Record a batch of Track events using the Pendo API
*
* @param Pendo_Track_Event[] $events Array of Pendo_Track_Event objects to record
* @return bool|WP_Error True on success or WP_Error if any error occured.
*/
public function batch_record_events( array $events, array $common_props = [] ): bool|WP_Error {
if ( empty( $this->api_key ) ) {
log2logstash( [
'severity' => 'error',
'feature' => 'telemetry',
'message' => 'Pendo Track secret key is not defined',
] );
return new WP_Error( 'pendo_track_secret_key_not_defined', 'Pendo Track secret key is not defined' );
}

// Filter out invalid events.
$valid_events = array_filter( $events, function ( $event ) {
return $event instanceof Pendo_Track_Event && $event->is_recordable() === true;
} );

// No events? Nothing to do.
if ( empty( $valid_events ) ) {
return true;
}

// Pendo's API does not accept bulk Track events, so we need to send them one
// at a time. :/
foreach ( $valid_events as $body ) {
$response = $this->http->post(
self::PENDO_ENDPOINT,
[
'body' => wp_json_encode( $body ),
'user-agent' => 'viptelemetry',
'headers' => array(
'Content-Type' => 'application/json',
'x-pendo-integration-key' => $this->api_key,
),
]
);

// Short-circuit on the first error.
if ( is_wp_error( $response ) ) {
log2logstash( [
'severity' => 'error',
'feature' => 'telemetry',
'message' => 'Error recording events to Pendo',
'extra' => [
'error' => $response->get_error_messages(),
],
] );

return $response;
}
}

return true;
}
}
38 changes: 38 additions & 0 deletions telemetry/pendo/class-pendo-track-event-dto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* Telemetry: Pendo Track Event DTO class
*
* @package Automattic\VIP\Telemetry\Pendo
*/

declare(strict_types=1);

namespace Automattic\VIP\Telemetry\Pendo;

/**
* Class that holds necessary properties of Pendo "Track events".
*
* https://engageapi.pendo.io/#e45be48e-e01f-4f0a-acaa-73ef6851c4ac
*/
class Pendo_Track_Event_DTO {
/** @var string $accountId */
public string $accountId; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase

/** @var object $context */
public object $context;

/** @var string $event */
public string $event;

/** @var object $properties */
public object $properties;

/** @var float $timestamp */
public float $timestamp;

/** @var string $type */
public string $type = 'track';

/** @var string $visitorId */
public string $visitorId; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
}
Loading
Loading