query( ("SELECT DATE(FROM_UNIXTIME(date)) AS date, " . "threads, " . "missedthreads, " . "sentinvitations, " . "acceptedinvitations, " . "rejectedinvitations, " . "ignoredinvitations, " . "operatormessages AS agents, " . "usermessages AS users, " . "averagewaitingtime AS avgwaitingtime, " . "averagechattime AS avgchattime " . "FROM {threadstatistics} s " . "WHERE s.date >= :start " . "AND s.date < :end " . "ORDER BY s.date DESC"), array( ':start' => $start, ':end' => $end, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Get statistics aggregated for all accessed interval $total = $db->query( ("SELECT SUM(threads) AS threads, " . "SUM(missedthreads) AS missedthreads, " . "SUM(sentinvitations) AS sentinvitations, " . "SUM(acceptedinvitations) AS acceptedinvitations, " . "SUM(rejectedinvitations) AS rejectedinvitations, " . "SUM(ignoredinvitations) AS ignoredinvitations, " . "SUM(operatormessages) AS agents, " . "SUM(usermessages) AS users, " . "ROUND(SUM(averagewaitingtime * s.threads) / SUM(s.threads),1) AS avgwaitingtime, " . "ROUND(SUM(averagechattime * s.threads) / SUM(s.threads),1) AS avgchattime " . "FROM {threadstatistics} s " . "WHERE s.date >= :start " . "AND s.date < :end"), array( ':start' => $start, ':end' => $end, ), array('return_rows' => Database::RETURN_ONE_ROW) ); return array( 'records' => $records, 'total' => $total, ); } /** * Loads statistics about operators for the time between $start and $end * timestamps. * * @param int $start Timestamp for beginning of the interval of interest. * @param int $end Timestamp for ending of the interval of interest. * @return array Set of statistics records */ function get_by_operator_statistics($start, $end) { $db = Database::getInstance(); $result = $db->query( ("SELECT o.vclocalename AS name, " . "SUM(s.threads) AS threads, " . "SUM(s.messages) AS msgs, " . "ROUND( " . "SUM(s.averagelength * s.messages) / SUM(s.messages), " . "1) AS avglen, " . "SUM(sentinvitations) AS sentinvitations, " . "SUM(acceptedinvitations) AS acceptedinvitations, " . "SUM(rejectedinvitations) AS rejectedinvitations, " . "SUM(ignoredinvitations) AS ignoredinvitations " . "FROM {operatorstatistics} s, {operator} o " . "WHERE s.operatorid = o.operatorid " . "AND s.date >= :start " . "AND s.date < :end " . "GROUP BY s.operatorid, o.vclocalename"), array( ':start' => $start, ':end' => $end, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); return $result; } /** * Loads statistics about pages for the time between $start and $end timestamps. * * @param int $start Timestamp for beginning of the interval of interest. * @param int $end Timestamp for ending of the interval of interest. * @return array Set of statistics records */ function get_by_page_statistics($start, $end) { $db = Database::getInstance(); $result = $db->query( ("SELECT SUM(visits) as visittimes, " . "address, " . "SUM(chats) as chattimes, " . "SUM(sentinvitations) AS sentinvitations, " . "SUM(acceptedinvitations) AS acceptedinvitations, " . "SUM(rejectedinvitations) AS rejectedinvitations, " . "SUM(ignoredinvitations) AS ignoredinvitations " . "FROM {visitedpagestatistics} " . "WHERE date >= :start " . "AND date < :end " . "GROUP BY address"), array(':start' => $start, ':end' => $end), array('return_rows' => Database::RETURN_ALL_ROWS) ); return $result; } /** * Calculate aggregated 'by thread' statistics */ function calculate_thread_statistics() { // Prepare database $db = Database::getInstance(); $db_throw_exceptions = $db->throwExceptions(true); try { // Start transaction $db->query('START TRANSACTION'); // Get last record date $result = $db->query( "SELECT MAX(date) as start FROM {threadstatistics}", array(), array('return_rows' => Database::RETURN_ONE_ROW) ); $start = empty($result['start']) ? 0 : $result['start']; $today = floor(time() / STATISTICS_AGGREGATION_INTERVAL) * STATISTICS_AGGREGATION_INTERVAL; // Calculate statistics // Get base threads info $db_results = $db->query( ("SELECT (FLOOR(t.dtmcreated / :interval) * :interval) AS date, " . "COUNT(t.threadid) AS threads, " . "SUM(tmp.operator_msgs) AS operator_msgs, " . "SUM(tmp.user_msgs) AS user_msgs, " // Prevent negative values of avgchattime field. // If avgchattime < 0 it becomes to zero. // For random value 'a' result of expression ((abs(a) + a) / 2) // equals to 'a' if 'a' more than zero // and equals to zero otherwise . "ROUND(AVG( " . "ABS(tmp.last_msg_time - t.dtmchatstarted) + " . "(tmp.last_msg_time - t.dtmchatstarted) " . ")/2,1) AS avg_chat_time " . "FROM {thread} t, " . "(SELECT SUM(m.ikind = :kind_agent) AS operator_msgs, " . "SUM(m.ikind = :kind_user) AS user_msgs, " . "MAX(m.dtmcreated) as last_msg_time, " . "threadid " . "FROM {message} m " // Calculate only users' and operators' messages . "WHERE m.ikind = :kind_user " . "OR m.ikind = :kind_agent " . "GROUP BY m.threadid) tmp " . "WHERE t.threadid = tmp.threadid " . "AND (t.dtmcreated - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > t.dtmcreated " // Ignore threads when operator does not start chat . "AND t.dtmchatstarted <> 0 " // Ignore not accepted invitations . "AND (t.invitationstate = :not_invited " . "OR t.invitationstate = :invitation_accepted) " . "GROUP BY date"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':not_invited' => Thread::INVITATION_NOT_INVITED, ':invitation_accepted' => Thread::INVITATION_ACCEPTED, ':kind_agent' => Thread::KIND_AGENT, ':kind_user' => Thread::KIND_USER, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store statistics data $statistics = extend_statistics_info( array(), $db_results, array('date') ); // Get info about missed threads $db_results = $db->query( ("SELECT (FLOOR(dtmcreated / :interval) * :interval) AS date, " . "COUNT(*) as missed_threads " . "FROM {thread} " . "WHERE (dtmcreated - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > dtmcreated " // Ignore threads when operator does not start chat . "AND dtmchatstarted = 0 " // Ignore not accepted invitations . "AND invitationstate = :not_invited " . "GROUP BY date ORDER BY date DESC"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':not_invited' => Thread::INVITATION_NOT_INVITED, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Add info about missed threads to statistics data $statistics = extend_statistics_info( $statistics, $db_results, array('date') ); // Get info about average waiting time. $db_results = $db->query( ("SELECT (FLOOR(dtmcreated / :interval) * :interval) AS date, " . "ROUND(AVG(dtmchatstarted-dtmcreated),1) AS avg_waiting_time " . "FROM {thread} " . "WHERE (dtmcreated - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > dtmcreated " // Ignore threads when operator does not start chat . "AND dtmchatstarted <> 0 " // Ignore all invitations . "AND invitationstate = :not_invited " . "GROUP BY date"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':not_invited' => Thread::INVITATION_NOT_INVITED, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Add average waiting time to statistics data $statistics = extend_statistics_info( $statistics, $db_results, array('date') ); // Get invitation info $db_results = $db->query( ("SELECT (FLOOR(dtmcreated / :interval) * :interval) AS date, " . "COUNT(*) AS invitations_sent, " . "SUM(invitationstate = :invitation_accepted) AS invitations_accepted, " . "SUM(invitationstate = :invitation_rejected) AS invitations_rejected, " . "SUM(invitationstate = :invitation_ignored) AS invitations_ignored " . "FROM {thread} " . "WHERE (dtmcreated - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > dtmcreated " . "AND (invitationstate = :invitation_accepted " . "OR invitationstate = :invitation_rejected " . "OR invitationstate = :invitation_ignored) " . "GROUP BY date"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':invitation_accepted' => Thread::INVITATION_ACCEPTED, ':invitation_rejected' => Thread::INVITATION_REJECTED, ':invitation_ignored' => Thread::INVITATION_IGNORED, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Add invitation info to statistics data $statistics = extend_statistics_info( $statistics, $db_results, array('date') ); // Sort statistics by date before save it in the database ksort($statistics); foreach ($statistics as $row) { // Add default values $row += array( 'threads' => 0, 'missed_threads' => 0, 'operator_msgs' => 0, 'user_msgs' => 0, 'avg_chat_time' => 0, 'avg_waiting_time' => 0, 'invitations_sent' => 0, 'invitations_accepted' => 0, 'invitations_rejected' => 0, 'invitations_ignored' => 0, ); // Prepare data for insert $insert_data = array(); foreach ($row as $field_name => $field_value) { $insert_data[':' . $field_name] = $field_value; } // Store data in database $db->query( ("INSERT INTO {threadstatistics} (" . "date, threads, missedthreads, sentinvitations, " . "acceptedinvitations, rejectedinvitations, " . "ignoredinvitations, operatormessages, usermessages, " . "averagewaitingtime, averagechattime " . ") VALUES (" . ":date, :threads, :missed_threads, :invitations_sent, " . ":invitations_accepted, :invitations_rejected, " . ":invitations_ignored, :operator_msgs, :user_msgs, " . ":avg_waiting_time, :avg_chat_time " . ")"), $insert_data ); } } catch (Exception $e) { // Something went wrong: warn and rollback transaction. trigger_error( 'Thread statistics calculating faild: ' . $e->getMessage(), E_USER_WARNING ); $db->query('ROLLBACK'); // Set throw exceptions back $db->throwExceptions($db_throw_exceptions); return; } // Commit transaction $db->query('COMMIT'); // Set throw exceptions back $db->throwExceptions($db_throw_exceptions); } /** * Calculate aggregated 'by operator' statistics */ function calculate_operator_statistics() { // Prepare database $db = Database::getInstance(); $db_throw_exceptions = $db->throwExceptions(true); try { // Start transaction $db->query('START TRANSACTION'); // Get last record date $result = $db->query( "SELECT MAX(date) as start FROM {operatorstatistics}", array(), array('return_rows' => Database::RETURN_ONE_ROW) ); $start = empty($result['start']) ? 0 : $result['start']; $today = floor(time() / STATISTICS_AGGREGATION_INTERVAL) * STATISTICS_AGGREGATION_INTERVAL; // Caclculate statistics // Get base operator's info $db_results = $db->query( ("SELECT (FLOOR(m.dtmcreated / :interval) * :interval) AS date, " . "m.agentid AS operator_id, " . "COUNT(distinct m.threadid) AS threads, " . "COUNT(m.messageid) AS messages, " . "AVG(CHAR_LENGTH(m.tmessage)) AS avg_msg_length " // Use {message} as base table because of one thread can // be related with more than one operator (they can change each // other during conversation). . "FROM {message} m, {thread} t " . "WHERE m.ikind = :kind_agent " . "AND m.threadid = t.threadid " . "AND (m.dtmcreated - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > m.dtmcreated " // Ignore not accepted invitations . "AND (t.invitationstate = :not_invited " . "OR t.invitationstate = :invitation_accepted) " . "GROUP BY date, operator_id"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':not_invited' => Thread::INVITATION_NOT_INVITED, ':invitation_accepted' => Thread::INVITATION_ACCEPTED, ':kind_agent' => Thread::KIND_AGENT, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store retrieved data as statistics info $statistics = extend_statistics_info( array(), $db_results, array('date', 'operator_id') ); // Get info about invitations $db_results = $db->query( ("SELECT (FLOOR(dtmcreated / :interval) * :interval) AS date, " . "agentid AS operator_id, " . "COUNT(threadid) AS invitations_sent, " . "SUM(invitationstate = :invitation_accepted) AS invitations_accepted, " . "SUM(invitationstate = :invitation_rejected) AS invitations_rejected, " . "SUM(invitationstate = :invitation_ignored) AS invitations_ignored " . "FROM {thread} " . "WHERE (dtmcreated - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > dtmcreated " // Check if thread has related operator . "AND agentid != 0 " // Ignore not accepted invitations . "AND (invitationstate = :invitation_accepted " . "OR invitationstate = :invitation_rejected " . "OR invitationstate = :invitation_ignored) " . "GROUP BY date, operator_id"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':invitation_accepted' => Thread::INVITATION_ACCEPTED, ':invitation_rejected' => Thread::INVITATION_REJECTED, ':invitation_ignored' => Thread::INVITATION_IGNORED, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store retrieved data as statistics info $statistics = extend_statistics_info( $statistics, $db_results, array('date', 'operator_id') ); // Sort statistics by date before save it in the database ksort($statistics); foreach ($statistics as $row) { // Set default values $row += array( 'threads' => 0, 'messages' => 0, 'avg_msg_length' => 0, 'invitations_sent' => 0, 'invitations_accepted' => 0, 'invitations_rejected' => 0, 'invitations_ignored' => 0, ); // Prepare data for insert $insert_data = array(); foreach ($row as $field_name => $field_value) { $insert_data[':' . $field_name] = $field_value; } $db->query( ("INSERT INTO {operatorstatistics} (" . "date, operatorid, threads, messages, averagelength, " . "sentinvitations, acceptedinvitations, " . "rejectedinvitations, ignoredinvitations " . ") VALUES (" . ":date, :operator_id, :threads, :messages, " . ":avg_msg_length, :invitations_sent, " . ":invitations_accepted, :invitations_rejected, " . ":invitations_ignored " . ")"), $insert_data ); } } catch (Exception $e) { // Something went wrong: warn and rollback transaction. trigger_error( 'Operator statistics calculating faild: ' . $e->getMessage(), E_USER_WARNING ); $db->query('ROLLBACK'); // Set throw exceptions back $db->throwExceptions($db_throw_exceptions); return; } // Commit transaction $db->query('COMMIT'); // Set throw exceptions back $db->throwExceptions($db_throw_exceptions); } /** * Calculate aggregated 'by page' statistics */ function calculate_page_statistics() { // Prepare database $db = Database::getInstance(); $db_throw_exceptions = $db->throwExceptions(true); try { // Start transaction $db->query('START TRANSACTION'); // Get last record date $result = $db->query( "SELECT MAX(date) as start FROM {visitedpagestatistics}", array(), array('return_rows' => Database::RETURN_ONE_ROW) ); $start = empty($result['start']) ? 0 : $result['start']; $today = floor(time() / STATISTICS_AGGREGATION_INTERVAL) * STATISTICS_AGGREGATION_INTERVAL; $statistics = array(); // Calculate statistics // Get main pages info $db_results = $db->query( ("SELECT FLOOR(visittime / :interval) * :interval AS date, " . "address, " . "COUNT(DISTINCT pageid) AS visits " . "FROM {visitedpage} " . "WHERE calculated = 0 " . "AND (visittime - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > visittime " . "GROUP BY date, address"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store info in statistics data $statistics = extend_statistics_info( array(), $db_results, array('date', 'address') ); // Get total chats count $db_results = $db->query( ("SELECT FLOOR(p.visittime / :interval) * :interval AS date, " . "p.address AS address, " . "COUNT(DISTINCT t.threadid) AS chats " . "FROM {visitedpage} p, {thread} t, " . "(SELECT " . "COUNT(*) AS msgs, " . "m.threadid " . "FROM {message} m " . "WHERE m.ikind = :kind_user OR m.ikind = :kind_agent " . "GROUP BY m.threadid) tmp " . "WHERE t.referer = p.address " . "AND p.calculated = 0 " . "AND t.threadid = tmp.threadid " . "AND tmp.msgs > 0 " . "AND t.dtmchatstarted <> 0 " . "AND (p.visittime - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > p.visittime " . "AND DATE(FROM_UNIXTIME(p.visittime)) " . "= DATE(FROM_UNIXTIME(t.dtmcreated)) " . "AND (t.invitationstate = :not_invited " . "OR t.invitationstate = :invitation_accepted) " . "GROUP BY date, address"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':not_invited' => Thread::INVITATION_NOT_INVITED, ':invitation_accepted' => Thread::INVITATION_ACCEPTED, ':kind_agent' => Thread::KIND_AGENT, ':kind_user' => Thread::KIND_USER, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store info in statistics data $statistics = extend_statistics_info( $statistics, $db_results, array('date', 'address') ); // Get info about accepted invitations $db_results = $db->query( ("SELECT FLOOR(p.visittime / :interval) * :interval AS date, " . "p.address AS address, " . "COUNT(DISTINCT t.threadid) AS invitations_accepted " . "FROM {visitedpage} p, {thread} t " . "WHERE t.referer = p.address " . "AND p.calculated = 0 " . "AND (p.visittime - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > p.visittime " . "AND DATE(FROM_UNIXTIME(p.visittime)) " . "= DATE(FROM_UNIXTIME(t.dtmcreated)) " . "AND t.invitationstate = :invitation_accepted " . "GROUP BY date, address"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':invitation_accepted' => Thread::INVITATION_ACCEPTED, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store info in statistics data $statistics = extend_statistics_info( $statistics, $db_results, array('date', 'address') ); // Get info about rejected invitations $db_results = $db->query( ("SELECT FLOOR(p.visittime / :interval) * :interval AS date, " . "p.address AS address, " . "COUNT(DISTINCT t.threadid) AS invitations_rejected " . "FROM {visitedpage} p, {thread} t " . "WHERE t.referer = p.address " . "AND p.calculated = 0 " . "AND (p.visittime - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > p.visittime " . "AND DATE(FROM_UNIXTIME(p.visittime)) " . "= DATE(FROM_UNIXTIME(t.dtmcreated)) " . "AND t.invitationstate = :invitation_rejected " . "GROUP BY date, address"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':invitation_rejected' => Thread::INVITATION_REJECTED, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store info in statistics data $statistics = extend_statistics_info( $statistics, $db_results, array('date', 'address') ); // Get info about ignored invitations $db_results = $db->query( ("SELECT FLOOR(p.visittime / :interval) * :interval AS date, " . "p.address AS address, " . "COUNT(DISTINCT t.threadid) AS invitations_ignored " . "FROM {visitedpage} p, {thread} t " . "WHERE t.referer = p.address " . "AND p.calculated = 0 " . "AND (p.visittime - :start) > :interval " // Calculate statistics only for yesterday . "AND :today > p.visittime " . "AND DATE(FROM_UNIXTIME(p.visittime)) " . "= DATE(FROM_UNIXTIME(t.dtmcreated)) " . "AND t.invitationstate = :invitation_ignored " . "GROUP BY date, address"), array( ':start' => $start, ':today' => $today, ':interval' => STATISTICS_AGGREGATION_INTERVAL, ':invitation_ignored' => Thread::INVITATION_IGNORED, ), array('return_rows' => Database::RETURN_ALL_ROWS) ); // Store info in statistics data $statistics = extend_statistics_info( $statistics, $db_results, array('date', 'address') ); // Sort statistics by date before save it in the database ksort($statistics); foreach ($statistics as $row) { // Set default values $row += array( 'visits' => 0, 'chats' => 0, 'invitations_accepted' => 0, 'invitations_rejected' => 0, 'invitations_ignored' => 0, ); $row['invitations_sent'] = $row['invitations_accepted'] + $row['invitations_rejected'] + $row['invitations_ignored']; // Prepare data for insert $insert_data = array(); foreach ($row as $field_name => $field_value) { $insert_data[':' . $field_name] = $field_value; } $db->query( ("INSERT INTO {visitedpagestatistics} (" . "date, address, visits, chats, " . "sentinvitations, acceptedinvitations, " . "rejectedinvitations, ignoredinvitations " . ") VALUES (" . ":date, :address, :visits, :chats, :invitations_sent, " . ":invitations_accepted, :invitations_rejected, " . ":invitations_ignored " . ")"), $insert_data ); } // Mark all visited pages as 'calculated' $db->query( ("UPDATE {visitedpage} SET calculated = 1 " . "WHERE :today > visittime " . "AND calculated = 0"), array( ':today' => $today ) ); // Remove old tracks from the system track_remove_old_tracks(); } catch (Exception $e) { // Something went wrong: warn and rollback transaction. trigger_error( 'Page statistics calculating faild: ' . $e->getMessage(), E_USER_WARNING ); $db->query('ROLLBACK'); // Set throw exceptions back $db->throwExceptions($db_throw_exceptions); return; } // Commit transaction $db->query('COMMIT'); // Set throw exceptions back $db->throwExceptions($db_throw_exceptions); } /** * Add info from $additional_info to $stat_info using specified keys. * * Triggers an error with E_WARNING level if row of $additional_info array does * not contain one of specified keys. * @param array $stat_info Statistics info * @param array $additional_info Data that must be added to statistics info * @param array $keys List of keys. * @return array|boolean Extended statistics info or boolean false on failure */ function extend_statistics_info($stat_info, $additional_info, $keys) { $result = $stat_info; foreach ($additional_info as $row) { // Build key field $key_field = array(); foreach ($keys as $key) { if (!array_key_exists($key, $row)) { trigger_error( "There is no '{$key}' key in additional_info row!", E_USER_WARNING ); return false; } $key_field[] = $row[$key]; } $key_field = implode('_', $key_field); // Index info if (empty($result[$key_field])) { $result[$key_field] = array(); } // Extend info $result[$key_field] += $row; } return $result; }