cards( $window, $filters ); if ( ! $compare ) { return $cards; } $previous = Saddle_Analytics_Window::previous( $window ); if ( null === $previous ) { // Lifetime has nothing before it; say so rather than inventing zeroes. return $cards + array( 'previous' => null, 'delta' => null ); } $before = $this->cards( $previous, $filters ); $delta = array(); foreach ( array( 'visitors', 'visits', 'pageviews', 'bounce_rate', 'duration' ) as $key ) { $delta[ $key ] = Saddle_Analytics_Window::delta( $cards[ $key ], $before[ $key ] ); } return $cards + array( 'previous' => $before, 'delta' => $delta, ); } /** Prepared when there is anything to bind, so prepare() never sees a bare string. */ private function bind( string $sql, array $params ): string { return $params ? $this->db->prepare( $sql, ...$params ) : $sql; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } /** One window's cards. The whole of summary() before compare existed. */ private function cards( array $window, array $filters = array() ): array { $t = Saddle_Analytics_Install::tables(); list( $ts_bounds, $ts_params ) = Saddle_Analytics_Window::bounds( $window, 'ts' ); list( $vis_bounds, $vis_params ) = Saddle_Analytics_Window::bounds( $window, 'last_seen' ); list( $filter_sql, $filter_params ) = Saddle_Analytics_Dimensions::filter_clause( $filters ); list( $scope_sql, $scope_params ) = Saddle_Analytics_Dimensions::visitor_scope( $filters ); $vis_where = $this->bind( 'WHERE 1=1' . $vis_bounds . $filter_sql, array_merge( $vis_params, $filter_params ) ); $hits_where = $this->bind( 'WHERE event_type = 1' . $ts_bounds . $scope_sql, array_merge( $ts_params, $scope_params ) ); $visitors = (int) $this->db->get_var( "SELECT COUNT(*) FROM {$t['visitors']} {$vis_where}" ); // phpcs:ignore WordPress.DB.PreparedSQL $online = (int) $this->db->get_var( $this->db->prepare( "SELECT COUNT(*) FROM {$t['visitors']} WHERE last_seen >= %s{$filter_sql}", gmdate( 'Y-m-d H:i:s', time() - 5 * MINUTE_IN_SECONDS ), ...$filter_params ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); // Session rollup: a visit bounces when it has one pageview; duration is last hit minus // first hit, so it holds even when the late duration beacon never arrived. // UNIX_TIMESTAMP + CASE (not TIMESTAMPDIFF / boolean SUM) so the SQLite driver // used by Studio/Playground can run it too. The range='all' scan is bounded in // practice by the daily 90-day prune of the hits table. $sessions = (array) $this->db->get_row( "SELECT COUNT(*) AS visits, COALESCE(SUM(pv),0) AS pageviews, COALESCE(SUM(CASE WHEN pv = 1 THEN 1 ELSE 0 END),0) AS bounces, COALESCE(ROUND(AVG(dur)),0) AS duration FROM (SELECT session_id, COUNT(*) AS pv, UNIX_TIMESTAMP(MAX(ts)) - UNIX_TIMESTAMP(MIN(ts)) AS dur FROM {$t['hits']} {$hits_where} GROUP BY session_id) s", // phpcs:ignore WordPress.DB.PreparedSQL ARRAY_A ); $visits = (int) ( $sessions['visits'] ?? 0 ); return array( 'visitors' => $visitors, 'visits' => $visits, 'pageviews' => (int) ( $sessions['pageviews'] ?? 0 ), 'bounce_rate' => $visits > 0 ? round( (int) $sessions['bounces'] / $visits, 4 ) : 0.0, 'duration' => (int) ( $sessions['duration'] ?? 0 ), 'online' => $online, ); } }