class-platform-checkout-tracker.php
8.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
<?php
/**
* Class Platform_Checkout_Tracker
*
* @package WooCommerce\Payments
*/
namespace WCPay;
use Jetpack_Tracks_Client;
use Jetpack_Tracks_Event;
use WC_Payments_Features;
use WP_Error;
defined( 'ABSPATH' ) || exit; // block direct access.
/**
* Track Platform Checkout related events
*/
class Platform_Checkout_Tracker extends Jetpack_Tracks_Client {
/**
* Platform checkout event prefix
*
* @var string
*/
private static $prefix = 'woocommerceanalytics';
/**
* WCPay http interface.
*
* @var Object
*/
private $http;
/**
* Constructor.
*
* @param \WC_Payments_Http_Interface $http A class implementing WC_Payments_Http_Interface.
*/
public function __construct( $http ) {
$this->http = $http;
add_action( 'wp_ajax_platform_tracks', [ $this, 'ajax_tracks' ] );
add_action( 'wp_ajax_nopriv_platform_tracks', [ $this, 'ajax_tracks' ] );
// Actions that should result in recorded Tracks events.
add_action( 'woocommerce_after_checkout_form', [ $this, 'checkout_start' ] );
add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after', [ $this, 'checkout_start' ] );
add_action( 'woocommerce_checkout_order_processed', [ $this, 'checkout_order_processed' ] );
add_action( 'woocommerce_blocks_checkout_order_processed', [ $this, 'checkout_order_processed' ] );
add_action( 'woocommerce_payments_save_user_in_platform_checkout', [ $this, 'must_save_payment_method_to_platform' ] );
}
/**
* Override jetpack-tracking's ajax handling to use internal maybe_record_event method.
*/
public function ajax_tracks() {
// Check for nonce.
if (
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
empty( $_REQUEST['tracksNonce'] ) || ! wp_verify_nonce( $_REQUEST['tracksNonce'], 'platform_tracks_nonce' )
) {
wp_send_json_error(
__( 'You aren’t authorized to do that.', 'woocommerce-payments' ),
403
);
}
if ( ! isset( $_REQUEST['tracksEventName'] ) ) {
wp_send_json_error(
__( 'No valid event name or type.', 'woocommerce-payments' ),
403
);
}
$tracks_data = [];
if ( isset( $_REQUEST['tracksEventProp'] ) ) {
// tracksEventProp is a JSON-encoded string.
$event_prop = json_decode( wc_clean( wp_unslash( $_REQUEST['tracksEventProp'] ) ), true );
if ( is_array( $event_prop ) ) {
$tracks_data = $event_prop;
}
}
$this->maybe_record_event( sanitize_text_field( wp_unslash( $_REQUEST['tracksEventName'] ) ), $tracks_data );
wp_send_json_success();
}
/**
* Generic method to track user events.
*
* @param string $event name of the event.
* @param array $data array of event properties.
*/
public function maybe_record_event( $event, $data = [] ) {
$user = wp_get_current_user();
$site_url = get_option( 'siteurl' );
//phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
$data['blog_url'] = $site_url;
$data['blog_id'] = \Jetpack_Options::get_option( 'id' );
// Top level events should not be namespaced.
if ( '_aliasUser' !== $event ) {
$event = self::$prefix . '_' . $event;
}
// Add event property for test mode vs. live mode events.
$gateway = \WC_Payments::get_gateway();
$data['test_mode'] = $gateway->is_in_test_mode() ? 1 : 0;
$data['wcpay_version'] = get_option( 'woocommerce_woocommerce_payments_version' );
return $this->tracks_record_event( $user, $event, $data );
}
/**
* Override parent method to omit the jetpack TOS check.
*
* @return bool
*/
public function should_enable_tracking() {
// Track only site pages.
if ( is_admin() && ! wp_doing_ajax() ) {
return false;
}
// Don't track site admins.
if ( is_user_logged_in() && in_array( 'administrator', wp_get_current_user()->roles, true ) ) {
return false;
}
// Don't track if the opt-out cookie is set.
if ( ! empty( $_COOKIE['tk_opt-out'] ) ) {
return false;
}
// Don't track when platform checkout is disabled.
$gateway = \WC_Payments::get_gateway();
$is_platform_checkout_eligible = WC_Payments_Features::is_platform_checkout_eligible(); // Feature flag.
$is_platform_checkout_enabled = 'yes' === $gateway->get_option( 'platform_checkout', 'no' );
if ( ! ( $is_platform_checkout_eligible && $is_platform_checkout_enabled ) ) {
return false;
}
return true;
}
/**
* Record an event in Tracks - this is the preferred way to record events from PHP.
*
* @param mixed $user username, user_id, or WP_user object.
* @param string $event_name The name of the event.
* @param array $properties Custom properties to send with the event.
*
* @return bool|array|\WP_Error|\Jetpack_Tracks_Event
*/
public function tracks_record_event( $user, $event_name, $properties = [] ) {
// We don't want to track user events during unit tests/CI runs.
if ( $user instanceof \WP_User && 'wptests_capabilities' === $user->cap_key ) {
return false;
}
if ( ! $this->should_enable_tracking() ) {
return false;
}
$event_obj = $this->tracks_build_event_obj( $user, $event_name, $properties );
if ( is_wp_error( $event_obj ) ) {
return $event_obj;
}
$pixel = $event_obj->build_pixel_url( $event_obj );
if ( ! $pixel ) {
return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
}
return self::record_pixel( $pixel );
}
/**
* Procedurally build a Tracks Event Object.
*
* @param \WP_User $user WP_user object.
* @param string $event_name The name of the event.
* @param array $properties Custom properties to send with the event.
*
* @return \Jetpack_Tracks_Event|\WP_Error
*/
private function tracks_build_event_obj( $user, $event_name, $properties = [] ) {
$identity = $this->tracks_get_identity( $user->ID );
$properties['user_lang'] = $user->get( 'WPLANG' );
$blog_details = [
'blog_lang' => isset( $properties['blog_lang'] ) ? $properties['blog_lang'] : get_bloginfo( 'language' ),
];
$timestamp = round( microtime( true ) * 1000 );
$timestamp_string = is_string( $timestamp ) ? $timestamp : number_format( $timestamp, 0, '', '' );
/**
* Ignore incorrect argument definition in Jetpack_Tracks_Event.
*
* @psalm-suppress InvalidArgument
*/
return new \Jetpack_Tracks_Event(
array_merge(
$blog_details,
(array) $properties,
$identity,
[
'_en' => $event_name,
'_ts' => $timestamp_string,
]
)
);
}
/**
* Get the identity to send to tracks.
*
* @param int $user_id The user id of the local user.
*
* @return array $identity
*/
public function tracks_get_identity( $user_id ) {
// Meta is set, and user is still connected. Use WPCOM ID.
$wpcom_id = get_user_meta( $user_id, 'jetpack_tracks_wpcom_id', true );
if ( $wpcom_id && $this->http->is_user_connected( $user_id ) ) {
return [
'_ut' => 'wpcom:user_id',
'_ui' => $wpcom_id,
];
}
// User is connected, but no meta is set yet. Use WPCOM ID and set meta.
if ( $this->http->is_user_connected( $user_id ) ) {
$wpcom_user_data = $this->http->get_connected_user_data( $user_id );
update_user_meta( $user_id, 'jetpack_tracks_wpcom_id', $wpcom_user_data['ID'] );
return [
'_ut' => 'wpcom:user_id',
'_ui' => $wpcom_user_data['ID'],
];
}
// User isn't linked at all. Fall back to anonymous ID.
$anon_id = get_user_meta( $user_id, 'jetpack_tracks_anon_id', true );
if ( ! $anon_id ) {
$anon_id = \Jetpack_Tracks_Client::get_anon_id();
add_user_meta( $user_id, 'jetpack_tracks_anon_id', $anon_id, false );
}
if ( ! isset( $_COOKIE['tk_ai'] ) && ! headers_sent() ) {
setcookie( 'tk_ai', $anon_id );
}
return [
'_ut' => 'anon',
'_ui' => $anon_id,
];
}
/**
* Record a Tracks event that the checkout has started.
*/
public function checkout_start() {
$this->maybe_record_event( 'order_checkout_start' );
}
/**
* Record a Tracks event that the order has been processed.
*/
public function checkout_order_processed() {
$this->maybe_record_event(
'order_checkout_complete',
[
'source' => isset( $_SERVER['HTTP_X_WCPAY_PLATFORM_CHECKOUT_USER'] ) ? 'platform' : 'standard',
]
);
}
/**
* Record a Tracks event that user chose to save payment information in platform checkout.
*/
public function must_save_payment_method_to_platform() {
$this->maybe_record_event(
'platform_checkout_registered',
[
'source' => 'checkout',
]
);
}
}