Completed
Push — fix/9129-allow-analytics-js-wi... ( 8a1a92...41fabb )
by
unknown
40:01 queued 30:41
created

Jetpack_Google_Analytics_Universal   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 435
Duplicated Lines 11.03 %

Coupling/Cohesion

Components 0
Dependencies 2

Importance

Changes 0
Metric Value
dl 48
loc 435
rs 4.5454
c 0
b 0
f 0
wmc 59
lcom 0
cbo 2

13 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 24 2
B wp_head() 0 53 5
A maybe_anonymize_ip() 0 7 2
C maybe_track_purchases() 14 68 11
B add_to_cart() 0 31 4
B loop_add_to_cart() 0 30 5
B remove_from_cart() 0 31 4
A remove_from_cart_attributes() 20 20 3
B listing_impression() 0 31 5
B listing_click() 0 43 5
B product_detail() 0 27 4
B checkout_process() 14 38 5
A send_pageview_in_footer() 0 15 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_Google_Analytics_Universal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_Google_Analytics_Universal, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
* Jetpack_Google_Analytics_Universal hooks and and enqueues support for analytics.js
5
* https://developers.google.com/analytics/devguides/collection/analyticsjs/
6
* https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce
7
*
8
* @author allendav
9
*/
10
11
/**
12
* Bail if accessed directly
13
*/
14
if ( ! defined( 'ABSPATH' ) ) {
15
	exit;
16
}
17
18
class Jetpack_Google_Analytics_Universal {
19
	public function __construct() {
20
		add_filter( 'jetpack_wga_universal_commands', array( $this, 'maybe_anonymize_ip' ) );
21
		add_filter( 'jetpack_wga_universal_commands', array( $this, 'maybe_track_purchases' ) );
22
23
		add_action( 'wp_head', array( $this, 'wp_head' ), 0 );
24
		// Priority zero rationale: Google's docs now advise to put it their script at/near the beginning of the head (not the end)
25
26
		add_action( 'woocommerce_after_add_to_cart_button', array( $this, 'add_to_cart' ) );
27
		add_action( 'wp_footer', array( $this, 'loop_add_to_cart' ) );
28
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) );
29
		add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) );
30
		add_filter( 'woocommerce_cart_item_remove_link', array( $this, 'remove_from_cart_attributes' ), 10, 2 );
31
		add_action( 'woocommerce_after_shop_loop_item', array( $this, 'listing_impression' ) );
32
		add_action( 'woocommerce_after_shop_loop_item', array( $this, 'listing_click' ) );
33
		add_action( 'woocommerce_after_single_product', array( $this, 'product_detail' ) );
34
		add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) );
35
36
		// If WooCommerce is active, we need to send a pageview command "last" (i.e. in the footer), so
37
		// that other universal commands can enqueue before it. We use priority 24 to add this command's
38
		// JavaScript just before wc_print_js is called (pri 25)
39
		if ( class_exists( 'WooCommerce' ) ) {
40
			add_action( 'wp_footer', array( $this, 'send_pageview_in_footer' ), 24 );
41
		}
42
	}
43
44
	public function wp_head() {
45
		$tracking_code = Jetpack_Google_Analytics_Options::get_tracking_code();
46
		if ( empty( $tracking_code ) ) {
47
			echo "<!-- No tracking ID configured for Jetpack Google Analytics -->\r\n";
48
			return;
49
		}
50
51
		// If we're in the admin_area, return without inserting code.
52
		if ( is_admin() ) {
53
			return;
54
		}
55
56
		$universal_commands = array();
57
58
		// If WooCommerce is present, ask Google Analytics to load ecommerce tools
59
		if ( class_exists( 'WooCommerce' ) ) {
60
			array_push( $universal_commands, "ga( 'require', 'ec' );" );
61
		}
62
63
		// If WooCommerce isn't present, it is OK to immediately enqueue the pageview command
64
		// (if WooCommerce is present, this command will be added in the footer instead, to
65
		// allow other commands to queue ahead of it)
66
		if ( ! class_exists( 'WooCommerce' ) ) {
67
			array_push( $universal_commands, "ga( 'send', 'pageview' );" );
68
		}
69
70
		/**
71
		 * Allow for additional elements to be added to the universal Google Analytics queue (ga) array
72
		 *
73
		 * @since 5.6.0
74
		 *
75
		 * @param array $custom_vars Array of universal Google Analytics queue elements
76
		 */
77
78
		$universal_commands = apply_filters( 'jetpack_wga_universal_commands', $universal_commands );
79
80
		$async_code = "
81
			<!-- Jetpack Google Analytics -->
82
			<script>
83
				window.ga = window.ga || function(){ ( ga.q = ga.q || [] ).push( arguments ) }; ga.l=+new Date;
84
				ga( 'create', '%tracking_id%', 'auto' );
85
				%universal_commands%
86
			</script>
87
			<script async src='https://www.google-analytics.com/analytics.js'></script>
88
			<!-- End Jetpack Google Analytics -->
89
		";
90
		$async_code = str_replace( '%tracking_id%', $tracking_code, $async_code );
91
92
		$universal_commands_string = implode( "\r\n", $universal_commands );
93
		$async_code = str_replace( '%universal_commands%', $universal_commands_string, $async_code );
94
95
		echo "$async_code\r\n";
96
	}
97
98
	public function maybe_anonymize_ip( $command_array ) {
99
		if ( Jetpack_Google_Analytics_Options::anonymize_ip_is_enabled() ) {
100
			array_push( $command_array, "ga( 'set', 'anonymizeIp', true );" );
101
		}
102
103
		return $command_array;
104
	}
105
106
	public function maybe_track_purchases( $command_array ) {
107
		global $wp;
108
109
		if ( ! Jetpack_Google_Analytics_Options::track_purchases_is_enabled() ) {
110
			return $command_array;
111
		}
112
113
		if ( ! class_exists( 'WooCommerce' ) ) {
114
			return $command_array;
115
		}
116
117
		$minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' );
118
		if ( ! $minimum_woocommerce_active ) {
119
			return $command_array;
120
		}
121
122
		if ( ! is_order_received_page() ) {
123
			return $command_array;
124
		}
125
126
		$order_id = isset( $wp->query_vars['order-received'] ) ? $wp->query_vars['order-received'] : 0;
127
		if ( 0 == $order_id ) {
128
			return $command_array;
129
		}
130
131
		// A 1 indicates we've already tracked this order - don't do it again
132
		if ( 1 == get_post_meta( $order_id, '_ga_tracked', true ) ) {
133
			return $command_array;
134
		}
135
136
		$order = new WC_Order( $order_id );
137
		$order_currency = $order->get_currency();
138
		$command = "ga( 'set', '&cu', '" . esc_js( $order_currency ) . "' );";
139
		array_push( $command_array, $command );
140
141
		// Order items
142
		if ( $order->get_items() ) {
143 View Code Duplication
			foreach ( $order->get_items() as $item ) {
144
				$product = $order->get_product_from_item( $item );
145
				$product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
146
147
				$item_details = array(
148
					'id' => $product_sku_or_id,
149
					'name' => $item['name'],
150
					'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
151
					'price' => $order->get_item_total( $item ),
152
					'quantity' => $item['qty'],
153
				);
154
				$command = "ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );";
155
				array_push( $command_array, $command );
156
			}
157
		}
158
159
		// Order summary
160
		$summary = array(
161
			'id' => $order->get_order_number(),
162
			'affiliation' => get_bloginfo( 'name' ),
163
			'revenue' => $order->get_total(),
164
			'tax' => $order->get_total_tax(),
165
			'shipping' => $order->get_total_shipping()
166
		);
167
		$command = "ga( 'ec:setAction', 'purchase', " . wp_json_encode( $summary ) . " );";
168
		array_push( $command_array, $command );
169
170
		update_post_meta( $order_id, '_ga_tracked', 1 );
171
172
		return $command_array;
173
	}
174
175
	public function add_to_cart() {
176
		if ( ! Jetpack_Google_Analytics_Options::track_add_to_cart_is_enabled() ) {
177
			return;
178
		}
179
180
		if ( ! class_exists( 'WooCommerce' ) ) {
181
			return;
182
		}
183
184
		if ( ! is_single() ) {
185
			return;
186
		}
187
188
		global $product;
189
190
		$product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
191
		$selector = ".single_add_to_cart_button";
192
193
		wc_enqueue_js(
194
			"$( '" . esc_js( $selector ) . "' ).click( function() {
195
				var productDetails = {
196
					'id': '" . esc_js( $product_sku_or_id ) . "',
197
					'name' : '" . esc_js( $product->get_title() ) . "',
198
					'quantity': $( 'input.qty' ).val() ? $( 'input.qty' ).val() : '1',
199
				};
200
				ga( 'ec:addProduct', productDetails );
201
				ga( 'ec:setAction', 'add' );
202
				ga( 'send', 'event', 'UX', 'click', 'add to cart' );
203
			} );"
204
		);
205
	}
206
207
	public function loop_add_to_cart() {
208
		if ( ! Jetpack_Google_Analytics_Options::track_add_to_cart_is_enabled() ) {
209
			return;
210
		}
211
212
		if ( ! class_exists( 'WooCommerce' ) ) {
213
			return;
214
		}
215
216
		$minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( WC_VERSION, '3.0', '>=' );
217
		if ( ! $minimum_woocommerce_active ) {
218
			return;
219
		}
220
221
		$selector = ".add_to_cart_button:not(.product_type_variable, .product_type_grouped)";
222
223
		wc_enqueue_js(
224
			"$( '" . esc_js( $selector ) . "' ).click( function() {
225
				var productSku = $( this ).data( 'product_sku' );
226
				var productID = $( this ).data( 'product_id' );
227
				var productDetails = {
228
					'id': productSku ? productSku : '#' + productID,
229
					'quantity': $( this ).data( 'quantity' ),
230
				};
231
				ga( 'ec:addProduct', productDetails );
232
				ga( 'ec:setAction', 'add' );
233
				ga( 'send', 'event', 'UX', 'click', 'add to cart' );
234
			} );"
235
		);
236
	}
237
238
	public function remove_from_cart() {
239
		if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
240
			return;
241
		}
242
243
		if ( ! Jetpack_Google_Analytics_Options::track_remove_from_cart_is_enabled() ) {
244
			return;
245
		}
246
247
		if ( ! class_exists( 'WooCommerce' ) ) {
248
			return;
249
		}
250
251
		// We listen at div.woocommerce because the cart 'form' contents get forcibly
252
		// updated and subsequent removals from cart would then not have this click
253
		// handler attached
254
		wc_enqueue_js(
255
			"$( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
256
				var productSku = $( this ).data( 'product_sku' );
257
				var productID = $( this ).data( 'product_id' );
258
				var quantity = $( this ).parent().parent().find( '.qty' ).val()
259
				var productDetails = {
260
					'id': productSku ? productSku : '#' + productID,
261
					'quantity': quantity ? quantity : '1',
262
				};
263
				ga( 'ec:addProduct', productDetails );
264
				ga( 'ec:setAction', 'remove' );
265
				ga( 'send', 'event', 'UX', 'click', 'remove from cart' );
266
			} );"
267
		);
268
	}
269
270
	/**
271
	* Adds the product ID and SKU to the remove product link (for use by remove_from_cart above) if not present
272
	*/
273 View Code Duplication
	public function remove_from_cart_attributes( $url, $key ) {
274
		if ( false !== strpos( $url, 'data-product_id' ) ) {
275
			return $url;
276
		}
277
278
		if ( ! class_exists( 'WooCommerce' ) ) {
279
			return $url;
280
		}
281
282
		$item = WC()->cart->get_cart_item( $key );
283
		$product = $item[ 'data' ];
284
285
		$new_attributes = sprintf( 'href="%s" data-product_id="%s" data-product_sku="%s"',
286
			esc_attr( $url ),
287
			esc_attr( $product->get_id() ),
288
			esc_attr( $product->get_sku() )
289
			);
290
		$url = str_replace( 'href=', $new_attributes, $url );
291
		return $url;
292
	}
293
294
	public function listing_impression() {
295
		if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
296
			return;
297
		}
298
299
		if ( ! Jetpack_Google_Analytics_Options::track_product_impressions_is_enabled() ) {
300
			return;
301
		}
302
303
		if ( ! class_exists( 'WooCommerce' ) ) {
304
			return;
305
		}
306
307
		if ( isset( $_GET['s'] ) ) {
308
			$list = "Search Results";
309
		} else {
310
			$list = "Product List";
311
		}
312
313
		global $product, $woocommerce_loop;
314
		$product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
315
316
		$item_details = array(
317
			'id' => $product_sku_or_id,
318
			'name' => $product->get_title(),
319
			'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
320
			'list' => $list,
321
			'position' => $woocommerce_loop['loop']
322
		);
323
		wc_enqueue_js( "ga( 'ec:addImpression', " . wp_json_encode( $item_details ) . " );" );
324
	}
325
326
	public function listing_click() {
327
		if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
328
			return;
329
		}
330
331
		if ( ! Jetpack_Google_Analytics_Options::track_product_clicks_is_enabled() ) {
332
			return;
333
		}
334
335
		if ( ! class_exists( 'WooCommerce' ) ) {
336
			return;
337
		}
338
339
		if ( isset( $_GET['s'] ) ) {
340
			$list = "Search Results";
341
		} else {
342
			$list = "Product List";
343
		}
344
345
		global $product, $woocommerce_loop;
346
		$product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
347
348
		$selector = ".products .post-" . esc_js( $product->get_id() ) . " a";
349
350
		$item_details = array(
351
			'id' => $product_sku_or_id,
352
			'name' => $product->get_title(),
353
			'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
354
			'position' => $woocommerce_loop['loop']
355
		);
356
357
		wc_enqueue_js(
358
			"$( '" . esc_js( $selector ) . "' ).click( function() {
359
				if ( true === $( this ).hasClass( 'add_to_cart_button' ) ) {
360
					return;
361
				}
362
363
				ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );
364
				ga( 'ec:setAction', 'click', { list: '" . esc_js( $list ) . "' } );
365
				ga( 'send', 'event', 'UX', 'click', { list: '" . esc_js( $list ) . "' } );
366
			} );"
367
		);
368
	}
369
370
	public function product_detail() {
371
		if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
372
			return;
373
		}
374
375
		if ( ! Jetpack_Google_Analytics_Options::track_product_detail_view_is_enabled() ) {
376
			return;
377
		}
378
379
		if ( ! class_exists( 'WooCommerce' ) ) {
380
			return;
381
		}
382
383
		global $product;
384
		$product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
385
386
		$item_details = array(
387
			'id' => $product_sku_or_id,
388
			'name' => $product->get_title(),
389
			'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
390
			'price' => $product->get_price()
391
		);
392
		wc_enqueue_js(
393
			"ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );" .
394
			"ga( 'ec:setAction', 'detail' );"
395
		);
396
	}
397
398
	public function checkout_process() {
399
		if ( ! Jetpack_Google_Analytics_Options::enhanced_ecommerce_tracking_is_enabled() ) {
400
			return;
401
		}
402
403
		if ( ! Jetpack_Google_Analytics_Options::track_checkout_started_is_enabled() ) {
404
			return;
405
		}
406
407
		if ( ! class_exists( 'WooCommerce' ) ) {
408
			return;
409
		}
410
411
		$universal_commands = array();
412
		$cart = WC()->cart->get_cart();
413
414 View Code Duplication
		foreach ( $cart as $cart_item_key => $cart_item ) {
415
			/**
416
			* This filter is already documented in woocommerce/templates/cart/cart.php
417
			*/
418
			$product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
419
			$product_sku_or_id = Jetpack_Google_Analytics_Utils::get_product_sku_or_id( $product );
420
421
			$item_details = array(
422
				'id' => $product_sku_or_id,
423
				'name' => $product->get_title(),
424
				'category' => Jetpack_Google_Analytics_Utils::get_product_categories_concatenated( $product ),
425
				'price' => $product->get_price(),
426
				'quantity' => $cart_item[ 'quantity' ]
427
			);
428
429
			array_push( $universal_commands, "ga( 'ec:addProduct', " . wp_json_encode( $item_details ) . " );" );
430
		}
431
432
		array_push( $universal_commands, "ga( 'ec:setAction','checkout' );" );
433
434
		wc_enqueue_js( implode( "\r\n", $universal_commands ) );
435
	}
436
437
	public function send_pageview_in_footer() {
438
		if ( ! Jetpack_Google_Analytics_Options::has_tracking_code() ) {
439
			return;
440
		}
441
442
		if ( ! class_exists( 'WooCommerce' ) ) {
443
			return;
444
		}
445
446
		if ( is_admin() ) {
447
			return;
448
		}
449
450
		wc_enqueue_js( "ga( 'send', 'pageview' );" );
451
	}
452
}
453