Choice Universal Form Tracker

get_current_screen_id(), $this->get_plugin_screen_ids(), true ) ) { return $text; } $maker = 'https://choice.marketing/?utm_source=wp-plugin&utm_medium=admin&utm_campaign=choice-uft&utm_content=footer'; $review = 'https://wordpress.org/support/plugin/choice-universal-form-tracker/reviews/#new-post'; return sprintf( /* translators: 1: link to Choice OMG, 2: link to the WordPress.org review form. */ esc_html__( 'Choice Universal Form Tracker is free, built and maintained by %1$s. If it saves you time, %2$s helps other site owners find it.', 'choice-universal-form-tracker' ), 'Choice OMG', '' . esc_html__( 'a review on WordPress.org', 'choice-universal-form-tracker' ) . '' ); } /** * Handle export actions early (before any output) */ public function handle_export_actions() { // Run on our admin pages $allowed_pages = array( 'choice-universal-form-tracker', 'cuft-click-tracking' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Routing only: the page slug selects which nonce-checked action below may run; nothing is changed on this read. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( ! in_array( $page, $allowed_pages, true ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Routing only: each action below verifies its own nonce before doing anything. $action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; if ( '' === $action ) { return; } // Every action below changes state or returns private data. if ( ! current_user_can( 'manage_options' ) ) { return; } // Handle CSV export if ( 'export_csv' === $action && isset( $_GET['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'cuft_export_csv' ) ) { $this->handle_csv_export(); // handle_csv_export() will exit, so this won't be reached } // Handle Google Ads OCI export if ( 'export_google_ads_oci' === $action && isset( $_GET['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'cuft_export_google_ads_oci' ) ) { $this->handle_google_ads_oci_export(); // handle_google_ads_oci_export() will exit, so this won't be reached } } /** * Add admin menu page */ public function add_admin_menu() { add_options_page( 'Choice Universal Form Tracker', 'Universal Form Tracker', 'manage_options', 'choice-universal-form-tracker', array( $this, 'admin_page' ) ); add_menu_page( 'Click Tracking', 'Click Tracking', 'manage_options', 'cuft-click-tracking', array( $this, 'click_tracking_page' ), 'dashicons-chart-line', 30 ); } /** * Render admin page */ public function admin_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'choice-universal-form-tracker' ) ); } // Handle form submission if ( isset( $_POST['cuft_save'], $_POST['cuft_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['cuft_nonce'] ) ), 'cuft_settings' ) ) { $this->save_settings(); } // Note: Export actions and webhook regeneration are now handled in handle_export_actions() via admin_init hook $gtm_id = get_option( 'cuft_gtm_id', '' ); $debug_enabled = get_option( 'cuft_debug_enabled', false ); $generate_lead_enabled = get_option( 'cuft_generate_lead_enabled', false ); $lead_currency = get_option( 'cuft_lead_currency', 'CAD' ); $lead_value = get_option( 'cuft_lead_value', 100 ); $console_logging = get_option( 'cuft_console_logging', 'no' ); $phone_validation_enabled = get_option( 'cuft_phone_validation_enabled', false ); // Get current tab // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab selector on a screen gated by manage_options above; nothing is changed. $current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings'; ?>
render_admin_header(); ?>
render_setup_progress(); ?> render_admin_tabs( $current_tab ); ?> render_settings_form( $gtm_id, $debug_enabled, $generate_lead_enabled, $lead_currency, $lead_value, $console_logging, $phone_validation_enabled ); ?> render_framework_status(); ?> render_utm_status(); ?> render_debug_section(); ?>

Settings

Google Tag Manager ID

Enter your GTM container ID (e.g., GTM-ABC123). Leave empty to disable GTM injection.

Server-Side GTM (sGTM)

Google tag gateway

The script path your edge serves for Google's tag gateway, for example /k7q2fx/. The edge must route it to Google before you enable this. State:

Enable Server-Side GTM

Enable this if you have a server-side GTM setup to proxy Google's scripts through your own domain.



✓ Server GTM endpoint validated ✗ Server GTM endpoint not validated - please test connection

The hostname of your server-side tagging server, with no https://, path or port. Example: gtm.example.com
The plugin loads https://<host>/gtm.js?id=<your GTM container ID> from it, and falls back to www.googletagmanager.com if the server fails its health check.

Health Check Status
Active Server: Loading...
Last Check: Loading...
Status: Loading...
Consecutive Success: Loading... | Consecutive Failure: Loading...
Next Check: Loading...
Debug Logging

When enabled, form tracking events will be logged for debugging purposes.

Generate Lead Events

Pushes a generate_lead event, and records it on the click, for every submission that includes a valid email address. When unchecked, no generate_lead event is pushed or recorded; form_submit and qualify_lead are not affected.

Monetary value for each lead in your chosen currency. Used for conversion tracking in Google Analytics 4.

Phone Validation

Validates submitted phone numbers using Twilio + Abstract API. Scores leads 0–10 by line type (mobile=9, landline=7, VoIP=4) and writes the result to the click tracking record. Requires site registration below.

✓ Registered: ()

● Not registered: set the Registration Secret below (or define CUFT_REGISTER_SECRET in wp-config.php) then click Register.

One number per line. Clicks on tel: links to these numbers are not tracked at all: no phone_click in the dataLayer and nothing recorded on the click. Numbers match on their last 10 digits, so +1 (780) 555-0173 and 780.555.0173 are the same number; they are saved in that 10-digit form. To exclude any single link (phone or email) instead, add the attribute data-cuft-ignore or the CSS class cuft-ignore to the link or to an element around it. Developers can add numbers with the cuft_ignore_phone_numbers filter.

days

Click tracking rows older than this are deleted once a day, in batches. Default ; the shortest accepted is . Enter 0 to keep rows forever.

Browser Console Logging

Controls browser console logging for debugging. "Admin Only" is recommended for production sites.

Optional. If set, CUFT posts click data to https://<host>/p on page load via navigator.sendBeacon(). Leave blank to disable.

API Credentials

Overridden by CUFT_REGISTER_SECRET in wp-config.php

Authenticates with the validator service. Leave blank to keep current value.

Required for server-side Measurement Protocol events.

Found in GA4 Admin > Data Streams > Measurement Protocol API secrets. Leave blank to keep current value.

Framework Detection Status

Form tracking enabled and active

Core Features

Form Tracking ✓ Active - pushes form_submit events with user_email and user_phone
Link Tracking ✓ Active - tracks phone_click on tel: links
GTM Integration is_valid_gtm_id( $gtm_id ) ): ?> ✓ Active - Container ✗ Disabled - No valid container ID configured
Server-Side GTM ✓ Active - ⚠ Configured but not validated - ✗ Enabled but no host configured - Disabled (using standard GTM)

🎯 UTM Campaign Tracking

Active Campaign Detected

$value ): ?>

✓ All form submissions will include this campaign data for attribution tracking.

No Active Campaign

When users visit your site with UTM parameters (e.g., ?utm_campaign=summer_sale), the campaign data will be stored and included with all form submissions for proper attribution.

Debug Logs

No debug logs yet. Logs will appear here when forms are submitted.

Time Level Message Context
' . esc_html( wp_json_encode( $log['context'], JSON_PRETTY_PRINT ) ) . '' : '-'; ?>

' . esc_html__( 'Debug logs cleared.', 'choice-universal-form-tracker' ) . '

'; } } /** * Get color for log level */ private function get_log_level_color( $level ) { switch ( $level ) { case 'error': return '#dc3545'; case 'warning': return '#ffc107'; case 'info': return '#17a2b8'; case 'debug': return '#6c757d'; default: return '#6c757d'; } } /** * Enqueue admin scripts */ public function enqueue_admin_scripts( $hook ) { $settings_hook = 'settings_page_choice-universal-form-tracker'; $click_hook = 'toplevel_page_cuft-click-tracking'; // The admin stylesheet loads on the plugin's own screens only. if ( in_array( $hook, array( $settings_hook, $click_hook ), true ) ) { wp_enqueue_style( 'cuft-admin', CUFT_URL . '/assets/cuft-admin.css', array(), CUFT_VERSION ); } // The admin script serves the settings page and the Click Tracking page. if ( $hook !== $settings_hook && $hook !== $click_hook ) { return; } wp_enqueue_script( 'cuft-admin', CUFT_URL . '/assets/cuft-admin.js', array( 'jquery' ), CUFT_VERSION, true ); CUFT_Utils::localize( 'cuft-admin', 'cuftAdmin', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'cuft_admin' ), 'register_nonce' => wp_create_nonce( 'cuft_token_register' ), 'current_version' => CUFT_VERSION, 'plugin_url' => CUFT_URL, 'admin_url' => admin_url( 'options-general.php?page=choice-universal-form-tracker' ), ) ); if ( $hook !== $settings_hook ) { return; } // Enqueue Force Update assets (Feature 009 - v3.19.0). Skipped in the // WordPress.org build, which ships neither the tab nor its handlers. if ( CUFT_Plugin::has_updater() ) { wp_enqueue_script( 'cuft-force-update', CUFT_URL . '/assets/admin/cuft-force-update.js', array( 'jquery' ), CUFT_VERSION, true ); CUFT_Utils::localize( 'cuft-force-update', 'cuftForceUpdate', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'cuft_force_update' ), ) ); wp_enqueue_style( 'cuft-force-update', CUFT_URL . '/assets/admin/cuft-force-update.css', array(), CUFT_VERSION ); } } /** * AJAX handler for testing sGTM endpoint */ public function ajax_test_sgtm() { // Verify nonce and permissions if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'cuft_admin' ) || ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Security check failed' ), 403 ); } $raw_host = isset( $_POST['sgtm_host'] ) ? sanitize_text_field( wp_unslash( $_POST['sgtm_host'] ) ) : ''; $gtm_id = CUFT_SGTM::sanitize_gtm_id( (string) get_option( 'cuft_gtm_id', '' ) ); if ( '' === trim( $raw_host ) ) { wp_send_json_error( array( 'message' => 'Please enter a Server GTM host' ) ); } if ( ! $gtm_id ) { wp_send_json_error( array( 'message' => 'Please configure a GTM ID first' ) ); } $sgtm_host = CUFT_SGTM::sanitize_host( $raw_host ); if ( ! $sgtm_host ) { wp_send_json_error( array( 'message' => 'Invalid host. Enter a hostname only, for example gtm.example.com.' ) ); } // Only the saved host may change stored state; testing a host that // has not been saved yet reports the result without recording it. $is_saved_host = ( CUFT_SGTM::get_host() === $sgtm_host ); $test_results = $this->test_sgtm_endpoints( $sgtm_host, $gtm_id, $is_saved_host ); if ( $is_saved_host ) { update_option( 'cuft_sgtm_validated', (bool) $test_results['success'] ); } wp_send_json_success( array( 'valid' => (bool) $test_results['success'], 'message' => $test_results['success'] ? 'Custom server validated successfully' : $test_results['message'], 'details' => $test_results['details'], 'endpoints_tested' => array( 'gtm_js' => isset( $test_results['details']['gtm_js'] ) && 0 === strpos( $test_results['details']['gtm_js'], 'OK' ), 'ns_html' => isset( $test_results['details']['ns_html'] ) && 0 === strpos( $test_results['details']['ns_html'], 'OK' ), ), ) ); } /** * AJAX handler for manual health check */ public function ajax_manual_health_check() { // Verify nonce and permissions if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'cuft_admin' ) || ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Security check failed' ) ); } // Check if custom server is enabled $enabled = get_option( 'cuft_sgtm_enabled', false ); $sgtm_host = CUFT_SGTM::get_host(); $gtm_id = CUFT_SGTM::sanitize_gtm_id( (string) get_option( 'cuft_gtm_id', '' ) ); if ( ! $enabled || '' === $sgtm_host || ! $gtm_id ) { wp_send_json_error( array( 'message' => 'Custom server not configured' ) ); } // Perform health check $test_results = $this->test_sgtm_endpoints( $sgtm_host, $gtm_id ); $health_check_passed = $test_results['success']; $timestamp = time(); $response_time = $test_results['success'] ? 200.0 : 5000.0; // Mock values // Get current counters $consecutive_success = get_option( 'cuft_sgtm_health_consecutive_success', 0 ); $consecutive_failure = get_option( 'cuft_sgtm_health_consecutive_failure', 0 ); $active_server = get_option( 'cuft_sgtm_active_server', 'fallback' ); $response_data = array( 'health_check_passed' => $health_check_passed, 'message' => $health_check_passed ? 'Health check successful' : 'Health check failed: ' . $test_results['message'], 'timestamp' => $timestamp, 'response_time' => $response_time, 'consecutive_success' => $consecutive_success, 'consecutive_failure' => $consecutive_failure, 'active_server' => $active_server ); wp_send_json_success( $response_data ); } /** * AJAX handler for getting custom server status */ public function ajax_get_sgtm_status() { // Verify nonce and permissions if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'cuft_admin' ) || ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Security check failed' ) ); } // Get configuration $enabled = get_option( 'cuft_sgtm_enabled', false ); $url = CUFT_SGTM::get_host(); $validated = get_option( 'cuft_sgtm_validated', false ); // Get status $active_server = get_option( 'cuft_sgtm_active_server', 'fallback' ); $last_check_time = get_option( 'cuft_sgtm_health_last_check', 0 ); $last_check_result = get_option( 'cuft_sgtm_health_last_result', false ); $last_check_message = get_option( 'cuft_sgtm_health_last_message', '' ); $consecutive_success = get_option( 'cuft_sgtm_health_consecutive_success', 0 ); $consecutive_failure = get_option( 'cuft_sgtm_health_consecutive_failure', 0 ); $response_time = get_option( 'cuft_sgtm_health_response_time', 0.0 ); // Calculate next check time (6 hours from last check) $next_check = $last_check_time + (6 * 60 * 60); // Human readable timestamps $human_readable = array( 'last_check' => $last_check_time > 0 ? human_time_diff( $last_check_time ) . ' ago' : 'Never', 'next_check' => $next_check > time() ? human_time_diff( $next_check ) . ' from now' : 'Overdue', 'status' => $active_server === 'custom' ? 'Healthy' : 'Using Fallback' ); $response_data = array( 'configuration' => array( 'enabled' => $enabled, 'host' => $url, 'validated' => $validated ), 'status' => array( 'active_server' => $active_server, 'last_check_time' => $last_check_time, 'last_check_result' => $last_check_result, 'last_check_message' => $last_check_message, 'consecutive_success' => $consecutive_success, 'consecutive_failure' => $consecutive_failure, 'response_time' => $response_time ), 'next_check' => $next_check, 'human_readable' => $human_readable ); wp_send_json_success( $response_data ); } /** * AJAX handler for downloading GTM templates */ public function ajax_download_gtm_template() { // Verify nonce and permissions if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'cuft_admin' ) || ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Security check failed' ) ); } // Get template type $template_type = isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : ''; if ( empty( $template_type ) || ! in_array( $template_type, array( 'web', 'server' ), true ) ) { wp_send_json_error( array( 'message' => 'Invalid template type' ) ); } // Determine file path based on template type $plugin_dir = plugin_dir_path( dirname( __FILE__ ) ); if ( $template_type === 'web' ) { $file_path = $plugin_dir . 'gtm-web-client/CUFT - Web Defaults.json'; $filename = 'CUFT-Web-Defaults.json'; } else { $file_path = $plugin_dir . 'gtm-server/CUFT - Server Defaults.json'; $filename = 'CUFT-Server-Defaults.json'; } // Check if file exists if ( ! file_exists( $file_path ) ) { wp_send_json_error( array( 'message' => 'Template file not found' ) ); } // Read file content $file_content = file_get_contents( $file_path ); if ( $file_content === false ) { wp_send_json_error( array( 'message' => 'Failed to read template file' ) ); } // Return file content as base64 to avoid JSON parsing issues wp_send_json_success( array( 'content' => base64_encode( $file_content ), 'filename' => $filename ) ); } /** * Add custom cron intervals */ public static function add_cron_intervals( $schedules ) { $schedules['six_hours'] = array( 'interval' => 6 * 60 * 60, // 6 hours in seconds 'display' => 'Every 6 hours' ); return $schedules; } /** * Register the six-hour health check on every request. * * WP-Cron requests are not admin requests, so the callback and its * six_hours schedule cannot live in the constructor. The event is kept * scheduled here rather than on activation, because sites updated by * copying files or through an updater never reactivate, and on activation * the six_hours schedule did not exist yet. */ public static function register_health_check() { add_filter( 'cron_schedules', array( __CLASS__, 'add_cron_intervals' ) ); add_action( 'cuft_scheduled_health_check', array( __CLASS__, 'run_scheduled_health_check' ) ); if ( get_option( 'cuft_sgtm_enabled', false ) ) { if ( ! wp_next_scheduled( 'cuft_scheduled_health_check' ) ) { wp_schedule_event( time(), 'six_hours', 'cuft_scheduled_health_check' ); } } elseif ( wp_next_scheduled( 'cuft_scheduled_health_check' ) ) { wp_clear_scheduled_hook( 'cuft_scheduled_health_check' ); } } /** * WP-Cron entry point for the scheduled health check. */ public static function run_scheduled_health_check() { ( new self() )->scheduled_health_check(); } /** * Scheduled health check callback */ public function scheduled_health_check() { // Check if custom server is enabled $enabled = get_option( 'cuft_sgtm_enabled', false ); $sgtm_host = CUFT_SGTM::get_host(); $gtm_id = CUFT_SGTM::sanitize_gtm_id( (string) get_option( 'cuft_gtm_id', '' ) ); if ( ! $enabled || '' === $sgtm_host || ! $gtm_id ) { return; // Skip health check if not configured } // Perform health check $this->test_sgtm_endpoints( $sgtm_host, $gtm_id ); // The store_health_check_result method will handle: // - Storing health check results // - Updating consecutive counters // - Switching between custom/fallback servers // - Creating admin notice triggers } /** * Test the tagging server's gtm.js and ns.html endpoints. * * Both URLs are built by CUFT_SGTM from the validated host and container * ID; TLS certificates are always verified. * * @param string $sgtm_host Validated bare host. * @param string $gtm_id Validated container ID. * @param bool $record Store the result and update the server switch. * @return array */ private function test_sgtm_endpoints( $sgtm_host, $gtm_id, $record = true ) { $results = array( 'success' => false, 'message' => '', 'details' => array(), ); $endpoints = array( 'gtm_js' => CUFT_SGTM::script_url( $sgtm_host, $gtm_id ), 'ns_html' => CUFT_SGTM::noscript_url( $sgtm_host, $gtm_id ), ); foreach ( $endpoints as $key => $url ) { $label = 'gtm_js' === $key ? 'gtm.js' : 'ns.html'; if ( '' === $url ) { $results['message'] = 'Invalid host or GTM container ID'; break; } $response = wp_remote_get( $url, array( 'timeout' => 5 ) ); if ( is_wp_error( $response ) ) { $results['message'] = 'Failed to connect to ' . $label . ' endpoint: ' . $response->get_error_message(); $results['details'][ $key ] = 'Error: ' . $response->get_error_message(); break; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = (string) wp_remote_retrieve_body( $response ); if ( 200 !== $code ) { $results['message'] = $label . ' endpoint returned status code: ' . $code; $results['details'][ $key ] = 'HTTP ' . $code; break; } $looks_right = 'gtm_js' === $key ? ( false !== strpos( $body, 'google' ) || false !== strpos( $body, 'gtm' ) ) : ( false !== strpos( $body, '<' ) ); if ( ! $looks_right ) { $results['message'] = $label . ' endpoint did not return the expected content'; $results['details'][ $key ] = 'Invalid response content'; break; } $results['details'][ $key ] = 'OK (HTTP 200)'; } if ( '' === $results['message'] ) { $results['success'] = true; $results['message'] = 'Both endpoints validated successfully'; } if ( $record ) { $this->store_health_check_result( $results ); } return $results; } /** * Store health check result in wp_options */ private function store_health_check_result( $results ) { $timestamp = time(); $success = $results['success']; $message = $results['message']; // Calculate response time (simplified - in real implementation, you'd measure actual time) $response_time = $success ? 200.0 : 5000.0; // Mock values for now // Store basic health check data update_option( 'cuft_sgtm_health_last_check', $timestamp ); update_option( 'cuft_sgtm_health_last_result', $success ); update_option( 'cuft_sgtm_health_last_message', $message ); update_option( 'cuft_sgtm_health_response_time', $response_time ); // Handle consecutive counters and server switching if ( $success ) { // Increment consecutive success, reset failure counter $consecutive_success = get_option( 'cuft_sgtm_health_consecutive_success', 0 ) + 1; update_option( 'cuft_sgtm_health_consecutive_success', $consecutive_success ); update_option( 'cuft_sgtm_health_consecutive_failure', 0 ); // Switch to custom server after 3 consecutive successes $current_server = get_option( 'cuft_sgtm_active_server', 'fallback' ); if ( $consecutive_success >= 3 ) { if ( $current_server !== 'custom' ) { update_option( 'cuft_sgtm_active_server', 'custom' ); // Trigger admin notice for server recovery add_option( 'cuft_sgtm_server_recovered', $timestamp ); } } elseif ( $current_server !== 'custom' ) { // Recovering: confirm within the hour, not over 18 hours. self::schedule_health_recheck(); } } else { // Increment consecutive failure, reset success counter $consecutive_failure = get_option( 'cuft_sgtm_health_consecutive_failure', 0 ) + 1; update_option( 'cuft_sgtm_health_consecutive_failure', $consecutive_failure ); update_option( 'cuft_sgtm_health_consecutive_success', 0 ); // Fall back on the second failure in a row. One timeout from a // busy host would otherwise take the site off its tagging server // until three passes six hours apart brought it back; a recheck // 15 minutes later settles it instead. $current_server = get_option( 'cuft_sgtm_active_server', 'fallback' ); if ( $consecutive_failure >= 2 ) { if ( $current_server === 'custom' ) { update_option( 'cuft_sgtm_active_server', 'fallback' ); // Trigger admin notice for server failure add_option( 'cuft_sgtm_server_failed', $timestamp ); } } else { self::schedule_health_recheck(); } } } /** * One-off health check 15 minutes from now, beside the six-hour run. */ private static function schedule_health_recheck() { if ( ! wp_next_scheduled( 'cuft_scheduled_health_check', array( 'recheck' ) ) ) { wp_schedule_single_event( time() + 15 * MINUTE_IN_SECONDS, 'cuft_scheduled_health_check', array( 'recheck' ) ); } } /** * Render setup progress indicator */ private function render_setup_progress() { $gtm_id = get_option( 'cuft_gtm_id', '' ); $frameworks = CUFT_Form_Detector::get_framework_status(); $detected_frameworks = array_filter( $frameworks, function($fw) { return $fw['detected']; } ); // Calculate setup completion $steps = array( 'gtm_setup' => !empty( $gtm_id ), 'framework_detected' => !empty( $detected_frameworks ) ); $completed_steps = array_filter( $steps ); $total_steps = count( $steps ); $completed_count = count( $completed_steps ); $progress_percentage = ( $completed_count / $total_steps ) * 100; // Only show if setup is not complete if ( $completed_count < $total_steps ) { ?>

Setup Progress

GTM Configuration
Framework Detected
Testing Complete
__( 'Settings', 'choice-universal-form-tracker' ), ); // Force Update drives the self-update subsystem, which the // WordPress.org build does not ship. See has_updater(). if ( CUFT_Plugin::has_updater() ) { $tabs['force-update'] = __( 'Force Update', 'choice-universal-form-tracker' ); } echo ''; } /** * Click Tracking dashboard page callback */ public function click_tracking_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'choice-universal-form-tracker' ) ); } // Handle POST actions if ( isset( $_POST['cuft_click_action'], $_POST['cuft_click_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['cuft_click_nonce'] ) ), 'cuft_click_tracking' ) ) { $this->handle_click_tracking_actions(); } ?>

render_click_tracking_tab(); ?>

Click tracking write failed. Clicks may not be recording. Last failure: UTC ()

get_query_param( 'paged', '1' ) ) ); $per_page = 20; $offset = ( $current_page - 1 ) * $per_page; // Handle filters $filter_qualified = $this->get_query_param( 'filter_qualified' ); $filter_event_type = $this->get_query_param( 'filter_event_type' ); $filter_date_from = $this->get_query_param( 'filter_date_from' ); $filter_date_to = $this->get_query_param( 'filter_date_to' ); $filter_ip_search = $this->get_query_param( 'filter_ip_search' ); // Default to showing every click, including gclid-only visits that // never received an event; the filter is still available for // narrowing to clicks that did fire an event. Was defaulted to '1' // (has events only) before 3.28.2, which could make a site with real // click rows look empty here even though the export and stats used // the same restrictive default underneath it. $filter_has_events = $this->get_query_param( 'filter_has_events', '' ); $sort_by = $this->get_query_param( 'sort_by', 'date_created' ); $args = array( 'limit' => $per_page, 'offset' => $offset, 'sort_by' => $sort_by ); if ( $filter_qualified !== '' ) { $args['qualified'] = (int) $filter_qualified; } if ( ! empty( $filter_event_type ) ) { $args['event_type'] = $filter_event_type; } if ( ! empty( $filter_date_from ) ) { $args['date_from'] = $filter_date_from . ' 00:00:00'; } if ( ! empty( $filter_date_to ) ) { $args['date_to'] = $filter_date_to . ' 23:59:59'; } if ( ! empty( $filter_ip_search ) ) { // Hash the IP address for lookup $args['ip_hash'] = CUFT_Click_Tracker::hash_ip( $filter_ip_search ); } if ( $filter_has_events !== '' ) { $args['has_events'] = $filter_has_events; } $clicks = class_exists( 'CUFT_Click_Tracker' ) ? CUFT_Click_Tracker::get_clicks( $args ) : array(); $total_clicks = class_exists( 'CUFT_Click_Tracker' ) ? CUFT_Click_Tracker::get_clicks_count( $args ) : 0; $total_pages = ceil( $total_clicks / $per_page ); ?>
render_click_write_error_notice(); ?>

🎯 Click Tracking Management

'export_csv' ), admin_url( 'admin.php?page=cuft-click-tracking' ) ), 'cuft_export_csv', 'nonce' ); $google_ads_export_url = wp_nonce_url( add_query_arg( array( 'action' => 'export_google_ads_oci' ), admin_url( 'admin.php?page=cuft-click-tracking' ) ), 'cuft_export_google_ads_oci', 'nonce' ); ?> 📊 Export CSV 🎯 Export for Google Ads
render_webhook_settings(); ?> render_click_tracking_filters( $filter_qualified, $filter_event_type, $filter_date_from, $filter_date_to, $filter_ip_search, $sort_by, $filter_has_events ); ?> render_click_tracking_stats( $args ); ?> render_click_tracking_table( $clicks ); ?> render_click_tracking_pagination( $current_page, $total_pages ); ?>
prefix . 'cuft_click_tracking'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin's own table cuft_click_tracking; no WP API covers it, and the newest row is per-request admin display data. $sample_click_id = $wpdb->get_var( $wpdb->prepare( 'SELECT click_id FROM %i ORDER BY date_created DESC LIMIT 1', $table_name ) ); $has_data = ! empty( $sample_click_id ); // Use placeholder for display if no data exists $display_click_id = $has_data ? $sample_click_id : 'YOUR_CLICK_ID'; $webhook_key = class_exists( 'CUFT_Click_Tracker' ) ? CUFT_Click_Tracker::get_webhook_key() : ''; $base_args = array( 'action' => 'cuft_webhook', 'key' => $webhook_key, ); $endpoint_url = add_query_arg( $base_args, $webhook_url ); $example_url = add_query_arg( array_merge( $base_args, array( 'click_id' => $display_click_id, 'qualified' => '1', 'score' => '8' ) ), $webhook_url ); ?>

📡 Webhook Endpoint

Updates a click's lead status from an email link, a CRM, or any system that can request a URL.
Every request must carry this site's webhook key in the key parameter. Requests without it are rejected with HTTP 403.


Append click_id and the status parameters below. Treat this URL as a secret: it contains the key.

Example Usage:

Parameters:
• action = cuft_webhook (required)
• key = this site's webhook key (required)
• click_id = the click ID to update (required; only existing clicks can be updated)
• qualified = 0 or 1 (optional)
• score = 0-10 (optional)
• status = qualify_lead, disqualify_lead, working_lead, close_convert_lead or close_unconvert_lead (optional)

Test Webhook:

Click any Click ID in the table below to copy it here. You need at least one click tracking record. Create one by clicking a tracked link or submitting a tracked form.

⚠️ No Click Data Available
The test button will appear once you have at least one click tracking record in the database. Click tracking records are created when users interact with tracked links or forms.







Clear
🔍 IP Search: Showing results for IP (Hash: ...)
1 ) ); $qualified_clicks = CUFT_Click_Tracker::get_clicks_count( $qualified_args ); $unqualified_clicks = $total_clicks - $qualified_clicks; $qualification_rate = $total_clicks > 0 ? round( ( $qualified_clicks / $total_clicks ) * 100, 1 ) : 0; ?>
Total Clicks
Qualified Clicks
Unqualified Clicks
%
Qualification Rate
Click ID Campaign Events Qualified Score Date Created Date Updated Actions
No click tracking data found. Click data will appear here when tracking is active.
click_id ); ?> ip_hash ) ): ?>
IP: ip_hash, 0, 12 ) ); ?>…
campaign ?: '-' ); ?> events ) ? CUFT_Click_Tracker::get_events( $click->click_id ) : array(); if ( ! empty( $events ) ) : // Sort events by timestamp descending (newest first) usort( $events, function( $a, $b ) { return strcmp( $b['timestamp'], $a['timestamp'] ); }); // Display first 3 events, collapse rest $visible_events = array_slice( $events, 0, 3 ); $hidden_events = array_slice( $events, 3 ); foreach ( $visible_events as $event ): $event_type = $event['event']; $event_time = gmdate( 'M j, g:i A', strtotime( $event['timestamp'] ) ); // Badge colors by event type $badge_colors = array( 'phone_click' => '#3b82f6', 'email_click' => '#8b5cf6', 'form_submit' => '#10b981', 'generate_lead' => '#f59e0b', 'qualify_lead' => '#10b981', 'disqualify_lead' => '#ef4444', 'working_lead' => '#f59e0b', 'close_convert_lead' => '#059669', 'close_unconvert_lead' => '#6b7280', 'score_updated' => '#06b6d4' ); $badge_color = isset( $badge_colors[ $event_type ] ) ? $badge_colors[ $event_type ] : '#6b7280'; ?>
+ more No events
qualified ? 'YES' : 'NO' ); ?> score; ?>/10 date_created ) ) ); ?>
UTC
date_updated !== $click->date_created ): ?> date_updated ) ) ); ?>
UTC -
get_query_param( $param ); if ( '' !== $value ) { $filter_params[ $param ] = $value; } } ?>
1 ): ?> « Previous Page of Next »
get_query_param( 'filter_qualified' ); if ( '' !== $filter_qualified ) { $args['qualified'] = (int) $filter_qualified; } $filter_event_type = $this->get_query_param( 'filter_event_type' ); if ( ! empty( $filter_event_type ) ) { $args['event_type'] = $filter_event_type; } $filter_date_from = $this->get_query_param( 'filter_date_from' ); if ( ! empty( $filter_date_from ) ) { $args['date_from'] = $filter_date_from . ' 00:00:00'; } $filter_date_to = $this->get_query_param( 'filter_date_to' ); if ( ! empty( $filter_date_to ) ) { $args['date_to'] = $filter_date_to . ' 23:59:59'; } $sort_by = $this->get_query_param( 'sort_by' ); if ( ! empty( $sort_by ) ) { $args['sort_by'] = $sort_by; } CUFT_Click_Tracker::export_csv( $args ); } /** * Handle Google Ads OCI export */ private function handle_google_ads_oci_export() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions', 'choice-universal-form-tracker' ) ); } // handle_export_actions() verified this nonce already; repeat it next // to the code that reads the request. check_admin_referer( 'cuft_export_google_ads_oci', 'nonce' ); if ( ! class_exists( 'CUFT_Click_Tracker' ) ) { wp_die( esc_html__( 'Click tracker not available', 'choice-universal-form-tracker' ) ); } // Get filter parameters (same as regular CSV export) $args = array(); $filter_qualified = $this->get_query_param( 'filter_qualified' ); if ( '' !== $filter_qualified ) { $args['qualified'] = (int) $filter_qualified; } $filter_date_from = $this->get_query_param( 'filter_date_from' ); if ( ! empty( $filter_date_from ) ) { $args['date_from'] = $filter_date_from . ' 00:00:00'; } $filter_date_to = $this->get_query_param( 'filter_date_to' ); if ( ! empty( $filter_date_to ) ) { $args['date_to'] = $filter_date_to . ' 23:59:59'; } CUFT_Click_Tracker::export_google_ads_oci_csv( $args ); } /** * Screen IDs of the plugin's own admin pages: the settings page, the * Click Tracking page, and the Testing Dashboard. * * @return string[] */ private function get_plugin_screen_ids() { return array( 'settings_page_choice-universal-form-tracker', 'toplevel_page_cuft-click-tracking', 'settings_page_cuft-testing-dashboard', ); } /** * Current admin screen ID, or an empty string before the screen is set. * * @return string */ private function get_current_screen_id() { if ( ! function_exists( 'get_current_screen' ) ) { return ''; } $screen = get_current_screen(); return $screen ? (string) $screen->id : ''; } /** * Display admin notices. * * Every notice renders only on the plugin's own screens (Settings > * Universal Form Tracker, Click Tracking, Testing Dashboard), only when * there is something to act on, and is dismissible: * - Missing or invalid GTM ID: on the Click Tracking and Testing * Dashboard screens; the settings screen carries the form itself. * - Custom GTM server switched to fallback, or recovered: shown once. */ public function admin_notices() { if ( ! current_user_can( 'manage_options' ) ) { return; } $screen_id = $this->get_current_screen_id(); if ( ! in_array( $screen_id, $this->get_plugin_screen_ids(), true ) ) { return; } $settings_url = admin_url( 'options-general.php?page=choice-universal-form-tracker' ); if ( ! $this->is_valid_gtm_id( get_option( 'cuft_gtm_id', '' ) ) && 'settings_page_choice-universal-form-tracker' !== $screen_id ) { echo '
'; echo '

Choice Universal Form Tracker: GTM container ID is missing or invalid. '; echo 'Please configure your GTM ID to enable conversion tracking.

'; echo '
'; } $this->check_server_status_notices(); } /** * Check and display server status change notices */ private function check_server_status_notices() { $settings_url = admin_url( 'options-general.php?page=choice-universal-form-tracker' ); // Check for server recovery notice $server_recovered = get_option( 'cuft_sgtm_server_recovered', false ); if ( $server_recovered ) { echo '
'; echo '

✅ Custom GTM server is now active
'; echo 'Your custom server has passed 3 consecutive health checks and is now being used for GTM script loading. '; echo 'View status

'; echo '
'; // Clean up the trigger delete_option( 'cuft_sgtm_server_recovered' ); } // Check for server failure notice $server_failed = get_option( 'cuft_sgtm_server_failed', false ); if ( $server_failed ) { echo '
'; echo '

⚠️ Custom GTM server unavailable, using fallback
'; echo 'Your custom server failed a health check and the system has automatically switched to Google\'s default endpoints. '; echo 'View status

'; echo '
'; // Clean up the trigger delete_option( 'cuft_sgtm_server_failed' ); } } }