prefix . 'cwp_ai_events'; } /** * Creates or updates the analytics table. */ function cwp_ai_events_install() { global $wpdb; $table = cwp_ai_events_table(); $charset = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( "CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, created datetime NOT NULL, event varchar(32) NOT NULL, src varchar(16) NOT NULL DEFAULT '', tab varchar(32) NOT NULL DEFAULT '', item varchar(190) NOT NULL DEFAULT '', link varchar(255) NOT NULL DEFAULT '', page varchar(255) NOT NULL DEFAULT '', fields varchar(190) NOT NULL DEFAULT '', session varchar(40) NOT NULL DEFAULT '', device varchar(10) NOT NULL DEFAULT '', is_admin tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY created (created), KEY event (event) ) {$charset};" ); update_option( 'cwp_ai_events_db', '1', false ); } add_action( 'admin_init', function () { if ( get_option( 'cwp_ai_events_db' ) !== '1' ) { cwp_ai_events_install(); } } ); /** * Returns the visitor actions analytics records, with their labels. * * @return array */ function cwp_ai_event_names() { return array( 'popup_shown' => 'Popup shown', 'tab_click' => 'Tab tapped', 'slide_click' => 'Item clicked', 'slide_nav' => 'Browsed slides', 'video_play' => 'Video played', 'popup_close' => 'Popup closed', 'chat_open' => 'Chat opened', 'message' => 'Message sent', 'form_open' => 'Form opened', 'form_start' => 'Form started', 'form_submit' => 'Form sent', 'form_abandon' => 'Form left unfinished', 'perf' => 'Speed sample', ); } /** * Returns the actions offered in the analytics event filter, as key and label pairs. * * @return array */ function cwp_ai_analytics_event_options() { $options = array(); foreach ( cwp_ai_event_names() as $key => $label ) { if ( in_array( $key, array( 'chat_open', 'message', 'form_open', 'form_start', 'form_submit', 'form_abandon' ), true ) ) { $options[] = array( $key, $label ); } } return $options; } /** * Reduces a page address to a safe path, without the query string. * * @param string $page The address the browser reported. * @return string */ function cwp_ai_clean_page_path( $page ) { $path = (string) wp_parse_url( (string) $page, PHP_URL_PATH ); $path = preg_replace( '#[^A-Za-z0-9/_.~%-]#', '', $path ); return ( '' !== $path && '/' === $path[0] ) ? '/' . ltrim( $path, '/' ) : '/'; } /** * Records one visitor action sent by the chat, with rate limiting. */ function cwp_ai_track_request() { $names = cwp_ai_event_names(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- anonymous visitor analytics; every field is whitelisted or sanitised and nothing is read back. $event = isset( $_POST['e'] ) ? sanitize_key( wp_unslash( $_POST['e'] ) ) : ''; if ( ! isset( $names[ $event ] ) || get_option( 'cwp_ai_events_db' ) !== '1' ) { status_header( 204 ); exit; } $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $key = 'cwp_ai_rate_' . md5( $ip . gmdate( 'YmdHi' ) ); $count = (int) get_transient( $key ); if ( $count > 600 ) { status_header( 204 ); exit; } set_transient( $key, $count + 1, 2 * MINUTE_IN_SECONDS ); $field = function ( $name, $max ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- see above. $value = isset( $_POST[ $name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $name ] ) ) : ''; return mb_substr( $value, 0, $max ); }; global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- the plugin's own analytics table. $wpdb->insert( cwp_ai_events_table(), array( 'created' => current_time( 'mysql', true ), 'event' => $event, 'src' => sanitize_key( $field( 'src', 16 ) ), 'tab' => sanitize_key( $field( 'tab', 32 ) ), 'item' => $field( 'item', 190 ), 'link' => esc_url_raw( $field( 'link', 255 ) ), 'page' => cwp_ai_clean_page_path( $field( 'page', 255 ) ), 'fields' => preg_replace( '/[^a-z0-9_, ]/i', '', $field( 'fields', 190 ) ), 'session' => preg_replace( '/[^a-z0-9]/i', '', $field( 'sid', 40 ) ), 'device' => $field( 'dev', 10 ) === 'mobile' ? 'mobile' : 'desktop', 'is_admin' => current_user_can( 'manage_options' ) ? 1 : 0, ) ); status_header( 204 ); exit; } add_action( 'wp_ajax_cwp_ai_track', 'cwp_ai_track_request' ); add_action( 'wp_ajax_nopriv_cwp_ai_track', 'cwp_ai_track_request' ); /** * Returns how many days of analytics are kept. * * @return int */ function cwp_ai_analytics_days() { return max( 1, (int) apply_filters( 'cwp_ai_analytics_days', 90 ) ); } /** * Returns the periods the Analytics screen offers, in days. * * @return int[] */ function cwp_ai_analytics_ranges() { $keep = cwp_ai_analytics_days(); $ranges = array_filter( array( 7, 30, 90 ), function ( $d ) use ( $keep ) { return $d <= $keep; } ); if ( ! in_array( $keep, $ranges, true ) ) { $ranges[] = $keep; } return array_values( $ranges ); } add_action( 'cwp_ai_events_cleanup', function () { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own analytics table. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE created < %s', cwp_ai_events_table(), gmdate( 'Y-m-d H:i:s', time() - cwp_ai_analytics_days() * DAY_IN_SECONDS ) ) ); } ); add_action( 'init', function () { if ( ! wp_next_scheduled( 'cwp_ai_events_cleanup' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'cwp_ai_events_cleanup' ); } } ); add_action( 'wp_ajax_cwp_ai_analytics', function () { check_ajax_referer( 'cwp_ai_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } global $wpdb; $days = isset( $_POST['days'] ) ? max( 1, min( cwp_ai_analytics_days(), absint( $_POST['days'] ) ) ) : min( 30, cwp_ai_analytics_days() ); $admins = ! empty( $_POST['admins'] ); $table = cwp_ai_events_table(); $since = gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS ); $maxadm = $admins ? 1 : 0; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own analytics table. $counts = $wpdb->get_results( $wpdb->prepare( 'SELECT event, src, COUNT(*) AS n FROM %i WHERE created >= %s AND is_admin <= %d GROUP BY event, src', $table, $since, $maxadm ), ARRAY_A ); $daily = $wpdb->get_results( $wpdb->prepare( "SELECT DATE(created) AS d, event, COUNT(*) AS n FROM %i WHERE created >= %s AND is_admin <= %d AND event IN ('popup_shown','slide_click','chat_open','message','form_submit') GROUP BY d, event ORDER BY d", $table, $since, $maxadm ), ARRAY_A ); $tabs = $wpdb->get_results( $wpdb->prepare( "SELECT tab, SUM(event = 'slide_click') AS clicks, SUM(event = 'tab_click') AS taps FROM %i WHERE created >= %s AND is_admin <= %d AND tab <> '' AND event IN ('slide_click','tab_click') GROUP BY tab ORDER BY clicks DESC, taps DESC", $table, $since, $maxadm ), ARRAY_A ); $items = $wpdb->get_results( $wpdb->prepare( "SELECT item, tab, MAX(link) AS link, COUNT(*) AS n FROM %i WHERE created >= %s AND is_admin <= %d AND event = 'slide_click' AND item <> '' GROUP BY item, tab ORDER BY n DESC LIMIT 10", $table, $since, $maxadm ), ARRAY_A ); $pages = $wpdb->get_results( $wpdb->prepare( "SELECT page, SUM(event = 'popup_shown') AS shown, SUM(event IN ('slide_click','chat_open','form_submit')) AS actions FROM %i WHERE created >= %s AND is_admin <= %d AND page <> '' GROUP BY page ORDER BY actions DESC, shown DESC LIMIT 10", $table, $since, $maxadm ), ARRAY_A ); $forms = $wpdb->get_results( $wpdb->prepare( "SELECT page, item, fields, created FROM %i WHERE created >= %s AND is_admin <= %d AND event = 'form_abandon' ORDER BY created DESC LIMIT 10", $table, $since, $maxadm ), ARRAY_A ); $mine = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE created >= %s AND is_admin = 1 AND event NOT IN ('popup_shown','perf')", $table, $since ) ); $people = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT session) FROM %i WHERE created >= %s AND is_admin <= %d AND event NOT IN ('popup_shown','perf') AND session <> ''", $table, $since, $maxadm ) ); $devices = $wpdb->get_results( $wpdb->prepare( "SELECT device, COUNT(DISTINCT session) AS n FROM %i WHERE created >= %s AND is_admin <= %d AND session <> '' AND event <> 'perf' GROUP BY device", $table, $since, $maxadm ), ARRAY_A ); // phpcs:enable $k = array_fill_keys( array_keys( cwp_ai_event_names() ), 0 ); $sources = array( 'popup' => 0, 'launcher' => 0, ); foreach ( $counts as $row ) { $k[ $row['event'] ] = ( $k[ $row['event'] ] ?? 0 ) + (int) $row['n']; if ( 'chat_open' === $row['event'] && isset( $sources[ $row['src'] ] ) ) { $sources[ $row['src'] ] += (int) $row['n']; } } $series = array(); for ( $i = $days - 1; $i >= 0; $i-- ) { $series[ gmdate( 'Y-m-d', time() - $i * DAY_IN_SECONDS ) ] = array( 'shown' => 0, 'clicks' => 0, 'chats' => 0, 'messages' => 0, 'forms' => 0, ); } $map = array( 'popup_shown' => 'shown', 'slide_click' => 'clicks', 'chat_open' => 'chats', 'message' => 'messages', 'form_submit' => 'forms', ); foreach ( $daily as $row ) { if ( isset( $series[ $row['d'] ] ) ) { $series[ $row['d'] ][ $map[ $row['event'] ] ] = (int) $row['n']; } } wp_send_json_success( array( 'days' => $days, 'k' => $k, 'people' => $people, 'mine' => $mine, 'sources' => $sources, 'devices' => $devices, 'series' => $series, 'tabs' => $tabs, 'items' => $items, 'pages' => $pages, 'left' => $forms, 'labels' => cwp_ai_event_names(), ) ); } ); add_action( 'wp_ajax_cwp_ai_events', function () { check_ajax_referer( 'cwp_ai_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } global $wpdb; $days = isset( $_POST['days'] ) ? max( 1, min( cwp_ai_analytics_days(), absint( $_POST['days'] ) ) ) : min( 30, cwp_ai_analytics_days() ); $admins = ! empty( $_POST['admins'] ); $page = isset( $_POST['page'] ) ? max( 1, absint( $_POST['page'] ) ) : 1; $per = isset( $_POST['per'] ) ? max( 10, min( 100, absint( $_POST['per'] ) ) ) : 25; $event = isset( $_POST['event'] ) ? sanitize_key( wp_unslash( $_POST['event'] ) ) : ''; $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; $table = cwp_ai_events_table(); $since = gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS ); $maxadm = $admins ? 1 : 0; $event = isset( cwp_ai_event_names()[ $event ] ) ? $event : ''; $like = '' !== $search ? '%' . $wpdb->esc_like( $search ) . '%' : ''; $all = ! empty( $_POST['all'] ); $limit = $all ? 5000 : $per; $offset = $all ? 0 : ( $page - 1 ) * $per; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own analytics table. $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE created >= %s AND is_admin <= %d AND ((%s = '' AND event <> 'perf') OR event = %s) AND (%s = '' OR item LIKE %s OR page LIKE %s OR tab LIKE %s OR link LIKE %s)", $table, $since, $maxadm, $event, $event, $like, $like, $like, $like, $like ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE created >= %s AND is_admin <= %d AND ((%s = '' AND event <> 'perf') OR event = %s) AND (%s = '' OR item LIKE %s OR page LIKE %s OR tab LIKE %s OR link LIKE %s) ORDER BY id DESC LIMIT %d OFFSET %d", $table, $since, $maxadm, $event, $event, $like, $like, $like, $like, $like, $limit, $offset ), ARRAY_A ); // phpcs:enable foreach ( $rows as &$row ) { $row['when'] = get_date_from_gmt( $row['created'], 'j M Y, g:i a' ); } wp_send_json_success( array( 'rows' => $rows, 'total' => $total, 'page' => $page, 'per' => $per, ) ); } ); /** * Prints the Analytics screen. */ function cwp_ai_render_analytics_tab() { $site = wp_parse_url( home_url( '/' ), PHP_URL_HOST ); ?>

Analytics

Loading your numbers...

Day by day

Tap a name to hide or show it.

From chat to enquiry

How many people took each next step.

Devices

Visitors who did something, by device.

Busiest pages

Where visitors used the chatbot.

Forms left unfinished

Someone started the form but left. Only which fields were filled is kept - never what was typed. Sent forms are under Form & entries.

Every action

The full record, newest first. Search and filter.

wp_create_nonce( 'cwp_ai_admin' ), 'site' => $site, 'days' => min( 30, cwp_ai_analytics_days() ), 'events' => cwp_ai_analytics_event_options(), ) ); ?>