prefix . 'runline_stats'; } /** * Creates or updates the table (on activation and after an update of the plugin). */ function runline_stats_install() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = runline_stats_table(); $charset = $wpdb->get_charset_collate(); dbDelta( "CREATE TABLE {$table} ( bar_id bigint(20) unsigned NOT NULL, day date NOT NULL, variant char(1) NOT NULL DEFAULT 'a', views int(10) unsigned NOT NULL DEFAULT 0, clicks int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (bar_id,day,variant) ) {$charset};" ); do_action( 'runline_install' ); update_option( 'runline_db_version', RUNLINE_DB_VERSION, false ); } add_action( 'plugins_loaded', function () { if ( RUNLINE_DB_VERSION !== get_option( 'runline_db_version' ) ) { runline_stats_install(); } } ); function runline_stats_on() { return (bool) runline_options()['stats']; } /** * Adds a view or a click. * * @param int $bar_id Bar ID. * @param string $type "view" or "click". * @param string $variant "a" or "b". */ function runline_stats_add( $bar_id, $type, $variant = 'a' ) { global $wpdb; $column = 'click' === $type ? 'clicks' : 'views'; $variant = 'b' === $variant ? 'b' : 'a'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- a counter; nothing to cache. $wpdb->query( $wpdb->prepare( 'INSERT INTO %i (bar_id, day, variant, %i) VALUES (%d, %s, %s, 1) ON DUPLICATE KEY UPDATE %i = %i + 1', runline_stats_table(), $column, $bar_id, current_time( 'Y-m-d' ), $variant, $column, $column ) ); } /** * Statistics of one bar for the last days: { days: [ { day, views, clicks } ], variants: { a: {…}, b: {…} } }. * * @param int $bar_id Bar ID. * @param int $days Number of days, today included. */ function runline_stats_get( $bar_id, $days = 30 ) { global $wpdb; $from = gmdate( 'Y-m-d', strtotime( current_time( 'Y-m-d' ) ) - ( $days - 1 ) * DAY_IN_SECONDS ); $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT day, variant, views, clicks FROM %i WHERE bar_id = %d AND day >= %s', runline_stats_table(), $bar_id, $from ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- statistics of the admin screen. $by_day = array(); for ( $i = $days - 1; $i >= 0; $i-- ) { $day = gmdate( 'Y-m-d', strtotime( current_time( 'Y-m-d' ) ) - $i * DAY_IN_SECONDS ); $by_day[ $day ] = array( 'day' => $day, 'views' => 0, 'clicks' => 0, ); } $variants = array( 'a' => array( 'views' => 0, 'clicks' => 0, ), 'b' => array( 'views' => 0, 'clicks' => 0, ), ); foreach ( (array) $rows as $row ) { if ( isset( $by_day[ $row['day'] ] ) ) { $by_day[ $row['day'] ]['views'] += (int) $row['views']; $by_day[ $row['day'] ]['clicks'] += (int) $row['clicks']; } $variants[ $row['variant'] ]['views'] += (int) $row['views']; $variants[ $row['variant'] ]['clicks'] += (int) $row['clicks']; } return array( 'days' => array_values( $by_day ), 'variants' => $variants, ); } /** * Totals of the last 30 days for all bars: bar ID => { views, clicks }. */ function runline_stats_totals() { global $wpdb; $from = gmdate( 'Y-m-d', strtotime( current_time( 'Y-m-d' ) ) - 29 * DAY_IN_SECONDS ); $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT bar_id, SUM(views) AS views, SUM(clicks) AS clicks FROM %i WHERE day >= %s GROUP BY bar_id', runline_stats_table(), $from ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- list of bars in the admin. $out = array(); foreach ( (array) $rows as $row ) { $out[ (int) $row['bar_id'] ] = array( 'views' => (int) $row['views'], 'clicks' => (int) $row['clicks'], ); } return $out; } /** * Deletes the statistics of a bar (button "Reset" and deleting the bar). */ function runline_stats_reset( $bar_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- deleting rows of our table. $wpdb->delete( runline_stats_table(), array( 'bar_id' => (int) $bar_id ), array( '%d' ) ); } add_action( 'before_delete_post', function ( $post_id ) { if ( RUNLINE_POST_TYPE === get_post_type( $post_id ) ) { runline_stats_reset( $post_id ); } } ); /** * A note from a visitor's browser. Bars that are off, unknown IDs and repeated IDs are skipped quietly. * * @param WP_REST_Request $request Request. */ function runline_rest_track( WP_REST_Request $request ) { $done = new WP_REST_Response( null, 204 ); if ( ! runline_stats_on() ) { return $done; } $list = (array) $request['bars']; if ( 'click' === $request['type'] || ! $list ) { $list = array( array( 'bar' => (int) $request['bar'], 'variant' => (string) $request['variant'], ), ); } $seen = array(); foreach ( array_slice( $list, 0, 10 ) as $item ) { $bar = isset( $item['bar'] ) ? (int) $item['bar'] : 0; if ( ! $bar || isset( $seen[ $bar ] ) || RUNLINE_POST_TYPE !== get_post_type( $bar ) || '1' !== get_post_meta( $bar, RUNLINE_ACTIVE_META, true ) ) { continue; } $seen[ $bar ] = true; runline_stats_add( $bar, (string) $request['type'], isset( $item['variant'] ) ? (string) $item['variant'] : 'a' ); } return $done; } add_action( 'rest_api_init', function () { // Notes from the browsers of visitors: public, but only for bars that are switched on. A click comes alone // ({ bar, type: "click", variant }); the views of a page come together ({ type: "view", bars: [ { bar, variant } ] }), // once per browser session for each bar. register_rest_route( RUNLINE_REST_NS, '/track', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'runline_rest_track', 'permission_callback' => '__return_true', 'args' => array( 'type' => array( 'type' => 'string', 'enum' => array( 'view', 'click' ), 'required' => true, ), 'bar' => array( 'type' => 'integer', ), 'variant' => array( 'type' => 'string', 'enum' => array( 'a', 'b' ), 'default' => 'a', ), 'bars' => array( 'type' => 'array', 'maxItems' => 10, 'items' => array( 'type' => 'object', 'properties' => array( 'bar' => array( 'type' => 'integer', 'required' => true, ), 'variant' => array( 'type' => 'string', 'enum' => array( 'a', 'b' ), ), ), ), ), ), ) ); register_rest_route( RUNLINE_REST_NS, '/stats', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => function ( WP_REST_Request $request ) { return rest_ensure_response( runline_stats_get( (int) $request['bar'], 30 ) ); }, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'bar' => array( 'type' => 'integer', 'required' => true, ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => function ( WP_REST_Request $request ) { runline_stats_reset( (int) $request['bar'] ); return rest_ensure_response( array( 'reset' => true ) ); }, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'bar' => array( 'type' => 'integer', 'required' => true, ), ), ), ) ); } );