Conversation
| private buildRangeQuery(range: TimeRange = {}): URLSearchParams { | ||
| return new URLSearchParams({ | ||
| startAt: (range.startAt ?? 0).toString(), | ||
| endAt: (range.endAt ?? Date.now()).toString() | ||
| }); | ||
| } | ||
|
|
||
| async getStats(baseUrl: string, shareId: string, params: StatsAPIParams): Promise<StatsAPIResponse> { | ||
| const { websiteId } = await this.getShareData(baseUrl, shareId); | ||
| const qp = this.buildRangeQuery(params); | ||
| if (params.path) qp.set('path', params.path); | ||
| if (params.url) qp.set('url', params.url); | ||
| return this.cachedGet<StatsAPIResponse>( | ||
| baseUrl, | ||
| shareId, | ||
| `/websites/${websiteId}/stats?${qp.toString()}`, | ||
| `${baseUrl}|${shareId}|stats|${qp.toString()}` | ||
| ); |
There was a problem hiding this comment.
🔴 Cache key includes Date.now(), making caching ineffective for default time-range queries
When endAt is not explicitly provided by the caller, buildRangeQuery() at src/modules/umami/api.ts:120 fills in Date.now(), and the resulting query-parameter string (which includes this ever-changing timestamp) is embedded in the cache key (lines 133, 176, 190). Because Date.now() returns a different value on every call, the cache key is unique each time, so cachedGet / cacheManager.get will never find a hit.
The old code used JSON.stringify(params) as the cache key (which did not include Date.now()), while Date.now() was only placed in the URL sent to the server. This regression completely defeats the "内存 + localStorage 双级缓存" feature advertised in the README for the most common usage patterns (getSiteStats(), getPageStats('/path'), getPageviews(), getMetrics('path')) — any call without explicit startAt/endAt will always miss the cache and make a network request.
Prompt for agents
The cache key for getStats, getPageviews, and getMetrics includes the full URL query-parameter string produced by buildRangeQuery(), which embeds Date.now() when endAt is not provided. This makes the cache key unique on every call, so the cache is never hit.
The fix should separate the cache key from the URL query parameters. For the cache key, use only the caller-supplied parameters (like the old JSON.stringify(params) approach), while continuing to use the Date.now()-based value in the URL sent to the API server.
Affected methods:
- getStats (line 124-135 in api.ts): cache key on line 133 uses qp.toString()
- getPageviews (line 163-178 in api.ts): cache key on line 176 uses qp.toString()
- getMetrics (line 180-203 in api.ts): cache key on line 190 uses qp.toString()
One approach is to compute the cache key from the user-supplied params before filling in defaults, e.g. building a separate stable key string. Another approach is to use a snapshot of the query params before Date.now() is injected.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
本 PR 分两批改动:
Phase 1 — 代码质量审查(已合并到本分支的前几次 commit)
api.ts:getStats丢弃url参数 →getPageStatsByUrl实际打的是站点级请求。runtime/client.tsSimpleCache:localStorage 既不在加载时恢复到内存,又在保存时被覆盖,刷新后缓存完全失效。astro/integration.ts:读不到runtime/client.global.js时仍注入引用__oddmiscRuntime的脚本会抛ReferenceError。CacheManager<T = unknown>、UmamiClient构造异常改抛UmamiUrlError、StatsResult.visits改必填、tsconfig补 5 条严格选项。package.json.description、新增LICENSE、新增.github/workflows/ci.yml。Phase 2 — cloud.umami.is 真实逆向对齐
用 Playwright + CDP 连到浏览器抓
share/HM88wjbqInOwBNz0的 F12 网络请求,比对实际请求头和响应,发现一个关键兼容性 bug:x-umami-share-context: 1头x-umami-share-token/websites/*请求返回 401 Unauthorizedbounces/totaltime/comparison/active//pageviews//metrics//daterange//websites/<id>等端点存在type=url/type=host的 metrics 返回 400curl 复现验证:
Phase 2 改动:
api.ts/runtime/client.ts:所有 share 鉴权请求自动附带x-umami-share-context: 1。api.ts:抽出authedGet辅助;401 时同时清空sharePromise,避免复用陈旧 token。UmamiClient方法:getActiveVisitors/getPageviews/getMetrics/getWebsite/getDateRange。StatsResult新增可选字段bounces/totaltime/comparison。types.ts新增MetricType/MetricEntry/PageviewsSeries/WebsiteInfo/DateRange,index.ts对外导出。runtime/client.ts同步新增getActiveVisitors,window.oddmisc暴露。StatsResult新字段透传)。没改:
name/version/homepage/repository.url保持与已发布 npm 包一致。Review & Testing Checklist for Human
x-umami-share-context: 1的值在你自托管 Umami 上不会引起副作用(新版代码接受该头;旧版会忽略,但若你的实例有自定义中间件请确认)。getPageStats/getSiteStats/getPageStatsByUrl调用:返回结构依然兼容,只是多了可选字段。const c = createUmamiClient({ shareUrl: '你的 share url' }); console.log(await c.getSiteStats(), await c.getActiveVisitors(), await c.getMetrics('path', { limit: 10 })),观察是否按预期返回数据。npm run dev一个使用umami({ shareUrl })的站点,在浏览器控制台里await window.oddmisc.getSiteStats()应该不再抛 401。Notes
pnpm test(38/38)、pnpm exec tsc --noEmit(无 error)、pnpm build(CJS/ESM/IIFE/.d.ts齐全)。x-umami-share-context中间件 的shareToken解析分支,此头会在该中间件里把 JWT token 放到auth.shareToken上;缺失时checkAuth直接判未授权。type=url/type=host没有放进MetricType是因为 cloud.umami.is 明确返回 400,path已经覆盖 URL 聚合场景。Link to Devin session: https://app.devin.ai/sessions/5daa18325127407c9e16b867e408fa16
Requested by: @3142979848