wp-google-analytics-universal.php 13 KB

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