src/Admin/Pages/Analytics.php
<?php
namespace BootPress\Admin\Pages;
use BootPress\Admin\Component as Admin;
use BootPress\Analytics\Component as BPA;
class Analytics
{
private $db;
private $now;
private $offset;
private $user_id;
public static function setup($auth, $path)
{
return ($auth->isAdmin(2)) ? array(Admin::$bp->icon('line-chart', 'fa').' Analytics' => array(
Admin::$bp->icon('area-chart', 'fa').' Visitors' => '',
Admin::$bp->icon('sitemap', 'fa').' Robots' => 'robots',
Admin::$bp->icon('user').' Users' => 'users',
Admin::$bp->icon('link', 'fa').' Referrers' => 'referrers',
Admin::$bp->icon('files-o', 'fa').' Pages' => 'pages',
Admin::$bp->icon('server', 'fa').' Server' => 'server',
)) : false;
}
public static function page()
{
extract(Admin::params('bp', 'page', 'auth', 'path', 'method'));
$bp->pagination->html('links', array('wrapper' => '<ul class="pagination pagination-sm no-margin">{{ value }}</ul>'));
if (empty($method)) {
$method = 'visitors';
}
BPA::process();
$analytics = new self();
return $analytics->$method();
}
private function __construct()
{
extract(Admin::params('page'));
$this->db = BPA::database();
$this->now = time();
$this->offset = ($user = $page->session->get('analytics')) ? (int) $user['offset'] : 0;
$this->user_id = ($user = $page->session->get('bootpress')) ? (int) $user['id'] : null;
}
private function visitors()
{
extract(Admin::params('bp', 'website', 'page', 'path', 'admin'));
$page->title = 'Visitors at '.$website;
$html = '';
if (($visitors = $page->get('visitors')) && ($from = $page->get('from')) && ($to = $page->get('to'))) {
if (!$bp->pagination->set('page', 100)) {
$bp->pagination->total($this->db->value(array(
'SELECT COUNT(DISTINCT h.session_id) AS count',
'FROM analytic_hits AS h',
'INNER JOIN analytic_sessions AS s ON h.session_id = s.id',
'LEFT JOIN analytic_users AS u ON h.session_id = u.session_id AND u.user_id = ?',
$this->where('h.time', $from, $to, array(
'u.user_id IS NULL',
)),
), $this->user_id));
}
$html .= $bp->table->open('class=hover');
$html .= $bp->table->head();
$html .= $bp->table->cell('', 'Location');
$html .= $bp->table->cell('class=text-center', 'Browser');
$html .= $bp->table->cell('class=text-center', 'Screen');
$html .= $bp->table->cell('class=text-center', 'Hits');
$html .= $bp->table->cell('class=text-center', 'Duration');
$html .= $bp->table->cell('', 'Time');
if ($result = $this->db->query(array(
'SELECT s.id, s.timezone, s.hemisphere, s.hits, s.duration, s.started, s.width, s.height, a.browser, a.mobile, a.desktop, a.robot, a.agent',
'FROM analytic_hits AS h',
'INNER JOIN analytic_sessions AS s ON h.session_id = s.id',
'LEFT JOIN analytic_users AS u ON h.session_id = u.session_id AND u.user_id = ?',
'INNER JOIN analytic_agents AS a ON s.agent_id = a.id',
$this->where('h.time', $from, $to, array(
'u.user_id IS NULL',
)),
'GROUP BY h.session_id ORDER BY s.started DESC'.$bp->pagination->limit,
), $this->user_id, 'assoc')) {
$url = $page->url($admin, $path, 'users');
while ($row = $this->db->fetch($result)) {
if (!empty($row['mobile'])) {
$row['browser'] = $row['mobile'];
}
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$page->url('add', $url, array(
'session' => $row['id'],
)).'">'.BPA::location($row['timezone'], $row['hemisphere']).'</a>');
$html .= $bp->table->cell('class=text-center', !empty($row['browser']) ? $row['browser'] : '-');
$html .= $bp->table->cell('class=text-center', $row['width'].' x '.$row['height']);
$html .= $bp->table->cell('class=text-center', $row['hits']);
$html .= $bp->table->cell('class=text-center', !empty($row['duration']) ? number_format(($row['duration'] / 60), 2).' min' : '-');
$html .= $bp->table->cell('', '<span class="timeago" title="'.date('c', $row['started']).'">'.$row['started'].'</span>');
}
}
$html .= $bp->table->close();
$html = Admin::box('default', array(
'head with-border' => $bp->icon('line-chart', 'fa').' '.$visitors,
'body no-padding table-responsive' => $html,
'foot clearfix' => $bp->pagination->links(),
));
} else {
$data = array();
foreach ($this->startStop(31, 'day', 'Y-m-d') as $x => $info) { // D M j
list($start, $stop) = $info;
list($user, $hits) = array_values($this->userHits($start, $stop));
$data[] = '{x:"'.$x.'", hits:'.$hits.', users:'.$user.', avg:'.($user > 0 ? round($hits / $user, 1) : 0).'}';
}
$page->jquery('
new Morris.Area({
behaveLikeLine: true,
element: "visitors-chart",
resize: true,
data: ['.implode(',', $data).'],
xkey: "x",
xLabels: "month",
xLabelFormat: function(x){ var str = x.toDateString(); return str.substr(4,4) + str.substr(-4); },
dateFormat: function(x){ return new Date(x).toDateString().slice(0,-5); },
ykeys: ["hits", "users", "avg"],
labels: ["Pageviews", "Number of Users", "Avg Views per User"],
lineColors: ["#3C8DBC", "#00A65A", "#F56954"],
hideHover: "auto"
});
');
$page->link(array(
'https://cdn.jsdelivr.net/morris.js/0.5.1/morris.min.js',
'https://cdn.jsdelivr.net/morris.js/0.5.1/morris.css',
));
$page->link('https://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.7/raphael.min.js', 'prepend');
$html .= $bp->table->open('class=hover');
$visits = array();
$visits['Since'] = array(date('M Y', $this->started()) => array($this->started(), time()));
$visits['Past Day'] = $this->startStop(24, 'hour', 'g:00a', array('This Hour', 'Last Hour'));
$visits['Past Week'] = $this->startStop(7, 'day', 'l', array('Today', 'Yesterday'));
$visits['Past Month'] = $this->startStop(5, 'week', '', array('This Week', 'Last Week', '2 weeks ago', '3 weeks ago', '4 weeks ago'));
$visits['Past Year'] = $this->startStop(12, 'month', 'M Y', array('This Month', 'Last Month'));
$url = $page->url($admin, $path);
foreach ($visits as $header => $values) {
$html .= $bp->table->head();
$html .= $bp->table->cell('', $header);
$html .= $bp->table->cell('class=text-right', 'Robots');
$html .= $bp->table->cell('', 'Hits');
$html .= $bp->table->cell('class=text-right', 'Users');
$html .= $bp->table->cell('', 'Hits');
$html .= $bp->table->cell('class=text-center', 'Avg Load Times');
$html .= $bp->table->cell('class=text-center', 'Avg Session Duration');
foreach ($values as $reference => $times) {
list($start, $stop) = $times;
$user = $this->userHits($start, $stop, '-');
$robot = $this->robotHits($start, $stop, '-');
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$page->url('add', $url, array(
'visitors' => ($header == 'Since') ? 'Since '.$reference : $reference,
'from' => $start,
'to' => $stop,
)).'">'.$reference.'</a>');
$html .= $bp->table->cell('class=text-right', $robot['ips']);
$html .= $bp->table->cell('', $robot['hits']);
$html .= $bp->table->cell('class=text-right', $user['sessions']);
$html .= $bp->table->cell('', $user['hits']);
$html .= $bp->table->cell('class=text-center', $user['loaded']);
$html .= $bp->table->cell('class=text-center', $user['duration']);
}
}
$html .= $bp->table->close();
$html = Admin::box('default', array(
'head with-border' => $bp->icon('line-chart', 'fa').' Visitors',
'body' => '<div id="visitors-chart" style="height:300px;"></div>',
'body no-padding table-responsive' => $html,
));
}
return $html;
}
private function robots()
{
extract(Admin::params('bp', 'blog', 'website', 'page'));
$page->title = 'Robot Analytics at '.$website;
$html = '';
$url = $page->url('delete', '', '?');
if (($agent = $page->get('agent')) && $row = $this->db->row(array(
'SELECT id, agent, robot',
'FROM analytic_agents',
'WHERE agent = ?',
), $agent, 'assoc')) {
$header = !empty($row['robot']) ? $row['robot'] : $row['agent'];
if (!$bp->pagination->set('page', 100)) {
$bp->pagination->total($this->db->value(array(
'SELECT COUNT(*) FROM analytic_bots WHERE agent_id = ?',
), $row['id']));
}
if ($result = $this->db->query(array(
'SELECT p.path, b.query, b.time, b.ip',
'FROM analytic_bots AS b',
'INNER JOIN analytic_paths AS p ON b.path_id = p.id',
'WHERE b.agent_id = ? ORDER BY time DESC'.$bp->pagination->limit,
), $row['id'], 'row')) {
$html .= $bp->table->open('class=hover');
$html .= $bp->table->head();
$html .= $bp->table->cell('', 'URL');
$html .= $bp->table->cell('', 'IP');
$html .= $bp->table->cell('class=text-center', 'Accessed');
$html .= $bp->table->cell('class=text-center', 'Next');
$delayed = null;
while (list($path, $query, $time, $ip) = $this->db->fetch($result)) {
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$page->url('base', $path.$query).'">'.$this->ellipsize($path, 50, '(index)').'</a>');
$html .= $bp->table->cell('', '<a href="'.$page->url('add', $url, 'ip', $ip).'">'.$ip.'</a>');
$html .= $bp->table->cell('class=text-center', date('D, M d Y, h:i a', $time - $this->offset));
$html .= $bp->table->cell('class=text-center', $this->next($delayed, $time));
}
$html .= $bp->table->close();
$this->db->close($result);
}
} elseif ($ip = $page->get('ip')) {
$header = strip_tags($ip);
if (!$bp->pagination->set('page', 100)) {
$bp->pagination->total($this->db->value(array(
'SELECT COUNT(*) FROM analytic_bots WHERE ip = ?',
), $header));
}
if ($result = $this->db->query(array(
'SELECT p.path, b.query, b.time, a.agent',
'FROM analytic_bots AS b',
'INNER JOIN analytic_paths AS p ON b.path_id = p.id',
'INNER JOIN analytic_agents AS a ON b.agent_id = a.id',
'WHERE b.ip = ? ORDER BY time DESC'.$bp->pagination->limit,
), $header, 'row')) {
$html .= $bp->table->open('class=hover');
$html .= $bp->table->head();
$html .= $bp->table->cell('', 'URL');
$html .= $bp->table->cell('', 'User Agent');
$html .= $bp->table->cell('class=text-center', 'Accessed');
$html .= $bp->table->cell('class=text-center', 'Next');
$delayed = null;
while (list($path, $query, $time, $agent) = $this->db->fetch($result)) {
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$page->url('base', $path.$query).'">'.$this->ellipsize($path, 50, '(index)').'</a>');
$html .= $bp->table->cell('', '<a href="'.$page->url('add', $url, 'agent', $agent).'">'.$this->ellipsize($agent, 50, '(empty)').'</a>');
$html .= $bp->table->cell('class=text-center', date('D, M d Y, h:i a', $time - $this->offset));
$html .= $bp->table->cell('class=text-center', $this->next($delayed, $time));
}
$html .= $bp->table->close();
$this->db->close($result);
}
} else {
$header = 'Robots <small style="margin-left:10px;">Last 30 Days</small>';
$file = $blog->folder.'content/robots.txt.twig';
if (!is_file($file)) {
file_put_contents($file, '');
}
\BootPress\Admin\Files::save(array('robots.txt' => $file));
if (!$sitemaps = $this->db->ids(array(
'SELECT id FROM analytic_paths WHERE path LIKE ? AND path NOT LIKE ?',
), array('sitemap%.xml', '%/%'))) {
$sitemaps = array();
}
if (!$robots = $this->db->ids(array(
'SELECT id FROM analytic_paths WHERE path = ?',
), 'robots.txt')) {
$robots = array();
}
$month = time() - 2592000; // last 30 days
if (!$bp->pagination->set('page', 100)) {
$bp->pagination->total($this->db->value(array(
'SELECT COUNT(DISTINCT agent_id)',
'FROM analytic_bots',
'WHERE time > ?',
), $month));
}
if ($result = $this->db->query(array(
'SELECT a.agent, a.robot,',
' MAX(b.time) AS time,',
' COUNT(b.agent_id) AS hits,',
' SUM(CASE WHEN b.path_id IN('.implode(',', $robots).') THEN 1 ELSE 0 END) AS robots,',
' SUM(CASE WHEN b.path_id IN('.implode(',', $sitemaps).') THEN 1 ELSE 0 END) AS sitemaps,',
' MAX(CASE WHEN b.path_id IN('.implode(',', array_merge($robots, $sitemaps)).') THEN b.time ELSE 0 END) AS checked',
'FROM analytic_bots AS b',
'INNER JOIN analytic_agents AS a ON b.agent_id = a.id',
'WHERE b.time > ?',
'GROUP BY b.agent_id',
'ORDER BY hits DESC'.$bp->pagination->limit,
), $month, 'row')) {
$html .= $bp->table->open('class=hover');
$html .= $bp->table->head();
$html .= $bp->table->cell('', 'User Agent');
$html .= $bp->table->cell('class=text-center', 'Hits');
$html .= $bp->table->cell('class=text-center', '<a href="#" class="wyciwyg txt text-nowrap" data-retrieve="robots.txt" data-file="robots.txt" title="Edit">'.$bp->icon('pencil-square-o', 'fa').' robots.txt</a>');
$html .= $bp->table->cell('class=text-center', '<a href="'.$page->url('sitemap.xml').'" class="text-nowrap" target="_blank" title="View">sitemap%.xml '.$bp->icon('external-link', 'fa').'</a>');
$html .= $bp->table->cell('class=text-center', 'Checked');
while (list($agent, $robot, $time, $hits, $robots, $sitemaps, $checked) = $this->db->fetch($result)) {
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$page->url('add', $url, 'agent', $agent).'">'.(!empty($robot) ? $robot : $this->ellipsize($agent, 50, '(empty)')).'</a>');
$html .= $bp->table->cell('class=text-center', $hits);
$html .= $bp->table->cell('class=text-center', !empty($robots) ? $robots : '-');
$html .= $bp->table->cell('class=text-center', !empty($sitemaps) ? $sitemaps : '-');
$html .= $bp->table->cell('class=text-center', !empty($checked) ? '<span class="timeago" title="'.date('c', $checked).'">'.$checked.'</span>' : '-');
}
$html .= $bp->table->close();
$this->db->close($result);
}
}
return Admin::box('default', array(
'head with-border' => $bp->icon('sitemap', 'fa').' '.$header,
'body no-padding table-responsive' => $html,
'foot clearfix' => $bp->pagination->links(),
));
}
private function users($data = null)
{
extract(Admin::params('bp', 'blog', 'website', 'page'));
$page->title = 'User Analytics at '.$website;
$html = '';
if ($id = $page->get('session')) {
$tb = $bp->table->open('class=hover');
$tb .= $bp->table->head();
$tb .= $bp->table->cell('', 'URL');
$tb .= $bp->table->cell('class=text-center', 'Server');
$tb .= $bp->table->cell('class=text-center', 'Loaded');
$tb .= $bp->table->cell('class=text-center', 'Accessed');
$tb .= $bp->table->cell('class=text-center', 'Next');
if ($row = $this->db->row(array(
'SELECT a.agent, a.mobile, a.desktop, a.browser, a.version, s.width, s.height, s.ip, s.timezone, s.hemisphere, s.referrer, p.path, s.query, s.started, s.hits',
'FROM analytic_sessions AS s',
'INNER JOIN analytic_agents AS a ON s.agent_id = a.id',
'INNER JOIN analytic_paths AS p ON s.path_id = p.id',
'WHERE s.id = ?',
), $id, 'assoc')) {
$dl = array();
if (!empty($row['agent'])) {
$dl['User Agent'] = $row['agent'];
}
if (!empty($row['mobile'])) {
$dl['Mobile'] = $row['mobile'];
} elseif (!empty($row['desktop'])) {
$dl['Desktop'] = $row['desktop'];
}
if (!empty($row['browser'])) {
$dl['Browser'] = $row['browser'];
if (!empty($row['version'])) {
$dl['Browser'] .= ' v.'.$row['version'];
}
}
$dl['Screen'] = $row['width'].' x '.$row['height'];
$dl['IP Address'] = $row['ip'];
if (!empty($row['referrer'])) {
$dl['Referrer'] = $row['referrer'];
}
$dl['Landing Page'] = '<a href="'.$page->url('base', $row['path'].$row['query']).'">'.$this->ellipsize($row['path'], 255, '(index)').'</a>';
$dl['Location'] = BPA::location($row['timezone'], $row['hemisphere']);
$dl['Time'] = '<span class="timeago" title="'.date('c', $row['started']).'">'.$row['started'].'</span>';
$html .= $bp->lister('dl dl-horizontal', $dl);
if (!$bp->pagination->set('page', 100)) {
$bp->pagination->total($row['hits']);
}
if ($result = $this->db->query(array(
'SELECT p.path, h.query, h.loaded, h.server, h.time',
'FROM analytic_hits AS h',
'INNER JOIN analytic_paths AS p ON h.path_id = p.id',
'WHERE h.session_id = ? ORDER BY time DESC'.$bp->pagination->limit,
), $id, 'row')) {
$delayed = null;
while (list($path, $query, $loaded, $server, $time) = $this->db->fetch($result)) {
$tb .= $bp->table->row();
$tb .= $bp->table->cell('', '<a href="'.$page->url('base', $path.$query).'">'.$this->ellipsize($path, 50, '(index)').'</a>');
$tb .= $bp->table->cell('class=text-center', number_format(($server / 1000), 2).' s');
$tb .= $bp->table->cell('class=text-center', number_format(($loaded / 1000), 2).' s');
$tb .= $bp->table->cell('class=text-center', date('D, M d Y, h:i a', $time - $this->offset));
$tb .= $bp->table->cell('class=text-center', $this->next($delayed, $time));
}
$this->db->close($result);
}
}
$tb .= $bp->table->close();
$html = Admin::box('default', array(
'head with-border' => $bp->icon('user').' Users',
'body' => $html,
'body no-padding table-responsive' => $tb,
'foot clearfix' => $bp->pagination->links(),
));
} else {
if (is_array($data)) {
$colors = array(
'#F39C12', // orange
'#F56954', // red
'#00A65A', // green
'#3C8DBC', // dk. blue
'#00C0EF', // lt. blue
'#D2D6DE', // lt. gray
);
// $colors = array('#F56954', '#00A65A', '#F39C12', '#00C0EF', '#3C8DBC', '#D2D6DE'); // red, green, orange, lt. blue, blue, lt. gray
foreach ($data as $key => $value) {
if (!empty($key) && ($percent = round($value)) > 0) {
$color = array_shift($colors);
$data[$key] = "{value:{$percent},color:\"{$color}\",label:\"{$key}\"}";
array_push($colors, $color);
} else {
unset($data[$key]);
}
}
return (!empty($data)) ? '['.implode(', ', $data).']' : null;
}
$page->style(array(
'canvas { display:inline; }',
'.canvas-container { width:100%; text-align:center; }',
'.vcenter' => array(
'display: inline-block;',
'vertical-align: middle;',
'float: none;',
),
));
$page->link('https://cdn.jsdelivr.net/chart.js/1.0.1/Chart.min.js');
$options = array(
'animation:false',
'legendTemplate:"<ul class=\"<%=name.toLowerCase()%>-legend list-unstyled\"><% for (var i=0; i<segments.length; i++){%><li><p><i class=\"fa fa-circle-o\" style=\"color:<%=segments[i].fillColor%>; margin-right:10px;\"></i><%=segments[i].value%>% - <%=segments[i].label%></p></li><%}%></ul>"',
'tooltipTemplate:"<%=value %>% - <%=label%>"',
);
$options = '{'.implode(', ', $options).'}';
$total = 0;
$mobile = array();
$platforms = array();
$browsers = array();
$versions = array();
if ($result = $this->db->query(array(
'SELECT s.hits, a.browser, a.version, a.mobile, a.desktop',
'FROM analytic_sessions AS s',
'INNER JOIN analytic_agents AS a ON s.agent_id = a.id',
'LEFT JOIN analytic_users AS u ON s.id = u.session_id AND u.user_id = ?',
'WHERE s.started > ? AND u.user_id IS NULL',
), array($this->user_id, ($this->now - 2592000)), 'row')) {
while (list($hits, $browser, $version, $phone, $desktop) = $this->db->fetch($result)) {
// Total
$total += $hits;
// Mobile
if (!empty($phone)) {
if (!isset($mobile[$phone])) {
$mobile[$phone] = 0;
}
$mobile[$phone] += $hits;
}
// Platforms
if (!isset($platforms[$desktop])) {
$platforms[$desktop] = 0;
}
$platforms[$desktop] += $hits;
// Browsers
if (!isset($browsers[$browser])) {
$browsers[$browser] = 0;
}
$browsers[$browser] += $hits;
// Versions
$version = (int) $version;
if (!isset($versions[$browser][$version])) {
$versions[$browser][$version] = 0;
}
$versions[$browser][$version] += $hits;
}
$this->db->close($result);
}
// Mobile
foreach ($mobile as $phone => $hits) {
$mobile[$phone] = ($hits / $total) * 100;
}
arsort($mobile);
if ($data = $this->users($mobile)) {
$html .= '<br>'.$bp->row('sm', array(
$bp->col('6 vcenter', '<div class="canvas-container"><canvas id="mobileChart" height="250"></canvas></div>'),
$bp->col('5 vcenter', '<p class="lead">Mobile ('.round(array_sum($mobile)).'% of Users)</p><div id="mobileChartLegend"></div>'),
)).'<br>';
$page->script(array(
'var mobileChartCanvas = document.getElementById("mobileChart").getContext("2d");',
'var mobileChart = new Chart(mobileChartCanvas).Doughnut('.$data.', '.$options.');',
'document.getElementById("mobileChartLegend").innerHTML = mobileChart.generateLegend();',
));
}
// Platforms
foreach ($platforms as $platform => $hits) {
$platforms[$platform] = ($hits / $total) * 100;
}
arsort($platforms);
if ($data = $this->users($platforms)) {
$html .= '<br>'.$bp->row('sm', array(
$bp->col('6 vcenter', '<div class="canvas-container"><canvas id="platformsChart" height="250"></canvas></div>'),
$bp->col('5 vcenter', '<p class="lead">Platforms</p><div id="platformsChartLegend"></div>'),
)).'<br>';
$page->script(array(
'var platformsChartCanvas = document.getElementById("platformsChart").getContext("2d");',
'var platformsChart = new Chart(platformsChartCanvas).Doughnut('.$data.', '.$options.');',
'document.getElementById("platformsChartLegend").innerHTML = platformsChart.generateLegend();',
));
}
// Browsers
foreach ($browsers as $browser => $hits) {
$browsers[$browser] = ($hits / $total) * 100;
foreach ($versions[$browser] as $version => $hits) {
$versions[$browser][$version] = ($hits / $total) * 100;
}
arsort($versions[$browser]);
}
arsort($browsers);
if ($data = $this->users($browsers)) {
$html .= '<br>'.$bp->row('sm', array(
$bp->col('6 vcenter', '<div class="canvas-container"><canvas id="browsersChart" height="250"></canvas></div>'),
$bp->col('5 vcenter', '<p class="lead">Browsers</p><div id="browsersChartLegend"></div>'),
)).'<br>';
$page->script(array(
'var browsersChartCanvas = document.getElementById("browsersChart").getContext("2d");',
'var browsersChart = new Chart(browsersChartCanvas).Doughnut('.$data.', '.$options.');',
'document.getElementById("browsersChartLegend").innerHTML = browsersChart.generateLegend();',
));
}
// Versions
$options = str_replace(' - ', ' - v.', $options);
foreach ($browsers as $browser => $share) {
if (!empty($browser) && ($percent = round($share)) > 0 && isset($versions[$browser]) && ($data = $this->users($versions[$browser]))) {
$seo = $blog->url($browser);
$html .= '<br>'.$bp->row('sm', array(
$bp->col('6 vcenter', '<div class="canvas-container"><canvas id="'.$seo.'Chart" height="250"></canvas></div>'),
$bp->col('5 vcenter', '<p class="lead">'.$browser.' ('.$percent.'% of Users)</p><div id="'.$seo.'ChartLegend"></div>'),
)).'<br>';
$page->script(array(
'var '.$seo.'ChartCanvas = document.getElementById("'.$seo.'Chart").getContext("2d");',
'var '.$seo.'Chart = new Chart('.$seo.'ChartCanvas).Doughnut('.$data.', '.$options.');',
'document.getElementById("'.$seo.'ChartLegend").innerHTML = '.$seo.'Chart.generateLegend();',
));
}
}
if (!empty($html)) {
$html = '<div style="margin:20px;">'.$html.'</div>';
}
$html = Admin::box('default', array(
'head with-border' => $bp->icon('user').' Users <small style="margin-left:10px;">Last 30 Days</small>',
'body' => $html,
));
}
return $html;
}
private function referrers()
{
extract(Admin::params('bp', 'website', 'page'));
$page->title = 'Referrer Analytics at '.$website;
$db = BPA::database();
$html = '';
if (!$bp->pagination->set('page', 100)) {
$bp->pagination->total($db->value(array(
'SELECT COUNT(*) FROM analytic_sessions AS s',
'LEFT JOIN analytic_users AS u ON s.id = u.session_id AND u.user_id = ?',
'WHERE s.started > ? AND s.referrer != "" AND u.user_id IS NULL',
), array($this->user_id, ($this->now - 2592000))));
}
if ($result = $db->query(array(
'SELECT s.hits, s.duration, s.started, s.referrer, p.path, s.query, s.hemisphere, s.timezone',
'FROM analytic_sessions AS s',
'INNER JOIN analytic_paths AS p ON s.path_id = p.id',
'LEFT JOIN analytic_users AS u ON s.id = u.session_id AND u.user_id = ?',
'WHERE s.started > ? AND s.referrer != "" AND u.user_id IS NULL',
'ORDER BY s.started DESC'.$bp->pagination->limit,
), array($this->user_id, ($this->now - 2592000)), 'row')) {
$html .= $bp->table->open('class=hover');
$html .= $bp->table->head();
$html .= $bp->table->cell('', 'Referrer');
$html .= $bp->table->cell('', 'Page');
$html .= $bp->table->cell('', 'Location');
$html .= $bp->table->cell('class=text-center', 'Hits');
$html .= $bp->table->cell('class=text-center', 'Time');
while (list($hits, $duration, $started, $referrer, $path, $query, $hemisphere, $timezone) = $db->fetch($result)) {
preg_match('/\/\/([\S]+\.[a-z]{2,4})\//i', $referrer, $matches);
$website = array_pop($matches);
$location = BPA::location($timezone, $hemisphere);
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$referrer.'" title="'.$website.'">'.$this->ellipsize($website, 50).'</a>');
$html .= $bp->table->cell('', '<a href="'.$page->url('base', $path.$query).'" title="'.$path.'">'.$this->ellipsize($path, 50, '(index)').'</a>');
$html .= $bp->table->cell('', '<span title="'.$location.'">'.$this->ellipsize($location, 50).'</span>');
$html .= $bp->table->cell('class=text-center', $hits);
$html .= $bp->table->cell('class=text-center', '<span class="timeago" title="'.date('c', $started).'">'.$started.'</span>');
}
$html .= $bp->table->close();
$db->close($result);
}
return Admin::box('default', array(
'head with-border' => $bp->icon('link', 'fa').' Referrers <small style="margin-left:10px;">Last 30 Days</small>',
'body no-padding table-responsive' => $html,
'foot clearfix' => $bp->pagination->links(),
));
}
private function pages()
{
extract(Admin::params('bp', 'website', 'page', 'path', 'admin'));
$page->title = 'Analytic Pages at '.$website;
$url = $page->url($admin, $path, 'users'); // for linking most recent users
$html = $bp->table->open('class=hover');
$html .= $bp->table->head();
$html .= $bp->table->cell('', 'Most Popular (last 30 days)');
$html .= $bp->table->cell('class=text-center', 'Hits');
if ($result = $this->db->query(array(
'SELECT p.path, COUNT(h.path_id) AS hits',
'FROM analytic_hits AS h',
'INNER JOIN analytic_paths AS p ON h.path_id = p.id',
'LEFT JOIN analytic_users AS u ON h.session_id = u.session_id AND u.user_id = ?',
'WHERE h.time > ? AND u.user_id IS NULL',
'GROUP BY h.path_id ORDER BY hits DESC LIMIT 25',
), array($this->user_id, ($this->now - 2592000)), 'row')) {
while (list($path, $hits) = $this->db->fetch($result)) {
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$page->url('base', $path).'">'.$this->ellipsize($path, 50, '(index)').'</a>');
$html .= $bp->table->cell('class=text-center', $hits);
}
$this->db->close($result);
}
$html .= $bp->table->close();
$popular = Admin::box('default', array(
'body no-padding table-responsive' => $html,
));
$html = $bp->table->open('class=hover');
$html .= $bp->table->head();
$html .= $bp->table->cell('', 'Most Recent (per user)');
$html .= $bp->table->cell('', 'Location');
$html .= $bp->table->cell('', 'Time');
if ($result = $this->db->query(array(
'SELECT p.path, h.query, h.time, s.id, s.timezone, s.hemisphere',
'FROM analytic_hits AS h',
'INNER JOIN analytic_paths AS p ON h.path_id = p.id',
'INNER JOIN analytic_sessions AS s ON h.session_id = s.id',
'LEFT JOIN analytic_users AS u ON h.session_id = u.session_id AND u.user_id = ?',
'WHERE h.time > ? AND u.user_id IS NULL',
'GROUP BY s.id ORDER BY h.id DESC LIMIT 25',
), array($this->user_id, ($this->now - 2592000)), 'row')) {
while (list($path, $query, $time, $id, $timezone, $hemisphere) = $this->db->fetch($result)) {
$html .= $bp->table->row();
$html .= $bp->table->cell('', '<a href="'.$page->url('base', $path.$query).'">'.$this->ellipsize($path, 50, '(index)').'</a>');
$html .= $bp->table->cell('', '<a href="'.$page->url('add', $url, array(
'session' => $id,
)).'">'.BPA::location($timezone, $hemisphere).'</a>');
$html .= $bp->table->cell('', '<span class="timeago" title="'.date('c', $time).'">'.$time.'</span>');
// $html .= $bp->table->cell('class=text-center', '<a href="'.$page->url('add', '', 'id', $id).'">'.$ip.'</a>');
}
}
$html .= $bp->table->close();
$recent = Admin::box('default', array(
'body no-padding table-responsive' => $html,
));
return Admin::box('default', array(
'head with-border' => $bp->icon('files-o', 'fa').' Pages <small style="margin-left:10px;">Last 30 Days</small>',
)).$popular.$recent;
}
private function server()
{
extract(Admin::params('bp', 'website', 'page'));
$page->title = 'Server Analytics at '.$website;
$page->link(array(
'https://cdn.jsdelivr.net/morris.js/0.5.1/morris.min.js',
'https://cdn.jsdelivr.net/morris.js/0.5.1/morris.css',
));
$page->link('https://cdnjs.cloudflare.com/ajax/libs/raphael/2.2.7/raphael.min.js', 'prepend');
// http://morrisjs.github.io/morris.js/lines.html
// xLabelFormat (below): function(x){ return x.toDateString().substr(4); },
// dateFormat (hover): function(x){ return new Date(x).toTimeString().slice(0,5); },
// http://www.w3schools.com/jsref/jsref_obj_date.asp
// x.toDateString() - Wed Dec 14 2016
// x.toTimeString() - 10:00:58 GMT-0900 (Alaskan Standard Time)
// x.toString() - Wed Dec 14 2016 10:02:01 GMT-0900 (Alaskan Standard Time)
$html = '';
// Prepare statement
$stmt = $this->db->prepare(array(
'SELECT',
' AVG(CASE WHEN h.loaded > 0 AND h.loaded < 20000 THEN h.loaded END) / 1000 AS loaded,',
' AVG(CASE WHEN h.server > 0 AND h.server < 20000 THEN h.server END) / 1000 AS server',
'FROM analytic_hits AS h',
'WHERE h.time >= ? AND h.time <= ?',
), 'row');
// Past 3 Hours
$data = array();
foreach ($this->startStop(180, 'minute', 'Y-m-d H:i:s') as $x => $info) { // D M j
list($start, $stop) = $info;
$this->db->execute($stmt, array($start, $stop));
list($loaded, $server) = $this->db->fetch($stmt);
$data[] = '{x:"'.$x.'", loaded:'.round($loaded, 2).', server:'.round($server, 2).'}';
}
$page->jquery('
var minute = new Morris.Area({
behaveLikeLine: true,
element: "minute-chart",
resize: true,
data: ['.implode(',', $data).'],
xkey: "x",
xLabels: "30min",
dateFormat: function(x){ return new Date(x).toTimeString().slice(0,5); },
ykeys: ["loaded", "server"],
labels: ["Loaded", "Server"],
lineColors: ["#a0d0e0", "#3c8dbc"],
hideHover: "auto"
});
');
$html .= Admin::box('default', array(
'head with-border' => $bp->icon('server', 'fa').' Past 3 Hours <small style="margin-left:10px;">By The Minute</small>',
'body' => '<div class="chart" id="minute-chart" style="height:150px;"></div>',
));
// Past Week
$data = array();
foreach ($this->startStop(168, 'hour', 'Y-m-d H:i:s') as $x => $info) { // D M j
list($start, $stop) = $info;
$this->db->execute($stmt, array($start, $stop));
list($loaded, $server) = $this->db->fetch($stmt);
$data[] = '{x:"'.$x.'", loaded:'.round($loaded, 2).', server:'.round($server, 2).'}';
}
$page->jquery('
var hour = new Morris.Area({
behaveLikeLine: true,
element: "hour-chart",
resize: true,
data: ['.implode(',', $data).'],
xkey: "x",
xLabels: "day",
xLabelFormat: function(x){ return x.toDateString().slice(0,-5); },
dateFormat: function(x){ return new Date(x).toDateString().slice(0,4) + new Date(x).toTimeString().slice(0,5); },
ykeys: ["loaded", "server"],
labels: ["Loaded", "Server"],
lineColors: ["#a0d0e0", "#3c8dbc"],
hideHover: "auto"
});
');
$html .= Admin::box('default', array(
'head with-border' => $bp->icon('server', 'fa').' Past Week <small style="margin-left:10px;">By The Hour</small>',
'body' => '<div class="chart" id="hour-chart" style="height:150px;"></div>',
));
// Past 6 Months
$data = array();
foreach ($this->startStop(180, 'day', 'Y-m-d H:i:s') as $x => $info) { // D M j
list($start, $stop) = $info;
$this->db->execute($stmt, array($start, $stop));
list($loaded, $server) = $this->db->fetch($stmt);
$data[] = '{x:"'.$x.'", loaded:'.round($loaded, 2).', server:'.round($server, 2).'}';
}
$page->jquery('
var day = new Morris.Area({
behaveLikeLine: true,
element: "day-chart",
resize: true,
data: ['.implode(',', $data).'],
xkey: "x",
xLabels: "month",
xLabelFormat: function(x){ return x.toDateString().substr(4); },
dateFormat: function(x){ return new Date(x).toDateString().slice(0,-5); },
ykeys: ["loaded", "server"],
labels: ["Loaded", "Server"],
lineColors: ["#a0d0e0", "#3c8dbc"],
hideHover: "auto"
});
');
$html .= Admin::box('default', array(
'head with-border' => $bp->icon('server', 'fa').' Past 6 Months <small style="margin-left:10px;">By The Day</small>',
'body' => '<div class="chart" id="day-chart" style="height:150px;"></div>',
));
// Close statement
$this->db->close($stmt);
return $html;
}
private function ellipsize($string, $length, $alt = '')
{
$string = trim(strip_tags($string));
if (empty($string)) {
$string = $alt;
}
return (mb_strlen($string) >= $length) ? mb_substr($string, 0, $length).'…' : $string;
}
private function next(&$delayed, $time)
{
$html = '-';
if (!empty($delayed)) {
$delay = $delayed - $time;
if ($delay < 60) {
$html = $delay.' s';
} elseif ($delay < 3600) {
$delay = round($delay / 60);
$html = $delay.' min';
} elseif ($delay < 86400) {
$delay = round($delay / 3600);
$html = $delay.' hr';
} elseif ($delay < 604800) {
$delay = round($delay / 86400);
$html = $delay.' day';
} else {
$delay = round($delay / 604800);
$html = $delay.' wk';
}
}
$delayed = $time;
return $html;
}
private function where($field, $start, $stop, array $and = array())
{
$where = array();
if (!empty($start)) {
$where[] = $field.' >= '.(int) $start;
}
if (!empty($stop)) {
$where[] = $field.' <= '.(int) $stop;
}
foreach ($and as $value) {
$where[] = $value;
}
return (!empty($where)) ? 'WHERE '.implode(' AND ', $where) : '';
}
private function started()
{
static $started = null;
if (is_null($started)) {
$started = ($time = $this->db->value('SELECT MIN(started) FROM analytic_sessions')) ? $time : $this->now;
}
return $started;
}
private function startStop($count, $range, $label = 'Y-m-d H:i:s', array $values = array())
{
$array = array();
$range = substr($range, 0, 3);
if (in_array($range, array('sec', 'min', 'hou', 'day', 'wee', 'mon', 'yea'))) {
for ($i = 0; $i < $count; ++$i) {
switch ($range) {
case 'sec':
$time = $this->now - $i;
break;
case 'min':
$time = $this->now - ($i * 60);
break;
case 'hou':
$time = $this->now - ($i * 3600);
break;
case 'day':
$time = $this->now - ($i * 86400);
break;
case 'wee':
$time = $this->now - ($i * 604800);
break;
case 'mon':
$time = strtotime("today -{$i} month");
break;
case 'yea':
$time = strtotime("today -{$i} year");
break;
}
list($start, $stop, $value) = $this->timeRange($time, $range, $label);
if ($stop < $this->started()) {
break;
}
if (!empty($values)) {
$value = array_shift($values);
}
if ($start < $this->now) {
$array[$value] = array($start, $stop);
}
}
}
return $array;
}
private function timeRange($time, $range, $label = '')
{
// H - hour - 00 to 23
// i - minute - 00 to 59
// s - second - 00 to 59
// n - month - 1 to 12
// j - day - 1 to 31
// Y - year - 2003
// N - weekday - 1 to 7 (monday to sunday)
$time = explode(' ', date('H i s n j Y N '.$label, ($time - $this->offset)));
list($H, $i, $s, $n, $j, $Y, $N) = array_map('intval', $time);
$label = implode(' ', array_slice($time, 7));
// mktime(H (hour), i (minute), s (second), n (month), j (day), Y (year))
switch (substr($range, 0, 3)) {
case 'sec':
$from = mktime($H, $i, $s, $n, $j, $Y) + $this->offset;
$to = mktime($H, $i, $s, $n, $j, $Y) + $this->offset;
break;
case 'min':
$from = mktime($H, $i, 0, $n, $j, $Y) + $this->offset;
$to = mktime($H, $i, 59, $n, $j, $Y) + $this->offset;
break;
case 'hou':
$from = mktime($H, 0, 0, $n, $j, $Y) + $this->offset;
$to = mktime($H, 59, 59, $n, $j, $Y) + $this->offset;
break;
case 'day':
$from = mktime(0, 0, 0, $n, $j, $Y) + $this->offset;
$to = mktime(23, 59, 59, $n, $j, $Y) + $this->offset;
break;
case 'wee':
$from = mktime(0, 0, 0, $n, ($j - $N) + 1, $Y) + $this->offset;
$to = mktime(23, 59, 59, $n, ($j - $N) + 7, $Y) + $this->offset;
break;
case 'mon':
$from = mktime(0, 0, 0, $n, 1, $Y) + $this->offset;
$to = mktime(23, 59, 59, $n + 1, 0, $Y) + $this->offset;
break;
case 'yea':
$from = mktime(0, 0, 0, 1, 1, $Y) + $this->offset;
$to = mktime(23, 59, 59, 1, 0, $Y + 1) + $this->offset;
break;
default:
throw new \LogicException("The requested range ({$range}) is invalid");
}
return array($from, $to, $label, date('Y-m-d H:i:s', $from), date('Y-m-d H:i:s', $to));
}
private function pageViews($path = null, $start = null, $stop = null, $default = 0)
{
if (is_null($path)) {
$path = Page::html()->url['path'];
}
if (empty($this->user_id)) {
$views = $this->db->value(array(
'SELECT COUNT(*) AS views',
'FROM analytic_hits AS h',
$this->where('h.time', $start, $stop, array(
'h.path_id = (SELECT p.id FROM analytic_paths AS p WHERE p.path = ?)',
)),
), array($path));
} else {
$views = $this->db->value(array(
'SELECT COUNT(*) AS views',
'FROM analytic_hits AS h',
'LEFT JOIN analytic_users AS u ON h.session_id = u.session_id AND u.user_id = ?',
$this->where('h.time', $start, $stop, array(
'h.path_id = (SELECT p.id FROM analytic_paths AS p WHERE p.path = ?)',
'u.user_id IS NULL',
)),
), array($this->user_id, $path));
}
return ($views) ? $views : $default;
}
private function userHits($start = null, $stop = null, $default = 0)
{
if (empty($this->user_id)) {
$row = $this->db->row(array(
'SELECT',
' COUNT(DISTINCT h.session_id) AS sessions,',
' COUNT(*) AS hits,',
' AVG(CASE WHEN s.duration > 0 THEN s.duration END) / 60 AS duration,',
' AVG(CASE WHEN h.loaded > 0 AND h.loaded < 20000 THEN h.loaded END) / 1000 AS loaded,',
' AVG(CASE WHEN h.server > 0 AND h.server < 20000 THEN h.server END) / 1000 AS server',
'FROM analytic_hits AS h',
'INNER JOIN analytic_sessions AS s ON h.session_id = s.id',
$this->where('h.time', $start, $stop),
), '', 'assoc');
} else {
$row = $this->db->row(array(
'SELECT',
' COUNT(DISTINCT h.session_id) AS sessions,',
' COUNT(*) AS hits,',
' AVG(CASE WHEN s.duration > 0 THEN s.duration END) / 60 AS duration,',
' AVG(CASE WHEN h.loaded > 0 AND h.loaded < 20000 THEN h.loaded END) / 1000 AS loaded,',
' AVG(CASE WHEN h.server > 0 AND h.server < 20000 THEN h.server END) / 1000 AS server',
'FROM analytic_hits AS h',
'INNER JOIN analytic_sessions AS s ON h.session_id = s.id',
'LEFT JOIN analytic_users AS u ON h.session_id = u.session_id AND u.user_id = ?',
$this->where('h.time', $start, $stop, array(
'u.user_id IS NULL',
)),
), array($this->user_id), 'assoc');
// exit('<pre>'.print_r($row, true).'</pre>');
}
$user = array();
$user['sessions'] = ($row && !empty($row['sessions'])) ? number_format($row['sessions']) : $default;
$user['hits'] = ($row && !empty($row['hits'])) ? number_format($row['hits']) : $default;
$user['duration'] = ($row && !empty($row['duration'])) ? number_format($row['duration'], 2).' minutes' : $default;
$user['loaded'] = ($row && !empty($row['loaded'])) ? number_format($row['loaded'], 2).' seconds' : $default;
$user['server'] = ($row && !empty($row['server'])) ? number_format($row['server'], 2).' seconds' : $default;
return $user;
}
private function robotHits($start = null, $stop = null, $default = 0)
{
$row = $this->db->row(array(
'SELECT',
' COUNT(DISTINCT b.ip) AS ips,',
' COUNT(DISTINCT b.agent_id) AS agents,',
' COUNT(*) AS hits',
'FROM analytic_bots AS b',
$this->where('b.time', $start, $stop),
), '', 'assoc');
$robot = array();
$robot['ips'] = ($row && !empty($row['ips'])) ? number_format($row['ips']) : $default;
$robot['hits'] = ($row && !empty($row['hits'])) ? number_format($row['hits']) : $default;
$robot['agents'] = ($row && !empty($row['agents'])) ? number_format($row['agents']) : $default;
return $robot;
}
}