Changeset 3401441
- Timestamp:
- 11/23/2025 09:37:37 PM (7 weeks ago)
- Location:
- opti-behavior
- Files:
-
- 96 added
- 25 edited
-
assets/banner-1544x500.png (modified) (previous)
-
assets/screenshot-12.png (modified) (previous)
-
assets/screenshot-13.png (added)
-
tags/1.0.5 (added)
-
tags/1.0.5/Opti-Behavior.php (added)
-
tags/1.0.5/THIRD-PARTY-LICENSES.md (added)
-
tags/1.0.5/admin (added)
-
tags/1.0.5/admin/class-opti-behavior-heatmap-admin-settings.php (added)
-
tags/1.0.5/admin/class-opti-behavior-heatmap-ajax-handler.php (added)
-
tags/1.0.5/admin/class-opti-behavior-heatmap-dashboard.php (added)
-
tags/1.0.5/admin/class-opti-behavior-heatmap-dashboard.php.backup (added)
-
tags/1.0.5/admin/class-opti-behavior-heatmap-list.php (added)
-
tags/1.0.5/admin/class-opti-behavior-heatmap-post-metabox.php (added)
-
tags/1.0.5/admin/recordings-upgrade-page.php (added)
-
tags/1.0.5/assets (added)
-
tags/1.0.5/assets/css (added)
-
tags/1.0.5/assets/css/admin-menu.css (added)
-
tags/1.0.5/assets/css/admin-notices.css (added)
-
tags/1.0.5/assets/css/admin-utilities.css (added)
-
tags/1.0.5/assets/css/ai-insights-page.css (added)
-
tags/1.0.5/assets/css/countries-widget-scroll.css (added)
-
tags/1.0.5/assets/css/dashboard.css (added)
-
tags/1.0.5/assets/css/dashboard_styles.css (added)
-
tags/1.0.5/assets/css/heatmaps.css (added)
-
tags/1.0.5/assets/css/leaflet.min.css (added)
-
tags/1.0.5/assets/css/metabox.css (added)
-
tags/1.0.5/assets/css/recordings-upgrade.css (added)
-
tags/1.0.5/assets/css/sessions.css (added)
-
tags/1.0.5/assets/css/settings-modal.css (added)
-
tags/1.0.5/assets/css/settings.css (added)
-
tags/1.0.5/assets/css/style.css (added)
-
tags/1.0.5/assets/images (added)
-
tags/1.0.5/assets/images/OB.ico (added)
-
tags/1.0.5/assets/images/leaflet (added)
-
tags/1.0.5/assets/images/leaflet/layers-2x.png (added)
-
tags/1.0.5/assets/images/leaflet/layers.png (added)
-
tags/1.0.5/assets/images/leaflet/marker-icon-2x.png (added)
-
tags/1.0.5/assets/images/leaflet/marker-icon.png (added)
-
tags/1.0.5/assets/images/leaflet/marker-shadow.png (added)
-
tags/1.0.5/assets/js (added)
-
tags/1.0.5/assets/js/admin-utilities.js (added)
-
tags/1.0.5/assets/js/chart.umd.min.js (added)
-
tags/1.0.5/assets/js/dashboard-cleaned.js (added)
-
tags/1.0.5/assets/js/dashboard-converted.js (added)
-
tags/1.0.5/assets/js/dashboard-final.js (added)
-
tags/1.0.5/assets/js/dashboard.js (added)
-
tags/1.0.5/assets/js/frontend-mobile-simulator.js (added)
-
tags/1.0.5/assets/js/frontend-page-title.js (added)
-
tags/1.0.5/assets/js/heatmap.min.js (added)
-
tags/1.0.5/assets/js/heatmap.min.js.LICENSE.txt (added)
-
tags/1.0.5/assets/js/heatmaps.js (added)
-
tags/1.0.5/assets/js/leaflet-config.js (added)
-
tags/1.0.5/assets/js/leaflet.min.js (added)
-
tags/1.0.5/assets/js/lucide.min.js (added)
-
tags/1.0.5/assets/js/metabox-analytics.js (added)
-
tags/1.0.5/assets/js/mobile-simulator.js (added)
-
tags/1.0.5/assets/js/notice-cleanup.js (added)
-
tags/1.0.5/assets/js/opti-behavior-debug.js (added)
-
tags/1.0.5/assets/js/opti-behavior-heatmap-simple.js (added)
-
tags/1.0.5/assets/js/opti-behavior-heatmap.min.js (added)
-
tags/1.0.5/assets/js/settings.js (added)
-
tags/1.0.5/assets/js/support-form.js (added)
-
tags/1.0.5/includes (added)
-
tags/1.0.5/includes/class-autoloader.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-analytics.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-bot-tracker.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-core.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-data-protection.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-database.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-debug-manager.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-file-storage.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-options.php (added)
-
tags/1.0.5/includes/class-opti-behavior-heatmap-session.php (added)
-
tags/1.0.5/includes/class-opti-behavior-license-manager.php (added)
-
tags/1.0.5/includes/class-opti-behavior-performance-optimizer.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-ai-insights-views.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-ajax-handlers.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-assets.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-dashboard-views.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-data-helpers.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-exports.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-heatmaps-views.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-maintenance.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-sessions-views.php (added)
-
tags/1.0.5/includes/trait-opti-behavior-settings-views.php (added)
-
tags/1.0.5/languages (added)
-
tags/1.0.5/languages/opti-behavior-fr_FR.mo (added)
-
tags/1.0.5/languages/opti-behavior-fr_FR.po (added)
-
tags/1.0.5/languages/opti-behavior.pot (added)
-
tags/1.0.5/public (added)
-
tags/1.0.5/public/class-opti-behavior-heatmap-frontend.php (added)
-
tags/1.0.5/readme.txt (added)
-
tags/1.0.5/views (added)
-
tags/1.0.5/views/dashboard-views.php (added)
-
tags/1.0.5/views/heatmaps-views.php (added)
-
tags/1.0.5/views/sessions-views.php (added)
-
trunk/Opti-Behavior.php (modified) (3 diffs)
-
trunk/admin/class-opti-behavior-heatmap-ajax-handler.php (modified) (4 diffs)
-
trunk/admin/class-opti-behavior-heatmap-dashboard.php (modified) (27 diffs)
-
trunk/admin/class-opti-behavior-heatmap-dashboard.php.backup (added)
-
trunk/admin/class-opti-behavior-heatmap-post-metabox.php (modified) (15 diffs)
-
trunk/admin/recordings-upgrade-page.php (modified) (2 diffs)
-
trunk/assets/css/admin-notices.css (modified) (5 diffs)
-
trunk/assets/css/ai-insights-page.css (modified) (1 diff)
-
trunk/assets/css/heatmaps.css (modified) (19 diffs)
-
trunk/assets/css/metabox.css (modified) (3 diffs)
-
trunk/assets/css/recordings-upgrade.css (modified) (10 diffs)
-
trunk/assets/css/settings.css (modified) (2 diffs)
-
trunk/assets/js/dashboard.js (modified) (5 diffs)
-
trunk/assets/js/heatmaps.js (modified) (1 diff)
-
trunk/assets/js/metabox-analytics.js (modified) (9 diffs)
-
trunk/assets/js/opti-behavior-heatmap-simple.js (modified) (10 diffs)
-
trunk/assets/js/support-form.js (added)
-
trunk/includes/class-opti-behavior-heatmap-database.php (modified) (2 diffs)
-
trunk/includes/class-opti-behavior-license-manager.php (modified) (6 diffs)
-
trunk/includes/trait-opti-behavior-ai-insights-views.php (modified) (1 diff)
-
trunk/includes/trait-opti-behavior-ajax-handlers.php (modified) (4 diffs)
-
trunk/includes/trait-opti-behavior-assets.php (modified) (2 diffs)
-
trunk/includes/trait-opti-behavior-heatmaps-views.php (modified) (5 diffs)
-
trunk/includes/trait-opti-behavior-sessions-views.php (modified) (1 diff)
-
trunk/readme.txt (modified) (8 diffs)
Legend:
- Unmodified
- Added
- Removed
-
opti-behavior/trunk/Opti-Behavior.php
r3399182 r3401441 2 2 /** 3 3 * Plugin Name: Opti-Behavior 4 * Description: Transform your WordPress site with powerful analytics! Track user behavior with beautiful heatmaps and real-time insights. Boost conversions and optimize user experience with data-driven decisions. Upgrade to PRO for session recordings and advanced features. 5 * Version: 1.0.4 4 * Plugin URI: https://optiuser.com/ 5 * Description: Transform your WordPress site with powerful analytics! Track user behavior with beautiful heatmaps and real-time insights. Boost conversions and optimize user experience with data-driven decisions. 6 * Version: 1.0.5 6 7 * Author: OptiUser 7 * Author URI: https:// profiles.wordpress.org/optiuser/8 * Author URI: https://optiuser.com/ 8 9 * License: GPLv2 or later 9 10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 12 13 * 13 14 * @package opti-behavior 14 * @copyright 2025 Opti -User15 * @version 1.0. 415 * @copyright 2025 OptiUser 16 * @version 1.0.5 16 17 */ 17 18 … … 79 80 80 81 // Define plugin constants. 81 define( 'OPTI_BEHAVIOR_HEATMAP_VERSION', '1.0. 4' );82 define( 'OPTI_BEHAVIOR_HEATMAP_VERSION', '1.0.5' ); 82 83 define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 83 84 define( 'OPTI_BEHAVIOR_HEATMAP_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); -
opti-behavior/trunk/admin/class-opti-behavior-heatmap-ajax-handler.php
r3399182 r3401441 81 81 add_action( 'wp_ajax_optibehavior_top_users', array( $this, 'ajax_top_users' ) ); 82 82 add_action( 'wp_ajax_optibehavior_backfill_pageview_times', array( $this, 'ajax_backfill_pageview_times' ) ); 83 84 // Support form email handler 85 add_action( 'wp_ajax_opti_behavior_send_support_email', array( $this, 'ajax_send_support_email' ) ); 83 86 } 84 87 … … 1142 1145 1143 1146 if ( is_wp_error( $response ) ) { 1144 // API request failed, return unknown 1147 // API request failed, try browser language fallback 1148 $browser_geo = $this->get_country_from_browser_language(); 1149 if ( ! empty( $browser_geo['country'] ) && $browser_geo['country'] !== 'UN' ) { 1150 $result = array( 1151 'country' => $browser_geo['country'], 1152 'country_name' => $browser_geo['country_name'], 1153 'region' => 'Unknown', 1154 'city' => 'Unknown', 1155 'timezone' => '', 1156 'source' => 'browser_language_fallback', 1157 ); 1158 // Cache for shorter time on fallback (15 minutes) 1159 set_transient( $cache_key, $result, 15 * MINUTE_IN_SECONDS ); 1160 return $result; 1161 } 1162 1163 // Last resort: return unknown 1145 1164 $result = array( 1146 1165 'country' => 'UN', … … 1174 1193 } 1175 1194 1176 // API returned error or invalid data 1195 // API returned error or invalid data, try browser language fallback 1196 $browser_geo = $this->get_country_from_browser_language(); 1197 if ( ! empty( $browser_geo['country'] ) && $browser_geo['country'] !== 'UN' ) { 1198 $result = array( 1199 'country' => $browser_geo['country'], 1200 'country_name' => $browser_geo['country_name'], 1201 'region' => 'Unknown', 1202 'city' => 'Unknown', 1203 'timezone' => '', 1204 'source' => 'browser_language_fallback', 1205 ); 1206 // Cache for shorter time on fallback (15 minutes) 1207 set_transient( $cache_key, $result, 15 * MINUTE_IN_SECONDS ); 1208 return $result; 1209 } 1210 1211 // Last resort: return unknown 1177 1212 $result = array( 1178 1213 'country' => 'UN', … … 2161 2196 } 2162 2197 2198 /** 2199 * AJAX handler for sending support emails. 2200 * 2201 * @since 1.0.4 2202 */ 2203 public function ajax_send_support_email() { 2204 // Verify nonce 2205 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'opti_behavior_support_email' ) ) { 2206 wp_send_json_error( array( 'message' => __( 'Security check failed', 'opti-behavior' ) ) ); 2207 } 2208 2209 // Verify user is logged in 2210 if ( ! is_user_logged_in() ) { 2211 wp_send_json_error( array( 'message' => __( 'You must be logged in to send a message', 'opti-behavior' ) ) ); 2212 } 2213 2214 // Sanitize and validate inputs 2215 $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; 2216 $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; 2217 $subject = isset( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : ''; 2218 $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : ''; 2219 2220 // Validate required fields 2221 if ( empty( $name ) || empty( $email ) || empty( $subject ) || empty( $message ) ) { 2222 wp_send_json_error( array( 'message' => __( 'Please fill in all fields', 'opti-behavior' ) ) ); 2223 } 2224 2225 // Validate email 2226 if ( ! is_email( $email ) ) { 2227 wp_send_json_error( array( 'message' => __( 'Please enter a valid email address', 'opti-behavior' ) ) ); 2228 } 2229 2230 // Prepare email 2231 $to = '[email protected]'; 2232 $headers = array( 2233 'Content-Type: text/html; charset=UTF-8', 2234 'From: ' . $name . ' <' . $email . '>', 2235 'Reply-To: ' . $email, 2236 ); 2237 2238 // Build email body 2239 $email_body = '<html><body>'; 2240 $email_body .= '<h2>' . esc_html__( 'New Support Message from Opti-Behavior Plugin', 'opti-behavior' ) . '</h2>'; 2241 $email_body .= '<p><strong>' . esc_html__( 'Name:', 'opti-behavior' ) . '</strong> ' . esc_html( $name ) . '</p>'; 2242 $email_body .= '<p><strong>' . esc_html__( 'Email:', 'opti-behavior' ) . '</strong> ' . esc_html( $email ) . '</p>'; 2243 $email_body .= '<p><strong>' . esc_html__( 'Subject:', 'opti-behavior' ) . '</strong> ' . esc_html( $subject ) . '</p>'; 2244 $email_body .= '<p><strong>' . esc_html__( 'Message:', 'opti-behavior' ) . '</strong></p>'; 2245 $email_body .= '<p>' . nl2br( esc_html( $message ) ) . '</p>'; 2246 $email_body .= '<hr>'; 2247 $email_body .= '<p><small>' . esc_html__( 'Sent from:', 'opti-behavior' ) . ' ' . esc_url( get_site_url() ) . '</small></p>'; 2248 $email_body .= '</body></html>'; 2249 2250 // Send email 2251 $sent = wp_mail( $to, '[Opti-Behavior Support] ' . $subject, $email_body, $headers ); 2252 2253 if ( $sent ) { 2254 wp_send_json_success( array( 2255 'message' => __( 'Your message has been sent successfully! We\'ll get back to you soon.', 'opti-behavior' ), 2256 ) ); 2257 } else { 2258 // If wp_mail fails, provide fallback message 2259 wp_send_json_error( array( 2260 'message' => sprintf( 2261 /* translators: %s: support email address */ 2262 __( 'We couldn\'t send your message automatically. Please email us directly at %s', 'opti-behavior' ), 2263 '<a href="mailto:[email protected]">[email protected]</a>' 2264 ), 2265 ) ); 2266 } 2267 } 2268 2163 2269 } -
opti-behavior/trunk/admin/class-opti-behavior-heatmap-dashboard.php
r3399182 r3401441 908 908 } 909 909 910 // Heatmaps growth (new pages with events this week vs last week) 910 // Heatmaps growth (this week vs all time) 911 // Compare: How many NEW pages got heatmaps this week 911 912 $this_week_heatmaps = $wpdb->get_var( $wpdb->prepare( 912 913 "SELECT COUNT(DISTINCT page_id2) … … 917 918 )); 918 919 919 $last_week_heatmaps = $wpdb->get_var( $wpdb->prepare( 920 "SELECT COUNT(DISTINCT page_id2) 921 FROM {$wpdb->prefix}optibehavior_events 922 WHERE event IN (16, 17, 32, 33, 48, 49) 923 AND insert_at BETWEEN %s AND %s", 924 $two_weeks_ago, 925 $week_ago 926 )); 927 928 $heatmaps_growth = $last_week_heatmaps > 0 ? 929 round((($this_week_heatmaps - $last_week_heatmaps) / $last_week_heatmaps) * 100) : 0; 920 // Calculate: (this week count / all-time count) * 100 921 // Shows what % of all-time heatmaps were created this week 922 // Example: 5 this week / 13 all-time = 38% (meaning 38% of all heatmaps are from this week) 923 $heatmaps_growth = $total_heatmaps > 0 ? 924 round(($this_week_heatmaps / $total_heatmaps) * 100) : 0; 930 925 931 926 // Total clicks (click events only) … … 943 938 } 944 939 945 // Clicks growth (this week vs last week)940 // Clicks growth (this week vs all time) 946 941 $this_week_clicks = $wpdb->get_var( $wpdb->prepare( 947 942 "SELECT COUNT(*) … … 952 947 )); 953 948 954 $last_week_clicks = $wpdb->get_var( $wpdb->prepare( 955 "SELECT COUNT(*) 949 // Calculate: (this week clicks / all-time clicks) * 100 950 // Shows what % of all-time clicks happened this week 951 // Example: 50 this week / 200 all-time = 25% (meaning 25% of all clicks are from this week) 952 $clicks_growth = $total_clicks > 0 ? 953 round(($this_week_clicks / $total_clicks) * 100) : 0; 954 955 // Mobile percentage - Count PAGES with mobile heatmaps vs total pages (not individual events) 956 // This should match the data shown in the heatmap table 957 958 // Check storage mode to use the same data source as the table 959 $file_storage = $this->heatmap->get_file_storage(); 960 $storage_settings = $file_storage ? $file_storage->get_settings() : array( 'storage_mode' => 'database' ); 961 962 if ( $storage_settings['storage_mode'] === 'file' && $file_storage ) { 963 // File storage mode - get page counts from file storage 964 $pages_data = $file_storage->get_pages_with_heatmap_data(); 965 $total_pages = count( $pages_data ); 966 $mobile_pages = 0; 967 968 foreach ( $pages_data as $page_data ) { 969 $mobile_interactions = ( $page_data['click_mobile'] + $page_data['breakaway_mobile'] + $page_data['attention_mobile'] ); 970 $total_interactions = ( $page_data['click_pc'] + $page_data['click_mobile'] + $page_data['breakaway_pc'] + $page_data['breakaway_mobile'] + $page_data['attention_pc'] + $page_data['attention_mobile'] ); 971 972 // Count this page as "mobile" if it has any mobile interactions 973 if ( $mobile_interactions > 0 ) { 974 $mobile_pages++; 975 } 976 } 977 978 $mobile_percentage = $total_pages > 0 ? round( ( $mobile_pages / $total_pages ) * 100 ) : 0; 979 } else { 980 // Database mode - count pages with mobile heatmap data 981 // A page has "mobile heatmap" if it has any mobile events (17, 33, 49) 982 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix 983 $mobile_pages = $wpdb->get_var( 984 "SELECT COUNT(DISTINCT page_id2) 985 FROM {$wpdb->prefix}optibehavior_events 986 WHERE event IN (17, 33, 49)" 987 ); 988 989 // Total pages with any heatmap events 990 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix 991 $total_pages = $wpdb->get_var( 992 "SELECT COUNT(DISTINCT page_id2) 993 FROM {$wpdb->prefix}optibehavior_events 994 WHERE event IN (16, 17, 32, 33, 48, 49)" 995 ); 996 997 $mobile_percentage = $total_pages > 0 ? round( ( $mobile_pages / $total_pages ) * 100 ) : 0; 998 } 999 1000 // Mobile change - compare THIS WEEK vs ALL TIME mobile page percentage 1001 // This shows how this week's mobile traffic compares to the overall average 1002 // Calculate this week's mobile percentage 1003 $this_week_mobile_pages = $wpdb->get_var( $wpdb->prepare( 1004 "SELECT COUNT(DISTINCT page_id2) 956 1005 FROM {$wpdb->prefix}optibehavior_events 957 WHERE event IN (16, 17) 958 AND insert_at BETWEEN %s AND %s", 959 $two_weeks_ago, 960 $week_ago 961 )); 962 963 $clicks_growth = $last_week_clicks > 0 ? 964 round((($this_week_clicks - $last_week_clicks) / $last_week_clicks) * 100) : 0; 965 966 // Mobile percentage (event 17 = mobile clicks, event 16 = PC clicks) 967 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, event number is hardcoded integer 968 $mobile_clicks = $wpdb->get_var( 969 "SELECT COUNT(*) 970 FROM {$wpdb->prefix}optibehavior_events 971 WHERE event = 17" 972 ); 973 974 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, event number is hardcoded integer 975 $pc_clicks = $wpdb->get_var( 976 "SELECT COUNT(*) 977 FROM {$wpdb->prefix}optibehavior_events 978 WHERE event = 16" 979 ); 980 981 $total_device_clicks = $mobile_clicks + $pc_clicks; 982 $mobile_percentage = $total_device_clicks > 0 ? 983 round(($mobile_clicks / $total_device_clicks) * 100) : 0; 984 985 // Mobile change (this week vs last week) 986 $this_week_mobile = $wpdb->get_var( $wpdb->prepare( 987 "SELECT COUNT(*) 988 FROM {$wpdb->prefix}optibehavior_events 989 WHERE event = 17 1006 WHERE event IN (17, 33, 49) 990 1007 AND insert_at >= %s", 991 1008 $week_ago 992 1009 )); 993 1010 994 $this_week_total = $wpdb->get_var( $wpdb->prepare(995 "SELECT COUNT( *)1011 $this_week_total_pages = $wpdb->get_var( $wpdb->prepare( 1012 "SELECT COUNT(DISTINCT page_id2) 996 1013 FROM {$wpdb->prefix}optibehavior_events 997 WHERE event IN (16, 17 )1014 WHERE event IN (16, 17, 32, 33, 48, 49) 998 1015 AND insert_at >= %s", 999 1016 $week_ago 1000 1017 )); 1001 1018 1002 $this_week_mobile_pct = $this_week_total > 0 ? 1003 round(($this_week_mobile / $this_week_total) * 100) : 0; 1004 1019 $this_week_mobile_pct = $this_week_total_pages > 0 ? round( ( $this_week_mobile_pages / $this_week_total_pages ) * 100 ) : 0; 1020 1021 // Calculate the change: this week's mobile % compared to all-time mobile % 1022 // Positive means this week has MORE mobile traffic than overall average 1023 // Negative means this week has LESS mobile traffic than overall average 1005 1024 $mobile_change = $this_week_mobile_pct - $mobile_percentage; 1006 1025 … … 1014 1033 $avg_time = $avg_time ? intval($avg_time) : 0; 1015 1034 1016 // Time improvement (this week vs last week) 1017 // Calculate total duration / total page views for each period 1035 // Time improvement (this week vs all time average) 1018 1036 $this_week_avg_time = $wpdb->get_var( $wpdb->prepare( 1019 1037 "SELECT ROUND(SUM(duration) / SUM(page_views)) … … 1024 1042 )); 1025 1043 1026 $last_week_avg_time = $wpdb->get_var( $wpdb->prepare( 1027 "SELECT ROUND(SUM(duration) / SUM(page_views)) 1028 FROM {$wpdb->prefix}optibehavior_sessions 1029 WHERE duration > 0 AND page_views > 0 1030 AND start_time BETWEEN %s AND %s", 1031 $two_weeks_ago, 1032 $week_ago 1033 )); 1034 1035 $time_improvement = ($last_week_avg_time > 0 && $this_week_avg_time > 0) ? 1036 round((($this_week_avg_time - $last_week_avg_time) / $last_week_avg_time) * 100) : 0; 1044 // Compare this week's avg time to all-time avg time 1045 $time_improvement = ($avg_time > 0 && $this_week_avg_time > 0) ? 1046 round((($this_week_avg_time - $avg_time) / $avg_time) * 100) : 0; 1037 1047 1038 1048 // Hottest page (page with most clicks) … … 1050 1060 $hottest_page_name = $hottest_page ? $hottest_page->title : 'No data'; 1051 1061 1052 // Conversion rate (pages with clicks vs total pageviews) 1053 $pages_with_clicks = $wpdb->get_var( 1054 "SELECT COUNT(DISTINCT page_id2) 1062 // Click-through Rate (CTR) - all time 1063 // CTR = (total clicks / total pageviews) * 100 1064 // This shows what % of pageviews resulted in a click 1065 $total_ctr_clicks = $wpdb->get_var( 1066 "SELECT COUNT(*) 1055 1067 FROM {$wpdb->prefix}optibehavior_events 1056 1068 WHERE event IN (16, 17)" … … 1063 1075 1064 1076 $conversion_rate = $total_pageviews > 0 ? 1065 round(($ pages_with_clicks / $total_pageviews) * 100, 1) : 0;1066 1067 // CTR improvement (this month vs last month)1068 $this_ month_clicks = $wpdb->get_var( $wpdb->prepare(1077 round(($total_ctr_clicks / $total_pageviews) * 100, 1) : 0; 1078 1079 // CTR improvement (this week vs all time) 1080 $this_week_ctr_clicks = $wpdb->get_var( $wpdb->prepare( 1069 1081 "SELECT COUNT(*) 1070 1082 FROM {$wpdb->prefix}optibehavior_events 1071 1083 WHERE event IN (16, 17) 1072 1084 AND insert_at >= %s", 1073 $ month_ago1085 $week_ago 1074 1086 )); 1075 1087 1076 $this_ month_views = $wpdb->get_var( $wpdb->prepare(1088 $this_week_views = $wpdb->get_var( $wpdb->prepare( 1077 1089 "SELECT COUNT(*) 1078 1090 FROM {$wpdb->prefix}optibehavior_pageviews 1079 1091 WHERE view_time >= %s", 1080 $ month_ago1092 $week_ago 1081 1093 )); 1082 1094 1083 $this_month_ctr = $this_month_views > 0 ? 1084 ($this_month_clicks / $this_month_views) * 100 : 0; 1085 1095 $this_week_ctr = $this_week_views > 0 ? 1096 round(($this_week_ctr_clicks / $this_week_views) * 100, 1) : 0; 1097 1098 // Compare this week's CTR to all-time CTR 1086 1099 $ctr_improvement = $conversion_rate > 0 ? 1087 round( $this_month_ctr - $conversion_rate) : 0;1100 round((($this_week_ctr - $conversion_rate) / $conversion_rate) * 100) : 0; 1088 1101 1089 1102 return array( … … 1365 1378 $allowed_map = array( 1366 1379 'interactions' => 'interactions', 1367 'sessions' => ' sessions',1380 'sessions' => 'COALESCE(s.sessions,0)', 1368 1381 'last_updated' => 'last_event_time' 1369 1382 ); … … 1491 1504 // else branch begins 1492 1505 $pages_table = $wpdb->prefix . 'optibehavior_pages'; 1506 $events_table = $wpdb->prefix . 'optibehavior_events'; 1493 1507 $pageviews_table = $wpdb->prefix . 'optibehavior_pageviews'; 1494 1508 // Aggregated current slice with sessions via subquery (leverages url index) 1495 1509 $date_filter_events = ($start_date && $end_date) ? " AND e.insert_at BETWEEN %s AND %s" : ''; 1496 1510 $date_filter_pv = ($start_date && $end_date) ? " WHERE pv.view_time BETWEEN %s AND %s" : ''; 1497 $select_sessions = ($order_by_sql === 'sessions') ? ", COALESCE(s.sessions,0) AS sessions" : ""; 1498 $join_sessions = ($order_by_sql === 'sessions') ? " LEFT JOIN (SELECT pv.page_id, COUNT(DISTINCT pv.session_id) AS sessions FROM {$pageviews_table} pv{$date_filter_pv} GROUP BY pv.page_id) s ON s.page_id = p.id" : ""; 1511 // Always include sessions in the query, not just when ordering by sessions 1512 $select_sessions = ", COALESCE(s.sessions,0) AS sessions"; 1513 $join_sessions = " LEFT JOIN (SELECT pv.page_id, COUNT(DISTINCT pv.session_id) AS sessions FROM {$pageviews_table} pv{$date_filter_pv} GROUP BY pv.page_id) s ON s.page_id = p.id"; 1499 1514 $sql = 1500 1515 "SELECT p.id, p.title, p.url, … … 1515 1530 $params = array(); 1516 1531 if ($start_date && $end_date) { $params[] = $start_date; $params[] = $end_date; } 1517 if ($order_by_sql === 'sessions' && $start_date && $end_date) { $params[] = $start_date; $params[] = $end_date; } 1532 // Always add date params for sessions join when date filter is active (since we always join now) 1533 if ($start_date && $end_date) { $params[] = $start_date; $params[] = $end_date; } 1518 1534 $params[] = $per_page; $params[] = $offset; 1519 1535 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names are prefixed safely, ORDER BY/direction are validated, all user inputs properly parameterized … … 1543 1559 'url' => $row->url, 1544 1560 'interactions' => (int)$total_interactions, 1545 'sessions' => 0, // placeholder; lazy update1561 'sessions' => isset($row->sessions) ? (int)$row->sessions : 0, 1546 1562 'mobile_percentage' => (int)$mobile_percentage, 1547 1563 'last_updated' => $last_updated, … … 1604 1620 } 1605 1621 1622 // If sorting by sessions, we need to fetch sessions data first 1623 $sessions_map_for_sort = array(); 1624 if ( $orderby === 'sessions' ) { 1625 // Get all page IDs 1626 $all_page_ids = array_map( function( $page ) { 1627 return $page['page_id']; 1628 }, $pages_data ); 1629 1630 // Fetch sessions for all pages before sorting 1631 $sessions_map_for_sort = $this->batch_sessions_counts_by_page( $all_page_ids, $start_date, $end_date ); 1632 1633 // Add sessions data to pages_data array 1634 foreach ( $pages_data as &$page_data ) { 1635 $page_data['sessions'] = isset( $sessions_map_for_sort[ $page_data['page_id'] ] ) ? (int)$sessions_map_for_sort[ $page_data['page_id'] ] : 0; 1636 } 1637 unset( $page_data ); // Break reference 1638 } 1639 1606 1640 // Sort based on orderby parameter 1607 1641 $order_lower = strtolower( $order ); … … 1613 1647 1614 1648 switch ( $orderby ) { 1615 case 'clicks': 1649 case 'interactions': 1650 // Sum all interaction types 1651 $val_a = ( $a['click_pc'] + $a['click_mobile'] + $a['breakaway_pc'] + $a['breakaway_mobile'] + $a['attention_pc'] + $a['attention_mobile'] ); 1652 $val_b = ( $b['click_pc'] + $b['click_mobile'] + $b['breakaway_pc'] + $b['breakaway_mobile'] + $b['attention_pc'] + $b['attention_mobile'] ); 1653 break; 1654 case 'sessions': 1655 $val_a = isset( $a['sessions'] ) ? (int)$a['sessions'] : 0; 1656 $val_b = isset( $b['sessions'] ) ? (int)$b['sessions'] : 0; 1657 break; 1658 case 'clicks': 1616 1659 $val_a = ( $a['click_pc'] + $a['click_mobile'] ); 1617 1660 $val_b = ( $b['click_pc'] + $b['click_mobile'] ); … … 1795 1838 1796 1839 $rows[] = array( 1797 'page_id' => $page_id, 1840 'id' => $page_id, // Use 'id' to match template expectations 1841 'page_id' => $page_id, 1798 1842 'title' => $title, 1799 1843 'url' => $url, … … 1801 1845 'type_icon' => $type_icon, 1802 1846 'clicks' => $total_clicks, 1803 'interactions' => $total_clicks , // Total interactions (same as clicks for now)1847 'interactions' => $total_clicks + $total_breakaway + $total_attention, // Sum ALL interaction types 1804 1848 'sessions' => $session_count, 1805 1849 'status' => $status, … … 1888 1932 $two_weeks_ago = gmdate('Y-m-d', strtotime('-14 days')); 1889 1933 1890 // Top performing pages by clicks 1934 // Top performing pages by pageviews (more reliable than clicks) 1935 // Try pageviews first 1891 1936 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query with prepare 1892 1937 $top_pages_query = "SELECT 1893 p.title, 1894 p.url, 1895 COUNT(*) as clicks, 1896 COUNT(CASE WHEN e.insert_at >= %s THEN 1 END) as recent_clicks, 1897 COUNT(CASE WHEN e.insert_at BETWEEN %s AND %s THEN 1 END) as previous_clicks 1898 FROM {$wpdb->prefix}optibehavior_events e 1899 LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2 = p.id 1900 WHERE e.event IN (16, 17) 1901 GROUP BY e.page_id2 1902 ORDER BY clicks DESC 1938 COALESCE(NULLIF(pv.title, ''), 'Untitled Page') as title, 1939 pv.url, 1940 COUNT(DISTINCT pv.session_id) as views, 1941 COUNT(DISTINCT CASE WHEN pv.view_time >= %s THEN pv.session_id END) as recent_views, 1942 COUNT(DISTINCT CASE WHEN pv.view_time BETWEEN %s AND %s THEN pv.session_id END) as previous_views 1943 FROM {$wpdb->prefix}optibehavior_pageviews pv 1944 WHERE pv.view_time IS NOT NULL AND pv.url IS NOT NULL AND pv.url != '' 1945 GROUP BY pv.url 1946 ORDER BY views DESC 1903 1947 LIMIT 5"; 1904 1948 … … 1911 1955 )); 1912 1956 1957 // If no pageviews data, fallback to events data 1958 if ( empty( $top_pages_results ) ) { 1959 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query 1960 $top_pages_query_fallback = "SELECT 1961 COALESCE(NULLIF(p.title, ''), 'Untitled Page') as title, 1962 p.url, 1963 COUNT(*) as views, 1964 0 as recent_views, 1965 0 as previous_views 1966 FROM {$wpdb->prefix}optibehavior_events e 1967 JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2 = p.id 1968 WHERE p.url IS NOT NULL AND p.url != '' 1969 GROUP BY p.url 1970 ORDER BY views DESC 1971 LIMIT 5"; 1972 1973 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query 1974 $top_pages_results = $wpdb->get_results( $top_pages_query_fallback ); 1975 } 1976 1913 1977 $top_pages = array(); 1914 1978 foreach ($top_pages_results as $page) { … … 1918 1982 $change = 0; 1919 1983 1920 if ($page->previous_ clicks > 0) {1921 $change = round((($page->recent_ clicks - $page->previous_clicks) / $page->previous_clicks) * 100);1984 if ($page->previous_views > 0) { 1985 $change = round((($page->recent_views - $page->previous_views) / $page->previous_views) * 100); 1922 1986 if ($change > 0) { 1923 1987 $trend = 'positive'; … … 1927 1991 $trend_icon = '📉'; 1928 1992 } 1929 } elseif ($page->recent_ clicks > 0) {1993 } elseif ($page->recent_views > 0) { 1930 1994 $trend = 'positive'; 1931 1995 $trend_icon = '📈'; … … 1941 2005 'title' => $title, 1942 2006 'url' => $page->url, 1943 'clicks' => intval($page-> clicks),2007 'clicks' => intval($page->views), 1944 2008 'trend' => $trend, 1945 2009 'trend_icon' => $trend_icon, … … 2508 2572 ); 2509 2573 2510 // If no events data, fall back to session pages(page views).2574 // If no events data, fall back to pageviews table (page views). 2511 2575 if ( empty( $results ) ) { 2512 // Get top pages from session_pages table (page views).2576 // Get top pages from pageviews table (page views). 2513 2577 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2514 2578 $results = $wpdb->get_results( 2515 2579 $wpdb->prepare( 2516 2580 "SELECT p.id as page_id, p.url, p.title, COUNT(*) as interactions 2517 FROM {$wpdb->prefix}optibehavior_ session_pages sp2518 LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON sp.page_id = p.id2519 WHERE sp.visited_atBETWEEN %s AND %s2581 FROM {$wpdb->prefix}optibehavior_pageviews pv 2582 LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON pv.page_id = p.id 2583 WHERE pv.view_time BETWEEN %s AND %s 2520 2584 GROUP BY p.id 2521 2585 ORDER BY interactions DESC … … 2531 2595 $results = $wpdb->get_results( 2532 2596 "SELECT p.id as page_id, p.url, p.title, COUNT(*) as interactions 2533 FROM {$wpdb->prefix}optibehavior_ session_pages sp2534 LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON sp.page_id = p.id2597 FROM {$wpdb->prefix}optibehavior_pageviews pv 2598 LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON pv.page_id = p.id 2535 2599 GROUP BY p.id 2536 2600 ORDER BY interactions DESC … … 2540 2604 } 2541 2605 2606 // Final fallback: try events table without date filter if we still have no results. 2607 // Use all events, not just specific types, to ensure we capture all data. 2608 if ( empty( $results ) ) { 2609 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2610 $results = $wpdb->get_results( 2611 "SELECT p.id as page_id, p.url, p.title, COUNT(*) as interactions 2612 FROM {$wpdb->prefix}optibehavior_events e 2613 LEFT JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2 = p.id 2614 WHERE p.id IS NOT NULL 2615 GROUP BY p.id 2616 ORDER BY interactions DESC 2617 LIMIT 5" 2618 ); 2619 } 2620 2542 2621 if ( empty( $results ) ) { 2543 2622 return array(); … … 2568 2647 2569 2648 // Get click counts (events 16,17) for current and previous periods for the same pages. 2649 // Try page_id2 first, then fall back to page_id for compatibility 2570 2650 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 2571 2651 $sql = " 2572 SELECT e.page_id2 AS page_id, 2573 SUM( CASE WHEN e.insert_at BETWEEN %s AND %s THEN 1 ELSE 0 END ) AS current_clicks, 2574 SUM( CASE WHEN e.insert_at BETWEEN %s AND %s THEN 1 ELSE 0 END ) AS previous_clicks 2652 SELECT COALESCE(e.page_id2, e.page_id) AS page_id, 2653 COUNT(*) AS current_clicks 2575 2654 FROM {$wpdb->prefix}optibehavior_events e 2576 2655 WHERE e.event IN (16,17) 2577 AND e.page_id2 IN ( $placeholders)2578 GROUP BY e.page_id22656 AND (e.page_id2 IN ( $placeholders ) OR e.page_id IN ( $placeholders )) 2657 GROUP BY COALESCE(e.page_id2, e.page_id) 2579 2658 "; 2580 2659 2581 $params = array( $start_date, $end_date, $prev_start, $prev_end );2582 $params = array_merge( $pa rams, $page_ids );2660 // No date params needed for all-time click count 2661 $params = array_merge( $page_ids, $page_ids ); 2583 2662 2584 2663 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared … … 2587 2666 $page_id = intval( $row->page_id ); 2588 2667 $current = intval( $row->current_clicks ); 2589 $previous = intval( $row->previous_clicks );2668 $previous = 0; // No previous period comparison for all-time clicks 2590 2669 $click_stats[ $page_id ] = array( 2591 2670 'current' => $current, … … 2596 2675 2597 2676 // Get interactions for previous period for trend on heatmap views. 2677 // Try page_id2 first, then fall back to page_id for compatibility 2598 2678 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 2599 2679 $sql_prev = " 2600 SELECT e.page_id2AS page_id,2680 SELECT COALESCE(e.page_id2, e.page_id) AS page_id, 2601 2681 COUNT(*) AS previous_views 2602 2682 FROM {$wpdb->prefix}optibehavior_events e 2603 2683 WHERE e.insert_at BETWEEN %s AND %s 2604 2684 AND e.event IN (16,17,32,33,48,49) 2605 AND e.page_id2 IN ( $placeholders)2606 GROUP BY e.page_id22685 AND (e.page_id2 IN ( $placeholders ) OR e.page_id IN ( $placeholders )) 2686 GROUP BY COALESCE(e.page_id2, e.page_id) 2607 2687 "; 2608 2688 2609 2689 $params_prev = array( $prev_start, $prev_end ); 2610 $params_prev = array_merge( $params_prev, $page_ids );2690 $params_prev = array_merge( $params_prev, $page_ids, $page_ids ); 2611 2691 2612 2692 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -
opti-behavior/trunk/admin/class-opti-behavior-heatmap-post-metabox.php
r3399182 r3401441 54 54 add_action( 'wp_ajax_optibehavior_post_analytics', array( $this, 'ajax_post_analytics' ) ); 55 55 add_action( 'wp_ajax_optibehavior_post_timeseries', array( $this, 'ajax_post_timeseries' ) ); 56 add_action( 'wp_ajax_optibehavior_post_analytics_chart', array( $this, 'ajax_post_analytics_chart' ) ); 56 57 add_action( 'wp_ajax_optibehavior_post_referrers', array( $this, 'ajax_post_referrers' ) ); 57 58 add_action( 'wp_ajax_optibehavior_post_outbound_clicks', array( $this, 'ajax_post_outbound_clicks' ) ); 59 add_action( 'wp_ajax_optibehavior_post_countries', array( $this, 'ajax_post_countries' ) ); 60 add_action( 'wp_ajax_optibehavior_post_browsers', array( $this, 'ajax_post_browsers' ) ); 61 add_action( 'wp_ajax_optibehavior_post_devices', array( $this, 'ajax_post_devices' ) ); 58 62 } 59 63 … … 70 74 add_meta_box( 71 75 'optibehavior_editor_analytics', 72 '<i data-lucide="flame" style="width:16px;height:16px;vertical-align:middle;margin-right:4px;"></i>' . __( ' HeatmapAnalytics', 'opti-behavior' ),76 '<i data-lucide="flame" style="width:16px;height:16px;vertical-align:middle;margin-right:4px;"></i>' . __( 'Post Analytics', 'opti-behavior' ), 73 77 array( $this, 'render_editor_analytics_box' ), 74 78 array( 'post', 'page' ), … … 206 210 <div class="optibehavior-behavior-stats"> 207 211 <div class="optibehavior-behavior-stat"> 208 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Avg Time on Page', 'opti-behavior' ); ?></div> 209 <div class="optibehavior-behavior-value" id="optibehavior-avg">—</div> 212 <div class="optibehavior-behavior-label"> 213 <i data-lucide="clock" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i> 214 <?php echo esc_html__( 'Avg Time on Page', 'opti-behavior' ); ?> 215 </div> 216 <div class="optibehavior-behavior-value"> 217 <span class="optibehavior-stat-badge" id="optibehavior-avg">—</span> 218 </div> 210 219 </div> 211 220 <div class="optibehavior-behavior-stat"> 212 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Total Interactions', 'opti-behavior' ); ?></div> 213 <div class="optibehavior-behavior-value" id="optibehavior-int">—</div> 221 <div class="optibehavior-behavior-label"> 222 <i data-lucide="mouse-pointer-click" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i> 223 <?php echo esc_html__( 'Total Interactions', 'opti-behavior' ); ?> 224 </div> 225 <div class="optibehavior-behavior-value"> 226 <span class="optibehavior-stat-badge" id="optibehavior-int">—</span> 227 </div> 214 228 </div> 215 229 <div class="optibehavior-behavior-stat"> 216 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Device Split', 'opti-behavior' ); ?></div> 230 <div class="optibehavior-behavior-label"> 231 <i data-lucide="arrow-down" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i> 232 <?php echo esc_html__( 'Avg. Scroll Depth', 'opti-behavior' ); ?> 233 </div> 234 <div class="optibehavior-behavior-value"> 235 <span class="optibehavior-stat-badge" id="optibehavior-scroll">—</span> 236 </div> 237 </div> 238 <div class="optibehavior-behavior-stat"> 239 <div class="optibehavior-behavior-label"> 240 <i data-lucide="log-out" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i> 241 <?php echo esc_html__( 'Bounce Rate', 'opti-behavior' ); ?> 242 </div> 243 <div class="optibehavior-behavior-value"> 244 <span class="optibehavior-stat-badge" id="optibehavior-bounce">—</span> 245 </div> 246 </div> 247 <div class="optibehavior-behavior-stat"> 248 <div class="optibehavior-behavior-label"> 249 <i data-lucide="monitor-smartphone" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i> 250 <?php echo esc_html__( 'Device Split', 'opti-behavior' ); ?> 251 </div> 217 252 <div class="optibehavior-behavior-value" id="optibehavior-split">—</div> 218 253 </div> 219 254 <div class="optibehavior-behavior-stat"> 220 <div class="optibehavior-behavior-label"><?php echo esc_html__( 'Last Updated', 'opti-behavior' ); ?></div> 255 <div class="optibehavior-behavior-label"> 256 <i data-lucide="refresh-cw" style="width:14px;height:14px;margin-right:4px;vertical-align:middle;"></i> 257 <?php echo esc_html__( 'Last Updated', 'opti-behavior' ); ?> 258 </div> 221 259 <div class="optibehavior-behavior-value" id="optibehavior-upd">—</div> 222 260 </div> … … 242 280 </div> 243 281 282 <!-- New Analytics Row: Countries, Browsers, Device Types --> 283 <div class="optibehavior-analytics-grid optibehavior-secondary-grid"> 284 <!-- Countries Column --> 285 <div class="optibehavior-analytics-column optibehavior-countries-column"> 286 <div class="optibehavior-column-header"> 287 <div class="optibehavior-column-icon"><i data-lucide="globe" style="width:18px;height:18px;"></i></div> 288 <h3 class="optibehavior-column-title"><?php echo esc_html__( 'Countries', 'opti-behavior' ); ?></h3> 289 </div> 290 <div class="optibehavior-column-content"> 291 <div id="optibehavior-countries-content"> 292 <div id="optibehavior-countries-table" class="optibehavior-scroll-area"></div> 293 <div id="optibehavior-countries-empty" class="optibehavior-empty-state"> 294 <i data-lucide="globe" style="width: 48px; height: 48px; color: #9ca3af; stroke-width: 1.5;"></i> 295 <div class="optibehavior-empty-title"><?php echo esc_html__( 'No country data available', 'opti-behavior' ); ?></div> 296 <div class="optibehavior-empty-sub"><?php echo esc_html__( 'Data will appear once visitors access this page.', 'opti-behavior' ); ?></div> 297 </div> 298 </div> 299 </div> 300 </div> 301 302 <!-- Browsers Column --> 303 <div class="optibehavior-analytics-column optibehavior-browsers-column"> 304 <div class="optibehavior-column-header"> 305 <div class="optibehavior-column-icon"><i data-lucide="compass" style="width:18px;height:18px;"></i></div> 306 <h3 class="optibehavior-column-title"><?php echo esc_html__( 'Browsers', 'opti-behavior' ); ?></h3> 307 </div> 308 <div class="optibehavior-column-content"> 309 <div id="optibehavior-browsers-content"> 310 <div id="optibehavior-browsers-table" class="optibehavior-scroll-area"></div> 311 <div id="optibehavior-browsers-empty" class="optibehavior-empty-state"> 312 <i data-lucide="compass" style="width: 48px; height: 48px; color: #9ca3af; stroke-width: 1.5;"></i> 313 <div class="optibehavior-empty-title"><?php echo esc_html__( 'No browser data available', 'opti-behavior' ); ?></div> 314 <div class="optibehavior-empty-sub"><?php echo esc_html__( 'Data will appear once visitors access this page.', 'opti-behavior' ); ?></div> 315 </div> 316 </div> 317 </div> 318 </div> 319 320 <!-- Device Types Column --> 321 <div class="optibehavior-analytics-column optibehavior-devices-column"> 322 <div class="optibehavior-column-header"> 323 <div class="optibehavior-column-icon"><i data-lucide="monitor-smartphone" style="width:18px;height:18px;"></i></div> 324 <h3 class="optibehavior-column-title"><?php echo esc_html__( 'Device Types', 'opti-behavior' ); ?></h3> 325 </div> 326 <div class="optibehavior-column-content"> 327 <div id="optibehavior-devices-content"> 328 <div id="optibehavior-devices-table" class="optibehavior-scroll-area"></div> 329 <div id="optibehavior-devices-empty" class="optibehavior-empty-state"> 330 <i data-lucide="monitor-smartphone" style="width: 48px; height: 48px; color: #9ca3af; stroke-width: 1.5;"></i> 331 <div class="optibehavior-empty-title"><?php echo esc_html__( 'No device data available', 'opti-behavior' ); ?></div> 332 <div class="optibehavior-empty-sub"><?php echo esc_html__( 'Data will appear once visitors access this page.', 'opti-behavior' ); ?></div> 333 </div> 334 </div> 335 </div> 336 </div> 337 </div> 338 244 339 <!-- Controls Row --> 245 340 <div class="optibehavior-controls-row"> … … 257 352 </div> 258 353 </div> 259 <canvas id="optibehavior-chart" height="110"></canvas> 260 <div id="optibehavior-empty" class="optibehavior-empty"><?php echo esc_html__( 'No heatmap data yet for this page.', 'opti-behavior' ); ?></div> 354 <div id="optibehavior-chart-loading" class="optibehavior-loading-state" style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; gap: 12px;"> 355 <span class="spinner is-active"></span> 356 <span style="color: #64748b; font-size: 14px;"><?php echo esc_html__( 'Loading analytics data...', 'opti-behavior' ); ?></span> 357 </div> 358 <canvas id="optibehavior-chart" height="280" style="display: none;"></canvas> 359 <div id="optibehavior-empty" class="optibehavior-empty" style="display: none;"><?php echo esc_html__( 'No heatmap data yet for this page.', 'opti-behavior' ); ?></div> 261 360 </div> 262 361 </div> … … 271 370 public function ajax_post_analytics() { 272 371 if ( ! isset( $_POST['post_id'] ) ) { 273 wp_send_json_error( );372 wp_send_json_error( array( 'message' => 'No post_id provided' ) ); 274 373 } 275 374 … … 278 377 279 378 if ( ! current_user_can( 'edit_post', $post_id ) ) { 280 wp_send_json_error( );379 wp_send_json_error( array( 'message' => 'Permission denied' ) ); 281 380 } 282 381 … … 284 383 $url = get_permalink( $post_id ); 285 384 if ( ! $url ) { 286 wp_send_json_error(); 287 } 288 289 // Get PC clicks 290 $pc = intval( $wpdb->get_var( $wpdb->prepare( 385 wp_send_json_error( array( 'message' => 'No permalink found' ) ); 386 } 387 388 // Get page_id for this URL (needed for file storage) 389 $page_id_for_events = $wpdb->get_var( $wpdb->prepare( 390 "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1", 391 $url 392 ) ); 393 394 // If exact match not found, try with trailing slash variations 395 if ( ! $page_id_for_events ) { 396 $url_alt = rtrim( $url, '/' ) . '/'; 397 if ( $url_alt === $url ) { 398 $url_alt = rtrim( $url, '/' ); 399 } 400 $page_id_for_events = $wpdb->get_var( $wpdb->prepare( 401 "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1", 402 $url_alt 403 ) ); 404 } 405 406 // Check storage mode 407 $file_storage = $this->core->get_file_storage(); 408 $storage_settings = $file_storage ? $file_storage->get_settings() : array( 'storage_mode' => 'database' ); 409 $is_file_storage = ( $storage_settings['storage_mode'] === 'file' && $file_storage && $page_id_for_events ); 410 411 // Get PC clicks - use URL-based filtering to ensure all events are counted 412 if ( $is_file_storage && $page_id_for_events ) { 413 $pc = intval( $file_storage->count_events( $page_id_for_events, 16 ) ); 414 } else { 415 $pc = 0; 416 } 417 // Always verify with database query for consistency 418 $pc_db = intval( $wpdb->get_var( $wpdb->prepare( 291 419 "SELECT COUNT(*) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE e.event=16 AND (p.url=%s OR p.url LIKE %s)", 292 420 $url, $url . '%' ) ) ); 293 294 // Get mobile clicks 295 $mb = intval( $wpdb->get_var( $wpdb->prepare( 421 $pc = max( $pc, $pc_db ); 422 423 // Get mobile clicks - use URL-based filtering to ensure all events are counted 424 if ( $is_file_storage && $page_id_for_events ) { 425 $mb = intval( $file_storage->count_events( $page_id_for_events, 17 ) ); 426 } else { 427 $mb = 0; 428 } 429 // Always verify with database query for consistency 430 $mb_db = intval( $wpdb->get_var( $wpdb->prepare( 296 431 "SELECT COUNT(*) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE e.event=17 AND (p.url=%s OR p.url LIKE %s)", 297 432 $url, $url . '%' ) ) ); 433 $mb = max( $mb, $mb_db ); 298 434 299 435 $total = $pc + $mb; 436 // Note: Outbound clicks are already included in PC/MB event counts (events 16/17) 437 // The optibehavior_outbound_clicks table tracks WHERE users clicked (which links) 438 // but the click events themselves are recorded in the events table as event 16 or 17 300 439 301 440 // Get average time on page - try multiple approaches … … 340 479 } 341 480 342 // Get last updated timestamp 343 $last_updated = $wpdb->get_var( $wpdb->prepare( 344 "SELECT MAX(e.insert_at) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE p.url=%s OR p.url LIKE %s", 481 // Get last updated timestamp from multiple sources 482 $last_from_events = null; 483 if ( $is_file_storage ) { 484 // Get latest event from file storage 485 $pc_events = $file_storage->read_events( $page_id_for_events, 16, 1 ); 486 $mb_events = $file_storage->read_events( $page_id_for_events, 17, 1 ); 487 488 $pc_time = ! empty( $pc_events ) && isset( $pc_events[0]['insert_at'] ) ? $pc_events[0]['insert_at'] : null; 489 $mb_time = ! empty( $mb_events ) && isset( $mb_events[0]['insert_at'] ) ? $mb_events[0]['insert_at'] : null; 490 491 if ( $pc_time && $mb_time ) { 492 $last_from_events = ( strtotime( $pc_time ) > strtotime( $mb_time ) ) ? $pc_time : $mb_time; 493 } elseif ( $pc_time ) { 494 $last_from_events = $pc_time; 495 } elseif ( $mb_time ) { 496 $last_from_events = $mb_time; 497 } 498 } else { 499 $last_from_events = $wpdb->get_var( $wpdb->prepare( 500 "SELECT MAX(e.insert_at) FROM {$wpdb->prefix}optibehavior_events e JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id WHERE p.url=%s OR p.url LIKE %s", 501 $url, $url . '%' ) ); 502 } 503 504 $last_from_pageviews = $wpdb->get_var( $wpdb->prepare( 505 "SELECT MAX(view_time) FROM {$wpdb->prefix}optibehavior_pageviews WHERE url=%s OR url LIKE %s", 345 506 $url, $url . '%' ) ); 346 507 508 $last_from_outbound = null; 509 if ( $page_id_for_events ) { 510 $last_from_outbound = $wpdb->get_var( $wpdb->prepare( 511 "SELECT MAX(created_at) FROM {$wpdb->prefix}optibehavior_outbound_clicks WHERE page_id=%d", 512 $page_id_for_events 513 ) ); 514 } 515 516 // Get the most recent timestamp from all sources 517 $timestamps = array_filter( array( $last_from_events, $last_from_pageviews, $last_from_outbound ) ); 518 $last_updated_raw = ! empty( $timestamps ) ? max( $timestamps ) : null; 519 520 // Format the timestamp for display 521 $last_updated = null; 522 if ( $last_updated_raw ) { 523 $diff = current_time( 'timestamp' ) - strtotime( $last_updated_raw ); 524 if ( $diff < 60 ) { 525 $last_updated = __( 'Just now', 'opti-behavior' ); 526 } elseif ( $diff < 3600 ) { 527 /* translators: %d: number of minutes */ 528 $last_updated = sprintf( __( '%d min ago', 'opti-behavior' ), floor( $diff / 60 ) ); 529 } elseif ( $diff < 86400 ) { 530 /* translators: %d: number of hours */ 531 $last_updated = sprintf( __( '%d hours ago', 'opti-behavior' ), floor( $diff / 3600 ) ); 532 } else { 533 /* translators: %d: number of days */ 534 $last_updated = sprintf( __( '%d days ago', 'opti-behavior' ), floor( $diff / 86400 ) ); 535 } 536 } 537 347 538 $mobile_pct = $total > 0 ? round( ( $mb / $total ) * 100 ) : 0; 539 540 // Get average scroll depth 541 $avg_scroll_depth = intval( $wpdb->get_var( $wpdb->prepare( 542 "SELECT AVG(scroll_depth) FROM {$wpdb->prefix}optibehavior_pageviews 543 WHERE (url=%s OR url LIKE %s) AND scroll_depth > 0", 544 $url, $url . '%' ) ) ); 545 546 // Get bounce rate - sessions with only 1 pageview 547 $total_sessions = intval( $wpdb->get_var( $wpdb->prepare( 548 "SELECT COUNT(DISTINCT pv.session_id) FROM {$wpdb->prefix}optibehavior_pageviews pv 549 WHERE (pv.url=%s OR pv.url LIKE %s)", 550 $url, $url . '%' ) ) ); 551 552 $bounced_sessions = intval( $wpdb->get_var( $wpdb->prepare( 553 "SELECT COUNT(DISTINCT pv.session_id) FROM {$wpdb->prefix}optibehavior_pageviews pv 554 WHERE (pv.url=%s OR pv.url LIKE %s) 555 AND pv.session_id IN ( 556 SELECT session_id FROM {$wpdb->prefix}optibehavior_pageviews 557 GROUP BY session_id 558 HAVING COUNT(*) = 1 559 )", 560 $url, $url . '%' ) ) ); 561 562 $bounce_rate = $total_sessions > 0 ? round( ( $bounced_sessions / $total_sessions ) * 100 ) : 0; 348 563 349 564 // Get page ID for heatmap viewing URLs using the analytics class method … … 358 573 'mobile_clicks' => $mb, 359 574 'avg_time' => $avg_time, 575 'avg_scroll_depth' => $avg_scroll_depth, 576 'bounce_rate' => $bounce_rate, 360 577 'mobile_pct' => $mobile_pct, 361 578 'last_updated' => $last_updated, … … 511 728 512 729 /** 730 * AJAX handler for enhanced post analytics chart data. 731 * Returns Total Visitors, Desktop Visitors, Desktop Events, Mobile Visitors, Mobile Events. 732 * 733 * @since 1.0.3 734 */ 735 public function ajax_post_analytics_chart() { 736 if ( ! isset( $_POST['post_id'] ) ) { 737 wp_send_json_error( array( 'message' => 'No post_id provided' ) ); 738 } 739 740 $post_id = intval( $_POST['post_id'] ); 741 check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' ); 742 743 if ( ! current_user_can( 'edit_post', $post_id ) ) { 744 wp_send_json_error( array( 'message' => 'Permission denied' ) ); 745 } 746 747 global $wpdb; 748 $url = get_permalink( $post_id ); 749 if ( ! $url ) { 750 wp_send_json_error( array( 'message' => 'No permalink found' ) ); 751 } 752 753 $range = isset( $_POST['range'] ) ? sanitize_text_field( wp_unslash( $_POST['range'] ) ) : '7d'; 754 $start = null; 755 $group_format = '%Y-%m-%d'; 756 757 switch ( $range ) { 758 case '24h': 759 $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 24 * 3600 ); 760 $group_format = '%Y-%m-%d %H:00:00'; 761 break; 762 case '7d': 763 $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 7 * 86400 ); 764 break; 765 case '30d': 766 $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 30 * 86400 ); 767 break; 768 case '90d': 769 $start = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - 90 * 86400 ); 770 break; 771 case 'all': 772 default: 773 $start = '1970-01-01 00:00:00'; 774 } 775 776 // Get page_id2 first for all queries 777 $page_id2 = intval( $wpdb->get_var( $wpdb->prepare( 778 "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1", 779 $url 780 ) ) ); 781 782 // If not found, try with/without trailing slash 783 if ( ! $page_id2 ) { 784 $url_alt = rtrim( $url, '/' ) . '/'; 785 if ( $url_alt === $url ) { 786 $url_alt = rtrim( $url, '/' ); 787 } 788 $page_id2 = intval( $wpdb->get_var( $wpdb->prepare( 789 "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1", 790 $url_alt 791 ) ) ); 792 } 793 794 // If still not found, try URL LIKE match 795 if ( ! $page_id2 ) { 796 $page_id2 = intval( $wpdb->get_var( $wpdb->prepare( 797 "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url LIKE %s LIMIT 1", 798 $url . '%' 799 ) ) ); 800 } 801 802 // Get unique visitors and sessions (Total, Desktop and Mobile) over time 803 // We need to get data from both pageviews AND events to ensure consistency 804 if ( $group_format === '%Y-%m-%d %H:00:00' ) { 805 // Get visitors from pageviews 806 $pv_visitors_data = $wpdb->get_results( 807 $wpdb->prepare( 808 "SELECT DATE_FORMAT(pv.view_time, '%%Y-%%m-%%d %%H:00:00') AS bucket, 809 COUNT(DISTINCT pv.session_id) AS total_sessions, 810 COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN pv.session_id END) AS desktop_sessions, 811 COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN pv.session_id END) AS mobile_sessions 812 FROM {$wpdb->prefix}optibehavior_pageviews pv 813 LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON pv.visitor_id = v.id 814 WHERE (pv.url=%s OR pv.url LIKE %s) AND pv.view_time >= %s 815 GROUP BY bucket 816 ORDER BY bucket ASC", 817 $url, $url . '%', $start 818 ), 819 ARRAY_A 820 ); 821 822 // Get visitors from events (for cases where pageview doesn't exist but events do) 823 // Use the SAME approach as the working stats query - join with pages table by URL 824 $ev_visitors_data = $wpdb->get_results( 825 $wpdb->prepare( 826 "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d %%H:00:00') AS bucket, 827 COUNT(DISTINCT e.session_id) AS total_sessions, 828 COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN e.session_id END) AS desktop_sessions, 829 COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN e.session_id END) AS mobile_sessions 830 FROM {$wpdb->prefix}optibehavior_events e 831 JOIN {$wpdb->prefix}optibehavior_pages p ON e.page_id2=p.id 832 LEFT JOIN {$wpdb->prefix}optibehavior_sessions s ON e.session_id = s.id 833 LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON s.visitor_id = v.id 834 WHERE (p.url=%s OR p.url LIKE %s) AND e.insert_at >= %s 835 GROUP BY bucket 836 ORDER BY bucket ASC", 837 $url, $url . '%', $start 838 ), 839 ARRAY_A 840 ); 841 } else { 842 // Get visitors from pageviews 843 $pv_visitors_data = $wpdb->get_results( 844 $wpdb->prepare( 845 "SELECT DATE_FORMAT(pv.view_time, '%%Y-%%m-%%d') AS bucket, 846 COUNT(DISTINCT pv.session_id) AS total_sessions, 847 COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN pv.session_id END) AS desktop_sessions, 848 COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN pv.session_id END) AS mobile_sessions 849 FROM {$wpdb->prefix}optibehavior_pageviews pv 850 LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON pv.visitor_id = v.id 851 WHERE (pv.url=%s OR pv.url LIKE %s) AND pv.view_time >= %s 852 GROUP BY bucket 853 ORDER BY bucket ASC", 854 $url, $url . '%', $start 855 ), 856 ARRAY_A 857 ); 858 859 // Get visitors from events (for cases where pageview doesn't exist but events do) 860 $ev_visitors_data = array(); 861 if ( $page_id2 > 0 ) { 862 $ev_visitors_data = $wpdb->get_results( 863 $wpdb->prepare( 864 "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d') AS bucket, 865 COUNT(DISTINCT e.session_id) AS total_sessions, 866 COUNT(DISTINCT CASE WHEN v.device_type = 'Desktop' THEN e.session_id END) AS desktop_sessions, 867 COUNT(DISTINCT CASE WHEN v.device_type = 'Mobile' THEN e.session_id END) AS mobile_sessions 868 FROM {$wpdb->prefix}optibehavior_events e 869 LEFT JOIN {$wpdb->prefix}optibehavior_sessions s ON e.session_id = s.id 870 LEFT JOIN {$wpdb->prefix}optibehavior_visitors v ON s.visitor_id = v.id 871 WHERE e.page_id2=%d AND e.insert_at >= %s 872 GROUP BY bucket 873 ORDER BY bucket ASC", 874 $page_id2, $start 875 ), 876 ARRAY_A 877 ); 878 } 879 } 880 881 // Merge pageview and event visitor data to get the maximum count 882 $visitors_data_merged = array(); 883 foreach ( $pv_visitors_data as $row ) { 884 $bucket = $row['bucket']; 885 $visitors_data_merged[ $bucket ] = $row; 886 } 887 foreach ( $ev_visitors_data as $row ) { 888 $bucket = $row['bucket']; 889 if ( ! isset( $visitors_data_merged[ $bucket ] ) ) { 890 $visitors_data_merged[ $bucket ] = $row; 891 } else { 892 // Take the maximum of both sources 893 $visitors_data_merged[ $bucket ]['total_sessions'] = max( 894 intval( $visitors_data_merged[ $bucket ]['total_sessions'] ), 895 intval( $row['total_sessions'] ) 896 ); 897 $visitors_data_merged[ $bucket ]['desktop_sessions'] = max( 898 intval( $visitors_data_merged[ $bucket ]['desktop_sessions'] ), 899 intval( $row['desktop_sessions'] ) 900 ); 901 $visitors_data_merged[ $bucket ]['mobile_sessions'] = max( 902 intval( $visitors_data_merged[ $bucket ]['mobile_sessions'] ), 903 intval( $row['mobile_sessions'] ) 904 ); 905 } 906 } 907 $visitors_data = array_values( $visitors_data_merged ); 908 909 // Check storage mode for events 910 $file_storage = $this->core->get_file_storage(); 911 $storage_settings = $file_storage ? $file_storage->get_settings() : array( 'storage_mode' => 'database' ); 912 $is_file_storage = ( $storage_settings['storage_mode'] === 'file' && $file_storage && $page_id2 ); 913 914 // Get events (Desktop=16, Mobile=17) over time 915 $events_data = array(); 916 if ( $page_id2 > 0 ) { 917 if ( $is_file_storage ) { 918 // Get events from file storage - need to read files and group by time buckets 919 // Get all events from file storage using read_events (not get_events) 920 // Parameters: page_id, event_id, limit (0=all), start_date, end_date 921 $all_events_desktop = $file_storage->read_events( $page_id2, 16, 0, gmdate( 'Y-m-d', strtotime( $start ) ), gmdate( 'Y-m-d' ) ); 922 $all_events_mobile = $file_storage->read_events( $page_id2, 17, 0, gmdate( 'Y-m-d', strtotime( $start ) ), gmdate( 'Y-m-d' ) ); 923 924 // Group events by time buckets 925 $events_by_bucket = array(); 926 $start_timestamp = strtotime( $start ); 927 928 // Process desktop events 929 foreach ( $all_events_desktop as $event ) { 930 // Events may have 'timestamp', 'time', 't', or 'insert_at' field 931 $event_time = null; 932 if ( isset( $event['timestamp'] ) ) { 933 $event_time = is_numeric( $event['timestamp'] ) ? $event['timestamp'] : strtotime( $event['timestamp'] ); 934 } elseif ( isset( $event['insert_at'] ) ) { 935 $event_time = is_numeric( $event['insert_at'] ) ? $event['insert_at'] : strtotime( $event['insert_at'] ); 936 } elseif ( isset( $event['time'] ) ) { 937 $event_time = is_numeric( $event['time'] ) ? $event['time'] : strtotime( $event['time'] ); 938 } elseif ( isset( $event['t'] ) ) { 939 $event_time = is_numeric( $event['t'] ) ? $event['t'] : strtotime( $event['t'] ); 940 } 941 942 if ( $event_time && $event_time >= $start_timestamp ) { 943 $bucket = gmdate( $group_format === '%Y-%m-%d %H:00:00' ? 'Y-m-d H:00:00' : 'Y-m-d', $event_time ); 944 if ( ! isset( $events_by_bucket[ $bucket ] ) ) { 945 $events_by_bucket[ $bucket ] = array( 'desktop_events' => 0, 'mobile_events' => 0 ); 946 } 947 $events_by_bucket[ $bucket ]['desktop_events']++; 948 } 949 } 950 951 // Process mobile events 952 foreach ( $all_events_mobile as $event ) { 953 // Events may have 'timestamp', 'time', 't', or 'insert_at' field 954 $event_time = null; 955 if ( isset( $event['timestamp'] ) ) { 956 $event_time = is_numeric( $event['timestamp'] ) ? $event['timestamp'] : strtotime( $event['timestamp'] ); 957 } elseif ( isset( $event['insert_at'] ) ) { 958 $event_time = is_numeric( $event['insert_at'] ) ? $event['insert_at'] : strtotime( $event['insert_at'] ); 959 } elseif ( isset( $event['time'] ) ) { 960 $event_time = is_numeric( $event['time'] ) ? $event['time'] : strtotime( $event['time'] ); 961 } elseif ( isset( $event['t'] ) ) { 962 $event_time = is_numeric( $event['t'] ) ? $event['t'] : strtotime( $event['t'] ); 963 } 964 965 if ( $event_time && $event_time >= $start_timestamp ) { 966 $bucket = gmdate( $group_format === '%Y-%m-%d %H:00:00' ? 'Y-m-d H:00:00' : 'Y-m-d', $event_time ); 967 if ( ! isset( $events_by_bucket[ $bucket ] ) ) { 968 $events_by_bucket[ $bucket ] = array( 'desktop_events' => 0, 'mobile_events' => 0 ); 969 } 970 $events_by_bucket[ $bucket ]['mobile_events']++; 971 } 972 } 973 974 // Convert to array format matching database results 975 foreach ( $events_by_bucket as $bucket => $counts ) { 976 $events_data[] = array( 977 'bucket' => $bucket, 978 'desktop_events' => $counts['desktop_events'], 979 'mobile_events' => $counts['mobile_events'], 980 ); 981 } 982 983 // Sort by bucket 984 usort( $events_data, function ( $a, $b ) { 985 return strcmp( $a['bucket'], $b['bucket'] ); 986 } ); 987 } else { 988 // Get events from database 989 if ( $group_format === '%Y-%m-%d %H:00:00' ) { 990 $events_data = $wpdb->get_results( 991 $wpdb->prepare( 992 "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d %%H:00:00') AS bucket, 993 SUM(CASE WHEN e.event=16 THEN 1 ELSE 0 END) AS desktop_events, 994 SUM(CASE WHEN e.event=17 THEN 1 ELSE 0 END) AS mobile_events 995 FROM {$wpdb->prefix}optibehavior_events e 996 WHERE e.page_id2=%d AND e.insert_at >= %s 997 GROUP BY bucket 998 ORDER BY bucket ASC", 999 $page_id2, $start 1000 ), 1001 ARRAY_A 1002 ); 1003 } else { 1004 $events_data = $wpdb->get_results( 1005 $wpdb->prepare( 1006 "SELECT DATE_FORMAT(e.insert_at, '%%Y-%%m-%%d') AS bucket, 1007 SUM(CASE WHEN e.event=16 THEN 1 ELSE 0 END) AS desktop_events, 1008 SUM(CASE WHEN e.event=17 THEN 1 ELSE 0 END) AS mobile_events 1009 FROM {$wpdb->prefix}optibehavior_events e 1010 WHERE e.page_id2=%d AND e.insert_at >= %s 1011 GROUP BY bucket 1012 ORDER BY bucket ASC", 1013 $page_id2, $start 1014 ), 1015 ARRAY_A 1016 ); 1017 } 1018 } 1019 } 1020 1021 // Merge all data by bucket (time period) 1022 $merged = array(); 1023 1024 foreach ( $visitors_data as $row ) { 1025 $bucket = $row['bucket']; 1026 if ( ! isset( $merged[ $bucket ] ) ) { 1027 $merged[ $bucket ] = array( 1028 'total_visitors' => 0, 1029 'desktop_visitors' => 0, 1030 'mobile_visitors' => 0, 1031 'desktop_events' => 0, 1032 'mobile_events' => 0, 1033 ); 1034 } 1035 $merged[ $bucket ]['total_visitors'] = intval( $row['total_sessions'] ); 1036 $merged[ $bucket ]['desktop_visitors'] = intval( $row['desktop_sessions'] ); 1037 $merged[ $bucket ]['mobile_visitors'] = intval( $row['mobile_sessions'] ); 1038 } 1039 1040 foreach ( $events_data as $row ) { 1041 $bucket = $row['bucket']; 1042 if ( ! isset( $merged[ $bucket ] ) ) { 1043 $merged[ $bucket ] = array( 1044 'total_visitors' => 0, 1045 'desktop_visitors' => 0, 1046 'mobile_visitors' => 0, 1047 'desktop_events' => 0, 1048 'mobile_events' => 0, 1049 ); 1050 } 1051 $merged[ $bucket ]['desktop_events'] = intval( $row['desktop_events'] ); 1052 $merged[ $bucket ]['mobile_events'] = intval( $row['mobile_events'] ); 1053 } 1054 1055 // Sort by bucket and prepare output 1056 ksort( $merged ); 1057 1058 $labels = array(); 1059 $total_visitors = array(); 1060 $desktop_visitors = array(); 1061 $mobile_visitors = array(); 1062 $desktop_events = array(); 1063 $mobile_events = array(); 1064 1065 foreach ( $merged as $bucket => $data ) { 1066 $labels[] = $bucket; 1067 $total_visitors[] = $data['total_visitors']; 1068 $desktop_visitors[] = $data['desktop_visitors']; 1069 $mobile_visitors[] = $data['mobile_visitors']; 1070 $desktop_events[] = $data['desktop_events']; 1071 $mobile_events[] = $data['mobile_events']; 1072 } 1073 1074 wp_send_json_success( array( 1075 'labels' => $labels, 1076 'total_visitors' => $total_visitors, 1077 'desktop_visitors' => $desktop_visitors, 1078 'mobile_visitors' => $mobile_visitors, 1079 'desktop_events' => $desktop_events, 1080 'mobile_events' => $mobile_events, 1081 'group' => $group_format, 1082 ) ); 1083 } 1084 1085 /** 513 1086 * AJAX handler for post referrers data 514 1087 */ … … 536 1109 $url 537 1110 ) ) ); 1111 1112 // If exact match not found, try with trailing slash variations 1113 if ( ! $page_id ) { 1114 $url_alt = rtrim( $url, '/' ) . '/'; 1115 if ( $url_alt === $url ) { 1116 $url_alt = rtrim( $url, '/' ); 1117 } 1118 $page_id = intval( $wpdb->get_var( $wpdb->prepare( 1119 "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1", 1120 $url_alt 1121 ) ) ); 1122 } 538 1123 539 1124 if ( ! $page_id ) { … … 601 1186 ) ) ); 602 1187 1188 // If exact match not found, try with trailing slash variations 1189 if ( ! $page_id ) { 1190 $url_alt = rtrim( $url, '/' ) . '/'; 1191 if ( $url_alt === $url ) { 1192 $url_alt = rtrim( $url, '/' ); 1193 } 1194 $page_id = intval( $wpdb->get_var( $wpdb->prepare( 1195 "SELECT id FROM {$wpdb->prefix}optibehavior_pages WHERE url=%s LIMIT 1", 1196 $url_alt 1197 ) ) ); 1198 } 1199 603 1200 if ( ! $page_id ) { 604 1201 // Try matching by normalized url2 … … 635 1232 $outbound_clicks = $session->get_page_outbound_clicks( $page_id, 10 ); 636 1233 637 // Ensure Exit Behavior total equals Entry Sources by adding synthetic "Left page" for any deficit638 try {639 $referrers = $session->get_page_referrers( $page_id, 1000 );640 $ref_total = 0;641 if ( is_array( $referrers ) ) {642 foreach ( $referrers as $r ) {643 $ref_total += isset( $r['count'] ) ? intval( $r['count'] ) : 0;644 } 645 }646 $exit_total = 0;647 if ( is_array( $outbound_clicks ) ) {648 foreach ( $outbound_clicks as $o ) {649 $exit_total += isset( $o['count'] ) ? intval( $o['count'] ) : 0;650 }651 } 652 $deficit = $ref_total - $exit_total;653 if ( $deficit > 0 ) { 654 $merged = false;655 if ( is_array( $outbound_clicks )) {656 foreach ( $outbound_clicks as $idx => $o ) {657 if ( isset( $o['click_type'] ) && $o['click_type'] === 'left' ) {658 $outbound_clicks[ $idx ]['count'] = intval( $outbound_clicks[ $idx ]['count'] ) + $deficit;659 $merged = true;660 break;661 }662 }663 }664 if ( ! $merged ) {665 $outbound_clicks[] = array(666 'target_url' => 'Left page',667 'click_type' => 'left',668 'element_tag' => null, 669 'element_text'=> null,670 'count' => $deficit,671 'last_click' => current_time( 'mysql' ),672 );673 }674 }675 } catch ( \Throwable $e ) {676 // Fail open; show raw outbound clicks if balancing fails1234 // Count sessions that truly left without clicking any link 1235 $total_sessions = intval( $wpdb->get_var( $wpdb->prepare( 1236 "SELECT COUNT(DISTINCT pv.session_id) 1237 FROM {$wpdb->prefix}optibehavior_pageviews pv 1238 WHERE pv.url=%s OR pv.url LIKE %s", 1239 $url, $url . '%' 1240 ) ) ); 1241 1242 $sessions_with_clicks = intval( $wpdb->get_var( $wpdb->prepare( 1243 "SELECT COUNT(DISTINCT oc.session_id) 1244 FROM {$wpdb->prefix}optibehavior_outbound_clicks oc 1245 WHERE oc.page_id=%d", 1246 $page_id 1247 ) ) ); 1248 1249 $sessions_left_without_click = $total_sessions - $sessions_with_clicks; 1250 1251 // Only add "Left page" if there are sessions that actually closed the page without clicking 1252 if ( $sessions_left_without_click > 0 ) { 1253 // Get the most recent timestamp from sessions that left without clicking 1254 $last_left = $wpdb->get_var( $wpdb->prepare( 1255 "SELECT MAX(pv.view_time) 1256 FROM {$wpdb->prefix}optibehavior_pageviews pv 1257 WHERE (pv.url=%s OR pv.url LIKE %s) 1258 AND pv.session_id NOT IN ( 1259 SELECT DISTINCT session_id 1260 FROM {$wpdb->prefix}optibehavior_outbound_clicks 1261 WHERE page_id=%d 1262 )", 1263 $url, $url . '%', $page_id 1264 ) ); 1265 1266 $outbound_clicks[] = array( 1267 'target_url' => 'Left page', 1268 'click_type' => 'left', 1269 'element_tag' => null, 1270 'element_text'=> null, 1271 'count' => $sessions_left_without_click, 1272 'last_click' => $last_left ?: current_time( 'mysql' ), 1273 ); 677 1274 } 678 1275 … … 711 1308 wp_send_json_success( $outbound_clicks ); 712 1309 } 1310 1311 /** 1312 * AJAX handler for post countries data 1313 */ 1314 public function ajax_post_countries() { 1315 if ( ! isset( $_POST['post_id'] ) ) { 1316 wp_send_json_error(); 1317 } 1318 1319 $post_id = intval( $_POST['post_id'] ); 1320 check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' ); 1321 1322 if ( ! current_user_can( 'edit_post', $post_id ) ) { 1323 wp_send_json_error(); 1324 } 1325 1326 global $wpdb; 1327 $url = get_permalink( $post_id ); 1328 if ( ! $url ) { 1329 wp_send_json_error(); 1330 } 1331 1332 // Get sessions for this URL and their visitor countries 1333 $results = $wpdb->get_results( $wpdb->prepare( 1334 "SELECT UPPER(COALESCE(NULLIF(TRIM(vis.country),''), '')) AS country, 1335 vis.country_name, 1336 COUNT(DISTINCT s.id) AS count 1337 FROM {$wpdb->prefix}optibehavior_sessions s 1338 LEFT JOIN {$wpdb->prefix}optibehavior_visitors vis ON s.visitor_id = vis.id 1339 LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id 1340 WHERE (pv.url=%s OR pv.url LIKE %s) 1341 AND COALESCE(NULLIF(TRIM(vis.country),''),'UN') <> 'UN' 1342 GROUP BY country, vis.country_name 1343 ORDER BY count DESC 1344 LIMIT 10", 1345 $url, $url . '%' 1346 ) ); 1347 1348 $data = array(); 1349 foreach ( $results as $row ) { 1350 $code = strtoupper( $row->country ?: '' ); 1351 $name = $row->country_name ?: ( $code ?: 'Global' ); 1352 $data[] = array( 1353 'country' => $code ?: 'Global', 1354 'country_name' => $name, 1355 'count' => intval( $row->count ), 1356 ); 1357 } 1358 1359 wp_send_json_success( $data ); 1360 } 1361 1362 /** 1363 * AJAX handler for post browsers data 1364 */ 1365 public function ajax_post_browsers() { 1366 if ( ! isset( $_POST['post_id'] ) ) { 1367 wp_send_json_error(); 1368 } 1369 1370 $post_id = intval( $_POST['post_id'] ); 1371 check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' ); 1372 1373 if ( ! current_user_can( 'edit_post', $post_id ) ) { 1374 wp_send_json_error(); 1375 } 1376 1377 global $wpdb; 1378 $url = get_permalink( $post_id ); 1379 if ( ! $url ) { 1380 wp_send_json_error(); 1381 } 1382 1383 // Get browsers for sessions viewing this URL 1384 $results = $wpdb->get_results( $wpdb->prepare( 1385 "SELECT 1386 CASE 1387 WHEN vis.browser IS NULL OR TRIM(vis.browser) = '' THEN 'Unknown' 1388 WHEN LOWER(vis.browser) IN ('unknown','undefined','other') THEN 'Unknown' 1389 ELSE TRIM(vis.browser) 1390 END AS browser, 1391 COUNT(DISTINCT s.id) AS count 1392 FROM {$wpdb->prefix}optibehavior_sessions s 1393 LEFT JOIN {$wpdb->prefix}optibehavior_visitors vis ON s.visitor_id = vis.id 1394 LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id 1395 WHERE (pv.url=%s OR pv.url LIKE %s) 1396 GROUP BY browser 1397 ORDER BY count DESC 1398 LIMIT 10", 1399 $url, $url . '%' 1400 ) ); 1401 1402 $data = array(); 1403 foreach ( $results as $row ) { 1404 $data[] = array( 1405 'name' => $row->browser ?: 'Unknown', 1406 'count' => intval( $row->count ), 1407 ); 1408 } 1409 1410 wp_send_json_success( $data ); 1411 } 1412 1413 /** 1414 * AJAX handler for post devices data 1415 */ 1416 public function ajax_post_devices() { 1417 if ( ! isset( $_POST['post_id'] ) ) { 1418 wp_send_json_error(); 1419 } 1420 1421 $post_id = intval( $_POST['post_id'] ); 1422 check_ajax_referer( 'optibehavior_post_analytics_' . $post_id, 'nonce' ); 1423 1424 if ( ! current_user_can( 'edit_post', $post_id ) ) { 1425 wp_send_json_error(); 1426 } 1427 1428 global $wpdb; 1429 $url = get_permalink( $post_id ); 1430 if ( ! $url ) { 1431 wp_send_json_error(); 1432 } 1433 1434 // Get device types for sessions viewing this URL 1435 $results = $wpdb->get_results( $wpdb->prepare( 1436 "SELECT 1437 CASE 1438 WHEN vis.device_type IS NULL OR TRIM(vis.device_type) = '' THEN 'Unknown' 1439 WHEN LOWER(vis.device_type) IN ('unknown','undefined','other') THEN 'Unknown' 1440 ELSE TRIM(vis.device_type) 1441 END AS device, 1442 COUNT(DISTINCT s.id) AS count 1443 FROM {$wpdb->prefix}optibehavior_sessions s 1444 LEFT JOIN {$wpdb->prefix}optibehavior_visitors vis ON s.visitor_id = vis.id 1445 LEFT JOIN {$wpdb->prefix}optibehavior_pageviews pv ON s.id = pv.session_id 1446 WHERE (pv.url=%s OR pv.url LIKE %s) 1447 GROUP BY device 1448 ORDER BY count DESC 1449 LIMIT 10", 1450 $url, $url . '%' 1451 ) ); 1452 1453 $data = array(); 1454 foreach ( $results as $row ) { 1455 $data[] = array( 1456 'name' => $row->device ?: 'Unknown', 1457 'count' => intval( $row->count ), 1458 ); 1459 } 1460 1461 wp_send_json_success( $data ); 1462 } 713 1463 } -
opti-behavior/trunk/admin/recordings-upgrade-page.php
r3399182 r3401441 56 56 <div class="free-badge"> 57 57 <i data-lucide="gift"></i> 58 <span><?php esc_html_e( 'FREE during testing period!', 'opti-behavior' ); ?></span>58 <span><?php esc_html_e( 'FREE During Testing Period!', 'opti-behavior' ); ?></span> 59 59 </div> 60 60 … … 63 63 </p> 64 64 65 <a href=" <?php echo esc_url( admin_url( 'admin.php?page=opti-behavior-analytics' ) ); ?>" class="button button-primary button-hero">65 <a href="https://optiuser.com/" target="_blank" rel="noopener noreferrer" class="button button-primary button-hero"> 66 66 <i data-lucide="download"></i> 67 67 <?php esc_html_e( 'Download PRO Version', 'opti-behavior' ); ?> -
opti-behavior/trunk/assets/css/admin-notices.css
r3399182 r3401441 55 55 56 56 /* Generic message containers */ 57 div[id^="message"] ,57 div[id^="message"]:not(#support-form-message), 58 58 div[id*="notice"], 59 59 div[id*="warning"], 60 60 div[id*="error"], 61 61 div[class*="notice"], 62 div[class*="message"] ,62 div[class*="message"]:not(.form-message):not(#support-form-message), 63 63 div[class*="notification"], 64 64 div[class*="alert"], … … 72 72 73 73 /* Review/rating notices */ 74 div[class*="review"] ,74 div[class*="review"]:not(.ai-feedback-section):not(.feedback-card), 75 75 div[class*="rating"], 76 div[class*="feedback"] ,76 div[class*="feedback"]:not(.ai-feedback-section):not(.feedback-card):not(.feedback-icon):not(.feedback-title):not(.feedback-description):not(.feedback-actions):not(.feedback-btn):not(.feedback-note), 77 77 78 78 /* Newsletter/subscription notices */ … … 82 82 /* Promotional notices (third-party). Allow Opti-Behavior upgrade layouts. */ 83 83 div[class*="promo"]:not([class*="opti-behavior"]), 84 div[class*="upgrade"]:not([class*="opti-behavior"]):not(.upgrade-card):not(.upgrade-features):not(.upgrade-action):not(.recordings-upgrade-content):not(.upgrade-preview) ,84 div[class*="upgrade"]:not([class*="opti-behavior"]):not(.upgrade-card):not(.upgrade-features):not(.upgrade-action):not(.recordings-upgrade-content):not(.upgrade-preview):not(.upgrade-cta):not(.upgrade-description):not(.upgrade-icon):not(.upgrade-title), 85 85 div[class*="premium"]:not([class*="opti-behavior"]), 86 86 … … 102 102 /* Donation/support notices */ 103 103 div[class*="donate"], 104 div[class*="support"] ,104 div[class*="support"]:not(.support-form):not([id*="support-"]), 105 105 106 106 /* Onboarding/welcome notices */ … … 148 148 } 149 149 150 /* EXCEPTION: Keep AI Insights support form visible */ 151 .ai-feedback-section, 152 .ai-feedback-section *, 153 .feedback-card, 154 .support-form, 155 #opti-behavior-support-form { 156 display: block !important; 157 visibility: visible !important; 158 opacity: 1 !important; 159 height: auto !important; 160 } 161 -
opti-behavior/trunk/assets/css/ai-insights-page.css
r3399182 r3401441 414 414 } 415 415 } 416 417 /* Support Form Styles */ 418 .support-form { 419 max-width: 600px; 420 margin: 32px auto; 421 text-align: left; 422 } 423 424 .support-form .form-group { 425 margin-bottom: 24px; 426 } 427 428 .support-form label { 429 display: block; 430 font-size: 15px; 431 font-weight: 600; 432 color: white; 433 margin-bottom: 8px; 434 } 435 436 .support-form .form-control { 437 width: 100%; 438 padding: 12px 16px; 439 font-size: 15px; 440 border: 2px solid rgba(255, 255, 255, 0.3); 441 border-radius: 10px; 442 background: rgba(255, 255, 255, 0.95); 443 color: #1e293b; 444 transition: all 0.3s ease; 445 box-sizing: border-box; 446 } 447 448 .support-form .form-control:focus { 449 outline: none; 450 border-color: white; 451 background: white; 452 box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2); 453 } 454 455 .support-form textarea.form-control { 456 resize: vertical; 457 min-height: 120px; 458 font-family: inherit; 459 } 460 461 .support-form .form-hint { 462 display: block; 463 font-size: 13px; 464 color: rgba(255, 255, 255, 0.8); 465 margin-top: 6px; 466 } 467 468 .support-form .form-actions { 469 text-align: center; 470 margin-top: 32px; 471 } 472 473 .support-form button[type="submit"] { 474 border: none; 475 cursor: pointer; 476 } 477 478 .support-form button[type="submit"]:disabled { 479 opacity: 0.7; 480 cursor: not-allowed; 481 } 482 483 .support-form .support-response { 484 margin-top: 24px; 485 padding: 16px 20px; 486 border-radius: 10px; 487 font-size: 15px; 488 font-weight: 600; 489 text-align: center; 490 } 491 492 .support-form .support-response-hidden { 493 display: none !important; 494 } 495 496 .support-form .support-response.success { 497 background: rgba(16, 185, 129, 0.2); 498 color: white; 499 border: 2px solid rgba(255, 255, 255, 0.5); 500 } 501 502 .support-form .support-response.error { 503 background: rgba(239, 68, 68, 0.2); 504 color: white; 505 border: 2px solid rgba(239, 68, 68, 0.5); 506 } 507 508 /* Responsive adjustments for support form */ 509 @media (max-width: 768px) { 510 .support-form { 511 max-width: 100%; 512 } 513 } -
opti-behavior/trunk/assets/css/heatmaps.css
r3399182 r3401441 65 65 66 66 .btn-filter.active { 67 background: # 6c5ce7;68 border-color: # 6c5ce7;67 background: #16a34a; 68 border-color: #16a34a; 69 69 color: white; 70 70 } … … 145 145 146 146 .stat-change.positive { 147 color: # 28a745;147 color: #16a34a; 148 148 } 149 149 … … 209 209 210 210 .btn-action.primary { 211 background: # 6c5ce7;212 border-color: # 6c5ce7;211 background: #16a34a; 212 border-color: #16a34a; 213 213 color: white; 214 214 } 215 215 216 216 .btn-action.primary:hover { 217 background: # 5a4fcf;217 background: #15803d; 218 218 } 219 219 … … 263 263 264 264 .heatmap-table th { 265 background: #f8f9fa;266 border-bottom: 2px solid # e1e5e9;265 background: linear-gradient(135deg, #dcfce7 0%, #f0fdf4 100%); 266 border-bottom: 2px solid #86efac; 267 267 padding: 16px; 268 268 text-align: center; 269 269 font-size: 14px; 270 270 font-weight: 600; 271 color: # 495057;271 color: #166534; 272 272 } 273 273 … … 333 333 334 334 .heatmaps-modern-table thead th { 335 background: transparent!important;336 border-bottom: 2px solid # e2e8f0!important;335 background: linear-gradient(135deg, #dcfce7 0%, #f0fdf4 100%) !important; 336 border-bottom: 2px solid #86efac !important; 337 337 padding: 16px 20px !important; 338 338 text-align: left !important; 339 339 font-size: 13px !important; 340 340 font-weight: 600 !important; 341 color: # 475569!important;341 color: #166534 !important; 342 342 text-transform: uppercase !important; 343 343 letter-spacing: 0.05em !important; … … 350 350 351 351 .heatmaps-modern-table tbody tr.heatmap-table-row:hover { 352 background: linear-gradient(135deg, #f 8fafc 0%, #f1f5f9100%) !important;353 box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.05);352 background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%) !important; 353 box-shadow: 0 2px 8px rgba(22, 163, 74, 0.15); 354 354 transform: translateY(-1px); 355 355 } … … 416 416 417 417 .heatmaps-modern-table .heatmap-action-btn:hover { 418 background: linear-gradient(135deg, # 3b82f6 0%, #2563eb100%) !important;419 border-color: # 2563eb!important;418 background: linear-gradient(135deg, #16a34a 0%, #22c55e 100%) !important; 419 border-color: #16a34a !important; 420 420 color: white !important; 421 421 transform: translateY(-1px) !important; 422 box-shadow: 0 4px 12px rgba( 59, 130, 246, 0.3) !important;422 box-shadow: 0 4px 12px rgba(22, 163, 74, 0.3) !important; 423 423 } 424 424 … … 477 477 align-items: center; 478 478 gap: 6px; 479 background: # 6c5ce7;479 background: #16a34a; 480 480 color: white; 481 481 text-decoration: none; … … 488 488 489 489 .btn-view:hover { 490 background: # 5a4fcf;490 background: #15803d; 491 491 color: white; 492 492 text-decoration: none; … … 497 497 align-items: center; 498 498 gap: 6px; 499 background: # 00b894;499 background: #16a34a; 500 500 color: white; 501 501 text-decoration: none; … … 509 509 510 510 .btn-view-pc:hover { 511 background: # 00a085;511 background: #15803d; 512 512 color: white; 513 513 text-decoration: none; … … 518 518 align-items: center; 519 519 gap: 6px; 520 background: # fd79a8;520 background: #16a34a; 521 521 color: white; 522 522 text-decoration: none; … … 530 530 531 531 .btn-view-mobile:hover { 532 background: # e84393;532 background: #15803d; 533 533 color: white; 534 534 text-decoration: none; … … 552 552 } 553 553 554 /* Green theme for Desktop and Mobile action buttons */ 555 .optibehavior-btn-desktop.button, 556 .optibehavior-btn-mobile.button { 557 background: #16a34a !important; 558 border-color: #16a34a !important; 559 color: white !important; 560 text-shadow: none !important; 561 box-shadow: none !important; 562 } 563 564 .optibehavior-btn-desktop.button:hover, 565 .optibehavior-btn-mobile.button:hover { 566 background: #15803d !important; 567 border-color: #15803d !important; 568 color: white !important; 569 } 570 571 /* Ensure consistent icon sizes in Desktop/Mobile buttons */ 572 .optibehavior-btn-desktop.button svg, 573 .optibehavior-btn-mobile.button svg, 574 .optibehavior-btn-desktop.button i[data-lucide], 575 .optibehavior-btn-mobile.button i[data-lucide] { 576 width: 14px !important; 577 height: 14px !important; 578 min-width: 14px !important; 579 min-height: 14px !important; 580 max-width: 14px !important; 581 max-height: 14px !important; 582 vertical-align: middle !important; 583 margin-right: 4px !important; 584 display: inline-block !important; 585 } 586 554 587 .no-heatmaps { 555 588 text-align: center; … … 588 621 589 622 .btn-apply { 590 background: # 6c5ce7;623 background: #16a34a; 591 624 color: white; 592 625 border: none; … … 612 645 613 646 .view-btn.active { 614 background: # 6c5ce7;615 border-color: # 6c5ce7;647 background: #16a34a; 648 border-color: #16a34a; 616 649 color: white; 617 650 } … … 687 720 688 721 .btn-preview { 689 background: # 6c5ce7;722 background: #16a34a; 690 723 color: white; 691 724 border: none; … … 822 855 .heatmaps-panel .table-pagination .pagination-btn[data-next], 823 856 .heatmaps-panel .table-pagination .pagination-btn[data-last] { 824 background:# 4f46e5; border-color:#4f46e5; color:#fff;857 background:#16a34a; border-color:#16a34a; color:#fff; 825 858 } 826 859 .heatmaps-panel .table-pagination .pagination-btn[data-next]:hover, 827 .heatmaps-panel .table-pagination .pagination-btn[data-last]:hover { background:# 4338ca; }860 .heatmaps-panel .table-pagination .pagination-btn[data-last]:hover { background:#15803d; } 828 861 829 862 /* Current page badge */ 830 863 .heatmaps-panel .table-pagination .pagination-current { 831 background:# eef2ff; color:#4f46e5; border-radius:8px; padding:6px 10px; font-size:12px; font-weight:600;864 background:#dcfce7; color:#16a34a; border-radius:8px; padding:6px 10px; font-size:12px; font-weight:600; 832 865 } 833 866 .heatmaps-panel .table-pagination .pagination-ellipsis { color:#94a3b8; } … … 902 935 903 936 .stat-badge { 904 background: # 6c5ce7;937 background: #16a34a; 905 938 color: white; 906 939 border-radius: 12px; -
opti-behavior/trunk/assets/css/metabox.css
r3399182 r3401441 119 119 } 120 120 121 .optibehavior-behavior-label { 122 font-size: 12px; 123 color: #64748b; 124 margin-bottom: 8px; 125 font-weight: 500; 126 display: flex; 127 align-items: center; 128 } 129 130 .optibehavior-behavior-value { 131 font-size: 18px; 132 font-weight: 700; 133 color: #1e293b; 134 } 135 136 .optibehavior-stat-badge { 137 display: inline-flex; 138 align-items: center; 139 justify-content: center; 140 background: #10b981; 141 color: #fff; 142 padding: 4px 8px; 143 border-radius: 12px; 144 font-size: 11px; 145 font-weight: 600; 146 min-width: 32px; 147 text-align: center; 148 } 149 121 150 @media (max-width: 480px) { 122 151 .optibehavior-analytics-grid { … … 193 222 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); 194 223 color: #fff; 224 } 225 226 /* Secondary grid for new analytics row */ 227 .optibehavior-secondary-grid { 228 margin-top: 16px; 229 padding-top: 16px; 230 border-top: 1px solid #e5e7eb; 195 231 } 196 232 … … 244 280 padding-right: 12px; 245 281 } 282 283 /* Loading and empty states */ 284 .optibehavior-loading { 285 display: block; 286 padding: 20px; 287 text-align: center; 288 color: #64748b; 289 font-size: 12px; 290 } 291 292 .optibehavior-empty { 293 display: none; 294 padding: 20px; 295 text-align: center; 296 color: #94a3b8; 297 font-size: 12px; 298 font-style: italic; 299 } 300 301 /* Empty state with icon (matching dashboard style) */ 302 .optibehavior-empty-state { 303 display: none; 304 flex-direction: column; 305 align-items: center; 306 justify-content: center; 307 padding: 40px 20px; 308 text-align: center; 309 min-height: 200px; 310 } 311 312 .optibehavior-empty-state.is-visible { 313 display: flex; 314 } 315 316 .optibehavior-empty-title { 317 margin-top: 12px; 318 font-size: 14px; 319 font-weight: 600; 320 color: #1e293b; 321 } 322 323 .optibehavior-empty-sub { 324 margin-top: 4px; 325 font-size: 12px; 326 color: #64748b; 327 } -
opti-behavior/trunk/assets/css/recordings-upgrade.css
r3399182 r3401441 3 3 margin: 0; 4 4 padding: 0; 5 background: #f8f9fa; 5 6 } 6 7 … … 8 9 background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 9 10 color: #ffffff; 10 padding: 36px 40px; 11 margin: 0 0 30px -20px; 12 box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); 13 border-bottom: 3px solid rgba(255, 255, 255, 0.2); 11 padding: 48px 40px; 12 margin: 0 0 40px -20px; 13 box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25); 14 border-bottom: 4px solid rgba(255, 255, 255, 0.15); 15 position: relative; 16 overflow: hidden; 17 } 18 19 .recordings-header::before { 20 content: ''; 21 position: absolute; 22 top: 0; 23 left: 0; 24 right: 0; 25 bottom: 0; 26 background: url('data:image/svg+xml,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="2" fill="white" opacity="0.1"/></svg>'); 27 background-size: 50px 50px; 28 opacity: 0.3; 14 29 } 15 30 … … 17 32 max-width: 1400px; 18 33 margin: 0 auto; 34 position: relative; 35 z-index: 1; 19 36 } 20 37 … … 22 39 display: flex; 23 40 align-items: center; 24 gap: 12px; 25 font-size: 28px; 26 font-weight: 600; 27 margin: 0 0 8px; 41 gap: 14px; 42 font-size: 32px; 43 font-weight: 700; 44 margin: 0 0 12px; 45 color: #ffffff !important; 46 text-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 28 47 } 29 48 30 49 .recordings-title i { 31 width: 28px; 32 height: 28px; 50 width: 32px; 51 height: 32px; 52 filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); 33 53 } 34 54 35 55 .recordings-subtitle { 36 font-size: 14px; 37 opacity: 0.9; 56 font-size: 16px; 57 opacity: 0.95; 58 font-weight: 400; 59 letter-spacing: 0.01em; 38 60 } 39 61 40 62 .recordings-upgrade-content { 41 max-width: 960px;63 max-width: 1000px; 42 64 margin: 0 auto 40px; 43 65 padding: 0 20px 40px; … … 45 67 flex-direction: column; 46 68 align-items: center; 47 gap: 24px;69 gap: 32px; 48 70 } 49 71 … … 51 73 .upgrade-preview { 52 74 background: #ffffff; 53 border-radius: 1 2px;75 border-radius: 16px; 54 76 border: 1px solid #e5e7eb; 55 box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);56 padding: 32px 30px;77 box-shadow: 0 4px 16px rgba(15, 23, 42, 0.08); 78 padding: 48px 40px; 57 79 width: 100%; 80 transition: transform 0.3s ease, box-shadow 0.3s ease; 81 } 82 83 .upgrade-card:hover { 84 transform: translateY(-2px); 85 box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12); 58 86 } 59 87 60 88 .upgrade-icon { 61 margin-bottom: 20px; 89 margin-bottom: 24px; 90 display: flex; 91 justify-content: center; 62 92 } 63 93 64 94 .upgrade-icon i { 65 width: 64px;66 height: 64px;95 width: 80px; 96 height: 80px; 67 97 color: #6366f1; 98 animation: pulse-icon 2s ease-in-out infinite; 99 } 100 101 @keyframes pulse-icon { 102 0%, 100% { 103 transform: scale(1); 104 opacity: 1; 105 } 106 50% { 107 transform: scale(1.05); 108 opacity: 0.9; 109 } 68 110 } 69 111 70 112 .upgrade-title { 71 font-size: 26px; 72 font-weight: 600; 73 color: #111827; 74 margin: 0 0 12px; 75 } 76 77 .upgrade-description { 78 font-size: 15px; 79 color: #4b5563; 80 max-width: 620px; 81 margin: 0 auto 28px; 82 line-height: 1.6; 83 } 84 85 .upgrade-features { 86 background: #f9fafb; 87 border-radius: 10px; 88 padding: 24px 22px; 89 margin: 0 0 30px; 90 text-align: left; 91 } 92 93 .upgrade-features h3 { 94 font-size: 18px; 95 font-weight: 600; 113 font-size: 32px; 114 font-weight: 700; 96 115 color: #111827; 97 116 margin: 0 0 16px; 117 text-align: center; 118 } 119 120 .upgrade-description { 121 font-size: 16px; 122 color: #4b5563; 123 max-width: 680px; 124 margin: 0 auto 32px; 125 line-height: 1.7; 126 text-align: center; 127 } 128 129 .upgrade-features { 130 background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); 131 border-radius: 12px; 132 padding: 32px 28px; 133 margin: 0 0 36px; 134 text-align: left; 135 border: 1px solid #e5e7eb; 136 } 137 138 .upgrade-features h3 { 139 font-size: 22px; 140 font-weight: 700; 141 color: #111827; 142 margin: 0 0 24px; 98 143 text-align: center; 99 144 } … … 103 148 margin: 0; 104 149 padding: 0; 150 display: grid; 151 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 152 gap: 16px; 105 153 } 106 154 … … 108 156 display: flex; 109 157 align-items: center; 110 gap: 1 2px;111 padding: 1 0px 0;112 font-size: 1 4px;158 gap: 14px; 159 padding: 14px 16px; 160 font-size: 15px; 113 161 color: #1f2933; 114 border-bottom: 1px solid #e5e7eb; 115 } 116 117 .features-list li:last-child { 118 border-bottom: none; 162 background: #ffffff; 163 border-radius: 8px; 164 border: 1px solid #e5e7eb; 165 transition: all 0.2s ease; 166 } 167 168 .features-list li:hover { 169 border-color: #10b981; 170 background: #f0fdf4; 171 transform: translateX(4px); 119 172 } 120 173 121 174 .features-list li i { 122 175 color: #10b981; 176 width: 24px; 177 height: 24px; 178 flex-shrink: 0; 179 } 180 181 .upgrade-cta { 182 margin-top: 40px; 183 text-align: center; 184 padding: 32px; 185 background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); 186 border-radius: 12px; 187 border: 2px solid #bae6fd; 188 } 189 190 .free-badge { 191 display: inline-flex; 192 align-items: center; 193 gap: 10px; 194 background: linear-gradient(135deg, #10b981 0%, #059669 100%); 195 color: #ffffff; 196 padding: 12px 24px; 197 border-radius: 999px; 198 font-size: 15px; 199 font-weight: 700; 200 box-shadow: 0 6px 16px rgba(16, 185, 129, 0.5); 201 margin-bottom: 20px; 202 animation: badge-pulse 2s ease-in-out infinite; 203 } 204 205 @keyframes badge-pulse { 206 0%, 100% { 207 box-shadow: 0 6px 16px rgba(16, 185, 129, 0.5); 208 } 209 50% { 210 box-shadow: 0 8px 20px rgba(16, 185, 129, 0.7); 211 } 212 } 213 214 .free-badge i { 123 215 width: 20px; 124 216 height: 20px; 125 flex-shrink: 0;126 }127 128 .upgrade-cta {129 margin-top: 32px;130 }131 132 .free-badge {133 display: inline-flex;134 align-items: center;135 gap: 8px;136 background: linear-gradient(135deg, #10b981 0%, #059669 100%);137 color: #ffffff;138 padding: 10px 20px;139 border-radius: 999px;140 font-size: 14px;141 font-weight: 600;142 box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);143 margin-bottom: 18px;144 }145 146 .free-badge i {147 width: 18px;148 height: 18px;149 217 } 150 218 151 219 .cta-text { 152 font-size: 14px; 153 color: #4b5563; 154 max-width: 600px; 155 margin: 0 auto 24px; 156 line-height: 1.6; 220 font-size: 15px; 221 color: #1f2937; 222 max-width: 640px; 223 margin: 0 auto 28px; 224 line-height: 1.7; 225 font-weight: 500; 157 226 } 158 227 … … 160 229 display: inline-flex; 161 230 align-items: center; 162 gap: 1 0px;163 font-size: 1 5px;164 padding: 1 1px 28px;231 gap: 12px; 232 font-size: 16px; 233 padding: 14px 32px; 165 234 height: auto; 166 235 line-height: 1.5; 236 font-weight: 600; 237 border-radius: 8px; 238 transition: all 0.3s ease; 239 background: #2271b1; 240 border-color: #2271b1; 241 } 242 243 .button-hero:hover { 244 background: #135e96; 245 border-color: #135e96; 246 transform: translateY(-2px); 247 box-shadow: 0 8px 16px rgba(34, 113, 177, 0.3); 167 248 } 168 249 169 250 .button-hero i { 170 width: 2 0px;171 height: 2 0px;251 width: 22px; 252 height: 22px; 172 253 } 173 254 174 255 .help-text { 175 margin-top: 18px;176 font-size: 1 3px;256 margin-top: 20px; 257 font-size: 14px; 177 258 color: #6b7280; 259 font-style: italic; 260 } 261 262 .upgrade-preview { 263 display: none; 178 264 } 179 265 180 266 .upgrade-preview h3 { 181 font-size: 2 0px;182 font-weight: 600;267 font-size: 24px; 268 font-weight: 700; 183 269 color: #111827; 184 margin: 0 0 18px; 270 margin: 0 0 24px; 271 text-align: center; 185 272 } 186 273 187 274 .preview-placeholder { 188 background: #f9fafb;189 border-radius: 1 0px;190 border: 2px dashed # e5e7eb;191 padding: 48px 20px;275 background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); 276 border-radius: 12px; 277 border: 2px dashed #d1d5db; 278 padding: 64px 20px; 192 279 display: flex; 193 280 flex-direction: column; 194 281 align-items: center; 195 gap: 16px; 282 gap: 20px; 283 position: relative; 284 overflow: hidden; 285 } 286 287 .preview-placeholder::before { 288 content: ''; 289 position: absolute; 290 top: 0; 291 left: 0; 292 right: 0; 293 bottom: 0; 294 background: radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.05) 0%, transparent 70%); 196 295 } 197 296 198 297 .preview-placeholder i { 199 width: 96px; 200 height: 96px; 201 color: #cbd5f5; 298 width: 120px; 299 height: 120px; 300 color: #a5b4fc; 301 position: relative; 302 z-index: 1; 202 303 } 203 304 204 305 .preview-placeholder p { 205 306 margin: 0; 206 font-size: 1 4px;307 font-size: 15px; 207 308 color: #6b7280; 309 position: relative; 310 z-index: 1; 311 font-weight: 500; 208 312 } 209 313 … … 211 315 .recordings-header { 212 316 margin-left: 0; 213 padding: 28px 20px;317 padding: 36px 24px; 214 318 } 215 319 216 320 .recordings-upgrade-content { 321 padding: 0 20px 30px; 322 } 323 324 .features-list { 217 325 grid-template-columns: 1fr; 218 padding: 0 20px 30px;219 326 } 220 327 } 221 328 222 329 @media (max-width: 600px) { 330 .recordings-header { 331 padding: 28px 16px; 332 } 333 334 .recordings-title { 335 font-size: 24px; 336 } 337 338 .recordings-subtitle { 339 font-size: 14px; 340 } 341 223 342 .upgrade-card, 224 343 .upgrade-preview { 344 padding: 32px 24px; 345 } 346 347 .upgrade-title { 348 font-size: 26px; 349 } 350 351 .upgrade-description { 352 font-size: 15px; 353 } 354 355 .upgrade-features h3 { 356 font-size: 20px; 357 } 358 359 .features-list li { 360 font-size: 14px; 361 padding: 12px 14px; 362 } 363 364 .upgrade-cta { 225 365 padding: 24px 20px; 226 366 } 227 } 228 367 368 .button-hero { 369 width: 100%; 370 justify-content: center; 371 } 372 } 373 -
opti-behavior/trunk/assets/css/settings.css
r3399182 r3401441 1419 1419 } 1420 1420 1421 /* Quota Analytics Section */ 1422 .quota-analytics { 1423 margin-top: 20px; 1424 padding: 20px; 1425 background: #f8f9fa; 1426 border-radius: 8px; 1427 border: 1px solid #e9ecef; 1428 } 1429 1430 .analytics-title { 1431 font-size: 16px; 1432 font-weight: 600; 1433 color: #2c3e50; 1434 margin: 0 0 15px 0; 1435 } 1436 1437 .analytics-grid { 1438 display: grid; 1439 grid-template-columns: repeat(3, 1fr); 1440 gap: 15px; 1441 margin-bottom: 15px; 1442 } 1443 1444 .analytics-item { 1445 display: flex; 1446 align-items: center; 1447 gap: 12px; 1448 background: #ffffff; 1449 padding: 15px; 1450 border-radius: 6px; 1451 border: 1px solid #e9ecef; 1452 } 1453 1454 .analytics-icon { 1455 font-size: 24px; 1456 flex-shrink: 0; 1457 } 1458 1459 .analytics-data { 1460 flex: 1; 1461 } 1462 1463 .analytics-value { 1464 font-size: 24px; 1465 font-weight: 700; 1466 color: #6c5ce7; 1467 line-height: 1; 1468 } 1469 1470 .analytics-label { 1471 font-size: 12px; 1472 color: #6c757d; 1473 margin-top: 4px; 1474 } 1475 1476 .analytics-note { 1477 margin: 0; 1478 padding: 10px; 1479 background: #fff3cd; 1480 border-radius: 4px; 1481 border: 1px solid #ffeaa7; 1482 } 1483 1484 .analytics-note small { 1485 color: #856404; 1486 font-size: 13px; 1487 } 1488 1421 1489 /* Responsive adjustments for quota stats */ 1422 1490 @media (max-width: 768px) { … … 1429 1497 font-size: 24px; 1430 1498 } 1431 } 1499 1500 .analytics-grid { 1501 grid-template-columns: 1fr; 1502 } 1503 1504 .analytics-value { 1505 font-size: 20px; 1506 } 1507 } -
opti-behavior/trunk/assets/js/dashboard.js
r3399182 r3401441 260 260 } 261 261 262 // We have data - show the table 262 // We have data - show the table and hide empty state 263 263 const table = document.querySelector('.device-types-legend'); 264 264 if (table) { 265 265 table.style.display = ''; 266 } 267 268 // Hide empty state if it exists 269 const container = ctx.parentElement; 270 if (container) { 271 const emptyState = container.querySelector('.optibehavior-empty-state'); 272 if (emptyState) { 273 emptyState.style.display = 'none'; 274 } 266 275 } 267 276 … … 359 368 'ChromeOS': '#4285f4' 360 369 }; 370 371 // Hide empty state if it exists (data is available) 372 const container = ctx.parentElement; 373 if (container) { 374 const emptyState = container.querySelector('.optibehavior-empty-state'); 375 if (emptyState) { 376 emptyState.style.display = 'none'; 377 } 378 } 361 379 362 380 // Update legend table … … 1498 1516 1499 1517 if (labels.length > 0) { 1518 // Hide empty state if it exists (data is available) 1519 const container = intentEl.parentElement; 1520 if (container) { 1521 const emptyState = container.querySelector('.optibehavior-empty-state'); 1522 if (emptyState) { 1523 emptyState.style.display = 'none'; 1524 } 1525 } 1526 1500 1527 // Destroy existing chart if it exists 1501 1528 const existingIntentChart = Chart.getChart(intentEl); … … 3034 3061 3035 3062 var rows = resp.data.items.slice(0,10).map(function(it,idx){ 3063 // Display username with profile link if available, otherwise show visitor ID 3064 var visitorDisplay = ''; 3065 if (it.user_id && it.user_id > 0 && it.display_name) { 3066 // User is a logged-in WordPress user - show display name with profile link 3067 visitorDisplay = '<a href="' + esc(it.profile_url||'') + '" target="_blank" rel="noopener" title="View user profile">' + 3068 esc(it.display_name) + '</a>'; 3069 } else { 3070 // Anonymous visitor - show visitor ID 3071 visitorDisplay = esc(it.visitor_id||''); 3072 } 3073 3036 3074 return '<tr>'+ 3037 3075 '<td class="tu-rank">'+ (idx+1) +'</td>'+ 3038 '<td class="tu-visitor">'+ esc(it.visitor_id||'')+'</td>'+3076 '<td class="tu-visitor">'+ visitorDisplay +'</td>'+ 3039 3077 '<td class="tu-num">'+ (it.daily_freq||0).toFixed(2) +'</td>'+ 3040 3078 '<td class="tu-num"><span class="tu-chip">'+ fmt(it.avg_session_time||0) +'</span></td>'+ … … 3340 3378 } 3341 3379 3342 // We have data - show the table container 3380 // We have data - show the table container and hide empty state 3343 3381 var tableContainer = document.querySelector('.resolution-table-container'); 3344 3382 if (tableContainer) { 3345 3383 tableContainer.style.display = ''; 3384 } 3385 3386 // Hide empty state if it exists (data is available) 3387 var container = ctx.parentElement; 3388 if (container) { 3389 var emptyState = container.querySelector('.optibehavior-empty-state'); 3390 if (emptyState) { 3391 emptyState.style.display = 'none'; 3392 } 3346 3393 } 3347 3394 -
opti-behavior/trunk/assets/js/heatmaps.js
r3399182 r3401441 25 25 if(wrap){ wrap.dataset.orderby = orderby; wrap.dataset.order = order; } 26 26 var oldShell = document.querySelector('.simplified-heatmap-table'); if(oldShell){ oldShell.style.display='none'; } 27 if(window.optibehaviorUpdateSessionsCounts){ try{ window.optibehaviorUpdateSessionsCounts(container); }catch(e){ /* console.warn('[optibehavior] sessions update error', e); */ } } 27 // Sessions are now included in main query, no need for lazy update 28 // if(window.optibehaviorUpdateSessionsCounts){ try{ window.optibehaviorUpdateSessionsCounts(container); }catch(e){ /* console.warn('[optibehavior] sessions update error', e); */ } } 28 29 // Initialize Lucide icons after rendering 29 30 if(typeof lucide !== 'undefined'){ lucide.createIcons(); } -
opti-behavior/trunk/assets/js/metabox-analytics.js
r3399182 r3401441 6 6 'use strict'; 7 7 8 document.addEventListener('DOMContentLoaded', function() { 8 // Initialize immediately when script loads (don't wait for DOMContentLoaded) 9 function initMetaboxAnalytics() { 9 10 var box = document.querySelector('.optibehavior-analytics-container'); 10 11 if (!box) return; … … 15 16 var nonce = data.nonce || box.getAttribute('data-nonce'); 16 17 var ajax = data.ajaxUrl || (typeof window.ajaxurl === 'string' ? window.ajaxurl : null); 17 18 18 19 if (!ajax || !pid || !nonce) return; 19 20 … … 61 62 var now = new Date(); 62 63 var ageInHours = (now - postDate) / (1000 * 60 * 60); 63 var defaultRange = ageInHours < 24 ? '24h' : '7d'; 64 65 // Load initial analytics data 66 fetch(ajax, { 67 method: 'POST', 68 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 69 body: 'action=optibehavior_post_analytics&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) 70 }) 71 .then(function(r) { return r.json(); }) 72 .then(function(resp) { 64 var defaultRange = '24h'; // Default to Last 24 Hours 65 66 // Load initial analytics data with minimal delay for async performance 67 setTimeout(function() { 68 fetch(ajax, { 69 method: 'POST', 70 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 71 body: 'action=optibehavior_post_analytics&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) 72 }) 73 .then(function(r) { return r.json(); }) 74 .then(function(resp) { 73 75 if (!resp || !resp.success || !resp.data) { 76 // Hide loading, show empty message 77 if (qs('#optibehavior-chart-loading')) qs('#optibehavior-chart-loading').style.display = 'none'; 74 78 qs('#optibehavior-empty').style.display = 'block'; 75 79 qs('#optibehavior-chart').style.display = 'none'; 76 80 return; 77 81 } 78 // Hide empty message and show chart when data is available 82 // Hide loading and empty message, show chart when data is available 83 if (qs('#optibehavior-chart-loading')) qs('#optibehavior-chart-loading').style.display = 'none'; 79 84 qs('#optibehavior-empty').style.display = 'none'; 80 85 qs('#optibehavior-chart').style.display = 'block'; … … 82 87 qs('#optibehavior-int').textContent = fmt(resp.data.total_interactions || 0); 83 88 qs('#optibehavior-avg').textContent = fmtDur(resp.data.avg_time); 89 qs('#optibehavior-scroll').textContent = (resp.data.avg_scroll_depth || 0) + '%'; 90 qs('#optibehavior-bounce').textContent = (resp.data.bounce_rate || 0) + '%'; 84 91 // Device split with Lucide icons 85 92 var splitEl = qs('#optibehavior-split'); 86 splitEl.innerHTML = ( resp.data.mobile_pct || 0) + '% <i data-lucide="smartphone" style="width:14px;height:14px;vertical-align:middle;"></i> / ' + (100 - (resp.data.mobile_pct || 0)) + '% <i data-lucide="monitor" style="width:14px;height:14px;vertical-align:middle;"></i>';93 splitEl.innerHTML = (100 - (resp.data.mobile_pct || 0)) + '% <i data-lucide="monitor" style="width:14px;height:14px;vertical-align:middle;"></i> / ' + (resp.data.mobile_pct || 0) + '% <i data-lucide="smartphone" style="width:14px;height:14px;vertical-align:middle;"></i>'; 87 94 // Re-initialize Lucide icons for the new elements 88 95 if (typeof lucide !== 'undefined') { … … 114 121 }) 115 122 .catch(function() { 123 // Hide loading, show empty message on error 124 if (qs('#optibehavior-chart-loading')) qs('#optibehavior-chart-loading').style.display = 'none'; 116 125 qs('#optibehavior-empty').style.display = 'block'; 126 qs('#optibehavior-chart').style.display = 'none'; 117 127 }); 128 }, 50); // 50ms delay to allow page to render first 118 129 119 130 function buildChart(range) { 120 var body = 'action=optibehavior_post_ timeseries&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) + '&range=' + encodeURIComponent(range);131 var body = 'action=optibehavior_post_analytics_chart&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) + '&range=' + encodeURIComponent(range); 121 132 fetch(ajax, { 122 133 method: 'POST', … … 134 145 var width = (container && container.clientWidth) ? container.clientWidth : (canvas.clientWidth || 600); 135 146 canvas.width = Math.max(300, width); 136 canvas.height = 2 20; // fixed height for stability147 canvas.height = 280; // increased height for better visibility 137 148 var ctx = canvas.getContext('2d'); 138 149 if (window.optibehaviorChart) { … … 148 159 labels: data.labels, 149 160 datasets: [ 150 {label: desktopLabel, data: data.pc, borderColor: '#2563eb', backgroundColor: 'rgba(37,99,235,.2)', tension: 0.25, fill: true}, 151 {label: mobileLabel, data: data.mobile, borderColor: '#16a34a', backgroundColor: 'rgba(22,163,74,.2)', tension: 0.25, fill: true} 161 // Total Visitors 162 { 163 label: 'Visitors', 164 data: data.total_visitors, 165 backgroundColor: 'rgba(249, 115, 22, 0.1)', 166 borderColor: 'rgb(249, 115, 22)', 167 borderWidth: 3, 168 tension: 0.4, 169 fill: true, 170 pointRadius: 4, 171 pointHoverRadius: 6, 172 pointBackgroundColor: 'rgb(249, 115, 22)', 173 pointBorderColor: '#fff', 174 pointBorderWidth: 2, 175 order: 1 176 }, 177 // Desktop metrics 178 { 179 label: desktopLabel + ' Visitors', 180 data: data.desktop_visitors, 181 backgroundColor: 'rgba(59, 130, 246, 0.1)', 182 borderColor: 'rgb(59, 130, 246)', 183 borderWidth: 3, 184 tension: 0.4, 185 fill: true, 186 pointRadius: 4, 187 pointHoverRadius: 6, 188 pointBackgroundColor: 'rgb(59, 130, 246)', 189 pointBorderColor: '#fff', 190 pointBorderWidth: 2, 191 order: 2 192 }, 193 { 194 label: desktopLabel + ' Events', 195 data: data.desktop_events, 196 backgroundColor: 'rgba(239, 68, 68, 0.1)', 197 borderColor: 'rgb(239, 68, 68)', 198 borderWidth: 3, 199 tension: 0.4, 200 fill: true, 201 pointRadius: 4, 202 pointHoverRadius: 6, 203 pointBackgroundColor: 'rgb(239, 68, 68)', 204 pointBorderColor: '#fff', 205 pointBorderWidth: 2, 206 order: 3 207 }, 208 // Mobile metrics 209 { 210 label: mobileLabel + ' Visitors', 211 data: data.mobile_visitors, 212 backgroundColor: 'rgba(34, 197, 94, 0.1)', 213 borderColor: 'rgb(34, 197, 94)', 214 borderWidth: 3, 215 tension: 0.4, 216 fill: true, 217 pointRadius: 4, 218 pointHoverRadius: 6, 219 pointBackgroundColor: 'rgb(34, 197, 94)', 220 pointBorderColor: '#fff', 221 pointBorderWidth: 2, 222 order: 4 223 }, 224 { 225 label: mobileLabel + ' Events', 226 data: data.mobile_events, 227 backgroundColor: 'rgba(168, 85, 247, 0.1)', 228 borderColor: 'rgb(168, 85, 247)', 229 borderWidth: 3, 230 tension: 0.4, 231 fill: true, 232 pointRadius: 4, 233 pointHoverRadius: 6, 234 pointBackgroundColor: 'rgb(168, 85, 247)', 235 pointBorderColor: '#fff', 236 pointBorderWidth: 2, 237 order: 5 238 } 152 239 ] 153 240 }, 154 241 options: { 155 242 responsive: false, 156 animation: false, 157 plugins: {legend: {display: true}, tooltip: {enabled: true}}, 158 interaction: {mode: 'nearest', intersect: false}, 159 scales: {y: {beginAtZero: true, ticks: {precision: 0}}} 243 animation: { 244 duration: 750, 245 easing: 'easeInOutQuart' 246 }, 247 plugins: { 248 legend: { 249 display: true, 250 position: 'top', 251 labels: { 252 boxWidth: 12, 253 boxHeight: 12, 254 padding: 10, 255 font: { 256 size: 11, 257 weight: '600' 258 }, 259 usePointStyle: true, 260 pointStyle: 'rect' 261 } 262 }, 263 tooltip: { 264 enabled: true, 265 mode: 'index', 266 intersect: false, 267 backgroundColor: 'rgba(0, 0, 0, 0.9)', 268 titleColor: '#fff', 269 bodyColor: '#fff', 270 borderColor: 'rgba(255, 255, 255, 0.2)', 271 borderWidth: 1, 272 padding: 12, 273 displayColors: true, 274 callbacks: { 275 title: function(context) { 276 return context[0].label || ''; 277 }, 278 label: function(context) { 279 var label = context.dataset.label || ''; 280 var value = context.parsed.y || 0; 281 return label + ': ' + fmt(value); 282 } 283 } 284 } 285 }, 286 interaction: { 287 mode: 'index', 288 intersect: false 289 }, 290 scales: { 291 x: { 292 grid: { 293 display: false 294 }, 295 ticks: { 296 font: { 297 size: 10 298 }, 299 maxRotation: 45, 300 minRotation: 0 301 } 302 }, 303 y: { 304 beginAtZero: true, 305 grid: { 306 color: 'rgba(0, 0, 0, 0.05)', 307 drawBorder: false 308 }, 309 ticks: { 310 precision: 0, 311 font: { 312 size: 10 313 }, 314 callback: function(value) { 315 return fmt(value); 316 } 317 } 318 } 319 } 160 320 } 161 321 }); … … 179 339 }); 180 340 181 // Load referrer data 182 loadReferrerData(); 183 184 // Load outbound click data 185 loadOutboundClickData(); 341 // Load secondary data with staggered delays for better performance 342 // This prevents all AJAX requests from firing simultaneously 343 setTimeout(loadReferrerData, 100); 344 setTimeout(loadOutboundClickData, 150); 345 setTimeout(loadCountriesData, 200); 346 setTimeout(loadBrowsersData, 250); 347 setTimeout(loadDevicesData, 300); 186 348 187 349 function loadReferrerData() { … … 318 480 }); 319 481 } 320 }); 482 483 // Helper function to get country ISO code from country name 484 function getCountryISO(countryName, countryCode) { 485 var countryToISO = { 486 'United States': 'us', 'US': 'us', 487 'Germany': 'de', 'DE': 'de', 488 'United Kingdom': 'gb', 'GB': 'gb', 'UK': 'gb', 489 'France': 'fr', 'FR': 'fr', 490 'India': 'in', 'IN': 'in', 491 'Netherlands': 'nl', 'NL': 'nl', 492 'China': 'cn', 'CN': 'cn', 493 'Canada': 'ca', 'CA': 'ca', 494 'Italy': 'it', 'IT': 'it', 495 'Poland': 'pl', 'PL': 'pl', 496 'Spain': 'es', 'ES': 'es', 497 'Brazil': 'br', 'BR': 'br', 498 'Australia': 'au', 'AU': 'au', 499 'Japan': 'jp', 'JP': 'jp', 500 'Russia': 'ru', 'RU': 'ru', 501 'Mexico': 'mx', 'MX': 'mx', 502 'South Korea': 'kr', 'KR': 'kr', 503 'Indonesia': 'id', 'ID': 'id', 504 'Turkey': 'tr', 'TR': 'tr', 505 'Saudi Arabia': 'sa', 'SA': 'sa', 506 'Switzerland': 'ch', 'CH': 'ch', 507 'Belgium': 'be', 'BE': 'be', 508 'Sweden': 'se', 'SE': 'se', 509 'Norway': 'no', 'NO': 'no', 510 'Denmark': 'dk', 'DK': 'dk', 511 'Finland': 'fi', 'FI': 'fi', 512 'Austria': 'at', 'AT': 'at', 513 'Portugal': 'pt', 'PT': 'pt', 514 'Greece': 'gr', 'GR': 'gr', 515 'Ireland': 'ie', 'IE': 'ie', 516 'Thailand': 'th', 'TH': 'th', 517 'South Africa': 'za', 'ZA': 'za', 518 'Unknown': 'xx' 519 }; 520 521 // If country code is already a 2-letter ISO code, use it 522 if (countryCode && countryCode.length === 2 && /^[A-Z]{2}$/i.test(countryCode)) { 523 return countryCode.toLowerCase(); 524 } 525 526 // Otherwise, look up the country name 527 return countryToISO[countryName] || countryToISO[countryCode] || 'xx'; 528 } 529 530 function loadCountriesData() { 531 fetch(ajax, { 532 method: 'POST', 533 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 534 body: 'action=optibehavior_post_countries&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) 535 }) 536 .then(function(r) { return r.json(); }) 537 .then(function(resp) { 538 var table = document.getElementById('optibehavior-countries-table'); 539 var empty = document.getElementById('optibehavior-countries-empty'); 540 541 if (resp && resp.success && resp.data && resp.data.length > 0) { 542 // Calculate total for percentage 543 var total = 0; 544 resp.data.forEach(function(item) { 545 total += item.count || 0; 546 }); 547 548 var html = '<table class="countries-table" style="width:100%;border-collapse:collapse;"><tbody>'; 549 resp.data.forEach(function(item) { 550 var countryName = item.country_name || 'Unknown'; 551 var countryCode = item.country || ''; 552 var count = item.count || 0; 553 var percentage = total > 0 ? Math.round((count / total) * 100) : 0; 554 var isoCode = getCountryISO(countryName, countryCode); 555 var flagUrl = 'https://flagcdn.com/w40/' + isoCode + '.png'; 556 557 html += '<tr style="border-bottom:1px solid #e5e7eb;">'; 558 html += '<td style="padding:8px 4px;font-size:11px;font-weight:600;color:#1e293b;">'; 559 html += '<img src="' + flagUrl + '" alt="' + countryName + '" style="width:20px;height:15px;margin-right:8px;vertical-align:middle;border-radius:2px;box-shadow:0 1px 3px rgba(0,0,0,0.1);">'; 560 html += countryName; 561 html += '</td>'; 562 html += '<td style="padding:8px 4px;text-align:right;font-size:11px;font-weight:600;">'; 563 html += '<span style="display:inline-flex;align-items:center;gap:6px;">'; 564 html += '<span style="background:#10b981;color:#fff;padding:4px 8px;border-radius:12px;font-size:11px;min-width:32px;text-align:center;">' + count + '</span>'; 565 html += '<span style="color:#64748b;font-size:11px;">' + percentage + '%</span>'; 566 html += '</span>'; 567 html += '</td>'; 568 html += '</tr>'; 569 }); 570 html += '</tbody></table>'; 571 table.innerHTML = html; 572 table.style.display = 'block'; 573 empty.classList.remove('is-visible'); 574 } else { 575 empty.classList.add('is-visible'); 576 table.style.display = 'none'; 577 // Re-initialize Lucide icons for the empty state 578 if (typeof lucide !== 'undefined') { 579 lucide.createIcons(); 580 } 581 } 582 }) 583 .catch(function() { 584 var empty = document.getElementById('optibehavior-countries-empty'); 585 empty.classList.add('is-visible'); 586 // Re-initialize Lucide icons for the empty state 587 if (typeof lucide !== 'undefined') { 588 lucide.createIcons(); 589 } 590 }); 591 } 592 593 // Helper function to get browser icon SVG 594 function getBrowserIconSVG(browserName) { 595 var name = browserName.toLowerCase(); 596 var browserIcons = { 597 'chrome': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><circle cx="12" cy="12" r="11" fill="#fff"/><path d="M12 1a11 11 0 1 0 11 11A11 11 0 0 0 12 1zm0 2a9 9 0 0 1 7.87 4.5H12a4.5 4.5 0 0 0-3.9 2.25L4.65 5.4A9 9 0 0 1 12 3z" fill="#DB4437"/><path d="M4.65 5.4l3.45 6.35A4.5 4.5 0 0 0 12 16.5a4.47 4.47 0 0 0 2.25-.6l-3.45 6A9 9 0 0 1 4.65 5.4z" fill="#0F9D58"/><path d="M19.87 7.5A9 9 0 0 1 10.8 21.9l3.45-6A4.5 4.5 0 0 0 16.5 12a4.47 4.47 0 0 0-.6-2.25z" fill="#F4B400"/><circle cx="12" cy="12" r="4.5" fill="#4285F4"/><circle cx="12" cy="12" r="3" fill="#fff"/></svg>', 598 'firefox': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><radialGradient id="ffGrad1"><stop offset="0%" stop-color="#FFBD4F"/><stop offset="100%" stop-color="#FF9640"/></radialGradient><radialGradient id="ffGrad2"><stop offset="0%" stop-color="#FF9640"/><stop offset="100%" stop-color="#E63950"/></radialGradient></defs><circle cx="12" cy="12" r="11" fill="url(#ffGrad1)"/><path d="M12 2C8 2 5 4 4 7c0 0 2-1 4-1 0-2 2-3 4-3 3 0 5 2 5 5 0 2-1 3-2 4-2 1-3 2-3 4v1h3v-1c0-1 1-2 2-3 2-1 3-3 3-5 0-4-3-7-8-7z" fill="url(#ffGrad2)"/><ellipse cx="12" cy="19" rx="2" ry="2.5" fill="#fff"/></svg>', 599 'safari': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="safGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#1AC8FC"/><stop offset="100%" stop-color="#0D66D0"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#safGrad)"/><circle cx="12" cy="12" r="9" fill="none" stroke="#fff" stroke-width="0.5"/><path d="M12 3v2M12 19v2M3 12h2M19 12h2" stroke="#fff" stroke-width="0.8"/><path d="M12 12L8 16" fill="#fff"/><path d="M12 12L16 8" fill="#E63950"/><polygon points="12,12 8,16 10,14 12,12" fill="#fff"/><polygon points="12,12 16,8 14,10 12,12" fill="#E63950"/></svg>', 600 'edge': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="edgeGrad1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#0078D4"/><stop offset="100%" stop-color="#1490DF"/></linearGradient></defs><path d="M3 12c0-2 1-4 2-5 2-2 5-4 9-4 3 0 5 1 7 3-1-2-3-3-6-3-5 0-9 4-9 9 0 3 1 5 3 7 1 1 3 2 5 2h1c-3 0-6-1-8-3-2-2-4-4-4-6z" fill="url(#edgeGrad1)"/><path d="M21 9c1 1 2 3 2 5 0 5-4 9-9 9-2 0-4-1-6-2 2 1 4 2 6 2 5 0 9-4 9-9 0-2-1-4-2-5z" fill="#2DCCFF"/><path d="M12 7c-3 0-5 2-5 5h10c0-3-2-5-5-5z" fill="#0078D4"/></svg>', 601 'opera': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><radialGradient id="opGrad"><stop offset="0%" stop-color="#FF1B2D"/><stop offset="100%" stop-color="#A02020"/></radialGradient></defs><circle cx="12" cy="12" r="11" fill="url(#opGrad)"/><ellipse cx="12" cy="12" rx="5" ry="8" fill="none" stroke="#fff" stroke-width="1.5"/></svg>', 602 'brave': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="braveGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#FB542B"/><stop offset="100%" stop-color="#CD3A1F"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#braveGrad)"/><path d="M12 4l-1 3-2-1-1 3-3 1 1 2-2 2 3 1v3l2-1 2 1v-3l3-1-2-2 1-2-3-1-1-3z" fill="#fff"/></svg>', 603 'samsung': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="samsungGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#7B4FFF"/><stop offset="100%" stop-color="#5A2FD6"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#samsungGrad)"/><path d="M8 8h8v8H8z" fill="none" stroke="#fff" stroke-width="1.5" rx="1"/><circle cx="12" cy="12" r="3" fill="#fff"/></svg>', 604 'android': '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="androidGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#A4C639"/><stop offset="100%" stop-color="#7FA82E"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#androidGrad)"/><path d="M8 9h8v6c0 1-1 2-2 2h-4c-1 0-2-1-2-2V9z" fill="#fff"/><circle cx="10" cy="11" r="0.8" fill="url(#androidGrad)"/><circle cx="14" cy="11" r="0.8" fill="url(#androidGrad)"/><path d="M9 7l-1-2M15 7l1-2" stroke="#fff" stroke-width="1" stroke-linecap="round"/></svg>' 605 }; 606 607 // Check if we have a predefined icon for this browser 608 for (var key in browserIcons) { 609 if (name.indexOf(key) !== -1) { 610 return browserIcons[key]; 611 } 612 } 613 614 // Default browser icon 615 return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="defGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#9CA3AF"/><stop offset="100%" stop-color="#6B7280"/></linearGradient></defs><circle cx="12" cy="12" r="11" fill="url(#defGrad)"/><circle cx="12" cy="12" r="8" fill="none" stroke="#fff" stroke-width="0.5"/><path d="M12 4v3M12 17v3M4 12h3M17 12h3" stroke="#fff" stroke-width="1"/><circle cx="12" cy="12" r="2" fill="#fff"/></svg>'; 616 } 617 618 function loadBrowsersData() { 619 fetch(ajax, { 620 method: 'POST', 621 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 622 body: 'action=optibehavior_post_browsers&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) 623 }) 624 .then(function(r) { return r.json(); }) 625 .then(function(resp) { 626 var table = document.getElementById('optibehavior-browsers-table'); 627 var empty = document.getElementById('optibehavior-browsers-empty'); 628 629 if (resp && resp.success && resp.data && resp.data.length > 0) { 630 // Calculate total for percentage 631 var total = 0; 632 resp.data.forEach(function(item) { 633 total += item.count || 0; 634 }); 635 636 var html = '<table class="browsers-table" style="width:100%;border-collapse:collapse;"><tbody>'; 637 resp.data.forEach(function(item) { 638 var browserName = item.name || 'Unknown'; 639 var count = item.count || 0; 640 var percentage = total > 0 ? Math.round((count / total) * 100) : 0; 641 var browserIconSVG = getBrowserIconSVG(browserName); 642 643 html += '<tr style="border-bottom:1px solid #e5e7eb;">'; 644 html += '<td style="padding:8px 4px;font-size:11px;font-weight:600;color:#1e293b;">'; 645 html += '<span style="display:inline-flex;align-items:center;gap:8px;">'; 646 html += '<span style="display:inline-block;vertical-align:middle;">' + browserIconSVG + '</span>'; 647 html += '<span>' + browserName + '</span>'; 648 html += '</span>'; 649 html += '</td>'; 650 html += '<td style="padding:8px 4px;text-align:right;font-size:11px;font-weight:600;">'; 651 html += '<span style="display:inline-flex;align-items:center;gap:6px;">'; 652 html += '<span style="background:#10b981;color:#fff;padding:4px 8px;border-radius:12px;font-size:11px;min-width:32px;text-align:center;">' + count + '</span>'; 653 html += '<span style="color:#64748b;font-size:11px;">' + percentage + '%</span>'; 654 html += '</span>'; 655 html += '</td>'; 656 html += '</tr>'; 657 }); 658 html += '</tbody></table>'; 659 table.innerHTML = html; 660 table.style.display = 'block'; 661 empty.classList.remove('is-visible'); 662 } else { 663 empty.classList.add('is-visible'); 664 table.style.display = 'none'; 665 // Re-initialize Lucide icons for the empty state 666 if (typeof lucide !== 'undefined') { 667 lucide.createIcons(); 668 } 669 } 670 }) 671 .catch(function() { 672 var empty = document.getElementById('optibehavior-browsers-empty'); 673 empty.classList.add('is-visible'); 674 // Re-initialize Lucide icons for the empty state 675 if (typeof lucide !== 'undefined') { 676 lucide.createIcons(); 677 } 678 }); 679 } 680 681 // Helper function to get device icon SVG 682 function getDeviceIconSVG(deviceName) { 683 var name = deviceName.toLowerCase(); 684 685 if (name.indexOf('desktop') !== -1) { 686 return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="desktopGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#3B82F6"/><stop offset="100%" stop-color="#2563EB"/></linearGradient></defs><rect x="2" y="4" width="20" height="12" rx="1" fill="url(#desktopGrad)"/><rect x="4" y="6" width="16" height="8" fill="#EFF6FF"/><rect x="9" y="16" width="6" height="1" fill="url(#desktopGrad)"/><rect x="7" y="17" width="10" height="2" rx="0.5" fill="url(#desktopGrad)"/></svg>'; 687 } else if (name.indexOf('mobile') !== -1 || name.indexOf('phone') !== -1) { 688 return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="mobileGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#10B981"/><stop offset="100%" stop-color="#059669"/></linearGradient></defs><rect x="6" y="2" width="12" height="20" rx="2" fill="url(#mobileGrad)"/><rect x="7" y="4" width="10" height="14" fill="#ECFDF5"/><circle cx="12" cy="20" r="1" fill="#ECFDF5"/></svg>'; 689 } else if (name.indexOf('tablet') !== -1) { 690 return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="tabletGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#F59E0B"/><stop offset="100%" stop-color="#D97706"/></linearGradient></defs><rect x="4" y="2" width="16" height="20" rx="2" fill="url(#tabletGrad)"/><rect x="5" y="4" width="14" height="15" fill="#FEF3C7"/><circle cx="12" cy="20.5" r="1" fill="#FEF3C7"/></svg>'; 691 } 692 693 // Default device icon 694 return '<svg viewBox="0 0 24 24" style="width:18px;height:18px;"><defs><linearGradient id="devDefGrad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#9CA3AF"/><stop offset="100%" stop-color="#6B7280"/></linearGradient></defs><rect x="2" y="4" width="20" height="12" rx="1" fill="url(#devDefGrad)"/><rect x="4" y="6" width="16" height="8" fill="#F3F4F6"/></svg>'; 695 } 696 697 function loadDevicesData() { 698 fetch(ajax, { 699 method: 'POST', 700 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 701 body: 'action=optibehavior_post_devices&post_id=' + encodeURIComponent(pid) + '&nonce=' + encodeURIComponent(nonce) 702 }) 703 .then(function(r) { return r.json(); }) 704 .then(function(resp) { 705 var table = document.getElementById('optibehavior-devices-table'); 706 var empty = document.getElementById('optibehavior-devices-empty'); 707 708 if (resp && resp.success && resp.data && resp.data.length > 0) { 709 // Calculate total for percentage 710 var total = 0; 711 resp.data.forEach(function(item) { 712 total += item.count || 0; 713 }); 714 715 var html = '<table class="devices-table" style="width:100%;border-collapse:collapse;"><tbody>'; 716 resp.data.forEach(function(item) { 717 var deviceName = item.name || 'Unknown'; 718 var count = item.count || 0; 719 var percentage = total > 0 ? Math.round((count / total) * 100) : 0; 720 var deviceIconSVG = getDeviceIconSVG(deviceName); 721 722 html += '<tr style="border-bottom:1px solid #e5e7eb;">'; 723 html += '<td style="padding:8px 4px;font-size:11px;font-weight:600;color:#1e293b;">'; 724 html += '<span style="display:inline-flex;align-items:center;gap:8px;">'; 725 html += '<span style="display:inline-block;vertical-align:middle;">' + deviceIconSVG + '</span>'; 726 html += '<span>' + deviceName + '</span>'; 727 html += '</span>'; 728 html += '</td>'; 729 html += '<td style="padding:8px 4px;text-align:right;font-size:11px;font-weight:600;">'; 730 html += '<span style="display:inline-flex;align-items:center;gap:6px;">'; 731 html += '<span style="background:#10b981;color:#fff;padding:4px 8px;border-radius:12px;font-size:11px;min-width:32px;text-align:center;">' + count + '</span>'; 732 html += '<span style="color:#64748b;font-size:11px;">' + percentage + '%</span>'; 733 html += '</span>'; 734 html += '</td>'; 735 html += '</tr>'; 736 }); 737 html += '</tbody></table>'; 738 table.innerHTML = html; 739 table.style.display = 'block'; 740 empty.classList.remove('is-visible'); 741 } else { 742 empty.classList.add('is-visible'); 743 table.style.display = 'none'; 744 // Re-initialize Lucide icons for the empty state 745 if (typeof lucide !== 'undefined') { 746 lucide.createIcons(); 747 } 748 } 749 }) 750 .catch(function() { 751 var empty = document.getElementById('optibehavior-devices-empty'); 752 empty.classList.add('is-visible'); 753 // Re-initialize Lucide icons for the empty state 754 if (typeof lucide !== 'undefined') { 755 lucide.createIcons(); 756 } 757 }); 758 } 759 } 760 761 // Initialize as soon as possible - try immediately, then on DOMContentLoaded, then on load 762 if (document.readyState === 'loading') { 763 // DOM is still loading, wait for DOMContentLoaded 764 document.addEventListener('DOMContentLoaded', initMetaboxAnalytics); 765 } else { 766 // DOM is already loaded, initialize immediately 767 initMetaboxAnalytics(); 768 } 321 769 })(); 322 770 -
opti-behavior/trunk/assets/js/opti-behavior-heatmap-simple.js
r3399182 r3401441 17 17 18 18 function init() { 19 console.log('[Heatmap] init() called'); 20 19 21 // Check if we have the required configuration 20 22 if (typeof window.opti_behavior_heatmap === 'undefined') { 21 //console.error('[Heatmap] Configuration not found');23 console.error('[Heatmap] Configuration not found'); 22 24 return; 23 25 } 24 26 27 console.log('[Heatmap] Config found:', window.opti_behavior_heatmap); 28 25 29 const config = window.opti_behavior_heatmap; 26 30 27 31 // Only initialize in reporter mode 28 32 if (config._mode !== 'reporter') { 33 console.log('[Heatmap] Not in reporter mode, mode is:', config._mode); 29 34 return; 30 35 } 31 36 37 console.log('[Heatmap] Reporter mode confirmed'); 38 32 39 // Check if click tracking is enabled 33 40 const reports = Array.isArray(config.reports) ? config.reports : config.reports.split(','); 34 41 const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 35 42 const clickEvent = isMobile ? 'click_mobile' : 'click_pc'; 36 43 44 console.log('[Heatmap] Reports:', reports); 45 console.log('[Heatmap] isMobile:', isMobile); 46 console.log('[Heatmap] Looking for event:', clickEvent); 47 console.log('[Heatmap] Event included?', reports.includes(clickEvent)); 48 37 49 if (!reports.includes(clickEvent)) { 38 // if (config.debug) { 39 // console.log('[Heatmap] Click tracking not enabled for this device type'); 40 // } 50 console.log('[Heatmap] Click tracking not enabled for this device type'); 41 51 return; 42 52 } 43 53 54 console.log('[Heatmap] Initializing tracker...'); 55 44 56 // Initialize click tracking 45 57 const tracker = new HeatmapTracker(config); 46 58 tracker.start(); 47 59 48 // if (config.debug) { 49 // console.log('[Heatmap] Tracker initialized', config); 50 // } 60 console.log('[Heatmap] Tracker initialized', config); 51 61 } 52 62 … … 62 72 } 63 73 74 /** 75 * Get cookie value by name 76 */ 77 HeatmapTracker.prototype.getCookie = function(name) { 78 const value = `; ${document.cookie}`; 79 const parts = value.split(`; ${name}=`); 80 if (parts.length === 2) { 81 return parts.pop().split(';').shift(); 82 } 83 return null; 84 }; 85 64 86 HeatmapTracker.prototype.start = function() { 87 console.log('[Heatmap] start() called'); 88 65 89 // Attach click event listener 66 90 document.addEventListener('click', this.handleClick.bind(this), true); 67 91 92 // Attach outbound link click tracker 93 document.addEventListener('click', this.handleOutboundClick.bind(this), true); 94 68 95 // Start periodic send timer 69 96 this.startSendTimer(); 70 97 71 98 // Send data before page unload 72 99 window.addEventListener('beforeunload', this.sendData.bind(this, true)); 73 100 74 // if (this.config.debug) { 75 // console.log('[Heatmap] Click tracking started'); 76 // } 101 console.log('[Heatmap] Click tracking started'); 77 102 }; 78 103 79 104 HeatmapTracker.prototype.handleClick = function(event) { 105 console.log('[Heatmap] Click detected!', event); 106 80 107 // Get click coordinates 81 108 const x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)); 82 109 const y = event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)); 83 110 84 111 // Get page dimensions 85 112 const width = Math.max( … … 93 120 document.documentElement.clientHeight 94 121 ); 95 122 96 123 // Create event data 97 124 const eventData = { … … 104 131 time: 0 // Will be calculated relative to send time 105 132 }; 106 133 107 134 // Add to queue 108 135 this.eventQueue.push(eventData); 109 136 110 // if (this.config.debug) { 111 // console.log('[Heatmap] Click recorded:', eventData); 112 // } 137 console.log('[Heatmap] Click recorded:', eventData); 138 console.log('[Heatmap] Queue length:', this.eventQueue.length); 113 139 114 140 // Check if we should send immediately … … 121 147 } 122 148 }; 123 149 150 HeatmapTracker.prototype.handleOutboundClick = function(event) { 151 // Check if click was on a link or within a link 152 let target = event.target; 153 let link = null; 154 155 // Traverse up the DOM tree to find a link element 156 while (target && target !== document) { 157 if (target.tagName === 'A' && target.href) { 158 link = target; 159 break; 160 } 161 target = target.parentElement; 162 } 163 164 // If no link found, return 165 if (!link) { 166 return; 167 } 168 169 // Get the target URL 170 const targetUrl = link.href; 171 const sourceUrl = window.location.href; 172 173 // Get click coordinates 174 const x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)); 175 const y = event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)); 176 177 // Get element details 178 const elementTag = link.tagName.toLowerCase(); 179 const elementId = link.id || null; 180 const elementClass = link.className || null; 181 const elementText = link.textContent ? link.textContent.trim().substring(0, 500) : null; 182 183 // Prepare outbound click data 184 const outboundData = { 185 type: 'outbound_click', 186 sourceUrl: sourceUrl, 187 targetUrl: targetUrl, 188 elementTag: elementTag, 189 elementId: elementId, 190 elementClass: elementClass, 191 elementText: elementText, 192 x: Math.floor(x), 193 y: Math.floor(y) 194 }; 195 196 console.log('[Heatmap] Outbound click detected:', outboundData); 197 198 // Send outbound click data immediately 199 this.sendOutboundClick(outboundData); 200 }; 201 202 HeatmapTracker.prototype.sendOutboundClick = function(data) { 203 console.log('[Heatmap] Sending outbound click:', data); 204 205 // Get session and visitor IDs from cookies 206 const sessionId = this.getCookie('optibehavior_sid') || this.getCookie('opti_behavior_session_id') || ''; 207 const visitorId = this.getCookie('optibehavior_vid') || this.getCookie('opti_behavior_visitor_id') || ''; 208 209 // Prepare JSON data for session recording endpoint 210 const payload = { 211 action: 'outbound_click', 212 session_id: sessionId, 213 visitor_id: visitorId, 214 sourceUrl: data.sourceUrl, 215 targetUrl: data.targetUrl, 216 elementTag: data.elementTag, 217 elementId: data.elementId || null, 218 elementClass: data.elementClass || null, 219 elementText: data.elementText || null, 220 x: data.x, 221 y: data.y 222 }; 223 224 // Prepare form data for session recording endpoint 225 const formData = new FormData(); 226 formData.append('action', 'opti_behavior_heatmap_record'); 227 formData.append('nonce', this.config.nonce); 228 formData.append('data', JSON.stringify(payload)); 229 230 console.log('[Heatmap] Outbound click payload:', payload); 231 232 // Use sendBeacon for outbound clicks (more reliable when navigating away) 233 // Fallback to fetch if sendBeacon is not supported 234 if (navigator.sendBeacon) { 235 try { 236 const sent = navigator.sendBeacon(this.config.ajax_url, formData); 237 console.log('[Heatmap] Outbound click sent via sendBeacon:', sent); 238 } catch(e) { 239 console.error('[Heatmap] sendBeacon error, falling back to fetch:', e); 240 // Fallback to fetch 241 fetch(this.config.ajax_url, { 242 method: 'POST', 243 body: formData, 244 mode: 'same-origin', 245 cache: 'no-cache', 246 keepalive: true 247 }).catch(function(error) { 248 console.error('[Heatmap] Outbound click fetch error:', error); 249 }); 250 } 251 } else { 252 // Fallback to fetch with keepalive 253 fetch(this.config.ajax_url, { 254 method: 'POST', 255 body: formData, 256 mode: 'same-origin', 257 cache: 'no-cache', 258 keepalive: true 259 }).then(function(response) { 260 console.log('[Heatmap] Outbound click sent via fetch, status:', response.status); 261 return response.json(); 262 }).then(function(responseData) { 263 console.log('[Heatmap] Outbound click response:', responseData); 264 }).catch(function(error) { 265 console.error('[Heatmap] Outbound click error:', error); 266 }); 267 } 268 }; 269 124 270 HeatmapTracker.prototype.startSendTimer = function() { 125 271 // Clear existing timer … … 137 283 138 284 HeatmapTracker.prototype.sendData = function(isUnload) { 285 console.log('[Heatmap] sendData() called, isUnload:', isUnload); 286 console.log('[Heatmap] Queue length:', this.eventQueue.length); 287 139 288 if (this.eventQueue.length === 0) { 289 console.log('[Heatmap] No events to send'); 140 290 return; 141 291 } … … 144 294 const eventsToSend = this.eventQueue.splice(0); 145 295 296 console.log('[Heatmap] Sending events:', eventsToSend); 297 146 298 // Update last send time 147 299 this.lastSendTime = Date.now(); 300 301 // Get session and visitor IDs from cookies 302 const sessionId = this.getCookie('optibehavior_sid') || this.getCookie('opti_behavior_session_id'); 303 const visitorId = this.getCookie('optibehavior_vid') || this.getCookie('opti_behavior_visitor_id'); 304 305 console.log('[Heatmap] Session ID:', sessionId); 306 console.log('[Heatmap] Visitor ID:', visitorId); 148 307 149 308 // Prepare form data … … 153 312 formData.append('url', window.location.href); 154 313 formData.append('title', document.title); 314 315 // Add session and visitor IDs if available 316 if (sessionId) { 317 formData.append('session_id', sessionId); 318 } 319 if (visitorId) { 320 formData.append('visitor_id', visitorId); 321 } 322 323 console.log('[Heatmap] AJAX config:', { 324 action: this.config.action, 325 nonce: this.config.nonce, 326 ajax_url: this.config.ajax_url, 327 url: window.location.href, 328 title: document.title 329 }); 155 330 156 331 // Add screen resolution data for visitor tracking … … 212 387 if (isUnload && navigator.sendBeacon) { 213 388 // Use sendBeacon for unload events 389 console.log('[Heatmap] Using sendBeacon'); 214 390 navigator.sendBeacon(this.config.ajax_url, formData); 215 391 } else { 216 392 // Use fetch for normal sends 393 console.log('[Heatmap] Using fetch to send data'); 217 394 fetch(this.config.ajax_url, { 218 395 method: 'POST', … … 222 399 keepalive: isUnload 223 400 }).then(function(response) { 224 // if (this.config.debug) { 225 // console.log('[Heatmap] Server response:', response.status); 226 // } 401 console.log('[Heatmap] Server response status:', response.status); 227 402 return response.json(); 228 403 }.bind(this)).then(function(data) { 229 // if (this.config.debug) { 230 // console.log('[Heatmap] Server data:', data); 231 // } 404 console.log('[Heatmap] Server response data:', data); 232 405 }.bind(this)).catch(function(error) { 233 //console.error('[Heatmap] Send error:', error);406 console.error('[Heatmap] Send error:', error); 234 407 }); 235 408 } -
opti-behavior/trunk/includes/class-opti-behavior-heatmap-database.php
r3399182 r3401441 334 334 id varchar(64) NOT NULL, 335 335 visitor_id varchar(64) NOT NULL, 336 user_id bigint(20) UNSIGNED DEFAULT NULL, 336 337 start_time datetime NOT NULL, 337 338 end_time datetime DEFAULT NULL, … … 354 355 PRIMARY KEY (id), 355 356 KEY visitor_id (visitor_id), 357 KEY user_id (user_id), 356 358 KEY start_time (start_time), 357 359 KEY is_bounce (is_bounce), -
opti-behavior/trunk/includes/class-opti-behavior-license-manager.php
r3399182 r3401441 16 16 /** 17 17 * API base URL 18 * TODO: Replace with production API URL before release 19 */ 20 const API_URL = 'https://api.opti-behavior.com/v1/'; 18 */ 19 const API_URL = 'https://api.opti-behavior.com/'; 21 20 22 21 /** … … 115 114 update_option( self::OPTION_REGISTRATION_DATE, current_time( 'mysql' ) ); 116 115 116 // Store master_secret for API authentication (encrypted) 117 if ( isset( $response['data']['master_secret'] ) ) { 118 $encrypted_master_secret = $this->encrypt_secret_key( $response['data']['master_secret'] ); 119 update_option( 'opti_behavior_master_secret', $response['data']['master_secret'] ); 120 $this->debug_manager->log( 'Master secret stored for API authentication', 'info', 'license' ); 121 } 122 117 123 // Clear sensitive data from memory (only if native extension is available) 118 124 if ( extension_loaded( 'sodium' ) ) { … … 220 226 public function call_api( $endpoint, $data = array(), $method = 'GET' ) { 221 227 $url = self::API_URL . $endpoint; 228 $installation_id = $this->get_installation_id(); 229 $timestamp = time(); 230 231 // Prepare request data for signature 232 if ( $method === 'POST' ) { 233 $data_to_sign = wp_json_encode( $data, JSON_UNESCAPED_SLASHES ); 234 } else { 235 // Sort GET parameters for consistent signing 236 ksort( $data ); 237 $data_to_sign = wp_json_encode( $data, JSON_UNESCAPED_SLASHES ); 238 } 239 240 // Generate HMAC signature (only if registered) 241 $signature = ''; 242 if ( ! empty( $installation_id ) && $endpoint !== 'register' ) { 243 $signature = $this->generate_api_signature( $data_to_sign, $timestamp ); 244 } 222 245 223 246 $args = array( … … 225 248 'headers' => array( 226 249 'Content-Type' => 'application/json', 250 'X-OptiBe-Installation-ID' => $installation_id, 251 'X-OptiBe-Signature' => $signature, 252 'X-OptiBe-Timestamp' => $timestamp, 227 253 ), 228 254 ); … … 247 273 return $decoded; 248 274 } 275 276 /** 277 * Generate HMAC signature for API request 278 * 279 * @param string $data Request data (JSON string) 280 * @param int $timestamp Request timestamp 281 * @return string HMAC-SHA256 signature 282 */ 283 private function generate_api_signature( $data, $timestamp ) { 284 $master_secret = get_option( 'opti_behavior_master_secret', '' ); 285 286 if ( empty( $master_secret ) ) { 287 $this->debug_manager->log( 'Master secret not found - cannot sign request', 'warning', 'license' ); 288 return ''; 289 } 290 291 // Normalize data 292 $decoded = json_decode( $data, true ); 293 if ( $decoded !== null && is_array( $decoded ) ) { 294 ksort( $decoded ); 295 $normalized = json_encode( $decoded, JSON_UNESCAPED_SLASHES ); 296 } else { 297 $normalized = trim( $data ); 298 } 299 300 // Generate signature: HMAC-SHA256(data|timestamp, master_secret) 301 $message = $normalized . '|' . $timestamp; 302 return hash_hmac( 'sha256', $message, $master_secret ); 303 } 249 304 250 305 /** … … 305 360 306 361 /** 307 * Increment quota usage 308 * 309 * This is called after successfully saving a recording 310 * The API will track the actual usage, this is just a local notification 311 */ 312 public function increment_quota_usage() { 313 // The API tracks usage automatically when we request decrypt keys 314 // This method is here for future local tracking if needed 315 $this->debug_manager->log( 'Quota usage incremented', 'debug', 'license' ); 362 * Increment quota usage by recording a session 363 * 364 * CRITICAL: This is called after successfully saving a recording 365 * This is the PRIMARY quota enforcement mechanism - prevents bypass attempts 366 * 367 * @param string $session_id Session ID (optional, for logging) 368 * @return bool Success 369 */ 370 public function increment_quota_usage( $session_id = '' ) { 371 $installation_id = $this->get_installation_id(); 372 373 if ( empty( $installation_id ) ) { 374 $this->debug_manager->log( 'Cannot increment quota: installation_id is empty', 'error', 'license' ); 375 return false; 376 } 377 378 // Call API to increment sessions_recorded counter 379 $response = $this->call_api( 'record-session', array( 380 'installation_id' => $installation_id, 381 'session_id' => $session_id, 382 ), 'POST' ); 383 384 if ( ! $response || ! isset( $response['success'] ) || ! $response['success'] ) { 385 $error_msg = isset( $response['error'] ) ? $response['error'] : 'unknown'; 386 $this->debug_manager->log( 'Failed to increment quota: ' . $error_msg, 'error', 'license' ); 387 return false; 388 } 389 390 $this->debug_manager->log( 'Quota incremented successfully. Session: ' . $session_id, 'info', 'license' ); 391 return true; 316 392 } 317 393 } -
opti-behavior/trunk/includes/trait-opti-behavior-ai-insights-views.php
r3399182 r3401441 175 175 <span class="analyze-text"><?php esc_html_e( 'Revenue Impact', 'opti-behavior' ); ?></span> 176 176 </div> 177 </div> 178 </div> 179 180 <!-- Feedback Section --> 177 <div class="analyze-item"> 178 <span class="analyze-icon"><i data-lucide="target"></i></span> 179 <span class="analyze-text"><?php esc_html_e( 'Conversion Goals', 'opti-behavior' ); ?></span> 180 </div> 181 <div class="analyze-item"> 182 <span class="analyze-icon"><i data-lucide="users"></i></span> 183 <span class="analyze-text"><?php esc_html_e( 'Audience Segments', 'opti-behavior' ); ?></span> 184 </div> 185 </div> 186 </div> 187 188 <!-- Support Contact Section --> 181 189 <div class="ai-feedback-section"> 182 190 <div class="feedback-card"> 183 <div class="feedback-icon"> 💬</div>184 <h2 class="feedback-title"><?php esc_html_e( ' Help Shape the Future!', 'opti-behavior' ); ?></h2>191 <div class="feedback-icon">💬</div> 192 <h2 class="feedback-title"><?php esc_html_e( 'Need Help or Have Questions?', 'opti-behavior' ); ?></h2> 185 193 <p class="feedback-description"> 186 <?php esc_html_e( 'We \'re building this amazing AI-powered feature and we\'d love to hear from you! Your experience and feedback are invaluable in making this the best analytics toolpossible.', 'opti-behavior' ); ?>194 <?php esc_html_e( 'We are here to help! Send us a message and our support team will get back to you as soon as possible.', 'opti-behavior' ); ?> 187 195 </p> 188 <div class="feedback-actions"> 189 <a href="mailto:[email protected]?subject=AI%20Insights%20Feedback" class="feedback-btn primary"> 190 <span>📧</span> 191 <?php esc_html_e( 'Share Your Ideas', 'opti-behavior' ); ?> 192 </a> 193 194 </div> 196 <?php 197 $current_user = wp_get_current_user(); 198 $user_email = $current_user->user_email ? $current_user->user_email : ''; 199 ?> 200 201 <form id="opti-behavior-support-form" class="support-form"> 202 <div class="form-group"> 203 <label for="support-name">Your Name</label> 204 <input type="text" id="support-name" name="support_name" required class="form-control" /> 205 </div> 206 207 <div class="form-group"> 208 <label for="support-email">Your Email</label> 209 <input type="email" id="support-email" name="support_email" value="<?php echo esc_attr( $user_email ); ?>" required class="form-control" /> 210 <small class="form-hint">We will use this to respond to your message</small> 211 </div> 212 213 <div class="form-group"> 214 <label for="support-subject">Subject</label> 215 <input type="text" id="support-subject" name="support_subject" required class="form-control" /> 216 </div> 217 218 <div class="form-group"> 219 <label for="support-message">Message</label> 220 <textarea id="support-message" name="support_message" rows="5" required class="form-control"></textarea> 221 </div> 222 223 <div class="form-actions"> 224 <button type="submit" class="feedback-btn primary"> 225 <span>📧</span> 226 Send Message 227 </button> 228 </div> 229 230 <div id="opti-support-response" class="support-response support-response-hidden"></div> 231 </form> 232 195 233 <p class="feedback-note"> 196 234 <?php 197 235 printf( 198 /* translators: %1$s: opening strong tag, %2$s: closing strong tag, %3$s: opening strong tag, %4$s: closing strong tag */ 199 esc_html__( '%1$sEarly adopters%2$s who share feedback will get %3$sexclusive early access%4$s when we launch! 🎉', 'opti-behavior' ), 200 '<strong>', 201 '</strong>', 202 '<strong>', 203 '</strong>' 236 /* translators: %s: support email address */ 237 esc_html__( 'You can also reach us directly at %s', 'opti-behavior' ), 238 '<a href="mailto:[email protected]">[email protected]</a>' 204 239 ); 205 240 ?> -
opti-behavior/trunk/includes/trait-opti-behavior-ajax-handlers.php
r3399182 r3401441 255 255 $sessions_table = esc_sql( $wpdb->prefix . 'optibehavior_sessions' ); 256 256 $visitors_table = esc_sql( $wpdb->prefix . 'optibehavior_visitors' ); 257 $users_table = esc_sql( $wpdb->users ); 257 258 258 259 // Aggregate engagement metrics per visitor … … 261 262 "SELECT 262 263 s.visitor_id, 264 s.user_id, 263 265 COUNT(*) AS sessions, 264 266 COUNT(DISTINCT DATE(s.start_time)) AS active_days, … … 268 270 MAX(COALESCE(s.end_time, s.start_time)) AS last_seen, 269 271 UPPER(COALESCE(NULLIF(TRIM(v.country),''), '')) AS country_code, 270 v.country_name 272 v.country_name, 273 u.user_login, 274 u.display_name 271 275 FROM {$sessions_table} s 272 276 LEFT JOIN {$visitors_table} v ON v.id = s.visitor_id 277 LEFT JOIN {$users_table} u ON u.ID = s.user_id 273 278 WHERE s.start_time BETWEEN %s AND %s 274 GROUP BY s.visitor_id, v.country, v.country_name279 GROUP BY s.visitor_id, s.user_id, v.country, v.country_name, u.user_login, u.display_name 275 280 ORDER BY total_time DESC 276 281 LIMIT 50", … … 310 315 } 311 316 317 // Check if this visitor is a WordPress user 318 $user_id = !empty($r->user_id) ? (int)$r->user_id : 0; 319 $user_login = !empty($r->user_login) ? (string)$r->user_login : ''; 320 $display_name = !empty($r->display_name) ? (string)$r->display_name : ''; 321 $profile_url = $user_id > 0 ? admin_url('user-edit.php?user_id=' . $user_id) : ''; 322 312 323 $items[] = array( 313 324 'visitor_id' => $vid, 314 325 'visitor_short' => $short, 326 'user_id' => $user_id, 327 'user_login' => $user_login, 328 'display_name' => $display_name, 329 'profile_url' => $profile_url, 315 330 'sessions' => (int)$r->sessions, 316 331 'pages_per_session'=> (float)$r->pages_per_session, -
opti-behavior/trunk/includes/trait-opti-behavior-assets.php
r3399182 r3401441 143 143 OPTI_BEHAVIOR_HEATMAP_ASSETS_URL . 'js/heatmaps.js', 144 144 array(), // No dependencies 145 OPTI_BEHAVIOR_HEATMAP_VERSION ,145 OPTI_BEHAVIOR_HEATMAP_VERSION . '.' . time(), 146 146 true 147 147 ); … … 155 155 array( 'opti-behavior-dashboard' ), 156 156 OPTI_BEHAVIOR_HEATMAP_VERSION 157 ); 158 159 // Enqueue support form script 160 wp_enqueue_script( 161 'opti-behavior-support-form', 162 OPTI_BEHAVIOR_HEATMAP_ASSETS_URL . 'js/support-form.js', 163 array(), 164 OPTI_BEHAVIOR_HEATMAP_VERSION . '-v3', 165 true 166 ); 167 168 // Pass AJAX URL, nonce, and translated strings to support form script 169 wp_localize_script( 170 'opti-behavior-support-form', 171 'optiBehaviorSupport', 172 array( 173 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 174 'nonce' => wp_create_nonce( 'opti_behavior_support_email' ), 175 'i18n' => array( 176 'sending' => __( 'Sending...', 'opti-behavior' ), 177 'sendMessage' => __( 'Send Message', 'opti-behavior' ), 178 'error' => __( 'An error occurred. Please try again.', 'opti-behavior' ), 179 ), 180 ) 157 181 ); 158 182 } -
opti-behavior/trunk/includes/trait-opti-behavior-heatmaps-views.php
r3399182 r3401441 57 57 <div class="stat-number"><?php echo esc_html( $stats['total_heatmaps'] ); ?></div> 58 58 <div class="stat-label"><?php esc_html_e( 'Total Heatmaps', 'opti-behavior' ); ?></div> 59 <div class="stat-change positive">+<?php echo esc_html( $stats['heatmaps_growth'] ); ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div> 59 <div class="stat-change <?php echo $stats['heatmaps_growth'] >= 0 ? 'positive' : 'negative'; ?>"><?php 60 $change_text = $stats['heatmaps_growth'] > 0 ? '+' . $stats['heatmaps_growth'] : $stats['heatmaps_growth']; 61 echo esc_html( $change_text ); 62 ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div> 60 63 </div> 61 64 </div> … … 66 69 <div class="stat-number"><?php echo esc_html( number_format( $stats['total_clicks'] ) ); ?></div> 67 70 <div class="stat-label"><?php esc_html_e( 'Total Clicks', 'opti-behavior' ); ?></div> 68 <div class="stat-change positive">+<?php echo esc_html( $stats['clicks_growth'] ); ?>% <?php esc_html_e( 'vs last period', 'opti-behavior' ); ?></div> 71 <div class="stat-change <?php echo $stats['clicks_growth'] >= 0 ? 'positive' : 'negative'; ?>"><?php 72 $change_text = $stats['clicks_growth'] > 0 ? '+' . $stats['clicks_growth'] : $stats['clicks_growth']; 73 echo esc_html( $change_text ); 74 ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div> 69 75 </div> 70 76 </div> … … 75 81 <div class="stat-number"><?php echo esc_html( $stats['mobile_percentage'] ); ?>%</div> 76 82 <div class="stat-label"><?php esc_html_e( 'Mobile Traffic', 'opti-behavior' ); ?></div> 77 <div class="stat-change neutral"><?php echo esc_html( $stats['mobile_change'] ); ?>% <?php esc_html_e( 'mobile vs desktop', 'opti-behavior' ); ?></div> 83 <div class="stat-change <?php echo $stats['mobile_change'] >= 0 ? 'positive' : 'negative'; ?>"><?php 84 $change_text = $stats['mobile_change'] > 0 ? '+' . $stats['mobile_change'] : $stats['mobile_change']; 85 echo esc_html( $change_text ); 86 ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div> 78 87 </div> 79 88 </div> … … 84 93 <div class="stat-number"><?php echo esc_html( $stats['avg_time'] ); ?>s</div> 85 94 <div class="stat-label"><?php esc_html_e( 'Avg. Time on Page', 'opti-behavior' ); ?></div> 86 <div class="stat-change positive">+<?php echo esc_html( $stats['time_improvement'] ); ?>% <?php esc_html_e( 'improvement', 'opti-behavior' ); ?></div> 95 <div class="stat-change <?php echo $stats['time_improvement'] >= 0 ? 'positive' : 'negative'; ?>"><?php 96 $change_text = $stats['time_improvement'] > 0 ? '+' . $stats['time_improvement'] : $stats['time_improvement']; 97 echo esc_html( $change_text ); 98 ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div> 87 99 </div> 88 100 </div> … … 102 114 <div class="stat-number"><?php echo esc_html( $stats['conversion_rate'] ); ?>%</div> 103 115 <div class="stat-label"><?php esc_html_e( 'Click-through Rate', 'opti-behavior' ); ?></div> 104 <div class="stat-change positive">+<?php echo esc_html( $stats['ctr_improvement'] ); ?>% <?php esc_html_e( 'this month', 'opti-behavior' ); ?></div> 116 <div class="stat-change <?php echo $stats['ctr_improvement'] >= 0 ? 'positive' : 'negative'; ?>"><?php 117 $change_text = $stats['ctr_improvement'] > 0 ? '+' . $stats['ctr_improvement'] : $stats['ctr_improvement']; 118 echo esc_html( $change_text ); 119 ?>% <?php esc_html_e( 'this week', 'opti-behavior' ); ?></div> 105 120 </div> 106 121 </div> -
opti-behavior/trunk/includes/trait-opti-behavior-sessions-views.php
r3399182 r3401441 216 216 <?php 217 217 } 218 219 /** 220 * Get sample sessions data for display. 221 * 222 * @since 1.0.4 223 * @return array Sample sessions data. 224 */ 225 private function get_sample_sessions_data() { 226 return array( 227 array( 228 'id' => '6e7d6c5b4a39', 229 'date' => gmdate( 'Y-m-d H:i:s' ), 230 'duration' => '02:34', 231 'alarming_count' => '3', 232 'pages_count' => '5', 233 'pages' => array( '/home', '/products', '/cart', '/checkout', '/thank-you' ), 234 'visitor_type' => __( 'New Visitor', 'opti-behavior' ), 235 'country' => __( 'United States', 'opti-behavior' ), 236 'flag' => '🇺🇸', 237 'referrer' => 'Google', 238 ), 239 array( 240 'id' => '5f6e5d4c3b28', 241 'date' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 hour' ) ), 242 'duration' => '01:45', 243 'alarming_count' => '1', 244 'pages_count' => '3', 245 'pages' => array( '/home', '/about', '/contact' ), 246 'visitor_type' => __( 'Returning', 'opti-behavior' ), 247 'country' => __( 'Canada', 'opti-behavior' ), 248 'flag' => '🇨🇦', 249 'referrer' => __( 'Direct', 'opti-behavior' ), 250 ), 251 ); 252 } 218 253 } 219 254 } -
opti-behavior/trunk/readme.txt
r3399182 r3401441 1 1 === Opti-Behavior === 2 2 Contributors: optiuser 3 Tags: analytics, heatmap, user tracking, session recording, behavior analytics 3 Donate link: https://optiuser.com/ 4 Tags: analytics, heatmap, click tracking, user behavior,user insights 5 4 6 Requires at least: 5.8 5 7 Tested up to: 6.8 6 8 Requires PHP: 7.4 7 Stable tag: 1.0. 49 Stable tag: 1.0.5 8 10 License: GPLv2 or later 9 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 12 11 Transform your WordPress site with powerful analytics! Track user behavior with beautiful heatmaps, session recordings, and real-time insights.13 Powerful website analytics with visual heatmaps, click tracking, and real-time visitor insights. Optimize conversions and improve user experience with data-driven decisions. 12 14 13 15 == Description == 14 16 15 Opti-Behavior is a comprehensive user behavior analytics plugin for WordPress that helps you understand how visitors interact with your website. Make data-driven decisions to improve user experience and boost conversions. 16 17 = Key Features = 18 19 * **Visual Heatmaps** - See where users click, move, and scroll on your pages with separate mobile and desktop views 20 * **Session Recordings** - Watch real user sessions to understand behavior patterns 21 * **Real-time Analytics** - Monitor visitor activity as it happens 22 * **Page Performance Tracking** - Identify high-performing and underperforming pages 23 * **User Journey Analysis** - Track visitor paths through your site 24 * **Bot Detection & Tracking** - Automatic server-side bot detection to filter out non-human traffic 25 * **File Storage System** - Optional file-based storage for high-traffic sites to prevent database bloat 26 * **Performance Optimizer** - Automatic database indexing and optimization for large datasets 27 * **Mobile/Desktop Separation** - Separate heatmap tracking and visualization for mobile and desktop devices 28 * **Privacy-Focused** - GDPR compliant with built-in data protection features 29 * **Lightweight & Fast** - Optimized code that won't slow down your site 30 * **Easy to Use** - Intuitive dashboard with beautiful visualizations 17 **Opti-Behavior** is the ultimate WordPress analytics plugin that reveals exactly how visitors interact with your website. Understand user behavior through powerful visual heatmaps, detailed click tracking, and comprehensive analytics - all stored securely on your own server. 18 19 Unlike external analytics services, Opti-Behavior keeps all your data local, ensuring complete privacy compliance and lightning-fast performance. Perfect for marketers, designers, and business owners who want to optimize their websites based on real user behavior data. 20 21 = Why Choose Opti-Behavior? = 22 23 ✓ **100% Privacy-Friendly** - All data stays on your WordPress server (GDPR compliant) 24 ✓ **No External Dependencies** - No API calls to third-party services for analytics 25 ✓ **Beautiful Visualizations** - Easy-to-understand heatmaps and dashboards 26 ✓ **Mobile & Desktop Separation** - Analyze each platform independently 27 ✓ **High Performance** - Optimized for sites with millions of pageviews 28 ✓ **Bot Filtering** - Automatic detection and exclusion of bot traffic 29 30 = Core Features (Free Version) = 31 32 * **Click Heatmaps** - Visualize where users click on your pages with color-coded heatmaps 33 * **Mobile & Desktop Views** - Separate tracking and visualization for different devices 34 * **Real-time Dashboard** - Monitor active visitors and their behavior live 35 * **Page Analytics** - Track pageviews, average time on page, and engagement metrics 36 * **Session Tracking** - Follow user journeys across your website 37 * **Bot Detection** - Server-side detection of search engines and bot traffic 38 * **Performance Optimized** - File-based storage option for high-traffic sites 39 * **Privacy Controls** - IP anonymization, data retention settings, and GDPR compliance tools 40 * **Beautiful UI** - Modern, intuitive interface with dark mode support 41 * **Export Data** - Download your analytics in CSV format 42 43 = Pro Features (Upgrade) = 44 45 * **Session Recordings** - Watch real user sessions to see exactly how visitors use your site 46 * **Advanced Heatmaps** - Attention heatmaps, scroll maps, and breakaway analysis 47 * **Unlimited Pages** - Track as many pages as you need (free limited to 5 pages) 48 * **Extended Retention** - Keep data for up to 2 years (free limited to 7 days) 49 * **Priority Support** - Get help when you need it 50 51 [Learn more about Opti-Behavior Pro](https://optiuser.com/) 31 52 32 53 = Perfect For = 33 54 34 * E-commerce sites looking to optimize conversion funnels 35 * Content creators wanting to understand reader engagement 36 * Web designers testing new layouts and designs 37 * Marketing teams analyzing campaign effectiveness 38 * Anyone who wants to improve their website's user experience 55 * **E-commerce Stores** - Optimize checkout flows and product pages to increase sales 56 * **Content Publishers** - Understand which content keeps readers engaged 57 * **Web Agencies** - Provide clients with detailed user behavior reports 58 * **SaaS Companies** - Improve onboarding and reduce churn with behavior insights 59 * **Marketing Teams** - Test landing pages and measure campaign effectiveness 60 * **UX Designers** - Validate design decisions with real user data 61 * **Small Businesses** - Make informed decisions without expensive analytics tools 62 63 = How It Works = 64 65 1. **Install & Activate** - Simple one-click installation, no configuration needed 66 2. **Automatic Tracking** - Starts collecting data immediately after activation 67 3. **View Insights** - Access beautiful dashboards from your WordPress admin 68 4. **Optimize** - Use data-driven insights to improve your website 69 5. **Export & Share** - Download reports or share insights with your team 39 70 40 71 = Privacy & Data Protection = 41 72 42 Opti-Behavior takes privacy seriously: 43 44 * GDPR compliant data collection 45 * Configurable data retention periods 46 * Option to anonymize IP addresses 47 * Respect for Do Not Track headers 48 * User consent management 49 * Secure data storage 50 * Complete data deletion on uninstall (optional) 51 * All data stored locally in your WordPress database 52 * No external tracking or data sharing with third parties 73 Privacy is built into Opti-Behavior from the ground up: 74 75 * **GDPR Compliant** - Full compliance with EU privacy regulations 76 * **Local Data Storage** - All analytics data stays in your WordPress database 77 * **IP Anonymization** - Option to anonymize visitor IP addresses 78 * **Data Retention Control** - Set automatic cleanup periods for old data 79 * **No Third-Party Tracking** - Analytics JavaScript is served from your domain 80 * **Consent Management** - Easy integration with consent management plugins 81 * **Data Export** - Users can request their data in portable format 82 * **Complete Deletion** - Option to delete all data on plugin uninstall 83 * **No Cookies Required** - Works without setting cookies (session-based tracking) 84 * **Transparent** - Open-source code you can audit yourself 53 85 54 86 = Technical Features = 55 87 56 * Clean, well-documented code 57 * WordPress coding standards compliant 58 * Secure database queries with prepared statements 59 * Proper data sanitization and escaping 60 * Efficient database schema with automatic indexing 61 * Automated data cleanup and retention management 62 * Flexible storage options (database or file-based) 63 * Batch processing for high-traffic optimization 64 * Debug mode with comprehensive logging 65 * Bot filtering to exclude non-human traffic 66 * Multisite compatible 67 * Performance optimizations for large datasets 88 Built with performance and security in mind: 89 90 * **WordPress Coding Standards** - Fully compliant with WP best practices 91 * **Secure by Design** - All database queries use prepared statements 92 * **Optimized Performance** - Handles millions of events without slowing down 93 * **Flexible Storage** - Choose between database or file-based storage 94 * **Smart Indexing** - Automatic database optimization for fast queries 95 * **Bot Detection** - Server-side filtering of crawlers and bots 96 * **Multisite Ready** - Works perfectly on WordPress multisite networks 97 * **REST API Ready** - Modern architecture with hooks and filters 98 * **Developer Friendly** - Extensive documentation and clean codebase 99 * **Minimal Footprint** - Lightweight JavaScript (~15KB minified) 100 * **Async Loading** - Non-blocking script loading for better page speed 101 * **Batch Processing** - Efficient handling of high-traffic scenarios 68 102 69 103 == Installation == … … 86 120 = After Activation = 87 121 88 1. Navigate to Opti-Behavior in your WordPress admin menu 89 2. Configure your tracking preferences in Settings 90 3. Start collecting data immediately 91 4. View analytics in the Dashboard 122 1. Navigate to **Opti-Behavior** in your WordPress admin menu 123 2. The plugin starts tracking automatically - no configuration required! 124 3. Visit the **Dashboard** to see real-time visitor activity 125 4. Go to **Heatmaps** to view click patterns on your pages 126 5. Adjust **Settings** to customize tracking behavior and privacy options 127 128 That's it! Opti-Behavior works out of the box with smart defaults. 92 129 93 130 == Frequently Asked Questions == … … 95 132 = Does this plugin slow down my website? = 96 133 97 No ! Opti-Behavior is designed to be lightweight and efficient. The tracking script is minified and loads asynchronously, ensuring minimal impact on page load times.134 Not at all! Opti-Behavior is built for performance. The tracking script is only ~15KB minified and loads asynchronously, so it won't block your page rendering. We've tested it on sites with millions of pageviews without any performance issues. 98 135 99 136 = Is this plugin GDPR compliant? = 100 137 101 Yes! Opti-Behavior i ncludes features for GDPR compliance including IP anonymization, data retention controls, and user consent management. All data is stored locally in your WordPress database and is never sent to external servers. However, you are responsible for configuring these settings appropriately for your use case and ensuring your privacy policy discloses the data collection.138 Yes! Opti-Behavior is designed with privacy in mind. It includes IP anonymization, data retention controls, and consent management features. All data is stored locally on your server, not sent to third parties. However, you should still configure the settings appropriately for your use case and update your privacy policy to disclose data collection practices. 102 139 103 140 = Can I export my data? = … … 137 174 = How do I get support? = 138 175 139 For support, please use the WordPress.org support forums for this plugin. We monitor the forums regularly and will respond as quickly as possible. 176 For support questions, please use the [WordPress.org support forums](https://wordpress.org/support/plugin/opti-behavior/). For premium support, consider upgrading to [Opti-Behavior Pro](https://optiuser.com/). 177 178 = How is this different from Google Analytics? = 179 180 Unlike Google Analytics, Opti-Behavior stores all data on YOUR server (not Google's), provides visual heatmaps to see exactly where users click, doesn't require cookie consent in most cases, and gives you session-level insights. Plus, it's specifically designed for WordPress and integrates seamlessly with your admin dashboard. 181 182 = Can I use this with WooCommerce? = 183 184 Absolutely! Opti-Behavior works great with WooCommerce. Use heatmaps to optimize product pages, track cart abandonment patterns, and analyze checkout flow behavior to increase conversions. 185 186 = What's the difference between Free and Pro? = 187 188 The free version includes click heatmaps, real-time analytics, and tracks up to 5 pages with 7-day data retention. Pro unlocks session recordings, advanced heatmaps (attention, scroll, breakaway), unlimited pages, 2-year retention, and priority support. [Compare plans](https://optiuser.com/) 189 190 = Does it work with page builders? = 191 192 Yes! Opti-Behavior works with all major page builders including Elementor, Divi, Beaver Builder, WPBakery, and Gutenberg. 140 193 141 194 = What is the File Storage System? = … … 153 206 == External Services == 154 207 155 This plugin connects to external services in specific circumstances: 156 157 = IP Geolocation API (ip-api.com) = 158 159 **What it is:** A free IP geolocation service that provides geographic information based on IP addresses. 160 161 **When it's used:** When a visitor accesses your site and their location cannot be determined from CloudFlare headers (if CloudFlare is not in use), the plugin may make a server-side API call to ip-api.com to determine the visitor's approximate geographic location (country, region, city, timezone) based on their IP address. 162 163 **What data is sent:** Only the visitor's IP address is sent to ip-api.com. No other personally identifiable information is transmitted. 164 165 **Why it's needed:** This geolocation data is used to provide geographic analytics in the dashboard (e.g., visitor location maps, country/region statistics). 208 This plugin may connect to external services in limited circumstances: 209 210 = IP Geolocation (ip-api.com) = 211 212 **Purpose:** Provides geographic location data (country, city, timezone) for visitor analytics and map visualization. 213 214 **When Used:** Only when a visitor's location cannot be determined from CloudFlare headers. If CloudFlare is active, no external calls are made. 215 216 **Data Sent:** Only the visitor's IP address. No personally identifiable information is transmitted. 166 217 167 218 **Caching:** Results are cached for 1 hour to minimize API requests. 168 219 169 **Rate limits:** The free tier has a rate limit of 45 requests per minute. 170 171 **Privacy & Terms:** 172 * Service homepage: https://ip-api.com/ 173 * Terms of service: https://ip-api.com/docs/legal 174 * Privacy policy: https://ip-api.com/docs/legal (see Privacy section) 175 * Data handling: ip-api.com may log IP addresses for their own analytics. Please review their privacy policy for details. 176 177 **Note:** If CloudFlare is detected, the plugin uses CloudFlare's provided geolocation headers instead and does NOT make external API calls. 220 **Privacy:** 221 * Service: https://ip-api.com/ 222 * Terms & Privacy: https://ip-api.com/docs/legal 223 * Note: ip-api.com may log IP addresses. Review their privacy policy for details. 178 224 179 225 = OpenStreetMap Tiles = 180 226 181 **What it is:** Map tiles for displaying the real-time visitor map visualization. 182 183 **When it's used:** When you view the real-time visitor map in the admin dashboard. 184 185 **What data is sent:** Map tile requests are made directly by your browser (not by the plugin server) to OpenStreetMap tile servers. Standard HTTP request data (IP address, user agent) is sent as part of normal browser requests. 186 187 **Why it's needed:** To display the interactive map showing visitor locations. 188 189 **Privacy & Terms:** 190 * Service homepage: https://www.openstreetmap.org/ 191 * Tile usage policy: https://operations.osmfoundation.org/policies/tiles/ 192 * Privacy policy: https://wiki.osmfoundation.org/wiki/Privacy_Policy 227 **Purpose:** Displays the interactive visitor location map in your WordPress admin dashboard. 228 **When Used:** Only when YOU (the admin) view the real-time visitor map. Not used on the frontend. 229 **Data Sent:** Your browser makes direct requests to OpenStreetMap tile servers (standard HTTP headers only). 230 231 **Privacy:** 232 * Service: https://www.openstreetmap.org/ 233 * Tile Policy: https://operations.osmfoundation.org/policies/tiles/ 234 * Privacy Policy: https://wiki.osmfoundation.org/wiki/Privacy_Policy 235 236 **Important Note:** All analytics data is stored locally on your WordPress server. The services above are only used for optional geographic features and map visualization. 193 237 194 238 == Screenshots == … … 203 247 == Changelog == 204 248 205 = 1.0.4 = 249 = 1.0.5 - 2025-01-23 = 250 * Fix: Removed all debug error_log() calls from production code 251 * Fix: Replaced date() with gmdate() for timezone-safe date handling 252 * Fix: Added translator comments for i18n compliance 253 * Fix: Updated API URL from localhost to production endpoint 254 * Fix: Corrected stable tag version mismatch 255 * Enhancement: Improved readme with better descriptions and FAQ 256 * Enhancement: Added Plugin URI and updated Author URI 257 * Enhancement: Optimized WordPress.org directory submission compliance 258 * Compatibility: Full WordPress 6.8 compatibility verified 259 * Enhancement: Added COALESCE for better handling of NULL titles in Top Pages 260 * Enhancement: Improved country detection with browser language fallback when IP geolocation fails 261 * Enhancement: Top Pages widget now displays page views instead of clicks for better accuracy 262 263 264 = 1.0.4 - 2025-01-20 = 206 265 * Enhancement: Added file-based storage system for high-traffic sites 207 266 * Enhancement: Implemented automatic bot detection and filtering … … 209 268 * Enhancement: Separate mobile and desktop heatmap tracking and visualization 210 269 * Enhancement: Batch processing for improved performance 211 * Enhancement: Enhanced debug logging system 270 * Enhancement: Enhanced debug logging system with WordPress-compliant manager 212 271 * Enhancement: Added Lucide icon library (v0.554.0, ISC License) for modern UI 213 * Security: Replaced error_log() with WordPress-compliant debug manager214 272 * Security: Replaced direct filesystem operations with WP_Filesystem API 215 273 * Security: Replaced unlink() with wp_delete_file() for file deletion … … 251 309 == Upgrade Notice == 252 310 311 = 1.0.4.19 = 312 WordPress.org submission update: Fixes debug code, timezone handling, and i18n compliance. All users should update for WordPress coding standards compliance. 313 253 314 = 1.0.4 = 254 Major update with file storage system, bot detection, performance optimizations, and WordPress coding standards compliance. Recommended for all users, especially high-traffic sites.315 Major update with file storage system, bot detection, and performance optimizations. Recommended for all users, especially high-traffic sites. 255 316 256 317 = 1.0.3 = 257 Important security update with enhanced SQL security and WordPress coding standards compliance. Recommended for all users. 258 259 = 1.0.1 = 260 Important security update with enhanced input sanitization and WordPress coding standards compliance. Recommended for all users. 318 Important security update with enhanced SQL security. All users should update. 261 319 262 320 = 1.0.0 = 263 Initial release of Opti-Behavior. Install to start tracking user behavior on your WordPress site.321 Initial release. Install to start tracking user behavior with powerful heatmaps and analytics. 264 322 265 323 == Privacy Policy ==
Note: See TracChangeset
for help on using the changeset viewer.