Analytics: 3-tab layout + fix geographic data loss; v1.1.34
Splits the analytics panel into Traffic / Engagement / System tabs so episode plays, engagement metrics, and contact/study data are no longer buried under the visitor log. Fixes a bug where topCountries, topStates, topCities, device breakdown, and referrers were computed from only the 100 most-recent visitor rows instead of all rows in the selected range — causing older geographic data (e.g. visits from India, Texas, Georgia) to silently disappear from the aggregate totals. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.33",
|
"version": "1.1.34",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -253,6 +253,10 @@ export function register(app) {
|
|||||||
const botReasons = Object.entries(state.hitStats.botReasons ?? {})
|
const botReasons = Object.entries(state.hitStats.botReasons ?? {})
|
||||||
.sort((a, b) => b[1] - a[1]).slice(0, 10).map(([reason, count]) => ({ reason, count }))
|
.sort((a, b) => b[1] - a[1]).slice(0, 10).map(([reason, count]) => ({ reason, count }))
|
||||||
|
|
||||||
|
// All range rows used for geographic/device/referrer aggregates — no cap so no visits are invisible
|
||||||
|
const allRangeRows = rangeVisitorRows
|
||||||
|
|
||||||
|
// Capped to 100 for the visitor log display (with page history hydrated)
|
||||||
const recentVisitorRows = rangeVisitorRows.slice(0, 100).map(row => {
|
const recentVisitorRows = rangeVisitorRows.slice(0, 100).map(row => {
|
||||||
const fullVisitor = state.visitorStats.visitors[row.visitorId]
|
const fullVisitor = state.visitorStats.visitors[row.visitorId]
|
||||||
return { ...row, pageHistory: fullVisitor?.pageHistory ?? [] }
|
return { ...row, pageHistory: fullVisitor?.pageHistory ?? [] }
|
||||||
@@ -315,13 +319,13 @@ export function register(app) {
|
|||||||
returningVisits: state.visitorStats.returningVisits,
|
returningVisits: state.visitorStats.returningVisits,
|
||||||
firstVisitAt: state.visitorStats.firstVisitAt,
|
firstVisitAt: state.visitorStats.firstVisitAt,
|
||||||
lastVisitAt: state.visitorStats.lastVisitAt,
|
lastVisitAt: state.visitorStats.lastVisitAt,
|
||||||
topCountries: buildTopLocations(recentVisitorRows, 'country'),
|
topCountries: buildTopLocations(allRangeRows, 'country'),
|
||||||
topStates: buildTopLocations(recentVisitorRows, 'state'),
|
topStates: buildTopLocations(allRangeRows, 'state'),
|
||||||
topCounties: buildTopLocations(recentVisitorRows, 'county'),
|
topCounties: buildTopLocations(allRangeRows, 'county'),
|
||||||
topCities: buildTopLocations(recentVisitorRows, 'city'),
|
topCities: buildTopLocations(allRangeRows, 'city'),
|
||||||
deviceBreakdown: (() => {
|
deviceBreakdown: (() => {
|
||||||
const counts = { mobile: 0, desktop: 0, tablet: 0, unknown: 0 }
|
const counts = { mobile: 0, desktop: 0, tablet: 0, unknown: 0 }
|
||||||
for (const row of recentVisitorRows) {
|
for (const row of allRangeRows) {
|
||||||
const d = row.device ?? 'unknown'
|
const d = row.device ?? 'unknown'
|
||||||
counts[d] = (counts[d] ?? 0) + 1
|
counts[d] = (counts[d] ?? 0) + 1
|
||||||
}
|
}
|
||||||
@@ -329,7 +333,7 @@ export function register(app) {
|
|||||||
})(),
|
})(),
|
||||||
topReferrers: (() => {
|
topReferrers: (() => {
|
||||||
const counts = {}
|
const counts = {}
|
||||||
for (const row of recentVisitorRows) {
|
for (const row of allRangeRows) {
|
||||||
if (!row.referrer) continue
|
if (!row.referrer) continue
|
||||||
counts[row.referrer] = (counts[row.referrer] ?? 0) + 1
|
counts[row.referrer] = (counts[row.referrer] ?? 0) + 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export function AnalyticsPanel({
|
|||||||
onDeployHook,
|
onDeployHook,
|
||||||
onRefreshStatus,
|
onRefreshStatus,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const [panelTab, setPanelTab] = useState<'traffic' | 'engagement' | 'system'>('traffic')
|
||||||
const [chartTab, setChartTab] = useState<'overview' | 'breakdown' | 'geographic' | 'referrers'>('overview')
|
const [chartTab, setChartTab] = useState<'overview' | 'breakdown' | 'geographic' | 'referrers'>('overview')
|
||||||
const [expandedVisitor, setExpandedVisitor] = useState<string | null>(null)
|
const [expandedVisitor, setExpandedVisitor] = useState<string | null>(null)
|
||||||
const [fullBackupFile, setFullBackupFile] = useState<File | null>(null)
|
const [fullBackupFile, setFullBackupFile] = useState<File | null>(null)
|
||||||
@@ -245,6 +246,14 @@ export function AnalyticsPanel({
|
|||||||
<p>Real-time insights into site traffic, bot detection, visitor behavior, and data management.</p>
|
<p>Real-time insights into site traffic, bot detection, visitor behavior, and data management.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Top-level panel tabs */}
|
||||||
|
<div className="admin-tabs admin-tabs--charts" style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<button type="button" className={`admin-tab${panelTab === 'traffic' ? ' admin-tab--active' : ''}`} onClick={() => setPanelTab('traffic')}>Traffic</button>
|
||||||
|
<button type="button" className={`admin-tab${panelTab === 'engagement' ? ' admin-tab--active' : ''}`} onClick={() => setPanelTab('engagement')}>Engagement</button>
|
||||||
|
<button type="button" className={`admin-tab${panelTab === 'system' ? ' admin-tab--active' : ''}`} onClick={() => setPanelTab('system')}>System</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{panelTab === 'traffic' && (<>
|
||||||
{/* Time-range tabs */}
|
{/* Time-range tabs */}
|
||||||
{onRangeChange && (
|
{onRangeChange && (
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
||||||
@@ -523,6 +532,9 @@ export function AnalyticsPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
{panelTab === 'engagement' && (<>
|
||||||
{/* Episode Plays */}
|
{/* Episode Plays */}
|
||||||
{stats.episodePlays && stats.episodePlays.length > 0 && (
|
{stats.episodePlays && stats.episodePlays.length > 0 && (
|
||||||
<>
|
<>
|
||||||
@@ -790,6 +802,9 @@ export function AnalyticsPanel({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
{panelTab === 'system' && (<>
|
||||||
{/* Deployment & Cache Status */}
|
{/* Deployment & Cache Status */}
|
||||||
<div className="admin-stats-head admin-stats-head--visitors">
|
<div className="admin-stats-head admin-stats-head--visitors">
|
||||||
<h2>Deployment & Cache Status</h2>
|
<h2>Deployment & Cache Status</h2>
|
||||||
@@ -872,6 +887,7 @@ export function AnalyticsPanel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{maintenanceMsg && <p className="admin-stats-note">{maintenanceMsg}</p>}
|
{maintenanceMsg && <p className="admin-stats-note">{maintenanceMsg}</p>}
|
||||||
|
</>)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user