PluginProbe
Plausible Analytics / trunk
Plausible Analytics vtrunk
2.6.1 2.6.0 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 52 releases
plausible-analytics / src / Integrations / WooCommerce.php

WooCommerce.php in Plausible Analytics trunk, at src/Integrations/WooCommerce.php

331 lines 9.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plausible Analytics | Integrations | WooCommerce.
4 *
5 * @since 2.1.0
6 * @package WordPress
7 * @subpackage Plausible Analytics
8 */
9
10 namespace Plausible\Analytics\WP\Integrations;
11
12 use Plausible\Analytics\WP\Admin\Provisioning;
13 use Plausible\Analytics\WP\EnhancedMeasurements;
14 use Plausible\Analytics\WP\Integrations;
15 use Plausible\Analytics\WP\Proxy;
16 use WC_Cart;
17 use WC_Product;
18
19 class WooCommerce {
20 /**
21 * @var array Custom Event Goals used to track Events in WooCommerce.
22 */
23 public $event_goals = [];
24
25 /**
26 * Build class.
27 *
28 * @codeCoverageIgnore
29 */
30 public function __construct( $init = true ) {
31 $uri = trim( wc_get_permalink_structure()['product_base'], '/' );
32
33 if ( is_multisite() ) {
34 $uri = get_blog_details()->path . $uri;
35 } else {
36 $uri = '/' . $uri;
37 }
38
39 $this->event_goals = [
40 // translators: %s: Product page URI pattern.
41 'view-product' => sprintf( __( 'Visit %s*', 'plausible-analytics' ), $uri ),
42 'add-to-cart' => __( 'Woo Add to Cart', 'plausible-analytics' ),
43 'remove-from-cart' => __( 'Woo Remove from Cart', 'plausible-analytics' ),
44 'checkout' => __( 'Woo Start Checkout', 'plausible-analytics' ),
45 'purchase' => __( 'Woo Complete Purchase', 'plausible-analytics' ),
46 ];
47
48 $this->init( $init );
49 }
50
51 /**
52 * Filter and action hooks.
53 *
54 * @return void
55 *
56 * @codeCoverageIgnore
57 */
58 private function init( $init ) {
59 if ( ! $init ) {
60 return;
61 }
62
63 /**
64 * Adds required JS and classes.
65 */
66 add_action( 'wp_enqueue_scripts', [ $this, 'add_js' ], 1 );
67 add_filter( 'woocommerce_store_api_add_to_cart_data', [ $this, 'add_http_referer' ], 10, 2 );
68
69 /**
70 * Trigger tracking events.
71 */
72 add_action( 'woocommerce_store_api_validate_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 2 );
73 add_action( 'woocommerce_ajax_added_to_cart', [ $this, 'track_ajax_add_to_cart' ] );
74 /** @see \WC_Form_Handler::add_to_cart_action() runs on priority 20. We need to run before that, in case redirect is enabled. */
75 add_action( 'wp_loaded', [ $this, 'track_direct_add_to_cart' ], 19 );
76 add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 );
77 add_action( 'wp_head', [ $this, 'track_entered_checkout' ] );
78 add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] );
79 }
80
81 /**
82 * A bit of a hacky approach to ensure the _wp_http_referer header is available to us when hitting the Proxy in @see self::track_remove_cart_item().
83 *
84 * @see self::track_add_to_cart()
85 *
86 * @param $add_to_cart_data
87 *
88 * @param $request
89 *
90 * @return mixed
91 *
92 * @codeCoverageIgnore Because there's nothing to test here.
93 */
94 public function add_http_referer( $add_to_cart_data, $request ) {
95 $http_referer = $request->get_param( '_wp_http_referer' );
96
97 if ( ! empty( $http_referer ) ) {
98 $_REQUEST['_wp_http_referer'] = sanitize_url( $http_referer );
99 }
100
101 return $add_to_cart_data;
102 }
103
104 /**
105 * Enqueue required JS in frontend.
106 *
107 * @return void
108 *
109 * @codeCoverageIgnore Because there's nothing to test here.
110 */
111 public function add_js() {
112 // Causes errors in checkout and isn't needed either way.
113 if ( is_checkout() ) {
114 return; // @codeCoverageIgnore
115 }
116
117 wp_enqueue_script(
118 'plausible-woocommerce-integration',
119 PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-woocommerce-integration.js',
120 [],
121 filemtime( PLAUSIBLE_ANALYTICS_PLUGIN_DIR . 'assets/dist/js/plausible-woocommerce-integration.js' ),
122 [ 'in_footer' => true ],
123 );
124 }
125
126 /**
127 * Track (non-Interactivity API i.e., AJAX) add to cart events.
128 *
129 * @param string|int $product_id ID of the product added to the cart.
130 *
131 * @return void
132 *
133 * @codeCoverageIgnore Because we can't test XHR requests here.
134 */
135 public function track_ajax_add_to_cart( $product_id ) {
136 $product = wc_get_product( $product_id );
137 $add_to_cart_data = [
138 'id' => $product_id,
139 'quantity' => $_POST['quantity'] ?? 1,
140 ];
141
142 $this->track_add_to_cart( $product, $add_to_cart_data );
143 }
144
145 /**
146 * Track regular (i.e., interactivity API) add to cart events.
147 *
148 * @param WC_Product $product General information about the product added to cart.
149 * @param array $add_to_cart_data Cart data for the product added to the cart, e.g. quantity, variation ID, etc.
150 *
151 * @return void
152 *
153 * @codeCoverageIgnore Because we can't test XHR requests here.
154 */
155 public function track_add_to_cart( $product, $add_to_cart_data ) {
156 if ( ! $product ) {
157 return;
158 }
159
160 $product_data = $this->clean_data( $product->get_data() );
161 $added_to_cart = $this->clean_data( $add_to_cart_data );
162 $cart = $this->get_wc_cart();
163 $props = apply_filters(
164 'plausible_analytics_woocommerce_add_to_cart_custom_properties',
165 [
166 'product_name' => $product_data['name'],
167 'product_id' => $added_to_cart['id'],
168 'quantity' => $added_to_cart['quantity'],
169 'price' => $product_data['price'],
170 'tax_class' => $product_data['tax_class'],
171 'cart_total_items' => count( $cart->get_cart_contents() ),
172 'cart_total' => $cart->get_total( null ),
173 ]
174 );
175 $proxy = new Proxy( false );
176
177 $proxy->do_request( $this->event_goals['add-to-cart'], null, null, $props );
178 }
179
180 /**
181 * Removes unneeded elements from the array.
182 *
183 * @param array $product Product Data.
184 *
185 * @return mixed
186 *
187 * @codeCoverageIgnore Because it can't be tested.
188 */
189 private function clean_data( $product ) {
190 foreach ( $product as $key => $value ) {
191 if ( ! in_array( $key, Provisioning::CUSTOM_PROPERTIES ) ) {
192 unset( $product[ $key ] );
193 }
194 }
195
196 return $product;
197 }
198
199 /**
200 * A wrapper to keep our code testable.
201 *
202 * @return WC_Cart|null
203 *
204 * @codeCoverageIgnore
205 */
206 protected function get_wc_cart() {
207 return WC()->cart;
208 }
209
210 /**
211 * Track add to cart actions by direct link, e.g. ?product_type=download&add-to-cart=1&quantity=1
212 *
213 * @return void
214 *
215 * @codeCoverageIgnore Because we can't test XHR here.
216 */
217 public function track_direct_add_to_cart() {
218 if ( ! isset( $_REQUEST['add-to-cart'] ) || ! is_numeric( wp_unslash( $_REQUEST['add-to-cart'] ) ) ) {
219 return;
220 }
221
222 $product_id = absint( wp_unslash( $_REQUEST['add-to-cart'] ) );
223 $product = wc_get_product( $product_id );
224 $quantity = isset( $_REQUEST['quantity'] ) ? absint( wp_unslash( $_REQUEST['quantity'] ) ) : 1;
225
226 $this->track_add_to_cart( $product, [ 'id' => $product_id, 'quantity' => $quantity ] );
227 }
228
229 /**
230 * Tracks when a user enters the checkout process and sends event data to Plausible Analytics.
231 *
232 * This method checks if the current page is the checkout page. If it is, it collects relevant
233 * cart information like subtotal, shipping, tax, and total, applies filters to allow custom properties,
234 * encodes the data as JSON, and generates a JavaScript snippet to send the data to Plausible Analytics.
235 *
236 * @return void
237 */
238 public function track_entered_checkout() {
239 if ( ! is_checkout() || is_wc_endpoint_url( 'order-received' ) ) {
240 return; // @codeCoverageIgnore
241 }
242
243 $cart = $this->get_wc_cart();
244 $props = apply_filters(
245 'plausible_analytics_woocommerce_entered_checkout_custom_properties',
246 [
247 'props' => [
248 'subtotal' => $cart->get_subtotal(),
249 'shipping' => $cart->get_shipping_total(),
250 'tax' => $cart->get_total_tax(),
251 'total' => $cart->get_total( null ),
252 ],
253 ]
254 );
255 $props = wp_json_encode( $props );
256 $label = $this->event_goals['checkout'];
257
258 echo sprintf( Integrations::SCRIPT_WRAPPER, "window.plausible( '$label', $props )" );
259 }
260
261 /**
262 * Track WooCommerce purchase on thank you page.
263 *
264 * @param $order_id
265 *
266 * @return void
267 */
268 public function track_purchase( $order_id ) {
269 $order = wc_get_order( $order_id );
270 $is_tracked = $order->get_meta( Integrations::PURCHASE_TRACKED_META_KEY );
271
272 if ( $is_tracked ) {
273 return; // @codeCoverageIgnore
274 }
275
276 $props = wp_json_encode(
277 [
278 EnhancedMeasurements::ECOMMERCE_REVENUE => [
279 'amount' => (string) $order->get_total(),
280 'currency' => $order->get_currency(),
281 ],
282 ]
283 );
284 $label = $this->event_goals['purchase'];
285
286 echo sprintf( Integrations::SCRIPT_WRAPPER, "window.plausible( '$label', $props )" );
287
288 $order->add_meta_data( Integrations::PURCHASE_TRACKED_META_KEY, true );
289 $order->save();
290 }
291
292 /**
293 * Track Remove from cart events.
294 *
295 * @param string $cart_item_key Key of item being removed from cart.
296 * @param WC_Cart $cart Instance of the current cart.
297 *
298 * @return void
299 *
300 * @codeCoverageIgnore because we can't test XHR requests here.
301 */
302 public function track_remove_cart_item( $cart_item_key, $cart ) {
303 $cart_contents = $cart->get_cart_contents();
304 $item_removed_from_cart = $this->clean_data( $cart_contents[ $cart_item_key ] ?? [] );
305 $product = null;
306
307 if ( isset( $item_removed_from_cart['product_id'] ) ) {
308 $product = wc_get_product( $item_removed_from_cart['product_id'] );
309 }
310
311 if ( ! $product ) {
312 return;
313 }
314
315 $props = apply_filters(
316 'plausible_analytics_woocommerce_remove_cart_item_custom_properties',
317 [
318 'product_name' => $product->get_name(),
319 'product_id' => $item_removed_from_cart['product_id'],
320 'variation_id' => $item_removed_from_cart['variation_id'],
321 'quantity' => $item_removed_from_cart['quantity'],
322 'cart_total_items' => count( $cart_contents ),
323 'cart_total' => $cart->get_total( null ),
324 ]
325 );
326 $proxy = new Proxy( false );
327
328 $proxy->do_request( $this->event_goals['remove-from-cart'], null, null, $props );
329 }
330 }
331