Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
name: Lint / Typecheck / Test / Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 9

- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm exec tsc --noEmit

- name: Run tests
run: pnpm test

- name: Build
run: pnpm build
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) yCENzh

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
226 changes: 162 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# oddmisc

杂七杂八奇怪小工具 npm 包
杂七杂八的小工具 npm 包。当前主要提供 Umami 分享链接的统计读取能力,包括 Node / 浏览器通用的 `UmamiClient`、纯浏览器运行时,以及一个开箱即用的 Astro 集成。

[![npm version](https://img.shields.io/npm/v/oddmisc.svg)](https://www.npmjs.com/package/oddmisc)
[![License](https://img.shields.io/npm/l/oddmisc.svg)](https://github.com/yCENzh/oddmisc/blob/main/LICENSE)
[![License](https://img.shields.io/npm/l/oddmisc.svg)](./LICENSE)

## 特性

- **零运行时依赖**:仅依赖平台自带的 `fetch` / `URL` / `localStorage`。
- **双端通用**:同一份 API 同时可在 Node 与浏览器中使用。
- **内存 + localStorage 双级缓存**:默认 1 小时 TTL,浏览器刷新后仍然命中缓存。
- **Astro 集成**:一行配置自动向页面注入运行时客户端,并挂载到 `window.oddmisc`。
- **完整类型声明**:CJS / ESM 双格式输出,附带 `.d.ts`。

## 安装

Expand All @@ -17,116 +25,206 @@ yarn add oddmisc

## 快速开始

### 基本使用
### 在 Node / 通用环境中使用

```javascript
```ts
import { createUmamiClient } from 'oddmisc';

// 创建客户端
const client = createUmamiClient({
shareUrl: 'https://your-umami-instance.com/share/your-share-id'
shareUrl: 'https://your-umami-instance.com/share/<shareId>'
});

// 按路径查询页面统计
const page = await client.getPageStats('/about');
console.log(page.pageviews, page.visitors, page.visits);

// 按完整 URL 查询
const pageByUrl = await client.getPageStatsByUrl('https://site.example/about');

// 站点整体统计
const site = await client.getSiteStats();
```

所有查询都会被缓存,命中缓存时返回值上会带有 `_fromCache: true` 字段,可据此判断来源。调用 `client.clearCache()` 可清空缓存。

除了基础统计,还可以读取在线访客、时间序列、TopN 维度聚合等:

```ts
// 当前在线访客(实时,不缓存)
const live = await client.getActiveVisitors();

// 按小时聚合的 pageviews / sessions 序列
const series = await client.getPageviews({
startAt: Date.now() - 24 * 3600_000,
endAt: Date.now(),
unit: 'hour',
timezone: 'Asia/Shanghai'
});

// 获取页面访问统计
const stats = await client.getPageStats('/about');
console.log(`页面浏览量: ${stats.pageviews}, 访客数: ${stats.visitors}, 访问次数: ${stats.visits}`);
// Top 10 路径 / 国家 / 浏览器
const topPaths = await client.getMetrics('path', { limit: 10 });
const topCountries = await client.getMetrics('country', { limit: 10 });

// 获取网站整体统计
const siteStats = await client.getSiteStats();
console.log(`总浏览量: ${siteStats.pageviews}, 总访客数: ${siteStats.visitors}, 总访问次数: ${siteStats.visits}`);
// 网站元信息 / 可用数据区间
const info = await client.getWebsite();
const range = await client.getDateRange();
```

### Astro 集成

在 `astro.config.mjs` 中配置
在 `astro.config.mjs` 中加入

```javascript
```js
import { defineConfig } from 'astro/config';
import { umami } from 'oddmisc';
import { umami } from 'oddmisc/astro';

export default defineConfig({
integrations: [
umami({
shareUrl: 'https://your-umami-instance.com/share/your-share-id'
shareUrl: 'https://your-umami-instance.com/share/<shareId>'
})
]
});
```

**禁用 umami:**
如需临时禁用注入,将 `shareUrl` 设为 `false` 即可:

```javascript
umami({
shareUrl: false // 设为 false 则跳过 umami 集成
})
```js
umami({ shareUrl: false });
```

然后在前端代码中使用
集成会向每个页面注入一段自包含的运行时脚本,并在 `window` 上挂载 `oddmisc` 命名空间

```javascript
// 全局可用
const stats = await window.oddmisc.getStats('/some-page');
const siteStats = await window.oddmisc.getSiteStats();
```
```js
// 站点级统计
const site = await window.oddmisc.getSiteStats();

## API 参考
// 指定页面
const about = await window.oddmisc.getPageStats('/about');

### UmamiClient
// 直接访问底层 client(同一接口)
const client = window.oddmisc.umami;
```

#### 构造函数
```javascript
const client = createUmamiClient(config);
脚本初始化完成后会派发 `oddmisc-ready` 事件,可用于避免在客户端上出现竞态:

```js
window.addEventListener('oddmisc-ready', (e) => {
e.detail.client.getSiteStats().then(console.log);
});
```

#### 配置选项
```javascript
const config = {
shareUrl: 'https://umami.example.com/share/abc123' // 必需
};
## API 参考

### `createUmamiClient(config)` / `new UmamiClient(config)`

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `shareUrl` | `string` | Umami 分享链接,必填。支持自部署与 `cloud.umami.is` 形式 |

返回的 `UmamiClient` 实例提供:

| 方法 | 说明 |
| --- | --- |
| `getPageStats(path, options?)` | 按 `path` 查询(底层等价于 `path=eq.<path>`) |
| `getPageStatsByUrl(url, options?)` | 按完整 URL 查询 |
| `getSiteStats(options?)` | 站点整体统计 |
| `getActiveVisitors()` | 当前在线访客数(实时,不缓存) |
| `getPageviews({ startAt, endAt, unit, timezone }?)` | 按时间聚合的 `pageviews` / `sessions` 序列 |
| `getMetrics(type, { startAt, endAt, limit }?)` | TopN 维度聚合 |
| `getWebsite()` | 网站元信息(`name` / `domain` 等) |
| `getDateRange()` | 此分享可用的数据范围 |
| `clearCache()` | 清空内存 + localStorage 缓存,以及已缓存的 share token |

`options` 支持 `startAt` / `endAt`(毫秒时间戳),默认 `startAt=0` 到 `endAt=Date.now()`,即「建站起至今」。

`getMetrics(type)` 支持的维度(与 Umami v2 对齐):`path`、`referrer`、`browser`、`os`、`device`、`country`、`region`、`city`、`event`、`title`、`language`、`screen`、`tag`。**注意** `cloud.umami.is` 对 `url` / `host` 会返回 400,因此这两种没有放在类型中。

统一的统计返回结构:

```ts
interface StatsResult {
pageviews: number;
visitors: number;
visits: number;
/** Umami v2+ 返回,表示跳出数 */
bounces?: number;
/** Umami v2+ 返回,总访问时长(秒) */
totaltime?: number;
/** Umami v2+ 返回,与上一周期的对比 */
comparison?: {
pageviews?: number;
visitors?: number;
visits?: number;
bounces?: number;
totaltime?: number;
};
_fromCache?: boolean;
}
```

#### 方法
> 针对 `cloud.umami.is` 与新版自托管 Umami,内部会自动附带 `x-umami-share-context: 1` 头;缺失该头时服务端会直接返回 401。

- `getPageStats(path)` - 获取指定页面统计
- `getPageStatsByUrl(url)` - 通过 URL 获取页面统计
- `getSiteStats()` - 获取网站整体统计
- `clearCache()` - 清除缓存
- `getConfig()` - 获取当前配置
- `updateConfig(newConfig)` - 更新配置
### 运行时 `initUmamiRuntime(config)`

### Astro 集成
若不使用 Astro,可以自行调用 `initUmamiRuntime({ shareUrl })` 在浏览器中挂载客户端:

```javascript
umami(options: UmamiIntegrationOptions)
```ts
import { initUmamiRuntime } from 'oddmisc';
initUmamiRuntime({ shareUrl: 'https://.../share/<id>' });
```

选项:
```javascript
interface UmamiIntegrationOptions {
shareUrl: string | false; // Umami 分享链接,设为 false 则跳过
}
```
挂载完成后会触发 `oddmisc-ready` 事件,`detail.client` 即为挂在 `window.oddmisc` 上的对象。

### 错误类型

所有错误都继承自 `UmamiError`(带 `code` 与可选 `status`):

- `UmamiUrlError` — `code: 'INVALID_URL'`,无效分享链接。
- `UmamiAuthError` — `code: 'AUTH_FAILED'`,常见于 401,通常意味着 shareId 失效。
- `UmamiNetworkError` — `code: 'NETWORK_ERROR'`,上游返回非预期状态码。

### 工具类

- `parseShareUrl(shareUrl)` — 解析分享链接,返回 `{ apiBase, shareId }`。
- `CacheManager` — 通用的内存 + localStorage 双级缓存,可在其它场景复用。

## 关于 Umami 分享 API

经 `cloud.umami.is` 线上验证,本库使用如下端点(均以 `x-umami-share-token` + `x-umami-share-context: 1` 双头进行认证):

- `GET /api/share/<shareId>` — 解析 shareId,得到 `websiteId` + JWT `token`
- `GET /api/websites/<websiteId>` — 网站元信息
- `GET /api/websites/<websiteId>/stats?startAt&endAt[&path|url]` — 聚合统计
- `GET /api/websites/<websiteId>/active` — 当前在线访客
- `GET /api/websites/<websiteId>/pageviews?startAt&endAt&unit&timezone` — 时间序列
- `GET /api/websites/<websiteId>/metrics?startAt&endAt&type[&limit]` — TopN 聚合
- `GET /api/websites/<websiteId>/daterange` — 可用数据区间

## 支持的 Umami URL 格式

- `https://umami.example.com/share/abc123`
- `https://cloud.umami.is/analytics/us/share/abc123`
- `https://umami.example.com/analytics/share/abc123`
- `https://umami.example.com/share/<shareId>`
- `https://cloud.umami.is/analytics/us/share/<shareId>`
- `https://umami.example.com/analytics/share/<shareId>`

## 浏览器兼容性

- 现代浏览器(Chrome 60+, Firefox 60+, Safari 12+)
- 支持 localStorage 的环境
现代浏览器(Chrome 60+、Firefox 60+、Safari 12+);需要 `fetch`、`URL`、`AbortController`、`localStorage`。

## License
## 开发

MIT © yCENzh
```bash
pnpm install
pnpm build # 使用 tsup 构建 dist/
pnpm test # 运行 vitest 单元测试
pnpm test:coverage # 生成覆盖率报告
```

## 贡献
## License

欢迎提交 Issue 和 Pull Request!
MIT © yCENzh

## 相关链接

- [Umami 官网](https://umami.is/)
- [GitHub 仓库](https://github.com/yCENzh/oddmisc)
- [Umami 官方站点](https://umami.is/)
- [GitHub 仓库](https://github.com/yCENzh/oddmisc)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "oddmisc",
"version": "1.1.6",
"description": "杂七杂八奇怪小工具 npm 包",
"description": "杂七杂八奇怪小工具 npm 包 — Umami 分享链接统计读取与 Astro 集成",
"homepage": "https://github.com/yCENzh/oddmisc#readme",
"bugs": {
"url": "https://github.com/yCENzh/oddmisc/issues"
Expand Down
1 change: 0 additions & 1 deletion src/astro/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
// Astro 集成入口
export { umami, oddmisc } from './integration';
export type { UmamiIntegrationOptions, OddmiscIntegrationOptions } from './integration';
11 changes: 5 additions & 6 deletions src/astro/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface OddmiscIntegrationOptions {

type AstroConfigSetupParams = HookParameters<'astro:config:setup'>;

function injectUmamiRuntime(shareUrl: string | false) {
function injectUmamiRuntime(shareUrl: string | false): string | undefined {
if (!shareUrl) return;

let runtimeCode = '';
Expand All @@ -23,19 +23,18 @@ function injectUmamiRuntime(shareUrl: string | false) {
const runtimePath = join(__dirname, './runtime/client.global.js');
runtimeCode = readFileSync(runtimePath, 'utf-8');
} catch {
console.warn('[oddmisc] 无法读取运行时文件,使用备用方案');
console.warn('[oddmisc] 无法读取运行时文件,已跳过客户端注入');
return;
}

const initCode = `
return `
// oddmisc Umami Runtime
${runtimeCode}

if (typeof window !== 'undefined') {
if (typeof window !== 'undefined' && typeof __oddmiscRuntime !== 'undefined') {
__oddmiscRuntime.initUmamiRuntime(${JSON.stringify({ shareUrl })});
}
`;

return initCode;
}

export function umami(options: UmamiIntegrationOptions): AstroIntegration {
Expand Down
Loading
Loading