-
Notifications
You must be signed in to change notification settings - Fork 103
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
base: develop
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
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. |
*/ | ||
public function __construct() { | ||
// Track events asynchronously (inject pixels in the footer). | ||
add_action( 'admin_footer', array( $this, 'render_tracking_pixels' ) ); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
|
/** | ||
* Outputs a Tracks pixel for every registered event. | ||
*/ | ||
public function render_tracking_pixels(): void { |
There was a problem hiding this comment.
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:
- Detect I'm on a page where I want to track something e.g.
$pagenow === 'edit.php'
- Call
register_events( [ new Tracks_Event( 'edit_loaded', ... ) ] )
during page loads - Pixels automatically fire off on browser render
Is that correct?
There was a problem hiding this comment.
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
There was a problem hiding this 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.
There was a problem hiding this 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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 featuretelemetry
and then pass the some of the properties from$event
as$extra
.
Here's an example:
vip-go-mu-plugins/config/class-sync.php
Lines 222 to 228 in 4d236a4
\Automattic\VIP\Logstash\log2logstash( array( 'severity' => $severity, 'feature' => self::LOG_FEATURE_NAME, 'message' => $message, 'extra' => $extra, ) ); }
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added log2logstash
in 1b308c7
foreach ( $this->events as $event ) { | ||
static::instance()->record_event_synchronously( $event ); | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
// Set event name. | ||
$event->_en = preg_replace( | ||
'/^(?:' . static::EVENT_NAME_PREFIX . ')?(.*)/', | ||
static::EVENT_NAME_PREFIX . '\1', | ||
$event_name | ||
) ?? ''; |
There was a problem hiding this comment.
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_
.
$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; | ||
} |
There was a problem hiding this comment.
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 );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this in 89b0248
// Don't record events during unit tests and CI runs. | ||
if ( 'wptests_capabilities' === wp_get_current_user()->cap_key ) { | ||
return false; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels odd...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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.
telemetry/Tracks/class-tracks.php
Outdated
string $event_name, | ||
array $event_properties = array() | ||
) { | ||
$event = new Tracks_Event( $event_name, $event_properties ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/Tracks/class-tracks.php
Outdated
* Registers the events into their respective WordPress hooks, so they | ||
* can be recorded when the hook fires. | ||
*/ | ||
protected function activate_tracking(): void { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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:
vip-go-mu-plugins/vip-parsely/vip-parsely.php
Lines 37 to 55 in 44e6293
$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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( [ ... ] ) );
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed in 6378910
Tracking in the events would be more explicit this way
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
@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 |
There was a problem hiding this 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', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ), '" />'; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$tracker = new Tracker(); | |
$tracker = new MyPluginTracker(); |
Quality Gate passedIssues Measures |
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 thevip-parsely
directory but meant to be more generic and can be used by other plugins.Changelog Description
Added
Pre-review checklist
Please make sure the items below have been covered before requesting a review:
Pre-deploy checklist
Steps to Test
This is a base library so no concrete implementation to test yet.