102 lines
4.2 KiB
JavaScript
102 lines
4.2 KiB
JavaScript
const PKG = 'mcpforunityserver';
|
|
const REPO = 'CoplayDev/unity-mcp';
|
|
const GH = 'https://api.github.com';
|
|
|
|
export function normalizePypiRecent(recent) {
|
|
if (recent == null) {
|
|
return { lastDay: null, lastWeek: null, lastMonth: null, unavailable: true };
|
|
}
|
|
const d = (recent && recent.data) || {};
|
|
return { lastDay: d.last_day ?? 0, lastWeek: d.last_week ?? 0, lastMonth: d.last_month ?? 0 };
|
|
}
|
|
|
|
// Builds the unified stats object. Honest user signals (GitHub clones/stars,
|
|
// and the in-product DAU/WAU surfaced elsewhere) sit alongside the noisy PyPI
|
|
// download proxy, which renderSummary clearly labels as inflated.
|
|
export function buildStats({ recent, repo = null, clones = null, views = null, webPageviews = null, generatedAt }) {
|
|
return {
|
|
generatedAt,
|
|
github: {
|
|
stars: repo?.stargazers_count ?? null,
|
|
forks: repo?.forks_count ?? null,
|
|
uniqueCloners14d: clones?.uniques ?? null,
|
|
uniqueViewers14d: views?.uniques ?? null,
|
|
},
|
|
pypi: normalizePypiRecent(recent),
|
|
web: webPageviews,
|
|
};
|
|
}
|
|
|
|
export function renderSummary(stats) {
|
|
const g = stats.github, p = stats.pypi;
|
|
const n = (x) => (x == null ? '—' : Number(x).toLocaleString('en-US'));
|
|
const web = stats.web && stats.web.total != null
|
|
? `${n(stats.web.total)} total pageviews`
|
|
: '_pending GoatCounter provisioning_';
|
|
const trafficMissing = g.uniqueCloners14d == null && g.uniqueViewers14d == null;
|
|
return [
|
|
'## MCP for Unity — adoption (maintainer-only)',
|
|
`_generated ${stats.generatedAt}_`,
|
|
'',
|
|
'### Real-user signals (the honest ones)',
|
|
'| Signal | Value | Reads as |',
|
|
'| --- | ---: | --- |',
|
|
`| In-product DAU / WAU | _pending_ | **true active users** — already collected by the in-product telemetry; needs a Coplay read API |`,
|
|
`| Unique repo cloners (14d) | ${n(g.uniqueCloners14d)} | developers actually pulling the code |`,
|
|
`| Unique repo viewers (14d) | ${n(g.uniqueViewers14d)} | distinct repo visitors |`,
|
|
`| GitHub stars | ${n(g.stars)} | real accounts interested |`,
|
|
`| GitHub forks | ${n(g.forks)} | |`,
|
|
...(trafficMissing
|
|
? ['', '> Cloners/viewers are blank: the workflow needs a maintainer PAT with **Administration: read** (`STATS_GITHUB_TOKEN`). See the external-analytics doc.']
|
|
: []),
|
|
'',
|
|
'### Reach proxy (inflated — NOT a user count)',
|
|
'| Window | PyPI downloads |',
|
|
'| --- | ---: |',
|
|
`| Last day | ${n(p.lastDay)} |`,
|
|
`| Last week | ${n(p.lastWeek)} |`,
|
|
`| Last month | ${n(p.lastMonth)} |`,
|
|
'',
|
|
...(p.unavailable
|
|
? ['> PyPI stats are unavailable: the pypistats.org request failed. Do not treat these dashes as zero downloads.', '']
|
|
: []),
|
|
'> PyPI counts are install events — CI, mirrors, `uvx` re-fetches, and Docker rebuilds inflate them heavily. Treat as a trend line, not people.',
|
|
'',
|
|
'### Docs traffic',
|
|
web,
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
async function main() {
|
|
const get = async (url, headers) => {
|
|
try { const r = await fetch(url, { headers }); return r.ok ? await r.json() : null; } catch { return null; }
|
|
};
|
|
const ghToken = process.env.STATS_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
|
|
const ghHeaders = {
|
|
'User-Agent': 'mcp-for-unity-stats',
|
|
Accept: 'application/vnd.github+json',
|
|
...(ghToken ? { Authorization: `Bearer ${ghToken}` } : {}),
|
|
};
|
|
|
|
const recent = await get(`https://pypistats.org/api/packages/${PKG}/recent`, { Accept: 'application/json' });
|
|
const repo = await get(`${GH}/repos/${REPO}`, ghHeaders);
|
|
const clones = await get(`${GH}/repos/${REPO}/traffic/clones`, ghHeaders);
|
|
const views = await get(`${GH}/repos/${REPO}/traffic/views`, ghHeaders);
|
|
|
|
let web = null;
|
|
const gcToken = process.env.GOATCOUNTER_TOKEN, gcSite = process.env.GOATCOUNTER_SITE;
|
|
if (gcToken && gcSite) {
|
|
const j = await get(`https://${gcSite}.goatcounter.com/api/v0/stats/total`, { Authorization: `Bearer ${gcToken}` });
|
|
if (j) web = { total: j.total ?? null };
|
|
}
|
|
|
|
const stats = buildStats({ recent, repo, clones, views, webPageviews: web, generatedAt: new Date().toISOString() });
|
|
process.stdout.write(renderSummary(stats));
|
|
}
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
main().catch((e) => { console.error(e); process.exit(1); });
|
|
}
|