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 base telemetry library #5770

Open
wants to merge 33 commits into
base: develop
Choose a base branch
from
Open

Add base telemetry library #5770

wants to merge 33 commits into from

Conversation

hanifn
Copy link
Contributor

@hanifn hanifn commented Aug 5, 2024

Description

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 meant to be more generic and can be used by other plugins.

Changelog Description

Added

  • Added new Telemetry lib for A8C Tracks integration

Pre-review checklist

Please make sure the items below have been covered before requesting a review:

  • This change works and has been tested locally or in Codespaces (or has an appropriate fallback).
  • This change works and has been tested on a sandbox.
  • This change has relevant unit tests (if applicable).
  • This change uses a rollout method to ease with deployment (if applicable - especially for large scale actions that require writes).
  • This change has relevant documentation additions / updates (if applicable).
  • I've created a changelog description that aligns with the provided examples.

Pre-deploy checklist

  • VIP staff: Ensure any alerts added/updated conform to internal standards (see internal documentation).

Steps to Test

This is a base library so no concrete implementation to test yet.

@hanifn hanifn self-assigned this Aug 5, 2024
@hanifn hanifn requested a review from a team as a code owner August 5, 2024 10:03
Copy link

codecov bot commented Aug 5, 2024

Codecov Report

Attention: Patch coverage is 33.33333% with 158 lines in your changes missing coverage. Please review.

Project coverage is 29.70%. Comparing base (f18c1d1) to head (51802ea).

Files with missing lines Patch % Lines
telemetry/tracks/class-tracks-client.php 0.00% 96 Missing ⚠️
telemetry/tracks/class-tracks-event.php 58.77% 54 Missing ⚠️
telemetry/tracks/class-tracks.php 25.00% 6 Missing ⚠️
telemetry/class-telemetry-system.php 0.00% 2 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #5770      +/-   ##
=============================================
+ Coverage      29.64%   29.70%   +0.06%     
- Complexity      4760     4817      +57     
=============================================
  Files            281      285       +4     
  Lines          20534    20759     +225     
=============================================
+ Hits            6087     6167      +80     
- Misses         14447    14592     +145     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

*/
public function __construct() {
// Track events asynchronously (inject pixels in the footer).
add_action( 'admin_footer', array( $this, 'render_tracking_pixels' ) );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only tracks WP Admin requests, is that intentional?
for frontend we'd need to use wp_footer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is intentional. We're limiting this to WP admin for now. I'm also still trying to figure out a good way to make this part flexible so that where it renders the pixel is determined by the plugin implementation 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it be consequential to the plugins?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be worth expanding this to the frontend as well imo. If we wanted, we could add a marker in the event to say when it's frontend or WP Admin and target the event recording based on that but I don't think it's worth it?

@rinatkhaziev
Copy link
Contributor

  1. Besides the inline feedback, let's drop go from vipgo.
  2. Needs tests

/**
* Outputs a Tracks pixel for every registered event.
*/
public function render_tracking_pixels(): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hanifn How are the actual pixel-style tracking pixels used? I'm more used to the server-side only pixel requests, so just want to understand. Is it:

  1. Detect I'm on a page where I want to track something e.g. $pagenow === 'edit.php'
  2. Call register_events( [ new Tracks_Event( 'edit_loaded', ... ) ] ) during page loads
  3. Pixels automatically fire off on browser render

Is that correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alecgeatches Yes that is about how it works although the register_events method no longer exist now. Any events that are triggered after the pixel rendered will be handled during the shutdown action by the record_remaining_events method

Copy link
Contributor

@alecgeatches alecgeatches left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Agree with Rinat that tests would be great, especially as a demo of how to build an event and fire it off.

Copy link
Member

@mjangda mjangda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @hanifn. I've added some comments / questions.

$pixel_url = static::instance()->generate_pixel_url( $event );

if ( null === $pixel_url ) {
return new WP_Error(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will know about these errors? We may want to use log2logstash to track them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These errors would eventually be returned to the callbacks that are hooked into the filters we're using to track events. The idea is to let the plugins implementing this handle the errors however they want to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should handle this in the library rather than asking each plugin to handle the error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to use log2logstash to track them.
I second that, we can set feature telemetry and then pass the some of the properties from $event as $extra.
Here's an example:

\Automattic\VIP\Logstash\log2logstash( array(
'severity' => $severity,
'feature' => self::LOG_FEATURE_NAME,
'message' => $message,
'extra' => $extra,
) );
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with @rinatkhaziev and @mjangda, we should handle this in the library so errors are raised uniformly across all implementations if they originate from within the library

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added log2logstash in 1b308c7

Comment on lines 172 to 174
foreach ( $this->events as $event ) {
static::instance()->record_event_synchronously( $event );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The REST API supports bulk events which would be better since we could just make one remote request instead of multiples: https://github.com/Automattic/vip-cli/blob/2a8c90afa9ed2e5d784e820c5da20183094b6711/src/lib/analytics/clients/tracks.ts#L84-L105

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good idea! I'll look into that 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 7ae681a

* @param array<string, mixed>|array<empty> $event_properties Any event properties to be processed.
* @return stdClass The resulting event object with processed properties.
*/
protected static function process_properties(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the use of static methods for this is a bit odd.

Copy link
Contributor Author

@hanifn hanifn Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied from the Parsely plugin so I'm not entirely sure the reason for it.

Okay I figured it out. This is because the method is called in the constructor when the object has not been instantiated yet.

Comment on lines 104 to 109
// Set event name.
$event->_en = preg_replace(
'/^(?:' . static::EVENT_NAME_PREFIX . ')?(.*)/',
static::EVENT_NAME_PREFIX . '\1',
$event_name
) ?? '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not totally clear what the preg_replace is doing here.

We should also set a different prefix for local environments (e.g. vip_dev_.

Comment on lines 150 to 156
$app_id = constant( 'VIP_GO_APP_ID' );
if ( is_integer( $app_id ) && 0 < $app_id ) {
$event->_ui = $app_id . '_' . $wp_user_id;
$event->_ut = 'vip_go_app_wp_user';

return $event;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use an identifier that is pseudo-anonymized but still allows tracking the user across different environments (e.g. if I'm working on wpvip.com and docs.wpvip.com, I should not be counted as two different users).

Maybe a hash of the email?

$salt = constant( 'VIP_TELEMETRY_SALT' ); // this would be private but globally shared across the platform
$tracks_user_id = hash_hmac( 'sha256', $user->user_email, $salt );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this in 89b0248

Comment on lines 78 to 81
// Don't record events during unit tests and CI runs.
if ( 'wptests_capabilities' === wp_get_current_user()->cap_key ) {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels odd...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to stop events from being recorded during tests, is this not the correct way to do it? Or is it just unnecessary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More just that it's happening in an unusual spot in the code. Feels like we should do this as late as possible (i.e. before the request is sent).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_recordable method is being called by methods like record_event_synchronously which are the methods that makes the request. This seems pretty late to me though 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one way that we limited such tracking from happening (for MC Stats) to only production sites. This would be worth considering so it's done pretty early on.

string $event_name,
array $event_properties = array()
) {
$event = new Tracks_Event( $event_name, $event_properties );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the Tracks Event had some errors, we should log those somewhere so we have visibility into failures.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my changes in 1b308c7 should cover this

telemetry/class-telemetry-system.php Outdated Show resolved Hide resolved
* Registers the events into their respective WordPress hooks, so they
* can be recorded when the hook fires.
*/
protected function activate_tracking(): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should get rid of this abstraction and make all our our tracking explicit via record_event() calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would still need a way register all the callbacks to the appropriate filters and record_event() is specifically used by the callbacks to send the events to the Tracks system so I'm not sure to achieve what you're asking 🤔

Copy link
Member

@mjangda mjangda Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that rather than abstracting like so:

$tracks = new Tracks();
$telemetry = new Telemetry( $tracks );
require_once __DIR__ . '/Telemetry/Events/track-settings-page-loaded.php';
$telemetry->register_event(
array(
'action_hook' => 'load-settings_page_parsely',
'callable' => 'Automattic\VIP\Parsely\Telemetry\track_settings_page_loaded',
)
);
require_once __DIR__ . '/Telemetry/Events/track-option-updated.php';
$telemetry->register_event(
array(
'action_hook' => 'update_option_parsely',
'callable' => 'Automattic\VIP\Parsely\Telemetry\track_option_updated',
'accepted_args' => 2,
)
);

With this approach the registration of the hook is separated too far away the actual tracking code which makes it harder to debug.

We should let plugins do the extra work needed to record what they need:

add_action( 'transition_post_status', function( $new, $old, $post ) {
    Automattic\VIP\Telemetry::get_instance()->record_event( 'post_status_changed', [ ... ] );
}, 10, 3 );

This requires more code but keeps the code more explicit and easier to read / follow / debug.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Automattic/vip-platform-cantina what do y'all think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @mjangda , and I don't even think this will be more code?

And if a particular plugin wants to enrich the data somehow it can do something like where it would make sure that the data has the necessary event properies

Automattic\VIP\Telemetry::get_instance()->record_event( 'post_status_changed', enrich_track_event( [ ... ] ) );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the above.

This also leads to a good point - we should add a README for this library with an example of how to use it. That'll help to simplify it down once you see how a plugin would implement it. IMO, it should be as easy to follow as the send_pixel method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 6378910

telemetry/Tracks/class-tracks-event.php Outdated Show resolved Hide resolved
@mjangda
Copy link
Member

mjangda commented Aug 15, 2024

Thanks for the fixes, @hanifn. Based on the latest changes, is this how a typical implementation would work?

$tracker = new Tracks( 'vip_bakery_' );
$tracker->record_event( 'bakery_opened' );

If we want to abstract a bit for easier re-usability, that could look something like this:

namespace Automattic\VIP\Bakery\Analytics;

use Automattic\VIP\Telemetry\Tracks;

function record_event( $event_name, $event_props = [] ) {
    static $tracker = new Tracks( 'vip_bakery_' ); // Our prefix for the events will be 'vip_bakery_'

    static $global_props = [
        'delicious' => 'always',
    ];


    $merged_props = array_merge( $global_props, event_props ); // We should probably allow passing global props to the `Tracks` constructor and handle this there.

    $tracker->record_event( $event_name, $merged_props );
}

Then, elsewhere in our code:

use function Automattic\VIP\Bakery\Analytics\record_event;

record_event( 'bakery_open' );

function bake_cookies( $number ) {
    // ...

    record_event( 'cookies_baked', [ 'number' => $number ] );
}

add_action( 'vip_bakery_eat_cookie', function( $cookie_type ) {
    record_event( 'cookies_eaten', [ 'cookie_type' => $cookie_type ] );
}, 10 );

Every plugin will need the same boilerplate but it's not so bad.

This should work better for cross-app tracking
@hanifn
Copy link
Contributor Author

hanifn commented Aug 19, 2024

Based on the latest changes, is this how a typical implementation would work?

@mjangda Yes that looks about right. The client plugin would need to have appropriate functions or methods to hook into the events they want to track and those functions or methods has to call the Tracks->record_event() method. How they achieve that is entirely up to them since this new approach is pretty flexible

Copy link
Contributor

@ingeniumed ingeniumed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got a minor comment about the errors, but a bigger one about the dual pixel and shutdown approach that I think we should tackle.


if ( null === $pixel_url ) {
log2logstash( [
'severity' => 'error',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an error, or warning? Considering the status code is 400 wondering if that's better? Just don't want the customer's logs filled up with errors because the plugin didn't implement an event correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the library can't generate the proper pixel URL then the site won't be able to send events for AJAX requests. I think that's a pretty serious problem that should be logged IMHO

continue;
}

echo '<img style="position: fixed;" src="', esc_url( $pixel_url ), '" />';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards starting with just doing this on shutdown, rather than the pixel approach and shutdown approach. I think we should try to minimize our impact on the page as much as possible. Considering this is telemetry, having it go on shutdown at the very should be fine imo.

Redundant since we already have batch recording on shutdown
With the class above, you can then initiate event tracking in the main plugin file with these lines:

```php
$tracker = new Tracker();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$tracker = new Tracker();
$tracker = new MyPluginTracker();

Copy link

sonarcloud bot commented Sep 18, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants