weifuwu 0.17.5 → 0.17.7

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.
@@ -1,8 +1,11 @@
1
- import { Router } from './router.ts';
1
+ import type { Handler, Middleware } from './types.ts';
2
2
  export interface AnalyticsOptions {
3
+ excluded?: string[];
3
4
  pg?: {
4
- sql: any;
5
+ sql: (strings: TemplateStringsArray, ...values: any[]) => Promise<any[]>;
5
6
  };
6
- excluded?: string[];
7
7
  }
8
- export declare function analytics(options?: AnalyticsOptions): Router;
8
+ export declare function analytics(options?: AnalyticsOptions): {
9
+ middleware: Middleware;
10
+ handler: Handler;
11
+ };
package/dist/index.js CHANGED
@@ -6819,11 +6819,10 @@ var MemStore = class {
6819
6819
  }
6820
6820
  let page = this.pages.get(path2);
6821
6821
  if (!page) {
6822
- page = { count: 0, dates: /* @__PURE__ */ new Set() };
6822
+ page = { count: 0 };
6823
6823
  this.pages.set(path2, page);
6824
6824
  }
6825
6825
  page.count++;
6826
- page.dates.add(date);
6827
6826
  if (refDomain) {
6828
6827
  let refs = this.refs.get(date);
6829
6828
  if (!refs) {
@@ -6852,8 +6851,6 @@ var MemStore = class {
6852
6851
  for (const p of day.uv) allUv.add(p);
6853
6852
  }
6854
6853
  for (const [path2, page] of this.pages) {
6855
- let count = 0;
6856
- for (const d of page.dates) if (d >= sinceStr) count += page.count / page.dates.size;
6857
6854
  pageMap.set(path2, page.count);
6858
6855
  }
6859
6856
  const topPages = [...pageMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([path2, count]) => ({ path: path2, pv: count }));
@@ -6874,44 +6871,41 @@ var MemStore = class {
6874
6871
  };
6875
6872
  }
6876
6873
  };
6877
- function analytics(options) {
6878
- const excluded = options?.excluded ?? DEFAULT_EXCLUDED;
6879
- const store = new MemStore();
6880
- function shouldExclude(path2) {
6881
- return excluded.some((e) => path2.startsWith(e));
6882
- }
6883
- const middleware = async (req, ctx, next) => {
6884
- const url = new URL(req.url);
6885
- const path2 = url.pathname;
6886
- if (!shouldExclude(path2)) {
6887
- const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6888
- const ref = req.headers.get("referer") || "";
6889
- const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
6890
- const ua = req.headers.get("user-agent") || "";
6891
- const mobile = /mobile|android|iphone|ipad/i.test(ua);
6892
- store.record(path2, date, refDomain, mobile);
6874
+ async function queryPg(sql2, days) {
6875
+ const since = /* @__PURE__ */ new Date();
6876
+ since.setDate(since.getDate() - days);
6877
+ const daily = await sql2`
6878
+ SELECT date, SUM(count) as pv, COUNT(DISTINCT path) as uv
6879
+ FROM __analytics WHERE date >= ${since.toISOString().slice(0, 10)}
6880
+ GROUP BY date ORDER BY date
6881
+ `;
6882
+ const pageRows = await sql2`
6883
+ SELECT path, SUM(count) as pv
6884
+ FROM __analytics WHERE date >= ${since.toISOString().slice(0, 10)}
6885
+ GROUP BY path ORDER BY pv DESC LIMIT 20
6886
+ `;
6887
+ const totalRes = await sql2`
6888
+ SELECT COALESCE(SUM(count), 0) as total_pv,
6889
+ COALESCE(SUM(mobile), 0) as total_mobile,
6890
+ COALESCE(SUM(desktop), 0) as total_desktop
6891
+ FROM __analytics WHERE date >= ${since.toISOString().slice(0, 10)}
6892
+ `;
6893
+ const total = totalRes[0];
6894
+ const totalMobileDesktop = total.total_mobile + total.total_desktop || 1;
6895
+ return {
6896
+ total_pv: total.total_pv,
6897
+ total_uv: pageRows.length,
6898
+ daily: daily.map((d) => ({ date: d.date, pv: Number(d.pv), uv: Number(d.uv) })),
6899
+ top_pages: pageRows.map((p) => ({ path: p.path, pv: Number(p.pv) })),
6900
+ referrers: [],
6901
+ devices: {
6902
+ mobile: Math.round(total.total_mobile / totalMobileDesktop * 1e3) / 10,
6903
+ desktop: Math.round(total.total_desktop / totalMobileDesktop * 1e3) / 10
6893
6904
  }
6894
- return next(req, ctx);
6895
6905
  };
6896
- const router = new Router();
6897
- router.use(middleware);
6898
- router.get("/__analytics/data", async (req) => {
6899
- const url = new URL(req.url);
6900
- const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 7, 1), 365);
6901
- return Response.json(store.query(days));
6902
- });
6903
- router.get("/analytics", async (req) => {
6904
- const url = new URL(req.url);
6905
- const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 7, 1), 365);
6906
- const data = store.query(days);
6907
- return new Response(renderDashboard(days, data), {
6908
- headers: { "content-type": "text/html; charset=utf-8" }
6909
- });
6910
- });
6911
- return router;
6912
6906
  }
6913
6907
  function renderDashboard(days, data) {
6914
- const { total_pv, total_uv, daily, top_pages, referrers, devices } = data;
6908
+ const { total_pv, total_uv, daily, top_pages, referrers } = data;
6915
6909
  const maxPv = Math.max(...daily.map((d) => d.pv), 1);
6916
6910
  const bars = daily.map(
6917
6911
  (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>`
@@ -6924,10 +6918,7 @@ function renderDashboard(days, data) {
6924
6918
  ).join("");
6925
6919
  return `<!DOCTYPE html>
6926
6920
  <html lang="en">
6927
- <head>
6928
- <meta charset="utf-8"/>
6929
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
6930
- <title>Analytics - weifuwu</title>
6921
+ <head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Analytics - weifuwu</title>
6931
6922
  <style>
6932
6923
  *,:before,:after{box-sizing:border-box;margin:0;padding:0}
6933
6924
  body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8f9fa;color:#333;padding:24px;max-width:960px;margin:0 auto}
@@ -6940,7 +6931,7 @@ h1{font-size:24px;font-weight:700;margin-bottom:24px}
6940
6931
  .section h2{font-size:14px;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:.05em;margin-bottom:16px}
6941
6932
  .chart{display:flex;align-items:flex-end;gap:4px;height:160px;padding-top:8px}
6942
6933
  .bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;justify-content:flex-end}
6943
- .bar{width:100%;background:#2563eb;border-radius:4px 4px 0 0;min-height:2px;transition:height .3s}
6934
+ .bar{width:100%;background:#2563eb;border-radius:4px 4px 0 0;min-height:2px}
6944
6935
  .bar-label{font-size:10px;color:#888;margin-top:6px;white-space:nowrap}
6945
6936
  table{width:100%;border-collapse:collapse;font-size:13px}
6946
6937
  th{text-align:left;padding:6px 8px;color:#888;font-weight:500;border-bottom:1px solid #eee}
@@ -6948,27 +6939,58 @@ td{padding:6px 8px;border-bottom:1px solid #f0f0f0}
6948
6939
  .num{text-align:right;font-variant-numeric:tabular-nums}
6949
6940
  .path{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px}
6950
6941
  tr:hover td{background:#f8faff}
6951
- </style>
6952
- </head>
6942
+ </style></head>
6953
6943
  <body>
6954
6944
  <h1>Analytics</h1>
6955
6945
  <div class="cards">
6956
6946
  <div class="card"><div class="val">${total_pv}</div><div class="lbl">Page Views (${days}d)</div></div>
6957
6947
  <div class="card"><div class="val">${total_uv}</div><div class="lbl">Unique Pages</div></div>
6958
- <div class="card"><div class="val">${devices.mobile}%</div><div class="lbl">Mobile</div></div>
6959
- <div class="card"><div class="val">${devices.desktop}%</div><div class="lbl">Desktop</div></div>
6960
- </div>
6961
- <div class="section">
6962
- <h2>Daily Page Views</h2>
6963
- <div class="chart">${bars}</div>
6964
- </div>
6965
- <div class="section">
6966
- <h2>Top Pages</h2>
6967
- <table><thead><tr><th style="width:32px">#</th><th>Path</th><th style="width:64px">Views</th></tr></thead><tbody>${rows}</tbody></table>
6948
+ <div class="card"><div class="val">${data.devices.mobile}%</div><div class="lbl">Mobile</div></div>
6949
+ <div class="card"><div class="val">${data.devices.desktop}%</div><div class="lbl">Desktop</div></div>
6968
6950
  </div>
6951
+ <div class="section"><h2>Daily Page Views</h2><div class="chart">${bars}</div></div>
6952
+ <div class="section"><h2>Top Pages</h2>
6953
+ <table><thead><tr><th style="width:32px">#</th><th>Path</th><th style="width:64px">Views</th></tr></thead><tbody>${rows}</tbody></table></div>
6969
6954
  ${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>` : ""}
6970
- </body>
6971
- </html>`;
6955
+ </body></html>`;
6956
+ }
6957
+ function analytics(options) {
6958
+ const excluded = options?.excluded ?? DEFAULT_EXCLUDED;
6959
+ const pg = options?.pg;
6960
+ const store = pg ? null : new MemStore();
6961
+ const middleware = async (req, ctx, next) => {
6962
+ const url = new URL(req.url);
6963
+ const path2 = url.pathname;
6964
+ if (excluded.some((e) => path2.startsWith(e))) return next(req, ctx);
6965
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6966
+ const ref = req.headers.get("referer") || "";
6967
+ const ua = req.headers.get("user-agent") || "";
6968
+ const mobile = /mobile|android|iphone|ipad/i.test(ua);
6969
+ if (pg) {
6970
+ await pg.sql`
6971
+ INSERT INTO __analytics (date, path, count, mobile, desktop)
6972
+ VALUES (${date}, ${path2}, 1, ${mobile ? 1 : 0}, ${mobile ? 0 : 1})
6973
+ ON CONFLICT (date, path) DO UPDATE SET
6974
+ count = __analytics.count + 1,
6975
+ mobile = __analytics.mobile + ${mobile ? 1 : 0},
6976
+ desktop = __analytics.desktop + ${mobile ? 0 : 1}
6977
+ `;
6978
+ } else {
6979
+ const refDomain = ref ? new URL(ref).hostname.replace(/^www\./, "") : "";
6980
+ store.record(path2, date, refDomain, mobile);
6981
+ }
6982
+ return next(req, ctx);
6983
+ };
6984
+ const handler = async (req) => {
6985
+ const url = new URL(req.url);
6986
+ const days = Math.min(Math.max(Number(url.searchParams.get("days")) || 7, 1), 365);
6987
+ const data = pg ? await queryPg(pg.sql, days) : store.query(days);
6988
+ if (url.pathname === "/__analytics/data") return Response.json(data);
6989
+ return new Response(renderDashboard(days, data), {
6990
+ headers: { "content-type": "text/html; charset=utf-8" }
6991
+ });
6992
+ };
6993
+ return { middleware, handler };
6972
6994
  }
6973
6995
 
6974
6996
  // preferences.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.17.5",
3
+ "version": "0.17.7",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",