Changeset 3742708
- Timestamp:
- 10/13/2026 01:11:55 AM (3 weeks ago)
- Location:
- lightweight-google-analytics
- Files:
-
- 6 edited
- 1 copied
-
tags/1.7.0 (copied) (copied from lightweight-google-analytics/trunk )
-
tags/1.7.0/lightweight-google-analytics.php (modified) (16 diffs)
-
tags/1.7.0/readme.txt (modified) (5 diffs)
-
tags/1.7.0/uninstall.php (modified) (1 diff)
-
trunk/lightweight-google-analytics.php (modified) (16 diffs)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
lightweight-google-analytics/tags/1.7.0/lightweight-google-analytics.php
r3689051 r3742708 4 4 * Plugin URI: https://wordpress.org/plugins/lightweight-google-analytics/ 5 5 * Description: Extremely simple plugin to add Google Analytics 4 to your WordPress site using your Measurement ID. 6 * Version: 1. 6.06 * Version: 1.7.0 7 7 * Requires at least: 5.7 8 8 * Requires PHP: 7.0 … … 18 18 } 19 19 20 define( 'LIGHTWEIGHT_GA_VERSION', '1. 6.0' );20 define( 'LIGHTWEIGHT_GA_VERSION', '1.7.0' ); 21 21 22 22 // Saved settings with defaults filled in … … 30 30 'tracking_code' => 'analytics-js', 31 31 'consent_mode' => '', 32 'gpc' => '', 33 'content_groups' => '', 32 34 'debug_mode' => '', 33 35 ) ); … … 46 48 $reason = 'preview'; 47 49 } elseif ( is_user_logged_in() ) { 50 $roles = explode( '-', $settings['tracking_level'] ); 48 51 if ( 'logged-in' === $settings['tracking_level'] ) { 49 52 $reason = 'logged-in'; 50 } elseif ( 'all' !== $settings['tracking_level'] && array_intersect( explode( '-', $settings['tracking_level'] ), (array) wp_get_current_user()->roles ) ) { 53 } elseif ( 'all' !== $settings['tracking_level'] && ( array_intersect( $roles, (array) wp_get_current_user()->roles ) || ( is_multisite() && is_super_admin() && in_array( 'administrator', $roles, true ) ) ) ) { 54 // Network super admins don't always have the administrator role on the site itself 51 55 $reason = 'role'; 52 56 } … … 57 61 } 58 62 59 // Print the tracking code in the head or footer, whichever the settings say 63 // GA4 content group (the post's category, the post type, or the kind of archive) and author name for the current page 64 function lightweight_ga_page_dimensions() { 65 $group = ''; 66 $author = ''; 67 if ( is_front_page() ) { 68 $group = __( 'Home', 'lightweight-google-analytics' ); 69 } elseif ( is_home() ) { 70 $group = __( 'Blog', 'lightweight-google-analytics' ); 71 } elseif ( is_singular() && get_queried_object() instanceof WP_Post ) { 72 $post = get_queried_object(); 73 if ( 'post' === $post->post_type ) { 74 $categories = get_the_category( $post->ID ); 75 // Prefer the primary category picked in Yoast SEO or Rank Math 76 $primary = (int) get_post_meta( $post->ID, '_yoast_wpseo_primary_category', true ) ?: (int) get_post_meta( $post->ID, 'rank_math_primary_category', true ); 77 foreach ( $categories as $category ) { 78 if ( $category->term_id === $primary ) { 79 $group = $category->name; 80 } 81 } 82 if ( ! $group && $categories ) { 83 $group = $categories[0]->name; 84 } 85 } else { 86 $type = get_post_type_object( $post->post_type ); 87 $group = $type ? $type->labels->singular_name : $post->post_type; 88 } 89 if ( post_type_supports( $post->post_type, 'author' ) ) { 90 $author = get_the_author_meta( 'display_name', $post->post_author ); 91 } 92 } elseif ( is_category() || is_tag() || is_tax() ) { 93 $group = single_term_title( '', false ); 94 } elseif ( is_post_type_archive() ) { 95 $group = post_type_archive_title( '', false ); 96 } elseif ( is_author() ) { 97 $group = __( 'Author archive', 'lightweight-google-analytics' ); 98 $author = get_the_author_meta( 'display_name', get_queried_object_id() ); 99 } elseif ( is_date() ) { 100 $group = __( 'Date archive', 'lightweight-google-analytics' ); 101 } elseif ( is_search() ) { 102 $group = __( 'Search', 'lightweight-google-analytics' ); 103 } elseif ( is_404() ) { 104 $group = __( 'Page not found', 'lightweight-google-analytics' ); 105 } 106 // Change or add page-level parameters, e.g. a custom content group for a section of the site 107 $dimensions = array_filter( (array) apply_filters( 'lightweight_ga_page_dimensions', array( 'content_group' => $group, 'author' => $author ) ), 'strlen' ); 108 // GA4 ignores parameter values longer than 100 characters 109 return array_map( function ( $value ) { 110 return function_exists( 'mb_substr' ) ? mb_substr( (string) $value, 0, 100 ) : substr( (string) $value, 0, 100 ); 111 }, $dimensions ); 112 } 113 114 // The WP Consent API (wordpress.org/plugins/wp-consent-api) lets consent banners share visitors' choices with other plugins. 115 // Its functions only exist after plugins_loaded, so this is checked at output time. 116 function lightweight_ga_has_consent_api() { 117 return function_exists( 'wp_has_consent' ); 118 } 119 120 // Tell the WP Consent API (and its Site Health check) that tracking follows visitors' choices, which is true while Consent Mode is on 121 add_filter( 'wp_consent_api_registered_' . plugin_basename( __FILE__ ), function () { 122 return (bool) lightweight_ga_settings()['consent_mode']; 123 } ); 124 125 // JS that waits for the WP Consent API to know the visitor's choices, then calls `callback` with wp_has_consent and again whenever they change. 126 // The API's script loads in the footer, so the first check runs once the page is parsed. Consent banners that set 127 // waitfor_consent_hook decide the consent type late; checking earlier would read "no banner" as consent. 128 function lightweight_ga_consent_api_js( $callback ) { 129 return '(function(){var c=' . $callback . ',s=function(){if(typeof wp_has_consent==="function"){c();}},w=function(){window.waitfor_consent_hook?document.addEventListener("wp_consent_type_defined",s):s();};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",w):w();document.addEventListener("wp_listen_for_consent_change",s);})();'; 130 } 131 132 // Print the tracking code in the head or footer, whichever the settings say. Consent Mode defaults always go in the 133 // head, so they come before any consent banner's update even when the tracking code itself is in the footer. 60 134 add_action( 'wp_head', 'lightweight_ga_display', 0 ); 61 135 add_action( 'wp_footer', 'lightweight_ga_display', 0 ); 62 136 function lightweight_ga_display() { 63 137 $settings = lightweight_ga_settings(); 64 $hook = 'footer' === $settings['tracking_position'] ? 'wp_footer' : 'wp_head'; 65 if ( current_action() !== $hook || empty( $settings['enable_lightweight_ga'] ) || ! lightweight_ga_has_ga4_id() || lightweight_ga_excluded_reason() ) { 138 if ( empty( $settings['enable_lightweight_ga'] ) || ! lightweight_ga_has_ga4_id() || lightweight_ga_excluded_reason() ) { 66 139 return; 67 140 } 68 $id = $settings['tracking_id']; 141 $id = $settings['tracking_id']; 142 $in_head = 'wp_head' === current_action(); 143 $is_position = $in_head ? 'footer' !== $settings['tracking_position'] : 'footer' === $settings['tracking_position']; 69 144 70 145 if ( 'minimalanalytics-inline' === $settings['tracking_code'] ) { 71 $script = file_get_contents( __DIR__ . '/js/minimal-analytics-4.min.js' ); 72 if ( ! $script ) { 73 return; 74 } 75 // The bundled script is configured by swapping the placeholder ID and the debug flag that sit at its top; check both anchors when updating the file 76 $script = str_replace( 77 array( 'tid:"G-XXXXXXXXXX"', ']},T=!1,' ), 78 array( 'tid:"' . esc_js( $id ) . '"', ']},T=' . ( $settings['debug_mode'] ? '!0' : '!1' ) . ',' ), 79 $script 80 ); 81 echo "<!-- Minimal Analytics 4 - Lightweight Google Analytics -->\n"; 82 wp_print_inline_script_tag( $script ); 146 if ( $is_position ) { 147 lightweight_ga_print_minimal_analytics( $settings ); 148 } 83 149 return; 84 150 } 85 151 86 echo "<!-- Google tag (gtag.js) - Lightweight Google Analytics -->\n";87 152 $gtag_stub = 'window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}'; 88 89 if ( $settings['consent_mode'] ) { 153 // Google's documented opt-out flag stops GA4 sending anything for visitors whose browser sends Global Privacy Control. 154 // It has to be set before any gtag() call, so it leads every inline script. 155 if ( $settings['gpc'] ) { 156 $gtag_stub = 'if(navigator.globalPrivacyControl){window["ga-disable-' . esc_js( $id ) . '"]=true;}' . $gtag_stub; 157 } 158 159 if ( $in_head && $settings['consent_mode'] ) { 90 160 // Consent Mode v2 defaults have to be set before gtag.js loads; the consent banner then calls gtag("consent","update",...) 91 161 $consent = apply_filters( 'lightweight_ga_consent_defaults', array( … … 94 164 'ad_user_data' => 'denied', 95 165 'ad_personalization' => 'denied', 96 'wait_for_update' => 500, 166 // How long Google waits for the consent update before sending anything; it carries on as soon as the update 167 // arrives. With the WP Consent API the update comes once the page is parsed (its script is in the footer), so 168 // allow for slow pages. Consent banners that call gtag themselves usually update early in the head. 169 'wait_for_update' => lightweight_ga_has_consent_api() ? 2000 : 500, 97 170 ) ); 98 wp_print_inline_script_tag( $gtag_stub . 'gtag("consent","default",' . wp_json_encode( $consent ) . ');' ); 99 } 100 171 $update = ''; 172 if ( lightweight_ga_has_consent_api() ) { 173 // Pass the visitor's choices from any consent banner that supports the WP Consent API on to Google 174 // Banners such as Complianz change categories one at a time, so only send an update when the result changes 175 $update = lightweight_ga_consent_api_js( '(function(){var l;return function(){var s=wp_has_consent("statistics")?"granted":"denied",m=wp_has_consent("marketing")?"granted":"denied";if(s+m===l){return;}l=s+m;gtag("consent","update",{analytics_storage:s,ad_storage:m,ad_user_data:m,ad_personalization:m});};})()' ); 176 } 177 echo "<!-- Google Consent Mode defaults - Lightweight Google Analytics -->\n"; 178 wp_print_inline_script_tag( $gtag_stub . 'gtag("consent","default",' . wp_json_encode( $consent ) . ');' . $update, array( 'data-lightweight-ga' => true ) ); 179 } 180 181 if ( ! $is_position ) { 182 return; 183 } 184 185 echo "<!-- Google tag (gtag.js) - Lightweight Google Analytics -->\n"; 101 186 wp_print_script_tag( array( 102 'src' => 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $id ), 103 'async' => true, 187 'src' => 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $id ), 188 'async' => true, 189 'data-lightweight-ga' => true, // marks this plugin's own tags for the duplicate tracking check 104 190 ) ); 105 191 … … 111 197 $config['debug_mode'] = true; 112 198 } 199 if ( $settings['content_groups'] ) { 200 $config = array_merge( $config, lightweight_ga_page_dimensions() ); 201 } 113 202 // Add or change gtag config options, e.g. send_page_view, cookie_flags or custom dimensions 114 203 $config = apply_filters( 'lightweight_ga_gtag_config', $config, $id ); 115 wp_print_inline_script_tag( $gtag_stub . 'gtag("js",new Date());gtag("config","' . esc_js( $id ) . '"' . ( $config ? ',' . wp_json_encode( $config ) : '' ) . ');' ); 204 wp_print_inline_script_tag( $gtag_stub . 'gtag("js",new Date());gtag("config","' . esc_js( $id ) . '"' . ( $config ? ',' . wp_json_encode( $config ) : '' ) . ');', array( 'data-lightweight-ga' => true ) ); 205 } 206 207 // Minimal Analytics 4 is configured by swapping values in the bundled script; check all three anchors whenever the file is updated 208 function lightweight_ga_print_minimal_analytics( $settings ) { 209 $script = file_get_contents( __DIR__ . '/js/minimal-analytics-4.min.js' ); 210 if ( ! $script ) { 211 return; 212 } 213 // Content group and author ride along with every hit as event parameters 214 $params = ''; 215 foreach ( $settings['content_groups'] ? lightweight_ga_page_dimensions() : array() as $key => $value ) { 216 $params .= wp_json_encode( 'ep.' . $key ) . ':' . wp_json_encode( $value ) . ','; 217 } 218 $script = str_replace( 219 array( 'tid:"G-XXXXXXXXXX"', ']},T=!1,', '_dbg:T?1:0[0]' ), 220 array( 'tid:"' . esc_js( $settings['tracking_id'] ) . '"', ']},T=' . ( $settings['debug_mode'] ? '!0' : '!1' ) . ',', $params . '_dbg:T?1:0[0]' ), 221 $script 222 ); 223 // Run once the page is parsed: from the head it would otherwise send its page view before <title> exists and read a null document.body 224 $run = 'document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r):r();'; 225 if ( $settings['consent_mode'] && lightweight_ga_has_consent_api() ) { 226 // Minimal Analytics has no Consent Mode, so with the WP Consent API it only starts once the visitor allows statistics 227 $run = 'var d=0;' . lightweight_ga_consent_api_js( 'function(){if(!d&&wp_has_consent("statistics")){d=1;r();}}' ); 228 } 229 if ( $settings['gpc'] ) { 230 $run = 'if(!navigator.globalPrivacyControl){' . $run . '}'; 231 } 232 echo "<!-- Minimal Analytics 4 - Lightweight Google Analytics -->\n"; 233 wp_print_inline_script_tag( '(function(){var r=function(){' . $script . "\n};" . $run . '})();', array( 'data-lightweight-ga' => true ) ); 116 234 } 117 235 … … 121 239 add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); 122 240 add_action( 'admin_init', array( $this, 'init_settings' ) ); 241 add_action( 'admin_init', array( $this, 'maybe_upgrade' ) ); 123 242 add_action( 'admin_notices', array( $this, 'admin_notices' ) ); 243 add_action( 'wp_ajax_lightweight_ga_scan', array( $this, 'ajax_scan' ) ); 124 244 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'add_settings_link' ) ); 125 245 } 126 246 247 // Remembers the plugin version so an update can run one-off checks 248 public function maybe_upgrade() { 249 $state = (array) get_option( 'lightweight_ga_state', array() ); 250 if ( isset( $state['version'] ) && LIGHTWEIGHT_GA_VERSION === $state['version'] ) { 251 return; 252 } 253 if ( empty( $state['version'] ) && false !== get_option( 'lightweight_ga_settings' ) ) { 254 // 1.5.0 to 1.6.0 ticked every checkbox the first time the settings were saved, so ask the site owner to check them 255 $settings = lightweight_ga_settings(); 256 $state['review'] = $settings['consent_mode'] || $settings['debug_mode'] || $settings['disable_display_features']; 257 } 258 $state['version'] = LIGHTWEIGHT_GA_VERSION; 259 update_option( 'lightweight_ga_state', $state ); 260 } 261 127 262 public function add_settings_link( $links ) { 128 $links[] = '<a href="' . esc_url( menu_page_url( 'lightweight-google-analytics', false) ) . '">' . esc_html__( 'Settings', 'lightweight-google-analytics' ) . '</a>';263 $links[] = '<a href="' . esc_url( admin_url( 'options-general.php?page=lightweight-google-analytics' ) ) . '">' . esc_html__( 'Settings', 'lightweight-google-analytics' ) . '</a>'; 129 264 return $links; 130 265 } 131 266 132 267 public function admin_notices() { 133 $settings = lightweight_ga_settings(); 134 $url = esc_url( menu_page_url( 'lightweight-google-analytics', false ) ); 268 if ( ! current_user_can( 'manage_options' ) ) { 269 return; 270 } 271 $settings = lightweight_ga_settings(); 272 $screen = function_exists( 'get_current_screen' ) && get_current_screen() ? get_current_screen()->id : ''; 273 $url = esc_url( admin_url( 'options-general.php?page=lightweight-google-analytics' ) ); 274 $state = (array) get_option( 'lightweight_ga_state', array() ); 275 276 if ( ! empty( $state['review'] ) ) { 277 $on = array(); 278 foreach ( array( 'consent_mode' => __( 'Consent Mode', 'lightweight-google-analytics' ), 'debug_mode' => __( 'Debug Mode', 'lightweight-google-analytics' ), 'disable_display_features' => __( 'Disable Display Features', 'lightweight-google-analytics' ) ) as $key => $label ) { 279 if ( $settings[ $key ] ) { 280 $on[] = $label; 281 } 282 } 283 /* translators: 1: list of setting names, 2: link to the plugin settings page */ 284 $message = sprintf( __( '<strong>Lightweight Google Analytics:</strong> a bug in versions 1.5.0 to 1.6.0 could switch settings on by themselves the first time they were saved. These are on for this site: %1$s. Consent Mode without a consent banner and Debug Mode can both reduce the data you see in Google Analytics. Please check them in the %2$s and save.', 'lightweight-google-analytics' ), esc_html( implode( ', ', $on ) ), '<a href="' . $url . '">' . esc_html__( 'plugin settings', 'lightweight-google-analytics' ) . '</a>' ); 285 printf( '<div class="notice notice-warning"><p>%s</p></div>', wp_kses_post( $message ) ); 286 } 287 288 // The settings page shows its own status message 289 if ( 'settings_page_lightweight-google-analytics' === $screen ) { 290 return; 291 } 135 292 if ( 0 === strpos( $settings['tracking_id'], 'UA-' ) ) { 136 293 /* translators: %s: link to the plugin settings page */ 137 294 $message = sprintf( __( '<strong>Lightweight Google Analytics:</strong> Google shut down Universal Analytics in 2023 and UA- IDs are no longer supported, so no tracking code is being output. Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) in the %s.', 'lightweight-google-analytics' ), '<a href="' . $url . '">' . esc_html__( 'plugin settings', 'lightweight-google-analytics' ) . '</a>' ); 138 295 printf( '<div class="notice notice-warning"><p>%s</p></div>', wp_kses_post( $message ) ); 139 } elseif ( '' === $settings['tracking_id'] ) {296 } elseif ( '' === $settings['tracking_id'] && in_array( $screen, array( 'dashboard', 'plugins' ), true ) ) { 140 297 /* translators: %s: link to the plugin settings page */ 141 298 $message = sprintf( __( 'Thank you for installing <strong>Lightweight Google Analytics</strong> - head to the %s to finish setting up Google Analytics.', 'lightweight-google-analytics' ), '<a href="' . $url . '">' . esc_html__( 'settings', 'lightweight-google-analytics' ) . '</a>' ); … … 145 302 146 303 public function sanitize_settings( $input ) { 147 $input = (array) $input; 148 $sanitized = array( 'tracking_id' => '' ); 149 150 $tracking_id = isset( $input['tracking_id'] ) ? strtoupper( trim( sanitize_text_field( $input['tracking_id'] ) ) ) : ''; 151 if ( preg_match( '/^G-[A-Z0-9]+$/', $tracking_id ) ) { 152 $sanitized['tracking_id'] = $tracking_id; 153 } elseif ( 0 === strpos( $tracking_id, 'UA-' ) ) { 154 add_settings_error( 'lightweight_ga_settings', 'ua_tracking_id', __( 'Universal Analytics (UA-) IDs are no longer supported because Google shut Universal Analytics down in 2023. Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) instead.', 'lightweight-google-analytics' ) ); 155 } elseif ( '' !== $tracking_id ) { 156 add_settings_error( 'lightweight_ga_settings', 'invalid_tracking_id', __( 'Invalid Measurement ID. It should look like G-XXXXXXXX and can be found in Google Analytics under Admin > Data Streams.', 'lightweight-google-analytics' ) ); 157 } 158 159 foreach ( array( 'enable_lightweight_ga', 'disable_display_features', 'consent_mode', 'debug_mode' ) as $checkbox ) { 160 $sanitized[ $checkbox ] = isset( $input[ $checkbox ] ) ? 'checked' : ''; 304 $input = (array) $input; 305 $previous = lightweight_ga_settings()['tracking_id']; 306 307 // Accept the ID itself, or a pasted gtag snippet or URL that contains it. The ID is looked for in the raw text because 308 // sanitize_text_field() strips <script> blocks whole, URL included; only the matched letters and digits are ever kept. 309 $raw = isset( $input['tracking_id'] ) && is_string( $input['tracking_id'] ) ? strtoupper( trim( $input['tracking_id'] ) ) : ''; 310 if ( preg_match( '/^G-[A-Z0-9]+$/', $raw ) ) { 311 $tracking_id = $raw; 312 } elseif ( preg_match( '/(?<![A-Z0-9])G-[A-Z0-9]{4,}(?![A-Z0-9])/', $raw, $found ) ) { 313 $tracking_id = $found[0]; 314 /* translators: %s: the Measurement ID found in the pasted text */ 315 add_settings_error( 'lightweight_ga_settings', 'extracted_tracking_id', sprintf( __( 'Found the Measurement ID %s in the pasted text and saved it.', 'lightweight-google-analytics' ), $tracking_id ), 'info' ); 316 } else { 317 $tracking_id = sanitize_text_field( $raw ); 318 } 319 320 $sanitized = array( 'tracking_id' => $tracking_id ); 321 if ( '' !== $tracking_id && ! preg_match( '/^G-[A-Z0-9]+$/', $tracking_id ) ) { 322 // Keep the working ID instead of switching tracking off because of a typo 323 $sanitized['tracking_id'] = $previous; 324 if ( 0 === strpos( $tracking_id, 'UA-' ) ) { 325 add_settings_error( 'lightweight_ga_settings', 'ua_tracking_id', __( 'Universal Analytics (UA-) IDs are no longer supported because Google shut Universal Analytics down in 2023. Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) instead.', 'lightweight-google-analytics' ) ); 326 } else { 327 add_settings_error( 'lightweight_ga_settings', 'invalid_tracking_id', __( 'Invalid Measurement ID. It should look like G-XXXXXXXX and can be found in Google Analytics under Admin > Data Streams.', 'lightweight-google-analytics' ) . ( $previous ? ' ' . __( 'Your previous Measurement ID was kept.', 'lightweight-google-analytics' ) : '' ) ); 328 } 329 } 330 331 // ! empty() rather than isset(): on the very first save WordPress runs this twice, the second time on the already sanitized values 332 foreach ( array( 'enable_lightweight_ga', 'disable_display_features', 'consent_mode', 'gpc', 'content_groups', 'debug_mode' ) as $checkbox ) { 333 $sanitized[ $checkbox ] = ! empty( $input[ $checkbox ] ) ? 'checked' : ''; 161 334 } 162 335 … … 167 340 168 341 $sanitized['tracking_code'] = isset( $input['tracking_code'] ) && 'minimalanalytics-inline' === $input['tracking_code'] ? 'minimalanalytics-inline' : 'analytics-js'; 342 343 // The duplicate tag check runs again with the new settings 344 delete_transient( 'lightweight_ga_duplicate_scan' ); 345 346 // Saving the settings answers the post-update request to review them 347 $state = (array) get_option( 'lightweight_ga_state', array() ); 348 if ( ! empty( $state['review'] ) ) { 349 $state['review'] = false; 350 update_option( 'lightweight_ga_state', $state ); 351 } 169 352 170 353 return $sanitized; … … 201 384 'tracking_code' => __( 'Tracking Code', 'lightweight-google-analytics' ), 202 385 'consent_mode' => __( 'Consent Mode', 'lightweight-google-analytics' ), 386 'gpc' => __( 'Global Privacy Control', 'lightweight-google-analytics' ), 387 'content_groups' => __( 'Content Groups', 'lightweight-google-analytics' ), 203 388 'debug_mode' => __( 'Debug Mode', 'lightweight-google-analytics' ), 204 389 ); 205 390 foreach ( $fields as $field => $label ) { 206 add_settings_field( $field, $label, array( $this, 'render_' . $field . '_field' ), 'lightweight_ga_settings', 'lightweight_ga_settings_section', array( 'class' => str_replace( '_', '-', $field ) ) ); 207 } 208 } 209 210 // Tells the admin whether tracking is live, and why they personally will not show up in reports 391 // label_for ties each row's title to its input for screen readers 392 add_settings_field( $field, $label, array( $this, 'render_' . $field . '_field' ), 'lightweight_ga_settings', 'lightweight_ga_settings_section', array( 'class' => str_replace( '_', '-', $field ), 'label_for' => $field . '_field' ) ); 393 } 394 } 395 396 // Tells the admin whether tracking is live, why they personally will not show up in reports, and what might need attention 211 397 public function status_notice() { 212 398 $settings = lightweight_ga_settings(); 213 if ( empty( $settings['enable_lightweight_ga'] ) ) { 399 if ( false === get_option( 'lightweight_ga_settings' ) ) { 400 $class = 'warning'; 401 $message = __( 'Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) and save to start tracking.', 'lightweight-google-analytics' ); 402 } elseif ( empty( $settings['enable_lightweight_ga'] ) ) { 214 403 $class = 'warning'; 215 404 $message = __( 'Tracking is turned off. Tick "Enable Google Analytics" and save to start tracking.', 'lightweight-google-analytics' ); … … 228 417 } 229 418 printf( '<div class="notice notice-%s inline" style="margin:15px 0 0"><p>%s</p></div>', esc_attr( $class ), esc_html( $message ) ); 419 420 foreach ( $this->warnings( $settings ) as $warning ) { 421 printf( '<div class="notice notice-warning inline" style="margin:10px 0 0"><p>%s</p></div>', wp_kses_post( $warning ) ); 422 } 423 } 424 425 // Settings combinations that don't do what the site owner probably expects 426 private function warnings( $settings ) { 427 $warnings = array(); 428 if ( 'minimalanalytics-inline' === $settings['tracking_code'] && $settings['consent_mode'] && ! lightweight_ga_has_consent_api() ) { 429 $warnings[] = __( 'Consent Mode only applies to Google\'s gtag.js. Minimal Analytics runs for every visitor whatever they choose in your consent banner, unless the <a href="https://wordpress.org/plugins/wp-consent-api/" target="_blank" rel="noopener">WP Consent API</a> plugin is active with a consent banner that supports it.', 'lightweight-google-analytics' ); 430 } 431 if ( lightweight_ga_has_consent_api() && ! $settings['consent_mode'] ) { 432 $warnings[] = __( 'The WP Consent API is active, but Consent Mode is off, so Google Analytics ignores your visitors\' choices in your consent banner. Turn on Consent Mode to follow them.', 'lightweight-google-analytics' ); 433 } 434 if ( $settings['enable_lightweight_ga'] && lightweight_ga_has_ga4_id() ) { 435 list( $ga, $gtm ) = $this->other_ga_plugins(); 436 if ( $ga ) { 437 /* translators: %s: plugin name(s) */ 438 $warnings[] = sprintf( __( '%s is also set up to add Google Analytics to your site, so your page views are probably counted twice. Turn off the tracking code in one of the plugins.', 'lightweight-google-analytics' ), esc_html( implode( ', ', $ga ) ) ); 439 } 440 if ( $gtm ) { 441 /* translators: %s: plugin name */ 442 $warnings[] = sprintf( __( '%s adds a Google Tag Manager container to your site. If that container also sends GA4 page views, they are counted twice.', 'lightweight-google-analytics' ), esc_html( implode( ', ', $gtm ) ) ); 443 } 444 } 445 return $warnings; 446 } 447 448 // Active plugins that are set up to print their own Google Analytics tag (and ones that print a Tag Manager container, 449 // which may or may not include GA4). Settings outlive deactivated plugins, so each check also needs the plugin active. 450 private function other_ga_plugins() { 451 if ( ! function_exists( 'is_plugin_active' ) ) { 452 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 453 } 454 $option = function ( $name, $key ) { 455 $value = get_option( $name ); 456 return is_array( $value ) && isset( $value[ $key ] ) ? $value[ $key ] : ''; 457 }; 458 $monsterinsights = function () { 459 return function_exists( 'monsterinsights_get_v4_id' ) && monsterinsights_get_v4_id(); 460 }; 461 $exactmetrics = function () { 462 return function_exists( 'exactmetrics_get_v4_id' ) && exactmetrics_get_v4_id(); 463 }; 464 $checks = array( 465 'google-site-kit/google-site-kit.php' => array( 'Site Kit by Google', function () use ( $option ) { 466 return in_array( 'analytics-4', (array) get_option( 'googlesitekit_active_modules' ), true ) && $option( 'googlesitekit_analytics-4_settings', 'useSnippet' ) && $option( 'googlesitekit_analytics-4_settings', 'measurementID' ); 467 } ), 468 'google-analytics-for-wordpress/googleanalytics.php' => array( 'MonsterInsights', $monsterinsights ), 469 'google-analytics-premium/googleanalytics-premium.php' => array( 'MonsterInsights Pro', $monsterinsights ), 470 'google-analytics-dashboard-for-wp/gadwp.php' => array( 'ExactMetrics', $exactmetrics ), 471 'exactmetrics-premium/exactmetrics-premium.php' => array( 'ExactMetrics Pro', $exactmetrics ), 472 'wp-analytify/wp-analytify.php' => array( 'Analytify', function () use ( $option ) { 473 return 'on' === $option( 'wp-analytify-profile', 'install_ga_code' ) && $option( 'wp-analytify-profile', 'profile_for_posts' ); 474 } ), 475 'ga-google-analytics/ga-google-analytics.php' => array( 'GA Google Analytics', function () use ( $option ) { 476 return $option( 'gap_options', 'gap_id' ) && $option( 'gap_options', 'gap_enable' ); 477 } ), 478 'woocommerce-google-analytics-integration/woocommerce-google-analytics-integration.php' => array( 'Google Analytics for WooCommerce', function () use ( $option ) { 479 return (bool) $option( 'woocommerce_google_analytics_settings', 'ga_id' ); 480 } ), 481 'host-analyticsjs-local/host-analyticsjs-local.php' => array( 'CAOS', function () use ( $option ) { 482 return $option( 'caos_settings', 'measurement_id' ) && 'manual' !== $option( 'caos_settings', 'script_position' ); 483 } ), 484 'seo-by-rank-math/rank-math.php' => array( 'Rank Math', function () use ( $option ) { 485 return in_array( 'analytics', (array) get_option( 'rank_math_modules' ), true ) && $option( 'rank_math_google_analytic_options', 'install_code' ) && $option( 'rank_math_google_analytic_options', 'measurement_id' ); 486 } ), 487 'wp-seopress/seopress.php' => array( 'SEOPress', function () use ( $option ) { 488 return '1' === $option( 'seopress_toggle', 'toggle-google-analytics' ) && '1' === $option( 'seopress_google_analytics_option_name', 'seopress_google_analytics_enable' ) && $option( 'seopress_google_analytics_option_name', 'seopress_google_analytics_ga4' ); 489 } ), 490 'complianz-gdpr/complianz-gpdr.php' => array( 'Complianz', function () use ( $option ) { 491 return 'google-analytics' === $option( 'cmplz_options', 'compile_statistics' ) && 'no' !== $option( 'cmplz_options', 'configuration_by_complianz' ) && $option( 'cmplz_options', 'ua_code' ); 492 } ), 493 ); 494 $ga = array(); 495 foreach ( $checks as $basename => $check ) { 496 if ( is_plugin_active( $basename ) && call_user_func( $check[1] ) ) { 497 $ga[] = $check[0]; 498 } 499 } 500 $gtm = array(); 501 if ( is_plugin_active( 'duracelltomi-google-tag-manager/duracelltomi-google-tag-manager-for-wordpress.php' ) && $option( 'gtm4wp-options', 'gtm-code' ) ) { 502 $gtm[] = 'GTM4WP'; 503 } 504 return array( array_unique( $ga ), $gtm ); 505 } 506 507 // Looks for other Google tags on the home page as a logged-out visitor sees it, which catches tracking code added by the 508 // theme or a snippet plugin. Runs in the background from the settings page; cached for 12 hours and cleared on save. 509 public function ajax_scan() { 510 check_ajax_referer( 'lightweight_ga_scan' ); 511 if ( ! current_user_can( 'manage_options' ) ) { 512 wp_send_json( array() ); 513 } 514 $ids = get_transient( 'lightweight_ga_duplicate_scan' ); 515 if ( ! is_array( $ids ) ) { 516 // A cache-busting parameter, never ?tagverify (Site Kit hides its own tag for that) 517 $url = add_query_arg( 'lga_check', time(), home_url( '/' ) ); 518 $response = wp_remote_get( $url, array( 519 'timeout' => 10, 520 'headers' => array( 'Cache-Control' => 'no-cache' ), 521 'sslverify' => apply_filters( 'https_local_ssl_verify', false, $url ), 522 ) ); 523 // A blocked or failed request means "don't know", not "no duplicates" 524 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { 525 wp_send_json( array() ); 526 } 527 // Drop this plugin's own tags, then collect the Google tag IDs that are left. IDs are matched rather than gtag.js 528 // URLs because some plugins serve gtag.js from the site itself. 529 $html = preg_replace( '#<script[^>]*data-lightweight-ga[^>]*>.*?</script>#is', '', wp_remote_retrieve_body( $response ) ); 530 preg_match_all( '/(?:id=|["\'])((?:G|GT)-[A-Z0-9]{6,12}|GTM-[A-Z0-9]{4,10})(?![A-Z0-9])/', $html, $matches ); 531 $ids = array_values( array_unique( $matches[1] ) ); 532 set_transient( 'lightweight_ga_duplicate_scan', $ids, 12 * HOUR_IN_SECONDS ); 533 } 534 $own = lightweight_ga_settings()['tracking_id']; 535 $messages = array(); 536 // A second copy of the site's own ID is the certain duplicate, so it goes first 537 usort( $ids, function ( $a, $b ) use ( $own ) { 538 return ( $b === $own ) - ( $a === $own ); 539 } ); 540 foreach ( $ids as $tag_id ) { 541 if ( $tag_id === $own ) { 542 /* translators: %s: Measurement ID */ 543 $messages[] = sprintf( __( 'Your Measurement ID (%s) is also added to your home page by your theme or another plugin, so your page views are counted twice. Remove the other copy of the tracking code.', 'lightweight-google-analytics' ), $tag_id ); 544 } elseif ( 0 === strpos( $tag_id, 'GTM-' ) ) { 545 /* translators: %s: Google Tag Manager container ID */ 546 $messages[] = sprintf( __( 'A Google Tag Manager container (%s) is on your home page. If it also sends GA4 page views, they are counted twice.', 'lightweight-google-analytics' ), $tag_id ); 547 } else { 548 /* translators: %s: Google tag ID */ 549 $messages[] = sprintf( __( 'Another Google tag (%s) is on your home page, added by your theme or another plugin. If it also sends page views to Google Analytics, they are counted twice.', 'lightweight-google-analytics' ), $tag_id ); 550 } 551 } 552 wp_send_json( $messages ); 230 553 } 231 554 … … 239 562 echo ' <h2>' . esc_html__( 'Options', 'lightweight-google-analytics' ) . '</h2>' . "\n"; 240 563 $this->status_notice(); 564 $settings = lightweight_ga_settings(); 565 if ( $settings['enable_lightweight_ga'] && lightweight_ga_has_ga4_id() ) { 566 // Checks the home page for other Google tags in the background, so a slow loopback request doesn't hold up this page 567 echo '<div id="lightweight-ga-scan" aria-live="polite"></div>'; 568 echo '<script>(function(){var b=new URLSearchParams({action:"lightweight_ga_scan",_ajax_nonce:' . wp_json_encode( wp_create_nonce( 'lightweight_ga_scan' ) ) . '});fetch(' . wp_json_encode( admin_url( 'admin-ajax.php' ) ) . ',{method:"POST",credentials:"same-origin",body:b}).then(function(r){return r.json();}).then(function(m){var w=document.getElementById("lightweight-ga-scan");(Array.isArray(m)?m:[]).forEach(function(t){var d=document.createElement("div"),p=document.createElement("p");d.className="notice notice-warning inline";d.style.margin="10px 0 0";p.textContent=t;d.appendChild(p);w.appendChild(d);});}).catch(function(){});})();</script>'; 569 } 241 570 echo ' <form action="options.php" method="post">' . "\n"; 242 571 … … 271 600 public function render_tracking_id_field() { 272 601 $settings = lightweight_ga_settings(); 273 echo '<input type="text" name="lightweight_ga_settings[tracking_id]" class="regular-text tracking_id_field" placeholder="G-XXXXXXXX" value="' . esc_attr( $settings['tracking_id'] ) . '">';274 echo '<span class="tracking_id_field_validate" style="display:none;color:#27ae60;font-weight:500;margin-left:0.8em;vertical-align:middle;"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="vertical-align:middle;" ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg> ' . esc_html__( 'Google Analytics 4', 'lightweight-google-analytics' ) . '</span>';275 echo '<p class="description">' . wp_kses_post( __( 'Your Google Analytics 4 Measurement ID, which starts with G-. Find it in <a href="https://analytics.google.com/" target="_blank" rel="noopener">Google Analytics</a> under Admin > Data Streams > your web stream. ', 'lightweight-google-analytics' ) ) . '</p>';602 echo '<input type="text" name="lightweight_ga_settings[tracking_id]" id="tracking_id_field" class="regular-text tracking_id_field" placeholder="G-XXXXXXXX" value="' . esc_attr( $settings['tracking_id'] ) . '">'; 603 echo '<span class="tracking_id_field_validate" style="display:none;color:#27ae60;font-weight:500;margin-left:0.8em;vertical-align:middle;"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="vertical-align:middle;" aria-hidden="true"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg> ' . esc_html__( 'Google Analytics 4', 'lightweight-google-analytics' ) . '</span>'; 604 echo '<p class="description">' . wp_kses_post( __( 'Your Google Analytics 4 Measurement ID, which starts with G-. Find it in <a href="https://analytics.google.com/" target="_blank" rel="noopener">Google Analytics</a> under Admin > Data Streams > your web stream. You can also paste Google\'s whole tag snippet and the ID will be picked out of it.', 'lightweight-google-analytics' ) ) . '</p>'; 276 605 } 277 606 278 607 public function render_tracking_position_field() { 279 608 $settings = lightweight_ga_settings(); 280 echo '<select name="lightweight_ga_settings[tracking_position]" class="tracking_position_field">';609 echo '<select name="lightweight_ga_settings[tracking_position]" id="tracking_position_field" class="tracking_position_field">'; 281 610 echo '<option value="header" ' . selected( $settings['tracking_position'], 'header', false ) . '>' . esc_html__( 'Header (default, recommended)', 'lightweight-google-analytics' ) . '</option>'; 282 611 echo '<option value="footer" ' . selected( $settings['tracking_position'], 'footer', false ) . '>' . esc_html__( 'Footer', 'lightweight-google-analytics' ) . '</option>'; … … 298 627 'all' => __( 'Track all users (including logged in admins)', 'lightweight-google-analytics' ), 299 628 ); 300 echo '<select name="lightweight_ga_settings[tracking_level]" class="tracking_level_field">';629 echo '<select name="lightweight_ga_settings[tracking_level]" id="tracking_level_field" class="tracking_level_field">'; 301 630 foreach ( $levels as $value => $label ) { 302 631 echo '<option value="' . esc_attr( $value ) . '" ' . selected( $settings['tracking_level'], $value, false ) . '>' . esc_html( $label ) . '</option>'; … … 308 637 public function render_tracking_code_field() { 309 638 $settings = lightweight_ga_settings(); 310 echo '<select name="lightweight_ga_settings[tracking_code]" class="tracking_code_field">';639 echo '<select name="lightweight_ga_settings[tracking_code]" id="tracking_code_field" class="tracking_code_field">'; 311 640 echo '<option value="analytics-js" ' . selected( $settings['tracking_code'], 'analytics-js', false ) . '>' . esc_html__( "Google's gtag.js (recommended)", 'lightweight-google-analytics' ) . '</option>'; 312 641 echo '<option value="minimalanalytics-inline" ' . selected( $settings['tracking_code'], 'minimalanalytics-inline', false ) . '>' . esc_html__( 'Minimal Analytics 4 by idarek (inline, no external request, fewer features)', 'lightweight-google-analytics' ) . '</option>'; 313 642 echo '</select>'; 314 echo '<p class="description">' . wp_kses_post( __( 'Minimal Analytics 4 is a 3.5 KB inline script that sends page views, 90% scrolls, file downloads, outbound clicks and site searches straight to Google Analytics without loading gtag.js. It stores its client and session IDs in localStorage, so it is not affected by Consent Mode. <a href="https://github.com/idarek/minimal-analytics-4" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ) . '</p>';643 echo '<p class="description">' . wp_kses_post( __( 'Minimal Analytics 4 is a 3.5 KB inline script that sends page views, 90% scrolls, file downloads, outbound clicks and site searches straight to Google Analytics without loading gtag.js. It stores its client and session IDs in localStorage, so Consent Mode does not apply to it; with the WP Consent API it waits for statistics consent instead. <a href="https://github.com/idarek/minimal-analytics-4" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ) . '</p>'; 315 644 } 316 645 317 646 public function render_consent_mode_field() { 318 $this->checkbox( 'consent_mode', __( 'Enable Google Consent Mode v2', 'lightweight-google-analytics' ), __( 'Analytics and ad storage are denied by default until a cookie consent plugin grants consent with <code>gtag("consent", "update", ...)</code>. Only applies to gtag.js. <a href="https://developers.google.com/tag-platform/security/guides/consent" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ); 647 $this->checkbox( 'consent_mode', __( 'Enable Google Consent Mode v2', 'lightweight-google-analytics' ), __( 'Analytics and ad storage are denied by default until your cookie consent plugin grants consent. Works automatically with consent plugins that support the <a href="https://wordpress.org/plugins/wp-consent-api/" target="_blank" rel="noopener">WP Consent API</a> (such as Complianz and CookieYes), which also makes Minimal Analytics wait for consent. Other consent plugins need to call <code>gtag("consent", "update", ...)</code> themselves. <a href="https://developers.google.com/tag-platform/security/guides/consent" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ); 648 } 649 650 public function render_gpc_field() { 651 $this->checkbox( 'gpc', __( 'Honor Global Privacy Control', 'lightweight-google-analytics' ), __( 'Visitors whose browser sends a <a href="https://globalprivacycontrol.org/" target="_blank" rel="noopener">Global Privacy Control</a> signal (an opt-out built into Brave, DuckDuckGo and Firefox) are not measured. Recommended if your site has to follow California\'s CCPA or a similar law that treats the signal as an opt-out request.', 'lightweight-google-analytics' ) ); 652 } 653 654 public function render_content_groups_field() { 655 $this->checkbox( 'content_groups', __( 'Send content groups and authors', 'lightweight-google-analytics' ), __( 'Sends the post\'s category (or the page type, such as Page or Search) as GA4\'s Content group, so you can compare sections of your site in reports. Also sends the author\'s name as an <code>author</code> parameter; to report on it, add an event-scoped custom dimension named <code>author</code> in Google Analytics under Admin > Custom definitions.', 'lightweight-google-analytics' ) ); 319 656 } 320 657 -
lightweight-google-analytics/tags/1.7.0/readme.txt
r3689051 r3742708 6 6 Tested up to: 7.1 7 7 Requires PHP: 7.0 8 Stable tag: 1. 6.08 Stable tag: 1.7.0 9 9 License: GPLv3 10 10 License URI: http://www.gnu.org/licenses/gpl.html … … 19 19 20 20 * Performance focused with no bloat and never any upsells 21 * Only needs your GA4 Measurement ID (G-XXXXXXXX) 21 * Only needs your GA4 Measurement ID (G-XXXXXXXX), or paste Google's whole tag snippet and the ID is picked out of it 22 22 * Load the tracking code in the header or footer 23 23 * Exclude logged-in users by role (admins, editors and authors are excluded by default) 24 24 * Post previews and the Customizer are never tracked 25 25 * Option to disable Google's advertising features (Google signals) 26 * Google Consent Mode v2 support for GDPR/CCPA compliance with cookie consent plugins 26 * Google Consent Mode v2 support for GDPR/CCPA compliance, working automatically with consent plugins that support the WP Consent API (such as Complianz and CookieYes) 27 * Option to honor Global Privacy Control, the opt-out signal sent by Brave, DuckDuckGo and Firefox 28 * Optional content groups: the post's category (or page type) as GA4's Content group, plus the author's name, so you can compare sections and authors in reports 29 * Warns you on the settings page when another plugin, your theme or a pasted snippet also adds Google Analytics, so page views aren't counted twice 27 30 * Debug Mode for testing in GA4 DebugView 28 31 * Optional Minimal Analytics 4 mode: a 3.5 KB inline script that reports page views, scrolls, downloads, outbound clicks and site searches without loading gtag.js at all 29 32 * The settings page tells you whether tracking is live and why your own visits may not show up 30 * Works with WP Rocket's local Google Analytics feature31 33 * Filters for developers to change the gtag config, the consent defaults or when tracking is skipped 32 34 … … 50 52 $defaults['region'] = array( 'AT', 'BE', 'DE', 'FR', 'IE', 'NL' ); 51 53 return $defaults; 54 } );` 55 56 Change the content group or author sent with each page (when Content Groups is on), or add more page-level parameters: 57 58 `add_filter( 'lightweight_ga_page_dimensions', function ( $dimensions ) { 59 if ( is_page( 'pricing' ) ) { 60 $dimensions['content_group'] = 'Sales'; 61 } 62 return $dimensions; 52 63 } );` 53 64 … … 78 89 Google Consent Mode v2 tells Google that analytics and advertising storage are denied until the visitor consents. Enable it if you use a cookie consent plugin that updates the consent state through `gtag("consent", "update", ...)` and you need to comply with GDPR or similar rules. It only applies to Google's gtag.js code. 79 90 91 = Which consent plugins work with Consent Mode? = 92 93 Any consent plugin that supports the [WP Consent API](https://wordpress.org/plugins/wp-consent-api/), such as Complianz and CookieYes, works automatically: install and activate the WP Consent API plugin, then turn on Consent Mode. Statistics consent controls Google Analytics storage and marketing consent controls the advertising storage types, and both follow the visitor's choice when it changes. Note that the WP Consent API treats a site with no consent plugin as having consent. Other consent plugins need to call `gtag("consent", "update", ...)` themselves. 94 95 = What does the Global Privacy Control option do? = 96 97 Global Privacy Control is an opt-out signal built into Brave and DuckDuckGo and available in Firefox. California's CCPA and similar US state laws treat it as a request to opt out of the sale or sharing of personal information. When the option is on, visitors who send the signal are not measured: gtag.js gets Google's documented opt-out flag, and Minimal Analytics doesn't run at all. 98 99 = How do I see content groups and authors in Google Analytics? = 100 101 Turn on Content Groups in the plugin settings. The content group shows up under the built-in "Content group" dimension in reports such as Pages and screens. To report on authors, add an event-scoped custom dimension named `author` in Google Analytics under Admin > Custom definitions; it fills in from then on, usually within 48 hours. 102 103 = Why does the settings page say my page views are counted twice? = 104 105 The plugin checks whether another analytics plugin is set up to add Google Analytics too (such as Site Kit, MonsterInsights or Rank Math), and looks at your home page for other Google tags added by your theme or a code snippet plugin. Remove the extra copy so each visit is only counted once. Tags that only appear on some pages, or only after cookie consent, can't be detected. 106 80 107 = What is the Minimal Analytics 4 option? = 81 108 82 It replaces Google's gtag.js with [Minimal Analytics 4](https://github.com/idarek/minimal-analytics-4) by Dariusz Wieckiewicz (MIT), a small inline script that talks to Google Analytics directly. It sends page views, session start and engagement events, 90% scrolls, file downloads, outbound link clicks and site searches, plus UTM campaign attribution. It does not support Consent Mode or Google's enhanced measurement, and it stores its client and session IDs in localStorage, so check your privacy requirements before using it. 109 It replaces Google's gtag.js with [Minimal Analytics 4](https://github.com/idarek/minimal-analytics-4) by Dariusz Wieckiewicz (MIT), a small inline script that talks to Google Analytics directly. It sends page views, session start and engagement events, 90% scrolls, file downloads, outbound link clicks and site searches, plus UTM campaign attribution. It does not support Consent Mode or Google's enhanced measurement, and it stores its client and session IDs in localStorage, so check your privacy requirements before using it. With the WP Consent API and Consent Mode turned on, it waits until the visitor allows statistics before it runs. 83 110 84 111 = How do I test if tracking is working? = … … 95 122 96 123 == Changelog == 124 125 = 1.7.0 = 126 * Fixed: saving the settings for the first time on a new install switched on Consent Mode, Debug Mode and Disable Display Features by themselves (since 1.5.0). Sites updating from an affected version that have any of them on get a one-time notice asking to check them. 127 * Fixed: with Minimal Analytics in the header (the default), page views were sent before the page title existed, so GA4 showed "(not set)" titles, and scrolling at the top of the page caused a JavaScript error. 128 * Fixed: saving a mistyped or pasted Measurement ID cleared the working one and stopped tracking. The previous ID is now kept, and the ID is picked out of a pasted gtag snippet or URL. 129 * Fixed: with the tracking code in the footer, the Consent Mode defaults were printed in the footer too, after consent banners could already update them. They now always go in the head. 130 * Fixed: setup notices were shown to all logged-in users on every admin screen. They now only show to admins, and the setup reminder only on the Dashboard and Plugins screens. 131 * Fixed: the settings page told new installs to tick "Enable Google Analytics" although it was already ticked. 132 * New: works with consent plugins that support the WP Consent API when Consent Mode is on, for gtag.js and Minimal Analytics. 133 * New: option to honor Global Privacy Control. 134 * New: optional content groups (post category or page type) and author names for GA4 reports, with a `lightweight_ga_page_dimensions` filter. 135 * New: duplicate tracking warning when another plugin, the theme or a snippet also adds Google Analytics. 136 * New: a warning when Consent Mode is on but Minimal Analytics would ignore it, and a hint when the WP Consent API is active but Consent Mode is off. 137 * Network super admins are excluded along with administrators, and deleting the plugin on a multisite network removes its settings from every site. 138 * Settings fields are now properly labeled for screen readers. 97 139 98 140 = 1.6.0 = -
lightweight-google-analytics/tags/1.7.0/uninstall.php
r3689051 r3742708 1 1 <?php 2 // Remove the plugin's only stored data2 // Remove everything the plugin stores, on every site of a multisite network 3 3 if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { 4 4 exit; 5 5 } 6 delete_option( 'lightweight_ga_settings' ); 6 7 function lightweight_ga_uninstall_site() { 8 delete_option( 'lightweight_ga_settings' ); 9 delete_option( 'lightweight_ga_state' ); 10 delete_transient( 'lightweight_ga_duplicate_scan' ); 11 } 12 13 if ( is_multisite() ) { 14 foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0 ) ) as $site_id ) { 15 switch_to_blog( $site_id ); 16 lightweight_ga_uninstall_site(); 17 restore_current_blog(); 18 } 19 } else { 20 lightweight_ga_uninstall_site(); 21 } -
lightweight-google-analytics/trunk/lightweight-google-analytics.php
r3689051 r3742708 4 4 * Plugin URI: https://wordpress.org/plugins/lightweight-google-analytics/ 5 5 * Description: Extremely simple plugin to add Google Analytics 4 to your WordPress site using your Measurement ID. 6 * Version: 1. 6.06 * Version: 1.7.0 7 7 * Requires at least: 5.7 8 8 * Requires PHP: 7.0 … … 18 18 } 19 19 20 define( 'LIGHTWEIGHT_GA_VERSION', '1. 6.0' );20 define( 'LIGHTWEIGHT_GA_VERSION', '1.7.0' ); 21 21 22 22 // Saved settings with defaults filled in … … 30 30 'tracking_code' => 'analytics-js', 31 31 'consent_mode' => '', 32 'gpc' => '', 33 'content_groups' => '', 32 34 'debug_mode' => '', 33 35 ) ); … … 46 48 $reason = 'preview'; 47 49 } elseif ( is_user_logged_in() ) { 50 $roles = explode( '-', $settings['tracking_level'] ); 48 51 if ( 'logged-in' === $settings['tracking_level'] ) { 49 52 $reason = 'logged-in'; 50 } elseif ( 'all' !== $settings['tracking_level'] && array_intersect( explode( '-', $settings['tracking_level'] ), (array) wp_get_current_user()->roles ) ) { 53 } elseif ( 'all' !== $settings['tracking_level'] && ( array_intersect( $roles, (array) wp_get_current_user()->roles ) || ( is_multisite() && is_super_admin() && in_array( 'administrator', $roles, true ) ) ) ) { 54 // Network super admins don't always have the administrator role on the site itself 51 55 $reason = 'role'; 52 56 } … … 57 61 } 58 62 59 // Print the tracking code in the head or footer, whichever the settings say 63 // GA4 content group (the post's category, the post type, or the kind of archive) and author name for the current page 64 function lightweight_ga_page_dimensions() { 65 $group = ''; 66 $author = ''; 67 if ( is_front_page() ) { 68 $group = __( 'Home', 'lightweight-google-analytics' ); 69 } elseif ( is_home() ) { 70 $group = __( 'Blog', 'lightweight-google-analytics' ); 71 } elseif ( is_singular() && get_queried_object() instanceof WP_Post ) { 72 $post = get_queried_object(); 73 if ( 'post' === $post->post_type ) { 74 $categories = get_the_category( $post->ID ); 75 // Prefer the primary category picked in Yoast SEO or Rank Math 76 $primary = (int) get_post_meta( $post->ID, '_yoast_wpseo_primary_category', true ) ?: (int) get_post_meta( $post->ID, 'rank_math_primary_category', true ); 77 foreach ( $categories as $category ) { 78 if ( $category->term_id === $primary ) { 79 $group = $category->name; 80 } 81 } 82 if ( ! $group && $categories ) { 83 $group = $categories[0]->name; 84 } 85 } else { 86 $type = get_post_type_object( $post->post_type ); 87 $group = $type ? $type->labels->singular_name : $post->post_type; 88 } 89 if ( post_type_supports( $post->post_type, 'author' ) ) { 90 $author = get_the_author_meta( 'display_name', $post->post_author ); 91 } 92 } elseif ( is_category() || is_tag() || is_tax() ) { 93 $group = single_term_title( '', false ); 94 } elseif ( is_post_type_archive() ) { 95 $group = post_type_archive_title( '', false ); 96 } elseif ( is_author() ) { 97 $group = __( 'Author archive', 'lightweight-google-analytics' ); 98 $author = get_the_author_meta( 'display_name', get_queried_object_id() ); 99 } elseif ( is_date() ) { 100 $group = __( 'Date archive', 'lightweight-google-analytics' ); 101 } elseif ( is_search() ) { 102 $group = __( 'Search', 'lightweight-google-analytics' ); 103 } elseif ( is_404() ) { 104 $group = __( 'Page not found', 'lightweight-google-analytics' ); 105 } 106 // Change or add page-level parameters, e.g. a custom content group for a section of the site 107 $dimensions = array_filter( (array) apply_filters( 'lightweight_ga_page_dimensions', array( 'content_group' => $group, 'author' => $author ) ), 'strlen' ); 108 // GA4 ignores parameter values longer than 100 characters 109 return array_map( function ( $value ) { 110 return function_exists( 'mb_substr' ) ? mb_substr( (string) $value, 0, 100 ) : substr( (string) $value, 0, 100 ); 111 }, $dimensions ); 112 } 113 114 // The WP Consent API (wordpress.org/plugins/wp-consent-api) lets consent banners share visitors' choices with other plugins. 115 // Its functions only exist after plugins_loaded, so this is checked at output time. 116 function lightweight_ga_has_consent_api() { 117 return function_exists( 'wp_has_consent' ); 118 } 119 120 // Tell the WP Consent API (and its Site Health check) that tracking follows visitors' choices, which is true while Consent Mode is on 121 add_filter( 'wp_consent_api_registered_' . plugin_basename( __FILE__ ), function () { 122 return (bool) lightweight_ga_settings()['consent_mode']; 123 } ); 124 125 // JS that waits for the WP Consent API to know the visitor's choices, then calls `callback` with wp_has_consent and again whenever they change. 126 // The API's script loads in the footer, so the first check runs once the page is parsed. Consent banners that set 127 // waitfor_consent_hook decide the consent type late; checking earlier would read "no banner" as consent. 128 function lightweight_ga_consent_api_js( $callback ) { 129 return '(function(){var c=' . $callback . ',s=function(){if(typeof wp_has_consent==="function"){c();}},w=function(){window.waitfor_consent_hook?document.addEventListener("wp_consent_type_defined",s):s();};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",w):w();document.addEventListener("wp_listen_for_consent_change",s);})();'; 130 } 131 132 // Print the tracking code in the head or footer, whichever the settings say. Consent Mode defaults always go in the 133 // head, so they come before any consent banner's update even when the tracking code itself is in the footer. 60 134 add_action( 'wp_head', 'lightweight_ga_display', 0 ); 61 135 add_action( 'wp_footer', 'lightweight_ga_display', 0 ); 62 136 function lightweight_ga_display() { 63 137 $settings = lightweight_ga_settings(); 64 $hook = 'footer' === $settings['tracking_position'] ? 'wp_footer' : 'wp_head'; 65 if ( current_action() !== $hook || empty( $settings['enable_lightweight_ga'] ) || ! lightweight_ga_has_ga4_id() || lightweight_ga_excluded_reason() ) { 138 if ( empty( $settings['enable_lightweight_ga'] ) || ! lightweight_ga_has_ga4_id() || lightweight_ga_excluded_reason() ) { 66 139 return; 67 140 } 68 $id = $settings['tracking_id']; 141 $id = $settings['tracking_id']; 142 $in_head = 'wp_head' === current_action(); 143 $is_position = $in_head ? 'footer' !== $settings['tracking_position'] : 'footer' === $settings['tracking_position']; 69 144 70 145 if ( 'minimalanalytics-inline' === $settings['tracking_code'] ) { 71 $script = file_get_contents( __DIR__ . '/js/minimal-analytics-4.min.js' ); 72 if ( ! $script ) { 73 return; 74 } 75 // The bundled script is configured by swapping the placeholder ID and the debug flag that sit at its top; check both anchors when updating the file 76 $script = str_replace( 77 array( 'tid:"G-XXXXXXXXXX"', ']},T=!1,' ), 78 array( 'tid:"' . esc_js( $id ) . '"', ']},T=' . ( $settings['debug_mode'] ? '!0' : '!1' ) . ',' ), 79 $script 80 ); 81 echo "<!-- Minimal Analytics 4 - Lightweight Google Analytics -->\n"; 82 wp_print_inline_script_tag( $script ); 146 if ( $is_position ) { 147 lightweight_ga_print_minimal_analytics( $settings ); 148 } 83 149 return; 84 150 } 85 151 86 echo "<!-- Google tag (gtag.js) - Lightweight Google Analytics -->\n";87 152 $gtag_stub = 'window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}'; 88 89 if ( $settings['consent_mode'] ) { 153 // Google's documented opt-out flag stops GA4 sending anything for visitors whose browser sends Global Privacy Control. 154 // It has to be set before any gtag() call, so it leads every inline script. 155 if ( $settings['gpc'] ) { 156 $gtag_stub = 'if(navigator.globalPrivacyControl){window["ga-disable-' . esc_js( $id ) . '"]=true;}' . $gtag_stub; 157 } 158 159 if ( $in_head && $settings['consent_mode'] ) { 90 160 // Consent Mode v2 defaults have to be set before gtag.js loads; the consent banner then calls gtag("consent","update",...) 91 161 $consent = apply_filters( 'lightweight_ga_consent_defaults', array( … … 94 164 'ad_user_data' => 'denied', 95 165 'ad_personalization' => 'denied', 96 'wait_for_update' => 500, 166 // How long Google waits for the consent update before sending anything; it carries on as soon as the update 167 // arrives. With the WP Consent API the update comes once the page is parsed (its script is in the footer), so 168 // allow for slow pages. Consent banners that call gtag themselves usually update early in the head. 169 'wait_for_update' => lightweight_ga_has_consent_api() ? 2000 : 500, 97 170 ) ); 98 wp_print_inline_script_tag( $gtag_stub . 'gtag("consent","default",' . wp_json_encode( $consent ) . ');' ); 99 } 100 171 $update = ''; 172 if ( lightweight_ga_has_consent_api() ) { 173 // Pass the visitor's choices from any consent banner that supports the WP Consent API on to Google 174 // Banners such as Complianz change categories one at a time, so only send an update when the result changes 175 $update = lightweight_ga_consent_api_js( '(function(){var l;return function(){var s=wp_has_consent("statistics")?"granted":"denied",m=wp_has_consent("marketing")?"granted":"denied";if(s+m===l){return;}l=s+m;gtag("consent","update",{analytics_storage:s,ad_storage:m,ad_user_data:m,ad_personalization:m});};})()' ); 176 } 177 echo "<!-- Google Consent Mode defaults - Lightweight Google Analytics -->\n"; 178 wp_print_inline_script_tag( $gtag_stub . 'gtag("consent","default",' . wp_json_encode( $consent ) . ');' . $update, array( 'data-lightweight-ga' => true ) ); 179 } 180 181 if ( ! $is_position ) { 182 return; 183 } 184 185 echo "<!-- Google tag (gtag.js) - Lightweight Google Analytics -->\n"; 101 186 wp_print_script_tag( array( 102 'src' => 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $id ), 103 'async' => true, 187 'src' => 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $id ), 188 'async' => true, 189 'data-lightweight-ga' => true, // marks this plugin's own tags for the duplicate tracking check 104 190 ) ); 105 191 … … 111 197 $config['debug_mode'] = true; 112 198 } 199 if ( $settings['content_groups'] ) { 200 $config = array_merge( $config, lightweight_ga_page_dimensions() ); 201 } 113 202 // Add or change gtag config options, e.g. send_page_view, cookie_flags or custom dimensions 114 203 $config = apply_filters( 'lightweight_ga_gtag_config', $config, $id ); 115 wp_print_inline_script_tag( $gtag_stub . 'gtag("js",new Date());gtag("config","' . esc_js( $id ) . '"' . ( $config ? ',' . wp_json_encode( $config ) : '' ) . ');' ); 204 wp_print_inline_script_tag( $gtag_stub . 'gtag("js",new Date());gtag("config","' . esc_js( $id ) . '"' . ( $config ? ',' . wp_json_encode( $config ) : '' ) . ');', array( 'data-lightweight-ga' => true ) ); 205 } 206 207 // Minimal Analytics 4 is configured by swapping values in the bundled script; check all three anchors whenever the file is updated 208 function lightweight_ga_print_minimal_analytics( $settings ) { 209 $script = file_get_contents( __DIR__ . '/js/minimal-analytics-4.min.js' ); 210 if ( ! $script ) { 211 return; 212 } 213 // Content group and author ride along with every hit as event parameters 214 $params = ''; 215 foreach ( $settings['content_groups'] ? lightweight_ga_page_dimensions() : array() as $key => $value ) { 216 $params .= wp_json_encode( 'ep.' . $key ) . ':' . wp_json_encode( $value ) . ','; 217 } 218 $script = str_replace( 219 array( 'tid:"G-XXXXXXXXXX"', ']},T=!1,', '_dbg:T?1:0[0]' ), 220 array( 'tid:"' . esc_js( $settings['tracking_id'] ) . '"', ']},T=' . ( $settings['debug_mode'] ? '!0' : '!1' ) . ',', $params . '_dbg:T?1:0[0]' ), 221 $script 222 ); 223 // Run once the page is parsed: from the head it would otherwise send its page view before <title> exists and read a null document.body 224 $run = 'document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r):r();'; 225 if ( $settings['consent_mode'] && lightweight_ga_has_consent_api() ) { 226 // Minimal Analytics has no Consent Mode, so with the WP Consent API it only starts once the visitor allows statistics 227 $run = 'var d=0;' . lightweight_ga_consent_api_js( 'function(){if(!d&&wp_has_consent("statistics")){d=1;r();}}' ); 228 } 229 if ( $settings['gpc'] ) { 230 $run = 'if(!navigator.globalPrivacyControl){' . $run . '}'; 231 } 232 echo "<!-- Minimal Analytics 4 - Lightweight Google Analytics -->\n"; 233 wp_print_inline_script_tag( '(function(){var r=function(){' . $script . "\n};" . $run . '})();', array( 'data-lightweight-ga' => true ) ); 116 234 } 117 235 … … 121 239 add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); 122 240 add_action( 'admin_init', array( $this, 'init_settings' ) ); 241 add_action( 'admin_init', array( $this, 'maybe_upgrade' ) ); 123 242 add_action( 'admin_notices', array( $this, 'admin_notices' ) ); 243 add_action( 'wp_ajax_lightweight_ga_scan', array( $this, 'ajax_scan' ) ); 124 244 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'add_settings_link' ) ); 125 245 } 126 246 247 // Remembers the plugin version so an update can run one-off checks 248 public function maybe_upgrade() { 249 $state = (array) get_option( 'lightweight_ga_state', array() ); 250 if ( isset( $state['version'] ) && LIGHTWEIGHT_GA_VERSION === $state['version'] ) { 251 return; 252 } 253 if ( empty( $state['version'] ) && false !== get_option( 'lightweight_ga_settings' ) ) { 254 // 1.5.0 to 1.6.0 ticked every checkbox the first time the settings were saved, so ask the site owner to check them 255 $settings = lightweight_ga_settings(); 256 $state['review'] = $settings['consent_mode'] || $settings['debug_mode'] || $settings['disable_display_features']; 257 } 258 $state['version'] = LIGHTWEIGHT_GA_VERSION; 259 update_option( 'lightweight_ga_state', $state ); 260 } 261 127 262 public function add_settings_link( $links ) { 128 $links[] = '<a href="' . esc_url( menu_page_url( 'lightweight-google-analytics', false) ) . '">' . esc_html__( 'Settings', 'lightweight-google-analytics' ) . '</a>';263 $links[] = '<a href="' . esc_url( admin_url( 'options-general.php?page=lightweight-google-analytics' ) ) . '">' . esc_html__( 'Settings', 'lightweight-google-analytics' ) . '</a>'; 129 264 return $links; 130 265 } 131 266 132 267 public function admin_notices() { 133 $settings = lightweight_ga_settings(); 134 $url = esc_url( menu_page_url( 'lightweight-google-analytics', false ) ); 268 if ( ! current_user_can( 'manage_options' ) ) { 269 return; 270 } 271 $settings = lightweight_ga_settings(); 272 $screen = function_exists( 'get_current_screen' ) && get_current_screen() ? get_current_screen()->id : ''; 273 $url = esc_url( admin_url( 'options-general.php?page=lightweight-google-analytics' ) ); 274 $state = (array) get_option( 'lightweight_ga_state', array() ); 275 276 if ( ! empty( $state['review'] ) ) { 277 $on = array(); 278 foreach ( array( 'consent_mode' => __( 'Consent Mode', 'lightweight-google-analytics' ), 'debug_mode' => __( 'Debug Mode', 'lightweight-google-analytics' ), 'disable_display_features' => __( 'Disable Display Features', 'lightweight-google-analytics' ) ) as $key => $label ) { 279 if ( $settings[ $key ] ) { 280 $on[] = $label; 281 } 282 } 283 /* translators: 1: list of setting names, 2: link to the plugin settings page */ 284 $message = sprintf( __( '<strong>Lightweight Google Analytics:</strong> a bug in versions 1.5.0 to 1.6.0 could switch settings on by themselves the first time they were saved. These are on for this site: %1$s. Consent Mode without a consent banner and Debug Mode can both reduce the data you see in Google Analytics. Please check them in the %2$s and save.', 'lightweight-google-analytics' ), esc_html( implode( ', ', $on ) ), '<a href="' . $url . '">' . esc_html__( 'plugin settings', 'lightweight-google-analytics' ) . '</a>' ); 285 printf( '<div class="notice notice-warning"><p>%s</p></div>', wp_kses_post( $message ) ); 286 } 287 288 // The settings page shows its own status message 289 if ( 'settings_page_lightweight-google-analytics' === $screen ) { 290 return; 291 } 135 292 if ( 0 === strpos( $settings['tracking_id'], 'UA-' ) ) { 136 293 /* translators: %s: link to the plugin settings page */ 137 294 $message = sprintf( __( '<strong>Lightweight Google Analytics:</strong> Google shut down Universal Analytics in 2023 and UA- IDs are no longer supported, so no tracking code is being output. Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) in the %s.', 'lightweight-google-analytics' ), '<a href="' . $url . '">' . esc_html__( 'plugin settings', 'lightweight-google-analytics' ) . '</a>' ); 138 295 printf( '<div class="notice notice-warning"><p>%s</p></div>', wp_kses_post( $message ) ); 139 } elseif ( '' === $settings['tracking_id'] ) {296 } elseif ( '' === $settings['tracking_id'] && in_array( $screen, array( 'dashboard', 'plugins' ), true ) ) { 140 297 /* translators: %s: link to the plugin settings page */ 141 298 $message = sprintf( __( 'Thank you for installing <strong>Lightweight Google Analytics</strong> - head to the %s to finish setting up Google Analytics.', 'lightweight-google-analytics' ), '<a href="' . $url . '">' . esc_html__( 'settings', 'lightweight-google-analytics' ) . '</a>' ); … … 145 302 146 303 public function sanitize_settings( $input ) { 147 $input = (array) $input; 148 $sanitized = array( 'tracking_id' => '' ); 149 150 $tracking_id = isset( $input['tracking_id'] ) ? strtoupper( trim( sanitize_text_field( $input['tracking_id'] ) ) ) : ''; 151 if ( preg_match( '/^G-[A-Z0-9]+$/', $tracking_id ) ) { 152 $sanitized['tracking_id'] = $tracking_id; 153 } elseif ( 0 === strpos( $tracking_id, 'UA-' ) ) { 154 add_settings_error( 'lightweight_ga_settings', 'ua_tracking_id', __( 'Universal Analytics (UA-) IDs are no longer supported because Google shut Universal Analytics down in 2023. Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) instead.', 'lightweight-google-analytics' ) ); 155 } elseif ( '' !== $tracking_id ) { 156 add_settings_error( 'lightweight_ga_settings', 'invalid_tracking_id', __( 'Invalid Measurement ID. It should look like G-XXXXXXXX and can be found in Google Analytics under Admin > Data Streams.', 'lightweight-google-analytics' ) ); 157 } 158 159 foreach ( array( 'enable_lightweight_ga', 'disable_display_features', 'consent_mode', 'debug_mode' ) as $checkbox ) { 160 $sanitized[ $checkbox ] = isset( $input[ $checkbox ] ) ? 'checked' : ''; 304 $input = (array) $input; 305 $previous = lightweight_ga_settings()['tracking_id']; 306 307 // Accept the ID itself, or a pasted gtag snippet or URL that contains it. The ID is looked for in the raw text because 308 // sanitize_text_field() strips <script> blocks whole, URL included; only the matched letters and digits are ever kept. 309 $raw = isset( $input['tracking_id'] ) && is_string( $input['tracking_id'] ) ? strtoupper( trim( $input['tracking_id'] ) ) : ''; 310 if ( preg_match( '/^G-[A-Z0-9]+$/', $raw ) ) { 311 $tracking_id = $raw; 312 } elseif ( preg_match( '/(?<![A-Z0-9])G-[A-Z0-9]{4,}(?![A-Z0-9])/', $raw, $found ) ) { 313 $tracking_id = $found[0]; 314 /* translators: %s: the Measurement ID found in the pasted text */ 315 add_settings_error( 'lightweight_ga_settings', 'extracted_tracking_id', sprintf( __( 'Found the Measurement ID %s in the pasted text and saved it.', 'lightweight-google-analytics' ), $tracking_id ), 'info' ); 316 } else { 317 $tracking_id = sanitize_text_field( $raw ); 318 } 319 320 $sanitized = array( 'tracking_id' => $tracking_id ); 321 if ( '' !== $tracking_id && ! preg_match( '/^G-[A-Z0-9]+$/', $tracking_id ) ) { 322 // Keep the working ID instead of switching tracking off because of a typo 323 $sanitized['tracking_id'] = $previous; 324 if ( 0 === strpos( $tracking_id, 'UA-' ) ) { 325 add_settings_error( 'lightweight_ga_settings', 'ua_tracking_id', __( 'Universal Analytics (UA-) IDs are no longer supported because Google shut Universal Analytics down in 2023. Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) instead.', 'lightweight-google-analytics' ) ); 326 } else { 327 add_settings_error( 'lightweight_ga_settings', 'invalid_tracking_id', __( 'Invalid Measurement ID. It should look like G-XXXXXXXX and can be found in Google Analytics under Admin > Data Streams.', 'lightweight-google-analytics' ) . ( $previous ? ' ' . __( 'Your previous Measurement ID was kept.', 'lightweight-google-analytics' ) : '' ) ); 328 } 329 } 330 331 // ! empty() rather than isset(): on the very first save WordPress runs this twice, the second time on the already sanitized values 332 foreach ( array( 'enable_lightweight_ga', 'disable_display_features', 'consent_mode', 'gpc', 'content_groups', 'debug_mode' ) as $checkbox ) { 333 $sanitized[ $checkbox ] = ! empty( $input[ $checkbox ] ) ? 'checked' : ''; 161 334 } 162 335 … … 167 340 168 341 $sanitized['tracking_code'] = isset( $input['tracking_code'] ) && 'minimalanalytics-inline' === $input['tracking_code'] ? 'minimalanalytics-inline' : 'analytics-js'; 342 343 // The duplicate tag check runs again with the new settings 344 delete_transient( 'lightweight_ga_duplicate_scan' ); 345 346 // Saving the settings answers the post-update request to review them 347 $state = (array) get_option( 'lightweight_ga_state', array() ); 348 if ( ! empty( $state['review'] ) ) { 349 $state['review'] = false; 350 update_option( 'lightweight_ga_state', $state ); 351 } 169 352 170 353 return $sanitized; … … 201 384 'tracking_code' => __( 'Tracking Code', 'lightweight-google-analytics' ), 202 385 'consent_mode' => __( 'Consent Mode', 'lightweight-google-analytics' ), 386 'gpc' => __( 'Global Privacy Control', 'lightweight-google-analytics' ), 387 'content_groups' => __( 'Content Groups', 'lightweight-google-analytics' ), 203 388 'debug_mode' => __( 'Debug Mode', 'lightweight-google-analytics' ), 204 389 ); 205 390 foreach ( $fields as $field => $label ) { 206 add_settings_field( $field, $label, array( $this, 'render_' . $field . '_field' ), 'lightweight_ga_settings', 'lightweight_ga_settings_section', array( 'class' => str_replace( '_', '-', $field ) ) ); 207 } 208 } 209 210 // Tells the admin whether tracking is live, and why they personally will not show up in reports 391 // label_for ties each row's title to its input for screen readers 392 add_settings_field( $field, $label, array( $this, 'render_' . $field . '_field' ), 'lightweight_ga_settings', 'lightweight_ga_settings_section', array( 'class' => str_replace( '_', '-', $field ), 'label_for' => $field . '_field' ) ); 393 } 394 } 395 396 // Tells the admin whether tracking is live, why they personally will not show up in reports, and what might need attention 211 397 public function status_notice() { 212 398 $settings = lightweight_ga_settings(); 213 if ( empty( $settings['enable_lightweight_ga'] ) ) { 399 if ( false === get_option( 'lightweight_ga_settings' ) ) { 400 $class = 'warning'; 401 $message = __( 'Enter your Google Analytics 4 Measurement ID (G-XXXXXXXX) and save to start tracking.', 'lightweight-google-analytics' ); 402 } elseif ( empty( $settings['enable_lightweight_ga'] ) ) { 214 403 $class = 'warning'; 215 404 $message = __( 'Tracking is turned off. Tick "Enable Google Analytics" and save to start tracking.', 'lightweight-google-analytics' ); … … 228 417 } 229 418 printf( '<div class="notice notice-%s inline" style="margin:15px 0 0"><p>%s</p></div>', esc_attr( $class ), esc_html( $message ) ); 419 420 foreach ( $this->warnings( $settings ) as $warning ) { 421 printf( '<div class="notice notice-warning inline" style="margin:10px 0 0"><p>%s</p></div>', wp_kses_post( $warning ) ); 422 } 423 } 424 425 // Settings combinations that don't do what the site owner probably expects 426 private function warnings( $settings ) { 427 $warnings = array(); 428 if ( 'minimalanalytics-inline' === $settings['tracking_code'] && $settings['consent_mode'] && ! lightweight_ga_has_consent_api() ) { 429 $warnings[] = __( 'Consent Mode only applies to Google\'s gtag.js. Minimal Analytics runs for every visitor whatever they choose in your consent banner, unless the <a href="https://wordpress.org/plugins/wp-consent-api/" target="_blank" rel="noopener">WP Consent API</a> plugin is active with a consent banner that supports it.', 'lightweight-google-analytics' ); 430 } 431 if ( lightweight_ga_has_consent_api() && ! $settings['consent_mode'] ) { 432 $warnings[] = __( 'The WP Consent API is active, but Consent Mode is off, so Google Analytics ignores your visitors\' choices in your consent banner. Turn on Consent Mode to follow them.', 'lightweight-google-analytics' ); 433 } 434 if ( $settings['enable_lightweight_ga'] && lightweight_ga_has_ga4_id() ) { 435 list( $ga, $gtm ) = $this->other_ga_plugins(); 436 if ( $ga ) { 437 /* translators: %s: plugin name(s) */ 438 $warnings[] = sprintf( __( '%s is also set up to add Google Analytics to your site, so your page views are probably counted twice. Turn off the tracking code in one of the plugins.', 'lightweight-google-analytics' ), esc_html( implode( ', ', $ga ) ) ); 439 } 440 if ( $gtm ) { 441 /* translators: %s: plugin name */ 442 $warnings[] = sprintf( __( '%s adds a Google Tag Manager container to your site. If that container also sends GA4 page views, they are counted twice.', 'lightweight-google-analytics' ), esc_html( implode( ', ', $gtm ) ) ); 443 } 444 } 445 return $warnings; 446 } 447 448 // Active plugins that are set up to print their own Google Analytics tag (and ones that print a Tag Manager container, 449 // which may or may not include GA4). Settings outlive deactivated plugins, so each check also needs the plugin active. 450 private function other_ga_plugins() { 451 if ( ! function_exists( 'is_plugin_active' ) ) { 452 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 453 } 454 $option = function ( $name, $key ) { 455 $value = get_option( $name ); 456 return is_array( $value ) && isset( $value[ $key ] ) ? $value[ $key ] : ''; 457 }; 458 $monsterinsights = function () { 459 return function_exists( 'monsterinsights_get_v4_id' ) && monsterinsights_get_v4_id(); 460 }; 461 $exactmetrics = function () { 462 return function_exists( 'exactmetrics_get_v4_id' ) && exactmetrics_get_v4_id(); 463 }; 464 $checks = array( 465 'google-site-kit/google-site-kit.php' => array( 'Site Kit by Google', function () use ( $option ) { 466 return in_array( 'analytics-4', (array) get_option( 'googlesitekit_active_modules' ), true ) && $option( 'googlesitekit_analytics-4_settings', 'useSnippet' ) && $option( 'googlesitekit_analytics-4_settings', 'measurementID' ); 467 } ), 468 'google-analytics-for-wordpress/googleanalytics.php' => array( 'MonsterInsights', $monsterinsights ), 469 'google-analytics-premium/googleanalytics-premium.php' => array( 'MonsterInsights Pro', $monsterinsights ), 470 'google-analytics-dashboard-for-wp/gadwp.php' => array( 'ExactMetrics', $exactmetrics ), 471 'exactmetrics-premium/exactmetrics-premium.php' => array( 'ExactMetrics Pro', $exactmetrics ), 472 'wp-analytify/wp-analytify.php' => array( 'Analytify', function () use ( $option ) { 473 return 'on' === $option( 'wp-analytify-profile', 'install_ga_code' ) && $option( 'wp-analytify-profile', 'profile_for_posts' ); 474 } ), 475 'ga-google-analytics/ga-google-analytics.php' => array( 'GA Google Analytics', function () use ( $option ) { 476 return $option( 'gap_options', 'gap_id' ) && $option( 'gap_options', 'gap_enable' ); 477 } ), 478 'woocommerce-google-analytics-integration/woocommerce-google-analytics-integration.php' => array( 'Google Analytics for WooCommerce', function () use ( $option ) { 479 return (bool) $option( 'woocommerce_google_analytics_settings', 'ga_id' ); 480 } ), 481 'host-analyticsjs-local/host-analyticsjs-local.php' => array( 'CAOS', function () use ( $option ) { 482 return $option( 'caos_settings', 'measurement_id' ) && 'manual' !== $option( 'caos_settings', 'script_position' ); 483 } ), 484 'seo-by-rank-math/rank-math.php' => array( 'Rank Math', function () use ( $option ) { 485 return in_array( 'analytics', (array) get_option( 'rank_math_modules' ), true ) && $option( 'rank_math_google_analytic_options', 'install_code' ) && $option( 'rank_math_google_analytic_options', 'measurement_id' ); 486 } ), 487 'wp-seopress/seopress.php' => array( 'SEOPress', function () use ( $option ) { 488 return '1' === $option( 'seopress_toggle', 'toggle-google-analytics' ) && '1' === $option( 'seopress_google_analytics_option_name', 'seopress_google_analytics_enable' ) && $option( 'seopress_google_analytics_option_name', 'seopress_google_analytics_ga4' ); 489 } ), 490 'complianz-gdpr/complianz-gpdr.php' => array( 'Complianz', function () use ( $option ) { 491 return 'google-analytics' === $option( 'cmplz_options', 'compile_statistics' ) && 'no' !== $option( 'cmplz_options', 'configuration_by_complianz' ) && $option( 'cmplz_options', 'ua_code' ); 492 } ), 493 ); 494 $ga = array(); 495 foreach ( $checks as $basename => $check ) { 496 if ( is_plugin_active( $basename ) && call_user_func( $check[1] ) ) { 497 $ga[] = $check[0]; 498 } 499 } 500 $gtm = array(); 501 if ( is_plugin_active( 'duracelltomi-google-tag-manager/duracelltomi-google-tag-manager-for-wordpress.php' ) && $option( 'gtm4wp-options', 'gtm-code' ) ) { 502 $gtm[] = 'GTM4WP'; 503 } 504 return array( array_unique( $ga ), $gtm ); 505 } 506 507 // Looks for other Google tags on the home page as a logged-out visitor sees it, which catches tracking code added by the 508 // theme or a snippet plugin. Runs in the background from the settings page; cached for 12 hours and cleared on save. 509 public function ajax_scan() { 510 check_ajax_referer( 'lightweight_ga_scan' ); 511 if ( ! current_user_can( 'manage_options' ) ) { 512 wp_send_json( array() ); 513 } 514 $ids = get_transient( 'lightweight_ga_duplicate_scan' ); 515 if ( ! is_array( $ids ) ) { 516 // A cache-busting parameter, never ?tagverify (Site Kit hides its own tag for that) 517 $url = add_query_arg( 'lga_check', time(), home_url( '/' ) ); 518 $response = wp_remote_get( $url, array( 519 'timeout' => 10, 520 'headers' => array( 'Cache-Control' => 'no-cache' ), 521 'sslverify' => apply_filters( 'https_local_ssl_verify', false, $url ), 522 ) ); 523 // A blocked or failed request means "don't know", not "no duplicates" 524 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { 525 wp_send_json( array() ); 526 } 527 // Drop this plugin's own tags, then collect the Google tag IDs that are left. IDs are matched rather than gtag.js 528 // URLs because some plugins serve gtag.js from the site itself. 529 $html = preg_replace( '#<script[^>]*data-lightweight-ga[^>]*>.*?</script>#is', '', wp_remote_retrieve_body( $response ) ); 530 preg_match_all( '/(?:id=|["\'])((?:G|GT)-[A-Z0-9]{6,12}|GTM-[A-Z0-9]{4,10})(?![A-Z0-9])/', $html, $matches ); 531 $ids = array_values( array_unique( $matches[1] ) ); 532 set_transient( 'lightweight_ga_duplicate_scan', $ids, 12 * HOUR_IN_SECONDS ); 533 } 534 $own = lightweight_ga_settings()['tracking_id']; 535 $messages = array(); 536 // A second copy of the site's own ID is the certain duplicate, so it goes first 537 usort( $ids, function ( $a, $b ) use ( $own ) { 538 return ( $b === $own ) - ( $a === $own ); 539 } ); 540 foreach ( $ids as $tag_id ) { 541 if ( $tag_id === $own ) { 542 /* translators: %s: Measurement ID */ 543 $messages[] = sprintf( __( 'Your Measurement ID (%s) is also added to your home page by your theme or another plugin, so your page views are counted twice. Remove the other copy of the tracking code.', 'lightweight-google-analytics' ), $tag_id ); 544 } elseif ( 0 === strpos( $tag_id, 'GTM-' ) ) { 545 /* translators: %s: Google Tag Manager container ID */ 546 $messages[] = sprintf( __( 'A Google Tag Manager container (%s) is on your home page. If it also sends GA4 page views, they are counted twice.', 'lightweight-google-analytics' ), $tag_id ); 547 } else { 548 /* translators: %s: Google tag ID */ 549 $messages[] = sprintf( __( 'Another Google tag (%s) is on your home page, added by your theme or another plugin. If it also sends page views to Google Analytics, they are counted twice.', 'lightweight-google-analytics' ), $tag_id ); 550 } 551 } 552 wp_send_json( $messages ); 230 553 } 231 554 … … 239 562 echo ' <h2>' . esc_html__( 'Options', 'lightweight-google-analytics' ) . '</h2>' . "\n"; 240 563 $this->status_notice(); 564 $settings = lightweight_ga_settings(); 565 if ( $settings['enable_lightweight_ga'] && lightweight_ga_has_ga4_id() ) { 566 // Checks the home page for other Google tags in the background, so a slow loopback request doesn't hold up this page 567 echo '<div id="lightweight-ga-scan" aria-live="polite"></div>'; 568 echo '<script>(function(){var b=new URLSearchParams({action:"lightweight_ga_scan",_ajax_nonce:' . wp_json_encode( wp_create_nonce( 'lightweight_ga_scan' ) ) . '});fetch(' . wp_json_encode( admin_url( 'admin-ajax.php' ) ) . ',{method:"POST",credentials:"same-origin",body:b}).then(function(r){return r.json();}).then(function(m){var w=document.getElementById("lightweight-ga-scan");(Array.isArray(m)?m:[]).forEach(function(t){var d=document.createElement("div"),p=document.createElement("p");d.className="notice notice-warning inline";d.style.margin="10px 0 0";p.textContent=t;d.appendChild(p);w.appendChild(d);});}).catch(function(){});})();</script>'; 569 } 241 570 echo ' <form action="options.php" method="post">' . "\n"; 242 571 … … 271 600 public function render_tracking_id_field() { 272 601 $settings = lightweight_ga_settings(); 273 echo '<input type="text" name="lightweight_ga_settings[tracking_id]" class="regular-text tracking_id_field" placeholder="G-XXXXXXXX" value="' . esc_attr( $settings['tracking_id'] ) . '">';274 echo '<span class="tracking_id_field_validate" style="display:none;color:#27ae60;font-weight:500;margin-left:0.8em;vertical-align:middle;"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="vertical-align:middle;" ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg> ' . esc_html__( 'Google Analytics 4', 'lightweight-google-analytics' ) . '</span>';275 echo '<p class="description">' . wp_kses_post( __( 'Your Google Analytics 4 Measurement ID, which starts with G-. Find it in <a href="https://analytics.google.com/" target="_blank" rel="noopener">Google Analytics</a> under Admin > Data Streams > your web stream. ', 'lightweight-google-analytics' ) ) . '</p>';602 echo '<input type="text" name="lightweight_ga_settings[tracking_id]" id="tracking_id_field" class="regular-text tracking_id_field" placeholder="G-XXXXXXXX" value="' . esc_attr( $settings['tracking_id'] ) . '">'; 603 echo '<span class="tracking_id_field_validate" style="display:none;color:#27ae60;font-weight:500;margin-left:0.8em;vertical-align:middle;"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="vertical-align:middle;" aria-hidden="true"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg> ' . esc_html__( 'Google Analytics 4', 'lightweight-google-analytics' ) . '</span>'; 604 echo '<p class="description">' . wp_kses_post( __( 'Your Google Analytics 4 Measurement ID, which starts with G-. Find it in <a href="https://analytics.google.com/" target="_blank" rel="noopener">Google Analytics</a> under Admin > Data Streams > your web stream. You can also paste Google\'s whole tag snippet and the ID will be picked out of it.', 'lightweight-google-analytics' ) ) . '</p>'; 276 605 } 277 606 278 607 public function render_tracking_position_field() { 279 608 $settings = lightweight_ga_settings(); 280 echo '<select name="lightweight_ga_settings[tracking_position]" class="tracking_position_field">';609 echo '<select name="lightweight_ga_settings[tracking_position]" id="tracking_position_field" class="tracking_position_field">'; 281 610 echo '<option value="header" ' . selected( $settings['tracking_position'], 'header', false ) . '>' . esc_html__( 'Header (default, recommended)', 'lightweight-google-analytics' ) . '</option>'; 282 611 echo '<option value="footer" ' . selected( $settings['tracking_position'], 'footer', false ) . '>' . esc_html__( 'Footer', 'lightweight-google-analytics' ) . '</option>'; … … 298 627 'all' => __( 'Track all users (including logged in admins)', 'lightweight-google-analytics' ), 299 628 ); 300 echo '<select name="lightweight_ga_settings[tracking_level]" class="tracking_level_field">';629 echo '<select name="lightweight_ga_settings[tracking_level]" id="tracking_level_field" class="tracking_level_field">'; 301 630 foreach ( $levels as $value => $label ) { 302 631 echo '<option value="' . esc_attr( $value ) . '" ' . selected( $settings['tracking_level'], $value, false ) . '>' . esc_html( $label ) . '</option>'; … … 308 637 public function render_tracking_code_field() { 309 638 $settings = lightweight_ga_settings(); 310 echo '<select name="lightweight_ga_settings[tracking_code]" class="tracking_code_field">';639 echo '<select name="lightweight_ga_settings[tracking_code]" id="tracking_code_field" class="tracking_code_field">'; 311 640 echo '<option value="analytics-js" ' . selected( $settings['tracking_code'], 'analytics-js', false ) . '>' . esc_html__( "Google's gtag.js (recommended)", 'lightweight-google-analytics' ) . '</option>'; 312 641 echo '<option value="minimalanalytics-inline" ' . selected( $settings['tracking_code'], 'minimalanalytics-inline', false ) . '>' . esc_html__( 'Minimal Analytics 4 by idarek (inline, no external request, fewer features)', 'lightweight-google-analytics' ) . '</option>'; 313 642 echo '</select>'; 314 echo '<p class="description">' . wp_kses_post( __( 'Minimal Analytics 4 is a 3.5 KB inline script that sends page views, 90% scrolls, file downloads, outbound clicks and site searches straight to Google Analytics without loading gtag.js. It stores its client and session IDs in localStorage, so it is not affected by Consent Mode. <a href="https://github.com/idarek/minimal-analytics-4" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ) . '</p>';643 echo '<p class="description">' . wp_kses_post( __( 'Minimal Analytics 4 is a 3.5 KB inline script that sends page views, 90% scrolls, file downloads, outbound clicks and site searches straight to Google Analytics without loading gtag.js. It stores its client and session IDs in localStorage, so Consent Mode does not apply to it; with the WP Consent API it waits for statistics consent instead. <a href="https://github.com/idarek/minimal-analytics-4" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ) . '</p>'; 315 644 } 316 645 317 646 public function render_consent_mode_field() { 318 $this->checkbox( 'consent_mode', __( 'Enable Google Consent Mode v2', 'lightweight-google-analytics' ), __( 'Analytics and ad storage are denied by default until a cookie consent plugin grants consent with <code>gtag("consent", "update", ...)</code>. Only applies to gtag.js. <a href="https://developers.google.com/tag-platform/security/guides/consent" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ); 647 $this->checkbox( 'consent_mode', __( 'Enable Google Consent Mode v2', 'lightweight-google-analytics' ), __( 'Analytics and ad storage are denied by default until your cookie consent plugin grants consent. Works automatically with consent plugins that support the <a href="https://wordpress.org/plugins/wp-consent-api/" target="_blank" rel="noopener">WP Consent API</a> (such as Complianz and CookieYes), which also makes Minimal Analytics wait for consent. Other consent plugins need to call <code>gtag("consent", "update", ...)</code> themselves. <a href="https://developers.google.com/tag-platform/security/guides/consent" target="_blank" rel="noopener">Learn more</a>', 'lightweight-google-analytics' ) ); 648 } 649 650 public function render_gpc_field() { 651 $this->checkbox( 'gpc', __( 'Honor Global Privacy Control', 'lightweight-google-analytics' ), __( 'Visitors whose browser sends a <a href="https://globalprivacycontrol.org/" target="_blank" rel="noopener">Global Privacy Control</a> signal (an opt-out built into Brave, DuckDuckGo and Firefox) are not measured. Recommended if your site has to follow California\'s CCPA or a similar law that treats the signal as an opt-out request.', 'lightweight-google-analytics' ) ); 652 } 653 654 public function render_content_groups_field() { 655 $this->checkbox( 'content_groups', __( 'Send content groups and authors', 'lightweight-google-analytics' ), __( 'Sends the post\'s category (or the page type, such as Page or Search) as GA4\'s Content group, so you can compare sections of your site in reports. Also sends the author\'s name as an <code>author</code> parameter; to report on it, add an event-scoped custom dimension named <code>author</code> in Google Analytics under Admin > Custom definitions.', 'lightweight-google-analytics' ) ); 319 656 } 320 657 -
lightweight-google-analytics/trunk/readme.txt
r3689051 r3742708 6 6 Tested up to: 7.1 7 7 Requires PHP: 7.0 8 Stable tag: 1. 6.08 Stable tag: 1.7.0 9 9 License: GPLv3 10 10 License URI: http://www.gnu.org/licenses/gpl.html … … 19 19 20 20 * Performance focused with no bloat and never any upsells 21 * Only needs your GA4 Measurement ID (G-XXXXXXXX) 21 * Only needs your GA4 Measurement ID (G-XXXXXXXX), or paste Google's whole tag snippet and the ID is picked out of it 22 22 * Load the tracking code in the header or footer 23 23 * Exclude logged-in users by role (admins, editors and authors are excluded by default) 24 24 * Post previews and the Customizer are never tracked 25 25 * Option to disable Google's advertising features (Google signals) 26 * Google Consent Mode v2 support for GDPR/CCPA compliance with cookie consent plugins 26 * Google Consent Mode v2 support for GDPR/CCPA compliance, working automatically with consent plugins that support the WP Consent API (such as Complianz and CookieYes) 27 * Option to honor Global Privacy Control, the opt-out signal sent by Brave, DuckDuckGo and Firefox 28 * Optional content groups: the post's category (or page type) as GA4's Content group, plus the author's name, so you can compare sections and authors in reports 29 * Warns you on the settings page when another plugin, your theme or a pasted snippet also adds Google Analytics, so page views aren't counted twice 27 30 * Debug Mode for testing in GA4 DebugView 28 31 * Optional Minimal Analytics 4 mode: a 3.5 KB inline script that reports page views, scrolls, downloads, outbound clicks and site searches without loading gtag.js at all 29 32 * The settings page tells you whether tracking is live and why your own visits may not show up 30 * Works with WP Rocket's local Google Analytics feature31 33 * Filters for developers to change the gtag config, the consent defaults or when tracking is skipped 32 34 … … 50 52 $defaults['region'] = array( 'AT', 'BE', 'DE', 'FR', 'IE', 'NL' ); 51 53 return $defaults; 54 } );` 55 56 Change the content group or author sent with each page (when Content Groups is on), or add more page-level parameters: 57 58 `add_filter( 'lightweight_ga_page_dimensions', function ( $dimensions ) { 59 if ( is_page( 'pricing' ) ) { 60 $dimensions['content_group'] = 'Sales'; 61 } 62 return $dimensions; 52 63 } );` 53 64 … … 78 89 Google Consent Mode v2 tells Google that analytics and advertising storage are denied until the visitor consents. Enable it if you use a cookie consent plugin that updates the consent state through `gtag("consent", "update", ...)` and you need to comply with GDPR or similar rules. It only applies to Google's gtag.js code. 79 90 91 = Which consent plugins work with Consent Mode? = 92 93 Any consent plugin that supports the [WP Consent API](https://wordpress.org/plugins/wp-consent-api/), such as Complianz and CookieYes, works automatically: install and activate the WP Consent API plugin, then turn on Consent Mode. Statistics consent controls Google Analytics storage and marketing consent controls the advertising storage types, and both follow the visitor's choice when it changes. Note that the WP Consent API treats a site with no consent plugin as having consent. Other consent plugins need to call `gtag("consent", "update", ...)` themselves. 94 95 = What does the Global Privacy Control option do? = 96 97 Global Privacy Control is an opt-out signal built into Brave and DuckDuckGo and available in Firefox. California's CCPA and similar US state laws treat it as a request to opt out of the sale or sharing of personal information. When the option is on, visitors who send the signal are not measured: gtag.js gets Google's documented opt-out flag, and Minimal Analytics doesn't run at all. 98 99 = How do I see content groups and authors in Google Analytics? = 100 101 Turn on Content Groups in the plugin settings. The content group shows up under the built-in "Content group" dimension in reports such as Pages and screens. To report on authors, add an event-scoped custom dimension named `author` in Google Analytics under Admin > Custom definitions; it fills in from then on, usually within 48 hours. 102 103 = Why does the settings page say my page views are counted twice? = 104 105 The plugin checks whether another analytics plugin is set up to add Google Analytics too (such as Site Kit, MonsterInsights or Rank Math), and looks at your home page for other Google tags added by your theme or a code snippet plugin. Remove the extra copy so each visit is only counted once. Tags that only appear on some pages, or only after cookie consent, can't be detected. 106 80 107 = What is the Minimal Analytics 4 option? = 81 108 82 It replaces Google's gtag.js with [Minimal Analytics 4](https://github.com/idarek/minimal-analytics-4) by Dariusz Wieckiewicz (MIT), a small inline script that talks to Google Analytics directly. It sends page views, session start and engagement events, 90% scrolls, file downloads, outbound link clicks and site searches, plus UTM campaign attribution. It does not support Consent Mode or Google's enhanced measurement, and it stores its client and session IDs in localStorage, so check your privacy requirements before using it. 109 It replaces Google's gtag.js with [Minimal Analytics 4](https://github.com/idarek/minimal-analytics-4) by Dariusz Wieckiewicz (MIT), a small inline script that talks to Google Analytics directly. It sends page views, session start and engagement events, 90% scrolls, file downloads, outbound link clicks and site searches, plus UTM campaign attribution. It does not support Consent Mode or Google's enhanced measurement, and it stores its client and session IDs in localStorage, so check your privacy requirements before using it. With the WP Consent API and Consent Mode turned on, it waits until the visitor allows statistics before it runs. 83 110 84 111 = How do I test if tracking is working? = … … 95 122 96 123 == Changelog == 124 125 = 1.7.0 = 126 * Fixed: saving the settings for the first time on a new install switched on Consent Mode, Debug Mode and Disable Display Features by themselves (since 1.5.0). Sites updating from an affected version that have any of them on get a one-time notice asking to check them. 127 * Fixed: with Minimal Analytics in the header (the default), page views were sent before the page title existed, so GA4 showed "(not set)" titles, and scrolling at the top of the page caused a JavaScript error. 128 * Fixed: saving a mistyped or pasted Measurement ID cleared the working one and stopped tracking. The previous ID is now kept, and the ID is picked out of a pasted gtag snippet or URL. 129 * Fixed: with the tracking code in the footer, the Consent Mode defaults were printed in the footer too, after consent banners could already update them. They now always go in the head. 130 * Fixed: setup notices were shown to all logged-in users on every admin screen. They now only show to admins, and the setup reminder only on the Dashboard and Plugins screens. 131 * Fixed: the settings page told new installs to tick "Enable Google Analytics" although it was already ticked. 132 * New: works with consent plugins that support the WP Consent API when Consent Mode is on, for gtag.js and Minimal Analytics. 133 * New: option to honor Global Privacy Control. 134 * New: optional content groups (post category or page type) and author names for GA4 reports, with a `lightweight_ga_page_dimensions` filter. 135 * New: duplicate tracking warning when another plugin, the theme or a snippet also adds Google Analytics. 136 * New: a warning when Consent Mode is on but Minimal Analytics would ignore it, and a hint when the WP Consent API is active but Consent Mode is off. 137 * Network super admins are excluded along with administrators, and deleting the plugin on a multisite network removes its settings from every site. 138 * Settings fields are now properly labeled for screen readers. 97 139 98 140 = 1.6.0 = -
lightweight-google-analytics/trunk/uninstall.php
r3689051 r3742708 1 1 <?php 2 // Remove the plugin's only stored data2 // Remove everything the plugin stores, on every site of a multisite network 3 3 if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { 4 4 exit; 5 5 } 6 delete_option( 'lightweight_ga_settings' ); 6 7 function lightweight_ga_uninstall_site() { 8 delete_option( 'lightweight_ga_settings' ); 9 delete_option( 'lightweight_ga_state' ); 10 delete_transient( 'lightweight_ga_duplicate_scan' ); 11 } 12 13 if ( is_multisite() ) { 14 foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0 ) ) as $site_id ) { 15 switch_to_blog( $site_id ); 16 lightweight_ga_uninstall_site(); 17 restore_current_blog(); 18 } 19 } else { 20 lightweight_ga_uninstall_site(); 21 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)