exec("CREATE TABLE IF NOT EXISTS analytics_events (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, ts INT UNSIGNED NOT NULL, day DATE NOT NULL,
type VARCHAR(16) NOT NULL, sid CHAR(32) NOT NULL DEFAULT '', path VARCHAR(160) NOT NULL DEFAULT '',
ref_host VARCHAR(120) NOT NULL DEFAULT '', src VARCHAR(40) NOT NULL DEFAULT '', device VARCHAR(8) NOT NULL DEFAULT '',
bot TINYINT NOT NULL DEFAULT 0, from_code VARCHAR(10) NOT NULL DEFAULT '', to_code VARCHAR(10) NOT NULL DEFAULT '',
train_no VARCHAR(10) NOT NULL DEFAULT '', jdate DATE NULL, n INT NOT NULL DEFAULT 0,
note VARCHAR(60) NOT NULL DEFAULT '', ip_hash CHAR(16) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
/* ---------------- filters ---------------- */
$today = new DateTime('now', new DateTimeZone('Asia/Kolkata'));
$preset = $_GET['preset'] ?? '30';
$to = $_GET['to'] ?? $today->format('Y-m-d');
$from = $_GET['from'] ?? (clone $today)->modify('-29 day')->format('Y-m-d');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) $from = (clone $today)->modify('-29 day')->format('Y-m-d');
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) $to = $today->format('Y-m-d');
if (isset($_GET['preset']) && !isset($_GET['from'])) {
$to = $today->format('Y-m-d');
if ($preset === 'today') $from = $to;
elseif ($preset === '7') $from = (clone $today)->modify('-6 day')->format('Y-m-d');
elseif ($preset === '30') $from = (clone $today)->modify('-29 day')->format('Y-m-d');
elseif ($preset === 'all') { $from = '2026-01-01'; }
}
$incbots = !empty($_GET['incbots']) ? 1 : 0;
$dev = in_array($_GET['dev'] ?? '', ['mobile', 'desktop', 'bot'], true) ? $_GET['dev'] : '';
$srcF = isset($_GET['src']) ? substr(preg_replace('/[^0-9A-Za-z_\-]/', '', $_GET['src']), 0, 40) : '';
$w = ["day BETWEEN ? AND ?"];
$p = [$from, $to];
if (!$incbots) { $w[] = "bot = 0"; }
if ($dev !== '') { $w[] = "device = ?"; $p[] = $dev; }
if ($srcF !== '') { $w[] = "src = ?"; $p[] = $srcF; }
$W = implode(' AND ', $w);
function q(PDO $pdo, string $sql, array $p): array {
$st = $pdo->prepare($sql); $st->execute($p); return $st->fetchAll(PDO::FETCH_ASSOC);
}
function names(PDO $pdo, string $table, string $keyCol, string $nameCol, array $keys): array {
$keys = array_values(array_unique(array_filter($keys)));
if (!$keys) return [];
$in = implode(',', array_fill(0, count($keys), '?'));
$st = $pdo->prepare("SELECT $keyCol k, $nameCol nm FROM $table WHERE $keyCol IN ($in)");
$st->execute($keys);
$m = []; foreach ($st as $r) $m[$r['k']] = $r['nm']; return $m;
}
/* ---------------- metrics ---------------- */
$sum = q($pdo, "SELECT
COALESCE(SUM(type='pageview'),0) pv,
COUNT(DISTINCT CASE WHEN type='pageview' THEN sid END) uv,
COALESCE(SUM(type='search'),0) s,
COALESCE(SUM(type='availability'),0) a,
COALESCE(SUM(type='pnr'),0) pnr
FROM analytics_events WHERE $W", $p)[0] ?? ['pv'=>0,'uv'=>0,'s'=>0,'a'=>0,'pnr'=>0];
$daily = q($pdo, "SELECT day,
COALESCE(SUM(type='pageview'),0) pv,
COUNT(DISTINCT CASE WHEN type='pageview' THEN sid END) uv,
COALESCE(SUM(type='search'),0) s,
COALESCE(SUM(type='availability'),0) a,
COALESCE(SUM(type='pnr'),0) pnr
FROM analytics_events WHERE $W GROUP BY day ORDER BY day", $p);
$routes = q($pdo, "SELECT from_code, to_code, COUNT(*) c
FROM analytics_events WHERE $W AND type='search' AND from_code<>'' AND to_code<>''
GROUP BY from_code, to_code ORDER BY c DESC LIMIT 25", $p);
$origins = q($pdo, "SELECT from_code k, COUNT(*) c FROM analytics_events
WHERE $W AND type='search' AND from_code<>'' GROUP BY from_code ORDER BY c DESC LIMIT 12", $p);
$dests = q($pdo, "SELECT to_code k, COUNT(*) c FROM analytics_events
WHERE $W AND type='search' AND to_code<>'' GROUP BY to_code ORDER BY c DESC LIMIT 12", $p);
$trains = q($pdo, "SELECT train_no k, COUNT(*) c FROM analytics_events
WHERE $W AND type='availability' AND train_no<>'' GROUP BY train_no ORDER BY c DESC LIMIT 15", $p);
$sources = q($pdo, "SELECT CASE WHEN src='' THEN '(direct / app)' ELSE src END src,
COUNT(DISTINCT sid) uv, COUNT(*) hits
FROM analytics_events WHERE $W AND type='pageview' GROUP BY src ORDER BY uv DESC LIMIT 15", $p);
$referrers = q($pdo, "SELECT ref_host, COUNT(DISTINCT sid) uv FROM analytics_events
WHERE $W AND type='pageview' AND ref_host<>'' GROUP BY ref_host ORDER BY uv DESC LIMIT 12", $p);
$devices = q($pdo, "SELECT device, COUNT(DISTINCT sid) uv, COUNT(*) hits FROM analytics_events
WHERE $W AND type='pageview' GROUP BY device ORDER BY uv DESC", $p);
$stCodes = []; foreach ($routes as $r) { $stCodes[] = $r['from_code']; $stCodes[] = $r['to_code']; }
foreach ($origins as $r) $stCodes[] = $r['k']; foreach ($dests as $r) $stCodes[] = $r['k'];
$stName = names($pdo, 'stations', 'code', 'name', $stCodes);
$trName = names($pdo, 'trains', 'train_no', 'train_name', array_column($trains, 'k'));
function sn(array $m, string $c): string { return ($m[$c] ?? '') !== '' ? $m[$c] . " ($c)" : $c; }
$maxPv = 1; foreach ($daily as $d) $maxPv = max($maxPv, (int) $d['pv']);
$pvN = (int) $sum['pv']; $uvN = (int) $sum['uv']; $sN = (int) $sum['s']; $aN = (int) $sum['a']; $pnrN = (int) $sum['pnr'];
$spv = $uvN ? round($sN / $uvN, 2) : 0;
$aps = $sN ? round($aN / $sN * 100) : 0;
function h($v): string { return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); }
function qs(array $over): string {
$base = ['from' => $_GET['from'] ?? null, 'to' => $_GET['to'] ?? null, 'preset' => $_GET['preset'] ?? null,
'incbots' => $_GET['incbots'] ?? null, 'dev' => $_GET['dev'] ?? null, 'src' => $_GET['src'] ?? null];
$m = array_merge($base, $over);
$m = array_filter($m, fn($v) => $v !== null && $v !== '');
return '?' . http_build_query($m);
}
$page_title = 'Analytics';
require APP_DIR . '/header.php';
?>
Analytics
= h($from) ?> → = h($to) ?>= $incbots?'':' · bots excluded' ?>= $dev?' · '.h($dev):'' ?>= $srcF?' · src='.h($srcF):'' ?>
Visitors
= number_format($uvN) ?>
unique sessions
Pageviews
= number_format($pvN) ?>
= $uvN?round($pvN/$uvN,1):0 ?> per visitor
Searches
= number_format($sN) ?>
= $spv ?> per visitor
Availability checks
= number_format($aN) ?>
= $aps ?> per 100 searches
PNR checks
= number_format($pnrN) ?>
this period
Daily trend — pageviews
= h(substr($d['day'],5)) ?>
No data in this range yet.
Day-by-day
| Date | Visitors | Pageviews | Searches | Avail. | PNR |
| No data. |
| = h($d['day']) ?> | = (int)$d['uv'] ?> | = (int)$d['pv'] ?> | = (int)$d['s'] ?> | = (int)$d['a'] ?> | = (int)$d['pnr'] ?> |
Top routes searched
| From | To | Searches |
| No searches yet. |
| = h(sn($stName,$r['from_code'])) ?> | = h(sn($stName,$r['to_code'])) ?> | = (int)$r['c'] ?> |
Top origin stations
| Station | Searches |
| = h(sn($stName,$r['k'])) ?> | = (int)$r['c'] ?> |
| — |
Top destination stations
| Station | Searches |
| = h(sn($stName,$r['k'])) ?> | = (int)$r['c'] ?> |
| — |
Most-checked trains (availability)
| Train | Checks |
| No availability checks yet. |
| = h($r['k']) ?>= ($trName[$r['k']] ?? '')!=='' ? ' — '.h($trName[$r['k']]) : '' ?> | = (int)$r['c'] ?> |
Traffic sources
| Source | Visitors | Views |
| = h($r['src']) ?> | = (int)$r['uv'] ?> | = (int)$r['hits'] ?> |
| — |
Tip: add ?src=wa to the URL in your WhatsApp message to see that campaign here.
Referrers & devices
| Referrer | Visitors |
| = h($r['ref_host']) ?> | = (int)$r['uv'] ?> |
| — (direct/app) |
| Device | Visitors | Views |
| = h($r['device']?:'(unknown)') ?> | = (int)$r['uv'] ?> | = (int)$r['hits'] ?> |
| — |