Fix analytics

This commit is contained in:
yuhao
2026-04-24 10:15:28 +00:00
parent 4a2e21e3f7
commit ab2556360f
2 changed files with 62 additions and 140 deletions

View File

@@ -2097,30 +2097,30 @@
<div class="analytics-title">Website Visits</div>
<div class="metric-cell total">
<span class="metric-label">Total</span>
<strong id="stat-total" class="metric-value">21,123</strong>
<strong id="stat-total" class="metric-value">...</strong>
</div>
<div class="metric-cell human">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Human</span>
<strong id="stat-human" class="metric-value">7,689</strong>
<strong id="stat-human" class="metric-value">...</strong>
</div>
<div class="metric-cell agent">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Agent</span>
<strong id="stat-agent" class="metric-value">13,434</strong>
<strong id="stat-agent" class="metric-value">...</strong>
</div>
</div>
<div class="analytics-row">
<div class="analytics-title">CLI-Hub Calls</div>
<div class="analytics-title">CLI-Hub Usage</div>
<div class="metric-cell total">
<span class="metric-label">Total</span>
<strong id="stat-cli-total" class="metric-value">16,361</strong>
<strong id="stat-cli-total" class="metric-value">...</strong>
</div>
<div class="metric-cell human">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Human</span>
<strong id="stat-cli-human" class="metric-value">3,237</strong>
<strong id="stat-cli-human" class="metric-value">...</strong>
</div>
<div class="metric-cell agent">
<span class="metric-label"><span class="metric-dot" aria-hidden="true"></span>Agent</span>
<strong id="stat-cli-agent" class="metric-value">13,124</strong>
<strong id="stat-cli-agent" class="metric-value">...</strong>
</div>
</div>
</div>
@@ -2633,13 +2633,11 @@
const ANALYTICS_PROVIDER = 'posthog';
const ANALYTICS_SITE = 'clianything.cc';
const COUNTED_HOSTS = new Set(['clianything.cc', 'www.clianything.cc']);
const BASELINE_STATS = {
total: 21123,
human: 7689,
agent: 13434,
cli_hub_total: 16361,
cli_hub_human: 3237,
cli_hub_agent: 13124,
// Fixed Umami baseline from remote main. Runtime PostHog counts are
// queried live and added to this.
const UMAMI_BASELINE_STATS = {
human: 7689, agent: 13434,
cli_hub_human: 3237, cli_hub_agent: 13124,
};
const ANALYTICS_TEST_RUN = new URLSearchParams(location.search).get('analytics_test_run') || '';
const POSTHOG_CONFIG = {
@@ -2910,20 +2908,14 @@
});
// ── Live analytics via Cloudflare Worker proxy ──
// PostHog key and Umami key live as Worker secrets (repo:
// ~/clianything-analytics-worker). Both sources are queried every
// refresh and summed, so pre-migration Umami events and post-migration
// PostHog events stay counted forever.
// PostHog key lives as a Worker secret (repo:
// ~/clianything-analytics-worker). Runtime PostHog counts are queried
// every refresh and added to the fixed pre-migration Umami baseline.
const ANALYTICS_PROXY_URL = 'https://analytics.clianything.cc/';
const UMAMI_SITE_IDS = [
'07082d05-efd3-4f85-a7a1-b426b0e8bfaa', // hkuds.github.io
'a076c661-bed1-405c-a522-813794e688b4', // clianything.cc
];
const LIVE_STATS_REFRESH_MS = 60_000;
const ZERO_STATS = {
human: 0, agent: 0,
cli_hub_human: 0, cli_hub_agent: 0,
cli_install: 0, cli_uninstall: 0, cli_launch: 0,
};
async function fetchPosthogTotals() {
@@ -2932,10 +2924,7 @@
"countIf(event = 'visit-human' and properties.source = 'web' and properties.site = 'clianything.cc') as human, " +
"countIf(event = 'visit-agent' and properties.source = 'web' and properties.site = 'clianything.cc') as agent, " +
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = false) as cli_hub_human, " +
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = true) as cli_hub_agent, " +
"countIf((event = 'cli-install' or like(event, 'cli-install:%')) and properties.source = 'cli') as cli_install, " +
"countIf((event = 'cli-uninstall' or like(event, 'cli-uninstall:%')) and properties.source = 'cli') as cli_uninstall, " +
"countIf(event = 'cli-launch' and properties.source = 'cli') as cli_launch " +
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = true) as cli_hub_agent " +
"from events"
);
const response = await fetch(ANALYTICS_PROXY_URL, {
@@ -2945,40 +2934,15 @@
});
if (!response.ok) throw new Error('posthog query http ' + response.status);
const data = await response.json();
const row = (data.results && data.results[0]) || [0, 0, 0, 0, 0, 0, 0];
const row = (data.results && data.results[0]) || [0, 0, 0, 0];
return {
human: Number(row[0]) || 0,
agent: Number(row[1]) || 0,
cli_hub_human: Number(row[2]) || 0,
cli_hub_agent: Number(row[3]) || 0,
cli_install: Number(row[4]) || 0,
cli_uninstall: Number(row[5]) || 0,
cli_launch: Number(row[6]) || 0,
};
}
async function fetchUmamiTotals() {
const totals = { ...ZERO_STATS };
const responses = await Promise.all(UMAMI_SITE_IDS.map((site) =>
fetch(ANALYTICS_PROXY_URL + 'umami/events?site=' + encodeURIComponent(site))
.then((r) => r.ok ? r.json() : [])
.catch(() => [])
));
for (const rows of responses) {
if (!Array.isArray(rows)) continue;
for (const row of rows) {
const name = row && row.x;
const count = Number(row && row.y) || 0;
if (!name || !count) continue;
if (name === 'visit-human') totals.human += count;
else if (name === 'visit-agent') totals.agent += count;
else if (name === 'cli-install' || (typeof name === 'string' && name.startsWith('cli-install:'))) totals.cli_install += count;
else if (name === 'cli-uninstall' || (typeof name === 'string' && name.startsWith('cli-uninstall:'))) totals.cli_uninstall += count;
}
}
return totals;
}
function sumStats(a, b) {
const out = {};
for (const key of Object.keys(ZERO_STATS)) out[key] = (a[key] || 0) + (b[key] || 0);
@@ -2986,31 +2950,30 @@
}
function withDerivedTotals(s) {
// cli_hub_total bundles all cli-hub-sourced events so the footer's
// "cli-hub" number reflects end-to-end activity, not just raw
// invocations. The human/agent split stays tied to cli-hub call
// only, because Umami install/uninstall events don't carry is_agent.
// cli-hub call is emitted once per invocation, so action events like
// install/launch must not be added here or usage would be double-counted.
return {
...s,
total: s.human + s.agent,
cli_hub_total: s.cli_hub_human + s.cli_hub_agent + s.cli_install + s.cli_uninstall + s.cli_launch,
cli_hub_total: s.cli_hub_human + s.cli_hub_agent,
};
}
function hasAnyStats(s) {
return Object.keys(ZERO_STATS).some((key) => (s[key] || 0) > 0);
}
async function refreshVisitorStats() {
const [posthog, umami] = await Promise.allSettled([fetchPosthogTotals(), fetchUmamiTotals()]);
const ph = posthog.status === 'fulfilled' ? posthog.value : null;
const um = umami.status === 'fulfilled' ? umami.value : null;
if (!ph && !um) return false;
const merged = sumStats(ph || ZERO_STATS, um || ZERO_STATS);
const posthog = await fetchPosthogTotals();
const merged = sumStats(UMAMI_BASELINE_STATS, posthog);
if (!hasAnyStats(merged)) return false;
applyVisitorStats(withDerivedTotals(merged));
return true;
}
async function loadVisitorStats() {
applyVisitorStats(BASELINE_STATS);
await refreshVisitorStats();
setInterval(refreshVisitorStats, LIVE_STATS_REFRESH_MS);
await refreshVisitorStats().catch(() => false);
setInterval(() => { refreshVisitorStats().catch(() => false); }, LIVE_STATS_REFRESH_MS);
}
loadVisitorStats();

View File

@@ -2087,37 +2087,37 @@
<div class="analytics-report-grid">
<article class="analytics-stat">
<span class="analytics-stat-label">Website visits</span>
<strong id="stat-total" class="analytics-stat-value">21,123</strong>
<strong id="stat-total" class="analytics-stat-value">...</strong>
<div class="analytics-bar" aria-hidden="true">
<span class="bar-human" id="bar-site-human" style="width: 36.4%"></span>
<span class="bar-agent" id="bar-site-agent" style="width: 63.6%"></span>
<span class="bar-human" id="bar-site-human" style="width: 0%"></span>
<span class="bar-agent" id="bar-site-agent" style="width: 0%"></span>
</div>
<dl class="analytics-split">
<div class="analytics-split-item">
<dt><span class="analytics-dot human"></span>Human</dt>
<dd><span id="stat-human" class="analytics-split-num">7,689</span><span id="pct-site-human" class="analytics-split-pct">36%</span></dd>
<dd><span id="stat-human" class="analytics-split-num">...</span><span id="pct-site-human" class="analytics-split-pct">...</span></dd>
</div>
<div class="analytics-split-item">
<dt><span class="analytics-dot agent"></span>Agent</dt>
<dd><span id="stat-agent" class="analytics-split-num">13,434</span><span id="pct-site-agent" class="analytics-split-pct">64%</span></dd>
<dd><span id="stat-agent" class="analytics-split-num">...</span><span id="pct-site-agent" class="analytics-split-pct">...</span></dd>
</div>
</dl>
</article>
<article class="analytics-stat">
<span class="analytics-stat-label">CLI-Hub calls</span>
<strong id="stat-cli-total" class="analytics-stat-value">16,361</strong>
<span class="analytics-stat-label">CLI-Hub usage</span>
<strong id="stat-cli-total" class="analytics-stat-value">...</strong>
<div class="analytics-bar" aria-hidden="true">
<span class="bar-human" id="bar-cli-human" style="width: 19.8%"></span>
<span class="bar-agent" id="bar-cli-agent" style="width: 80.2%"></span>
<span class="bar-human" id="bar-cli-human" style="width: 0%"></span>
<span class="bar-agent" id="bar-cli-agent" style="width: 0%"></span>
</div>
<dl class="analytics-split">
<div class="analytics-split-item">
<dt><span class="analytics-dot human"></span>Human</dt>
<dd><span id="stat-cli-human" class="analytics-split-num">3,237</span><span id="pct-cli-human" class="analytics-split-pct">20%</span></dd>
<dd><span id="stat-cli-human" class="analytics-split-num">...</span><span id="pct-cli-human" class="analytics-split-pct">...</span></dd>
</div>
<div class="analytics-split-item">
<dt><span class="analytics-dot agent"></span>Agent</dt>
<dd><span id="stat-cli-agent" class="analytics-split-num">13,124</span><span id="pct-cli-agent" class="analytics-split-pct">80%</span></dd>
<dd><span id="stat-cli-agent" class="analytics-split-num">...</span><span id="pct-cli-agent" class="analytics-split-pct">...</span></dd>
</div>
</dl>
</article>
@@ -2827,13 +2827,11 @@
const ANALYTICS_PROVIDER = 'posthog';
const ANALYTICS_SITE = 'clianything.cc';
const COUNTED_HOSTS = new Set(['clianything.cc', 'www.clianything.cc']);
const BASELINE_STATS = {
total: 21123,
human: 7689,
agent: 13434,
cli_hub_total: 16361,
cli_hub_human: 3237,
cli_hub_agent: 13124,
// Fixed Umami baseline from remote main. Runtime PostHog counts are
// queried live and added to this.
const UMAMI_BASELINE_STATS = {
human: 7689, agent: 13434,
cli_hub_human: 3237, cli_hub_agent: 13124,
};
const ANALYTICS_TEST_RUN = new URLSearchParams(location.search).get('analytics_test_run') || '';
const POSTHOG_CONFIG = {
@@ -3124,21 +3122,14 @@
});
// ── Live analytics via Cloudflare Worker proxy ──
// PostHog personal key and Umami API key live as Worker secrets
// (repo: ~/clianything-analytics-worker). Both sources are queried
// every refresh and summed, so pre-migration Umami events and
// post-migration PostHog events both stay counted forever.
// BASELINE_STATS is a last-resort fallback when both live fetches fail.
// PostHog personal key lives as a Worker secret (repo:
// ~/clianything-analytics-worker). Runtime PostHog counts are queried
// every refresh and added to the fixed pre-migration Umami baseline.
const ANALYTICS_PROXY_URL = 'https://analytics.clianything.cc/';
const UMAMI_SITE_IDS = [
'07082d05-efd3-4f85-a7a1-b426b0e8bfaa', // hkuds.github.io
'a076c661-bed1-405c-a522-813794e688b4', // clianything.cc
];
const LIVE_STATS_REFRESH_MS = 60_000;
const ZERO_STATS = {
human: 0, agent: 0,
cli_hub_human: 0, cli_hub_agent: 0,
cli_install: 0, cli_uninstall: 0, cli_launch: 0,
};
async function fetchPosthogTotals() {
@@ -3147,10 +3138,7 @@
"countIf(event = 'visit-human' and properties.source = 'web' and properties.site = 'clianything.cc') as human, " +
"countIf(event = 'visit-agent' and properties.source = 'web' and properties.site = 'clianything.cc') as agent, " +
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = false) as cli_hub_human, " +
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = true) as cli_hub_agent, " +
"countIf((event = 'cli-install' or like(event, 'cli-install:%')) and properties.source = 'cli') as cli_install, " +
"countIf((event = 'cli-uninstall' or like(event, 'cli-uninstall:%')) and properties.source = 'cli') as cli_uninstall, " +
"countIf(event = 'cli-launch' and properties.source = 'cli') as cli_launch " +
"countIf(event = 'cli-hub call' and properties.source = 'cli' and properties.channel = 'cli-hub' and properties.is_agent = true) as cli_hub_agent " +
"from events"
);
const response = await fetch(ANALYTICS_PROXY_URL, {
@@ -3160,43 +3148,15 @@
});
if (!response.ok) throw new Error('posthog query http ' + response.status);
const data = await response.json();
const row = (data.results && data.results[0]) || [0, 0, 0, 0, 0, 0, 0];
const row = (data.results && data.results[0]) || [0, 0, 0, 0];
return {
human: Number(row[0]) || 0,
agent: Number(row[1]) || 0,
cli_hub_human: Number(row[2]) || 0,
cli_hub_agent: Number(row[3]) || 0,
cli_install: Number(row[4]) || 0,
cli_uninstall: Number(row[5]) || 0,
cli_launch: Number(row[6]) || 0,
};
}
async function fetchUmamiTotals() {
// Umami events/series returns [{x: eventName, t: timestamp, y: count}, ...].
// Pre-migration we only emitted visit-human, visit-agent, and
// cli-install:{name} / cli-uninstall:{name} — no cli-hub call, no cli-launch.
const totals = { ...ZERO_STATS };
const responses = await Promise.all(UMAMI_SITE_IDS.map((site) =>
fetch(ANALYTICS_PROXY_URL + 'umami/events?site=' + encodeURIComponent(site))
.then((r) => r.ok ? r.json() : [])
.catch(() => [])
));
for (const rows of responses) {
if (!Array.isArray(rows)) continue;
for (const row of rows) {
const name = row && row.x;
const count = Number(row && row.y) || 0;
if (!name || !count) continue;
if (name === 'visit-human') totals.human += count;
else if (name === 'visit-agent') totals.agent += count;
else if (name === 'cli-install' || (typeof name === 'string' && name.startsWith('cli-install:'))) totals.cli_install += count;
else if (name === 'cli-uninstall' || (typeof name === 'string' && name.startsWith('cli-uninstall:'))) totals.cli_uninstall += count;
}
}
return totals;
}
function sumStats(a, b) {
const out = {};
for (const key of Object.keys(ZERO_STATS)) out[key] = (a[key] || 0) + (b[key] || 0);
@@ -3204,31 +3164,30 @@
}
function withDerivedTotals(s) {
// cli_hub_total bundles all cli-hub-sourced events so the footer's
// "cli-hub" number reflects end-to-end activity, not just raw
// invocations. The human/agent split stays tied to cli-hub call
// only, because Umami install/uninstall events don't carry is_agent.
// cli-hub call is emitted once per invocation, so action events like
// install/launch must not be added here or usage would be double-counted.
return {
...s,
total: s.human + s.agent,
cli_hub_total: s.cli_hub_human + s.cli_hub_agent + s.cli_install + s.cli_uninstall + s.cli_launch,
cli_hub_total: s.cli_hub_human + s.cli_hub_agent,
};
}
function hasAnyStats(s) {
return Object.keys(ZERO_STATS).some((key) => (s[key] || 0) > 0);
}
async function refreshVisitorStats() {
const [posthog, umami] = await Promise.allSettled([fetchPosthogTotals(), fetchUmamiTotals()]);
const ph = posthog.status === 'fulfilled' ? posthog.value : null;
const um = umami.status === 'fulfilled' ? umami.value : null;
if (!ph && !um) return false;
const merged = sumStats(ph || ZERO_STATS, um || ZERO_STATS);
const posthog = await fetchPosthogTotals();
const merged = sumStats(UMAMI_BASELINE_STATS, posthog);
if (!hasAnyStats(merged)) return false;
applyVisitorStats(withDerivedTotals(merged));
return true;
}
async function loadVisitorStats() {
applyVisitorStats(BASELINE_STATS);
await refreshVisitorStats();
setInterval(refreshVisitorStats, LIVE_STATS_REFRESH_MS);
await refreshVisitorStats().catch(() => false);
setInterval(() => { refreshVisitorStats().catch(() => false); }, LIVE_STATS_REFRESH_MS);
}
loadVisitorStats();