diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 38ef16f5..8797932b 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -40,6 +40,9 @@ jobs: --health-retries 10 steps: + - name: Install SVN + run: sudo apt-get install -y subversion + - name: Checkout code uses: actions/checkout@v4 diff --git a/assets/src/js/integrations/form-submit.js b/assets/src/js/integrations/form-submit.js new file mode 100644 index 00000000..7b6f5a34 --- /dev/null +++ b/assets/src/js/integrations/form-submit.js @@ -0,0 +1,41 @@ +/** + * Plausible Analytics + * + * Track Form Submissions JS + */ +document.addEventListener('DOMContentLoaded', () => { + let plausible_track_form_submit = { + forms: document.querySelectorAll('form'), + + /** + * Initialization. + */ + init: function () { + this.bindEvents(); + }, + + /** + * Bind Events. + */ + bindEvents: function () { + let self = this; + + this.forms.forEach((form) => { + form.addEventListener('submit', (e) => { + if (e.target.checkValidity()) { + self.trackSubmission(); + } + }) + }) + }, + + /** + * Send a custom event to Plausible. + */ + trackSubmission: function () { + plausible(plausible_analytics_i18n.form_completions, {'props': {'form': document.location.pathname}}); + } + }; + + plausible_track_form_submit.init(); +}); diff --git a/src/Actions.php b/src/Actions.php index e29b98d5..10fafb8d 100644 --- a/src/Actions.php +++ b/src/Actions.php @@ -1,7 +1,6 @@ tag "tells" the Plausible API which version of the plugin is used, to allow tailored error messages, specific to the plugin - * version. - * + * This tag "tells" the Plausible API which version of the plugin is used, to allow tailored error messages, + * specific to the plugin version. * @return void */ public function insert_version_meta_tag() { @@ -37,7 +34,6 @@ public function insert_version_meta_tag() { /** * Register Assets. - * * @since 1.0.0 * @access public * @return void @@ -56,9 +52,16 @@ public function maybe_register_assets() { } $version = - Helpers::proxy_enabled() && file_exists( Helpers::get_js_path() ) ? filemtime( Helpers::get_js_path() ) : PLAUSIBLE_ANALYTICS_VERSION; + Helpers::proxy_enabled() && file_exists( Helpers::get_js_path() ) ? filemtime( Helpers::get_js_path() ) : + PLAUSIBLE_ANALYTICS_VERSION; - wp_enqueue_script( 'plausible-analytics', Helpers::get_js_url( true ), '', $version, apply_filters( 'plausible_load_js_in_footer', false ) ); + wp_enqueue_script( + 'plausible-analytics', + Helpers::get_js_url( true ), + '', + $version, + apply_filters( 'plausible_load_js_in_footer', false ) + ); // Goal tracking inline script (Don't disable this as it is required by 404). wp_add_inline_script( @@ -77,7 +80,7 @@ public function maybe_register_assets() { ); /** - * Documentation.location.pathname is a variable. @see wp_json_encode() doesn't allow passing variable, only strings. This fixes that. + * document.location.pathname is a variable. @see wp_json_encode() doesn't allow passing variable, only strings. This fixes that. */ $data = str_replace( '"document.location.pathname"', 'document.location.pathname', $data ); @@ -95,14 +98,17 @@ public function maybe_register_assets() { [ 'props' => [ // convert queries to lowercase and remove trailing whitespace to ensure same terms are grouped together - 'search_query' => strtolower(trim(get_search_query())), + 'search_query' => strtolower( trim( get_search_query() ) ), 'result_count' => $wp_query->found_posts, ], ] ); $script = "plausible('WP Search Queries', $data );"; - wp_add_inline_script( 'plausible-analytics', "document.addEventListener('DOMContentLoaded', function() {\n$script\n});" ); + wp_add_inline_script( + 'plausible-analytics', + "document.addEventListener('DOMContentLoaded', function() {\n$script\n});" + ); } // This action allows you to add your own custom scripts! @@ -111,7 +117,6 @@ public function maybe_register_assets() { /** * Create admin bar nodes. - * * @since 1.3.0 * @access public * diff --git a/src/Admin/Provisioning.php b/src/Admin/Provisioning.php index 424d2ab8..ad8f1b4a 100644 --- a/src/Admin/Provisioning.php +++ b/src/Admin/Provisioning.php @@ -1,7 +1,6 @@ custom_event_goals = [ - '404' => __( '404', 'plausible-analytics' ), - 'outbound-links' => __( 'Outbound Link: Click', 'plausible-analytics' ), - 'file-downloads' => __( 'File Download', 'plausible-analytics' ), - 'search' => __( 'WP Search Queries', 'plausible-analytics' ), + '404' => __( '404', 'plausible-analytics' ), + 'outbound-links' => __( 'Outbound Link: Click', 'plausible-analytics' ), + 'file-downloads' => __( 'File Download', 'plausible-analytics' ), + 'search' => __( 'WP Search Queries', 'plausible-analytics' ), + 'form-completions' => __( 'Form Completions', 'plausible-analytics' ), ]; $this->init(); @@ -79,10 +79,8 @@ public function __construct( $client = null ) { /** * Action & filter hooks. - * * @return void * @throws ApiException - * * @codeCoverageIgnore */ private function init() { @@ -210,11 +208,11 @@ private function create_goals( $goals ) { * @param $settings * * @return void - * * @codeCoverageIgnore Because we don't want to test the API. */ public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { - if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_wc_active() ) { + if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || + ! Integrations::is_wc_active() ) { return; // @codeCoverageIgnore } @@ -254,7 +252,6 @@ public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { * @param $steps * * @return void - * * @codeCoverageIgnore Because this method should be mocked in tests if needed. */ private function create_funnel( $name, $steps ) { @@ -325,14 +322,13 @@ public function maybe_delete_goals( $old_settings, $settings ) { } /** - * Delete all custom WooCommerce event goals if Revenue setting is disabled. The funnel is deleted when the minimum required no. of goals is no - * longer met. + * Delete all custom WooCommerce event goals if Revenue setting is disabled. The funnel is deleted when the minimum + * required no. of goals is no longer met. * * @param $old_settings * @param $settings * * @return void - * * @codeCoverageIgnore Because we don't want to test if the API is working. */ public function maybe_delete_woocommerce_goals( $old_settings, $settings ) { @@ -361,14 +357,13 @@ public function maybe_delete_woocommerce_goals( $old_settings, $settings ) { } /** - * Searches an array for the presence of $string within each element's value. Strips currencies using a regex, e.g. (USD), because these are - * added to revenue goals by Plausible. + * Searches an array for the presence of $string within each element's value. Strips currencies using a regex, e.g. + * (USD), because these are added to revenue goals by Plausible. * * @param string $string * @param array $haystack * * @return false|mixed - * * @codeCoverageIgnore Because it can't be unit tested. */ private function array_search_contains( $string, $haystack ) { @@ -390,7 +385,6 @@ private function array_search_contains( $string, $haystack ) { * @param array $settings * * @return void - * * @codeCoverageIgnore Because we don't want to test if the API is working. */ public function maybe_create_custom_properties( $old_settings, $settings ) { @@ -398,7 +392,8 @@ public function maybe_create_custom_properties( $old_settings, $settings ) { if ( ! Helpers::is_enhanced_measurement_enabled( 'pageview-props', $enhanced_measurements ) && ! Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) && - ! Helpers::is_enhanced_measurement_enabled( 'search', $enhanced_measurements ) ) { + ! Helpers::is_enhanced_measurement_enabled( 'search', $enhanced_measurements ) && + ! Helpers::is_enhanced_measurement_enabled( 'form-completions', $enhanced_measurements ) ) { return; // @codeCoverageIgnore } @@ -417,7 +412,8 @@ public function maybe_create_custom_properties( $old_settings, $settings ) { /** * Create Custom Properties for WooCommerce integration. */ - if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) && Integrations::is_wc_active() ) { + if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) && + Integrations::is_wc_active() ) { foreach ( WooCommerce::CUSTOM_PROPERTIES as $property ) { $properties[] = new Client\Model\CustomProp( [ 'custom_prop' => [ 'key' => $property ] ] ); } @@ -432,6 +428,10 @@ public function maybe_create_custom_properties( $old_settings, $settings ) { } } + if ( Helpers::is_enhanced_measurement_enabled( 'form-completions', $enhanced_measurements ) ) { + $properties[] = new Client\Model\CustomProp( [ 'custom_prop' => [ 'key' => 'form' ] ] ); + } + if ( empty( $properties ) ) { return; // @codeCoverageIgnore } diff --git a/src/Admin/Settings/Page.php b/src/Admin/Settings/Page.php index d6b70632..742e570c 100644 --- a/src/Admin/Settings/Page.php +++ b/src/Admin/Settings/Page.php @@ -2,7 +2,6 @@ /** * Plausible Analytics | Settings API. - * * @since 1.3.0 * @package WordPress * @subpackage Plausible Analytics @@ -72,7 +71,6 @@ class Page extends API { /** * Constructor. - * * @since 1.3.0 * @access public * @return void @@ -120,7 +118,8 @@ public function __construct() { ], [ 'label' => empty( $settings[ 'domain_name' ] ) || empty( $settings[ 'api_token' ] ) ? - esc_html__( 'Connect', 'plausible-analytics' ) : esc_html__( 'Connected', 'plausible-analytics' ), + esc_html__( 'Connect', 'plausible-analytics' ) : + esc_html__( 'Connected', 'plausible-analytics' ), 'slug' => 'connect_plausible_analytics', 'type' => 'button', 'disabled' => empty( $settings[ 'domain_name' ] ) || @@ -140,42 +139,42 @@ public function __construct() { 'plausible-analytics' ), 'fields' => [ - '404' => [ + '404' => [ 'label' => esc_html__( '404 error pages', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-404-error-pages', 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => '404', ], - 'outbound-links' => [ + 'outbound-links' => [ 'label' => esc_html__( 'Outbound links', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-external-link-clicks', 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'outbound-links', ], - 'file-downloads' => [ + 'file-downloads' => [ 'label' => esc_html__( 'File downloads', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-file-downloads', 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'file-downloads', ], - 'search' => [ + 'search' => [ 'label' => esc_html__( 'Search queries', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-enable-site-search-tracking', 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'search', ], - 'tagged-events' => [ + 'tagged-events' => [ 'label' => esc_html__( 'Custom events', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-setup-custom-events-to-track-goal-conversions', 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'tagged-events', ], - 'revenue' => [ + 'revenue' => [ 'label' => esc_html__( 'Ecommerce revenue', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-ecommerce-revenue', 'slug' => 'enhanced_measurements', @@ -183,21 +182,28 @@ public function __construct() { 'value' => 'revenue', 'disabled' => ! empty( $settings[ 'self_hosted_domain' ] ), ], - 'pageview-props' => [ + 'pageview-props' => [ 'label' => esc_html__( 'Authors and categories', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-send-custom-properties', 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'pageview-props', ], - 'hash' => [ + 'form-completions' => [ + 'label' => esc_html__( 'Form completions', 'plausible-analytics' ), + 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-form-completions', + 'slug' => 'enhanced_measurements', + 'type' => 'checkbox', + 'value' => 'form-completions', + ], + 'hash' => [ 'label' => esc_html__( 'Hash-based routing', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-enable-hash-based-url-tracking', 'slug' => 'enhanced_measurements', 'type' => 'checkbox', 'value' => 'hash', ], - 'compat' => [ + 'compat' => [ 'label' => esc_html__( 'IE compatibility', 'plausible-analytics' ), 'docs' => 'https://plausible.io/wordpress-analytics-plugin#how-to-track-visitors-who-use-internet-explorer', 'slug' => 'enhanced_measurements', @@ -221,7 +227,8 @@ public function __construct() { get_site_url( null, rest_get_url_prefix() ), empty( Helpers::get_settings()[ 'proxy_enabled' ] - ) ? 'a random directory/file for storing the JS file' : 'a JS file, called ' . str_replace( + ) ? 'a random directory/file for storing the JS file' : + 'a JS file, called ' . str_replace( ABSPATH, '', Helpers::get_proxy_resource( 'cache_dir' ) . Helpers::get_proxy_resource( @@ -256,7 +263,8 @@ public function __construct() { 'slug' => 'enable_analytics_dashboard', 'type' => 'checkbox', 'value' => 'on', - 'disabled' => empty( Helpers::get_settings()[ 'api_token' ] ) && empty( Helpers::get_settings()[ 'self_hosted_domain' ] ), + 'disabled' => empty( Helpers::get_settings()[ 'api_token' ] ) && + empty( Helpers::get_settings()[ 'self_hosted_domain' ] ), ], ], ], @@ -377,15 +385,13 @@ public function __construct() { 'slug' => 'self_hosted_shared_link', 'type' => 'group', 'desc' => sprintf( - '
  1. ' . - __( + '
    1. ' . __( 'Create a secure and private shared link in your Plausible account.', 'plausible-analytics' - ) . - '
    2. ' . - __( 'Paste the shared link in the text box to view your stats in your WordPress dashboard.', 'plausible-analytics' ) . - '
    3. ' . - '
    ', + ) . '
  2. ' . __( + 'Paste the shared link in the text box to view your stats in your WordPress dashboard.', + 'plausible-analytics' + ) . '
  3. ' . '
', esc_url( 'https://plausible.io/docs/embed-dashboard' ) ), 'fields' => [ @@ -425,7 +431,6 @@ public function __construct() { /** * If proxy is enabled, or self-hosted domain has a value, display warning box. - * * @see self::proxy_warning() */ if ( Helpers::proxy_enabled() || ! empty( $settings[ 'self_hosted_domain' ] ) ) { @@ -458,7 +463,6 @@ public function __construct() { /** * Init action hooks. - * * @return void */ private function init() { @@ -509,7 +513,6 @@ private function build_user_roles_array( $slug, $disable_elements = [] ) { /** * Register Menu. - * * @since 1.0.0 * @access public * @return void @@ -579,7 +582,6 @@ public function register_menu() { /** * A little hack to add some classes to the core #wpcontent div. - * * @return void */ public function add_background_color() { @@ -590,7 +592,6 @@ public function add_background_color() { /** * Statistics Page via Embed feature. - * * @since 1.2.0 * @access public * @return void @@ -643,7 +644,6 @@ public function render_analytics_dashboard() { * When this option was saved to the database, underlying code would fail, throwing a CORS related error in browsers. * Now, we explicitly check for the existence of this example "auth" key, and display a human-readable error message to * those who haven't properly set it up. - * * @since v1.2.5 * For self-hosters the View Stats option doesn't need to be enabled, if a Shared Link is entered, we can assume they want to View Stats. * For regular users, the shared link is provisioned by the API, so it shouldn't be empty. @@ -707,7 +707,10 @@ public function render_analytics_dashboard() { ); ?> click here to enable View Stats in WordPress.', 'plausible-analytics' ), + __( + 'Please click here to enable View Stats in WordPress.', + 'plausible-analytics' + ), admin_url( 'options-general.php?page=plausible_analytics#is_shared_link' ) ); ?> diff --git a/src/Integrations.php b/src/Integrations.php index 97d22bd3..d0458e6e 100644 --- a/src/Integrations.php +++ b/src/Integrations.php @@ -2,7 +2,6 @@ /** * Plausible Analytics | Integrations - * * @since 2.1.0 * @package WordPress * @subpackage Plausible Analytics @@ -25,7 +24,6 @@ public function __construct() { /** * Run available integrations. - * * @return void */ private function init() { @@ -38,11 +36,14 @@ private function init() { if ( self::is_edd_active() ) { // new Integrations\EDD(); } + + if ( self::is_form_submit_active() ) { + new Integrations\FormSubmit(); + } } /** * Checks if WooCommerce is installed and activated. - * * @return bool */ public static function is_wc_active() { @@ -51,10 +52,20 @@ public static function is_wc_active() { /** * Checks if Easy Digital Downloads is installed and activated. - * * @return bool */ public static function is_edd_active() { return apply_filters( 'plausible_analytics_integrations_edd', function_exists( 'EDD' ) ); } + + /** + * Check if Form Submissions option is enabled in Enhanced Measurements. + * @return mixed|null + */ + public static function is_form_submit_active() { + return apply_filters( + 'plausible_analytics_integrations_form_submit', + Helpers::is_enhanced_measurement_enabled( 'form-submit' ) + ); + } } diff --git a/src/Integrations/FormSubmit.php b/src/Integrations/FormSubmit.php new file mode 100644 index 00000000..af2e6796 --- /dev/null +++ b/src/Integrations/FormSubmit.php @@ -0,0 +1,91 @@ +init(); + } + + /** + * Init + * @return void + * @codeCoverageIgnore + */ + private function init() { + /** + * Adds required JS and classes. + */ + add_action( 'wp_enqueue_scripts', [ $this, 'add_js' ], 1 ); + /** + * Contact Form 7 doesn't respect JS checkValidity() function, so this is a custom compatibility fix. + */ + add_filter( 'wpcf7_validate', [ $this, 'maybe_track_submission' ], 10, 2 ); + } + + /** + * Enqueues the required JavaScript for form submissions integration. + * @return void + * @codeCoverageIgnore because there's nothing to test here. + */ + public function add_js() { + if ( defined( 'WPCF7_VERSION' ) ) { + return; + } + + wp_register_script( + 'plausible-form-submit-integration', + PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-form-submit-integration.js', + [ 'plausible-analytics' ], + filemtime( PLAUSIBLE_ANALYTICS_PLUGIN_DIR . 'assets/dist/js/plausible-form-submit-integration.js' ) + ); + + wp_localize_script( + 'plausible-form-submit-integration', + 'plausible_analytics_i18n', + [ 'form_completions' => __( 'Form Completions', 'plausible-analytics' ), ] + ); + + wp_enqueue_script( 'plausible-form-submit-integration' ); + } + + /** + * Tracks the form submission if CF7 says it's valid. + * + * @param \WPCF7_Validation $result Form submission result object containing validation results. + * @param array $tags Array of tags associated with the form fields. + * + * @return \WPCF7_Validation + * @codeCoverageIgnore because we can't test XHR requests here. + */ + public function maybe_track_submission( $result, $tags ) { + $invalid_fields = $result->get_invalid_fields(); + + if ( empty( $invalid_fields ) ) { + $post = get_post( $_POST[ '_wpcf7_container_post' ] ); + $uri = '/' . $post->post_name . '/'; + + $proxy = new Proxy( false ); + $proxy->do_request( + __( 'Form Completions', 'plausible-analytics' ), + null, + null, + [ 'form' => $uri ] + ); + } + + return $result; + } +} diff --git a/webpack.config.js b/webpack.config.js index 1949692e..585f156c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,7 +13,8 @@ const config = { mode, entry: { 'plausible-admin': ['./assets/src/css/admin/main.css', './assets/src/js/admin/main.js'], - 'plausible-woocommerce-integration': ['./assets/src/js/integrations/woocommerce.js'] + 'plausible-woocommerce-integration': ['./assets/src/js/integrations/woocommerce.js'], + 'plausible-form-submit-integration': ['./assets/src/js/integrations/form-submit.js'] }, output: { path: path.join(__dirname, './assets/dist/'),