weifuwu 0.17.4 → 0.17.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/analytics.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +159 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1146,6 +1146,56 @@ await deploy(config)
|
|
|
1146
1146
|
|
|
1147
1147
|
---
|
|
1148
1148
|
|
|
1149
|
+
## Analytics
|
|
1150
|
+
|
|
1151
|
+
In-memory page view tracking with a built-in dashboard. Zero extra dependencies.
|
|
1152
|
+
|
|
1153
|
+
```ts
|
|
1154
|
+
import { analytics } from 'weifuwu'
|
|
1155
|
+
|
|
1156
|
+
app.use(analytics()) // mounts middleware + /analytics + /__analytics/data
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
| Endpoint | Description |
|
|
1160
|
+
|----------|-------------|
|
|
1161
|
+
| `GET /analytics` | Dashboard — PV trend, top pages, referrers, device breakdown |
|
|
1162
|
+
| `GET /__analytics/data?days=7` | Raw JSON data for custom dashboards |
|
|
1163
|
+
|
|
1164
|
+
Excluded paths (not recorded): `/__analytics/*`, `__wfw/*`, `/static/*`, `/analytics`.
|
|
1165
|
+
|
|
1166
|
+
### Dashboard
|
|
1167
|
+
|
|
1168
|
+
The built-in `/analytics` page renders a server-generated HTML dashboard with:
|
|
1169
|
+
|
|
1170
|
+
- **Summary cards** — total PV, unique pages, mobile/desktop ratio
|
|
1171
|
+
- **Bar chart** — daily page views for the selected period
|
|
1172
|
+
- **Top pages table** — most visited paths ranked by views
|
|
1173
|
+
- **Referrers table** — top referring domains
|
|
1174
|
+
|
|
1175
|
+
### JSON API
|
|
1176
|
+
|
|
1177
|
+
```ts
|
|
1178
|
+
// GET /__analytics/data?days=30
|
|
1179
|
+
{
|
|
1180
|
+
"total_pv": 2847,
|
|
1181
|
+
"total_uv": 1231,
|
|
1182
|
+
"daily": [{ "date": "2026-06-05", "pv": 520, "uv": 310 }],
|
|
1183
|
+
"top_pages": [{ "path": "/tools/uppercase", "pv": 1284 }],
|
|
1184
|
+
"referrers": [{ "domain": "google.com", "count": 380 }],
|
|
1185
|
+
"devices": { "mobile": 45.2, "desktop": 54.8 }
|
|
1186
|
+
}
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
### Options
|
|
1190
|
+
|
|
1191
|
+
```ts
|
|
1192
|
+
app.use(analytics({
|
|
1193
|
+
excluded: ['/admin', '/api'], // custom exclude patterns (defaults listed above)
|
|
1194
|
+
}))
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
---
|
|
1198
|
+
|
|
1149
1199
|
## Health check
|
|
1150
1200
|
|
|
1151
1201
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export { opencode } from './opencode/index.ts';
|
|
|
54
54
|
export type { OpencodeOptions, OpencodeModule, SkillDef, OpencodePermissions, Session as OpencodeSession, } from './opencode/types.ts';
|
|
55
55
|
export { health } from './health.ts';
|
|
56
56
|
export type { HealthOptions } from './health.ts';
|
|
57
|
+
export { analytics } from './analytics.ts';
|
|
58
|
+
export type { AnalyticsOptions } from './analytics.ts';
|
|
57
59
|
export { preferences } from './preferences.ts';
|
|
58
60
|
export type { PrefOptions } from './preferences.ts';
|
|
59
61
|
export { seo, seoMiddleware, seoTags } from './seo.ts';
|
package/dist/index.js
CHANGED
|
@@ -6798,6 +6798,164 @@ function health(options) {
|
|
|
6798
6798
|
return r;
|
|
6799
6799
|
}
|
|
6800
6800
|
|
|
6801
|
+
// analytics.ts
|
|
6802
|
+
var DEFAULT_EXCLUDED = ["/__analytics", "/__wfw", "/static", "/analytics"];
|
|
6803
|
+
var MemStore = class {
|
|
6804
|
+
days = /* @__PURE__ */ new Map();
|
|
6805
|
+
pages = /* @__PURE__ */ new Map();
|
|
6806
|
+
refs = /* @__PURE__ */ new Map();
|
|
6807
|
+
record(path2, date, refDomain, mobile) {
|
|
6808
|
+
let day = this.days.get(date);
|
|
6809
|
+
if (!day) {
|
|
6810
|
+
day = { pv: 0, uv: /* @__PURE__ */ new Set(), mobile: 0, desktop: 0 };
|
|
6811
|
+
this.days.set(date, day);
|
|
6812
|
+
}
|
|
6813
|
+
day.pv++;
|
|
6814
|
+
day.uv.add(path2);
|
|
6815
|
+
if (mobile) {
|
|
6816
|
+
day.mobile++;
|
|
6817
|
+
} else {
|
|
6818
|
+
day.desktop++;
|
|
6819
|
+
}
|
|
6820
|
+
let page = this.pages.get(path2);
|
|
6821
|
+
if (!page) {
|
|
6822
|
+
page = { count: 0 };
|
|
6823
|
+
this.pages.set(path2, page);
|
|
6824
|
+
}
|
|
6825
|
+
page.count++;
|
|
6826
|
+
if (refDomain) {
|
|
6827
|
+
let refs = this.refs.get(date);
|
|
6828
|
+
if (!refs) {
|
|
6829
|
+
refs = /* @__PURE__ */ new Map();
|
|
6830
|
+
this.refs.set(date, refs);
|
|
6831
|
+
}
|
|
6832
|
+
refs.set(refDomain, (refs.get(refDomain) || 0) + 1);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
query(days) {
|
|
6836
|
+
const since = /* @__PURE__ */ new Date();
|
|
6837
|
+
since.setDate(since.getDate() - days);
|
|
6838
|
+
const sinceStr = since.toISOString().slice(0, 10);
|
|
6839
|
+
const daily = [];
|
|
6840
|
+
let totalPv = 0;
|
|
6841
|
+
let totalMobile = 0;
|
|
6842
|
+
let totalDesktop = 0;
|
|
6843
|
+
const pageMap = /* @__PURE__ */ new Map();
|
|
6844
|
+
const allUv = /* @__PURE__ */ new Set();
|
|
6845
|
+
for (const [date, day] of this.days) {
|
|
6846
|
+
if (date < sinceStr) continue;
|
|
6847
|
+
daily.push({ date, pv: day.pv, uv: day.uv.size });
|
|
6848
|
+
totalPv += day.pv;
|
|
6849
|
+
totalMobile += day.mobile;
|
|
6850
|
+
totalDesktop += day.desktop;
|
|
6851
|
+
for (const p of day.uv) allUv.add(p);
|
|
6852
|
+
}
|
|
6853
|
+
for (const [path2, page] of this.pages) {
|
|
6854
|
+
pageMap.set(path2, page.count);
|
|
6855
|
+
}
|
|
6856
|
+
const topPages = [...pageMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([path2, count]) => ({ path: path2, pv: count }));
|
|
6857
|
+
const refMap = /* @__PURE__ */ new Map();
|
|
6858
|
+
for (const [date, refs] of this.refs) {
|
|
6859
|
+
if (date < sinceStr) continue;
|
|
6860
|
+
for (const [domain, count] of refs) refMap.set(domain, (refMap.get(domain) || 0) + count);
|
|
6861
|
+
}
|
|
6862
|
+
const referrers = [...refMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count }));
|
|
6863
|
+
const total = totalMobile + totalDesktop || 1;
|
|
6864
|
+
return {
|
|
6865
|
+
total_pv: totalPv,
|
|
6866
|
+
total_uv: allUv.size,
|
|
6867
|
+
daily,
|
|
6868
|
+
top_pages: topPages,
|
|
6869
|
+
referrers,
|
|
6870
|
+
devices: { mobile: Math.round(totalMobile / total * 1e3) / 10, desktop: Math.round(totalDesktop / total * 1e3) / 10 }
|
|
6871
|
+
};
|
|
6872
|
+
}
|
|
6873
|
+
handler() {
|
|
6874
|
+
return async (req) => {
|
|
6875
|
+
const url = new URL(req.url);
|
|
6876
|
+
const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 7, 1), 365);
|
|
6877
|
+
const data = this.query(days);
|
|
6878
|
+
if (url.pathname === "/__analytics/data") {
|
|
6879
|
+
return Response.json(data);
|
|
6880
|
+
}
|
|
6881
|
+
return new Response(this.renderDashboard(days, data), {
|
|
6882
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
6883
|
+
});
|
|
6884
|
+
};
|
|
6885
|
+
}
|
|
6886
|
+
renderDashboard(days, data) {
|
|
6887
|
+
const { total_pv, total_uv, daily, top_pages, referrers, devices } = data;
|
|
6888
|
+
const maxPv = Math.max(...daily.map((d) => d.pv), 1);
|
|
6889
|
+
const bars = daily.map(
|
|
6890
|
+
(d) => `<div class="bar-wrap"><div class="bar" style="height:${d.pv / maxPv * 100}%"></div><span class="bar-label">${d.date.slice(5)}</span></div>`
|
|
6891
|
+
).join("");
|
|
6892
|
+
const rows = top_pages.map(
|
|
6893
|
+
(p, i) => `<tr><td class="num">${i + 1}</td><td class="path">${p.path}</td><td class="num">${p.pv}</td></tr>`
|
|
6894
|
+
).join("");
|
|
6895
|
+
const refRows = referrers.map(
|
|
6896
|
+
(r) => `<tr><td>${r.domain}</td><td class="num">${r.count}</td></tr>`
|
|
6897
|
+
).join("");
|
|
6898
|
+
return `<!DOCTYPE html>
|
|
6899
|
+
<html lang="en">
|
|
6900
|
+
<head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Analytics - weifuwu</title>
|
|
6901
|
+
<style>
|
|
6902
|
+
*,:before,:after{box-sizing:border-box;margin:0;padding:0}
|
|
6903
|
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8f9fa;color:#333;padding:24px;max-width:960px;margin:0 auto}
|
|
6904
|
+
h1{font-size:24px;font-weight:700;margin-bottom:24px}
|
|
6905
|
+
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:24px}
|
|
6906
|
+
.card{background:#fff;border-radius:10px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
|
6907
|
+
.card .val{font-size:28px;font-weight:700;color:#2563eb}
|
|
6908
|
+
.card .lbl{font-size:12px;color:#888;margin-top:4px}
|
|
6909
|
+
.section{background:#fff;border-radius:10px;padding:20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
|
6910
|
+
.section h2{font-size:14px;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:.05em;margin-bottom:16px}
|
|
6911
|
+
.chart{display:flex;align-items:flex-end;gap:4px;height:160px;padding-top:8px}
|
|
6912
|
+
.bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end}
|
|
6913
|
+
.bar{width:100%;background:#2563eb;border-radius:4px 4px 0 0;min-height:2px}
|
|
6914
|
+
.bar-label{font-size:10px;color:#888;margin-top:6px;white-space:nowrap}
|
|
6915
|
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
6916
|
+
th{text-align:left;padding:6px 8px;color:#888;font-weight:500;border-bottom:1px solid #eee}
|
|
6917
|
+
td{padding:6px 8px;border-bottom:1px solid #f0f0f0}
|
|
6918
|
+
.num{text-align:right;font-variant-numeric:tabular-nums}
|
|
6919
|
+
.path{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px}
|
|
6920
|
+
tr:hover td{background:#f8faff}
|
|
6921
|
+
</style></head>
|
|
6922
|
+
<body>
|
|
6923
|
+
<h1>Analytics</h1>
|
|
6924
|
+
<div class="cards">
|
|
6925
|
+
<div class="card"><div class="val">${total_pv}</div><div class="lbl">Page Views (${days}d)</div></div>
|
|
6926
|
+
<div class="card"><div class="val">${total_uv}</div><div class="lbl">Unique Pages</div></div>
|
|
6927
|
+
<div class="card"><div class="val">${devices.mobile}%</div><div class="lbl">Mobile</div></div>
|
|
6928
|
+
<div class="card"><div class="val">${devices.desktop}%</div><div class="lbl">Desktop</div></div>
|
|
6929
|
+
</div>
|
|
6930
|
+
<div class="section"><h2>Daily Page Views</h2><div class="chart">${bars}</div></div>
|
|
6931
|
+
<div class="section"><h2>Top Pages</h2>
|
|
6932
|
+
<table><thead><tr><th style="width:32px">#</th><th>Path</th><th style="width:64px">Views</th></tr></thead><tbody>${rows}</tbody></table></div>
|
|
6933
|
+
${referrers.length ? `<div class="section"><h2>Referrers</h2><table><thead><tr><th>Domain</th><th style="width:64px">Views</th></tr></thead><tbody>${refRows}</tbody></table></div>` : ""}
|
|
6934
|
+
</body></html>`;
|
|
6935
|
+
}
|
|
6936
|
+
};
|
|
6937
|
+
function analytics(options) {
|
|
6938
|
+
const excluded = options?.excluded ?? DEFAULT_EXCLUDED;
|
|
6939
|
+
const store = new MemStore();
|
|
6940
|
+
const middleware = async (req, ctx, next) => {
|
|
6941
|
+
const url = new URL(req.url);
|
|
6942
|
+
const path2 = url.pathname;
|
|
6943
|
+
if (!excluded.some((e) => path2.startsWith(e))) {
|
|
6944
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6945
|
+
const ref = req.headers.get("referer") || "";
|
|
6946
|
+
const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
|
|
6947
|
+
const ua = req.headers.get("user-agent") || "";
|
|
6948
|
+
const mobile = /mobile|android|iphone|ipad/i.test(ua);
|
|
6949
|
+
store.record(path2, date, refDomain, mobile);
|
|
6950
|
+
}
|
|
6951
|
+
return next(req, ctx);
|
|
6952
|
+
};
|
|
6953
|
+
return {
|
|
6954
|
+
middleware,
|
|
6955
|
+
handler: store.handler()
|
|
6956
|
+
};
|
|
6957
|
+
}
|
|
6958
|
+
|
|
6801
6959
|
// preferences.ts
|
|
6802
6960
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
6803
6961
|
import { existsSync as existsSync4 } from "node:fs";
|
|
@@ -8272,6 +8430,7 @@ export {
|
|
|
8272
8430
|
TsxContext,
|
|
8273
8431
|
agent,
|
|
8274
8432
|
aiStream,
|
|
8433
|
+
analytics,
|
|
8275
8434
|
auth,
|
|
8276
8435
|
compress,
|
|
8277
8436
|
cors,
|