diff --git a/.commitlintrc b/.commitlintrc deleted file mode 100644 index c30e5a9..0000000 --- a/.commitlintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["@commitlint/config-conventional"] -} diff --git a/.czrc b/.czrc deleted file mode 100644 index d1bcc20..0000000 --- a/.czrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "path": "cz-conventional-changelog" -} diff --git a/.dockerignore b/.dockerignore index 10d62bf..671bcd3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,9 @@ node_modules *.log .DS_Store .cache -public/build -build +.git .env +.next +.vinxi +.output +dist diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index efbead9..0000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -build -public/build diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 967cde2..0000000 --- a/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@chenyueban" -} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d606d33..0166703 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [chenyueban] +github: [xinyao27] diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6fef93b..aaaefc0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: Deploy on: push: tags: - - '*' + - "*" jobs: docker: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6f98918..429b282 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,25 +14,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2.2.1 + uses: pnpm/action-setup@v4 - name: Set node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: 16.x + node-version: 20.x cache: pnpm - - name: Setup - run: npm i -g @antfu/ni - - name: Install - run: nci + run: pnpm install - name: Build - run: nr build + run: pnpm run build - name: Lint - run: nr lint + run: pnpm run lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d69d7a..482df37 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,30 +15,27 @@ jobs: strategy: matrix: - node: [16.x] + node: [20.x] os: [ubuntu-latest, macos-latest] fail-fast: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2.2.1 + uses: pnpm/action-setup@v4 - name: Set node version to ${{ matrix.node }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: pnpm - - name: Setup - run: npm i -g @antfu/ni - - name: Install - run: nci + run: pnpm install - name: Build - run: nr build + run: pnpm run build - name: Test - run: nr test --if-present + run: pnpm test --if-present diff --git a/.gitignore b/.gitignore index 855ad0c..859e71c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ tsconfig.tsbuildinfo secrets # Logs -logs +logs/* *.log npm-debug.log* yarn-debug.log* @@ -44,11 +44,13 @@ ohbug.config.development.yml # docker postgres-data -logs # vercel .vercel -# next.js -.next -out +# TanStack Start / Vinxi +.vinxi +.output + +# Drizzle +packages/db/migrations/*.sql diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 36af219..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx lint-staged diff --git a/.lintstagedrc b/.lintstagedrc deleted file mode 100644 index fd8d9ec..0000000 --- a/.lintstagedrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "*.{js?(x),ts?(x),vue,html,md,json,yml}": [ - "eslint --fix", - "git add" - ] -} diff --git a/.npmrc b/.npmrc index 6c59086..e69de29 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ -enable-pre-post-scripts=true diff --git a/.release-it.json b/.release-it.json index ba60f77..e5230cd 100644 --- a/.release-it.json +++ b/.release-it.json @@ -12,6 +12,6 @@ } }, "hooks": { - "before:init": ["npm run lint", "npm test"] + "before:init": ["pnpm run lint", "pnpm test"] } } diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit new file mode 100755 index 0000000..85fb65b --- /dev/null +++ b/.vite-hooks/pre-commit @@ -0,0 +1 @@ +vp staged diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..99e2f7d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["oxc.oxc-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 19f3161..bbbdbe0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,12 @@ { - "cSpell.words": [ - "ohbug" - ], - "typescript.tsdk": "node_modules/typescript/lib" + "cSpell.words": ["ohbug"], + "typescript.tsdk": "node_modules/typescript/lib", + "editor.defaultFormatter": "oxc.oxc-vscode", + "oxc.fmt.configPath": "./vite.config.ts", + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.fixAll.oxc": "explicit" + }, + "i18n-ally.localesPaths": ["packages/web/messages"] } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..95c0f94 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,304 @@ +# Ohbug Dashboard — 技术架构 + +## 项目概述 + +Ohbug Dashboard 是一个开源的前端错误监控平台,包含事件上报、Issue 聚合、告警通知、SourceMap 解析、用户行为追踪等功能。 + +## 技术栈 + +| 层级 | 技术 | 说明 | +| ------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------- | +| **前端框架** | [TanStack Start](https://tanstack.com/start) | 基于 Vinxi 的全栈 React 框架 | +| **路由** | [TanStack Router](https://tanstack.com/router) | 类型安全的文件路由 | +| **API 层** | [oRPC](https://orpc.dev/) | 类型安全的 RPC 框架 | +| **数据获取** | [TanStack Query](https://tanstack.com/query) | 通过 [oRPC 集成](https://orpc.dev/docs/integrations/tanstack-query) 自动生成 hooks | +| **状态管理** | [Zustand](https://zustand.docs.pmnd.rs/) | 轻量级客户端状态管理 | +| **认证** | [Better Auth](https://better-auth.com/) | Email/Password + GitHub OAuth,Drizzle adapter | +| **ORM** | [Drizzle ORM](https://orm.drizzle.team/) | 类型安全的 SQL ORM | +| **数据库** | PostgreSQL | 主数据库 | +| **后端 (事件上报)** | [Hono](https://hono.dev/) | 轻量级 HTTP 框架,处理 SDK 事件上报 + SourceMap 上传 | +| **消息队列** | [BullMQ](https://docs.bullmq.io/) | 基于 Redis 的任务队列 | +| **定时任务** | [node-cron](https://github.com/node-cron/node-cron) | 过期数据清理 | +| **UI 组件** | Radix UI + Tailwind CSS | shadcn/ui 风格组件库 | +| **表单** | React Hook Form + Zod | 类型安全的表单验证 | +| **构建工具** | Vite+ (vp) | 统一工具链 | +| **包管理** | pnpm | monorepo workspace | + +## 架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ TanStack Start (packages/web) │ +│ │ +│ app/ │ +│ routes/ │ +│ ├── api/auth/$.ts Better Auth handler │ +│ ├── api/rpc/$.ts oRPC adapter │ +│ ├── __root.tsx 根布局 │ +│ ├── _authed.tsx 认证守卫 (beforeLoad) │ +│ ├── _authed/index.tsx 首页 (项目列表) │ +│ ├── _authed/create-project.tsx 创建项目 │ +│ ├── _authed/$projectId.tsx 项目布局 │ +│ ├── _authed/$projectId/ │ +│ │ ├── issues/ Issue 列表 + 详情 │ +│ │ ├── feedbacks/ 用户反馈 │ +│ │ ├── alerts/ 告警规则 │ +│ │ ├── metrics.tsx 性能指标 │ +│ │ ├── releases/ 版本管理 + SourceMap │ +│ │ ├── users/ 事件用户 │ +│ │ ├── settings.tsx 项目设置 │ +│ │ ├── profile.tsx 个人资料 │ +│ │ └── mock.tsx 测试页面 │ +│ └── auth/signin.tsx 登录页 │ +│ │ +│ server/ │ +│ ├── orpc.ts oRPC router 定义 │ +│ ├── procedures/ │ +│ │ ├── project.ts 项目 CRUD │ +│ │ ├── issue.ts Issue 查询/删除 │ +│ │ ├── event.ts 事件查询 │ +│ │ ├── alert.ts 告警规则 CRUD │ +│ │ ├── feedback.ts 反馈查询 │ +│ │ ├── metric.ts 指标查询 │ +│ │ ├── release.ts 版本查询 │ +│ │ ├── user.ts 用户管理 │ +│ │ └── view.ts PV/UV 查询 │ +│ ├── auth.ts Better Auth 配置 │ +│ └── middleware.ts 认证中间件 │ +│ │ +│ lib/ │ +│ ├── orpc.ts oRPC client + Query │ +│ └── auth-client.ts Better Auth client │ +│ │ +│ components/ UI 组件 (Radix + TW) │ +│ store/ Zustand store │ +└─────────────────────────────────────────────────────────────┘ + │ + │ 共享 packages/db + │ +┌───────────────────────┴─────────────────────────────────────┐ +│ Hono Server (packages/server) │ +│ │ +│ 仅处理 Ohbug SDK 的事件上报,不处理 Dashboard UI 请求 │ +│ │ +│ src/ │ +│ ├── index.ts Hono app 入口 │ +│ ├── routes/ │ +│ │ ├── report.ts POST /report 事件上报 │ +│ │ └── source-map.ts POST /source-map 上传 │ +│ ├── workers/ │ +│ │ ├── event.worker.ts 事件处理 (BullMQ) │ +│ │ ├── alert.worker.ts 告警判定 + 通知 │ +│ │ └── source-map.worker.ts SourceMap 存储 │ +│ ├── cron/ │ +│ │ └── cleanup.ts 过期数据清理 (node-cron) │ +│ └── lib/ │ +│ ├── report-core.ts 事件聚合逻辑 │ +│ └── alert-core.ts 告警规则引擎 │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌─────────┴─────────┐ + │ PostgreSQL │ + │ Redis │ + └───────────────────┘ +``` + +## Monorepo 结构 + +``` +ohbug-dashboard/ +├── packages/ +│ ├── db/ 共享数据库层 +│ │ ├── src/ +│ │ │ ├── schema.ts Drizzle 表定义 (所有 model) +│ │ │ ├── relations.ts Drizzle 关系定义 +│ │ │ └── index.ts Drizzle client 导出 +│ │ ├── drizzle.config.ts +│ │ └── migrations/ +│ │ +│ ├── web/ TanStack Start 前端应用 +│ │ ├── app/ +│ │ │ ├── routes/ 页面路由 +│ │ │ ├── server/ oRPC procedures + auth + middleware +│ │ │ ├── lib/ oRPC client + auth client + utils +│ │ │ ├── components/ UI 组件 +│ │ │ ├── store/ Zustand store +│ │ │ ├── router.tsx TanStack Router 配置 +│ │ │ ├── client.tsx 客户端入口 +│ │ │ └── ssr.tsx SSR 入口 +│ │ ├── public/ +│ │ └── package.json +│ │ +│ ├── server/ Hono 事件上报服务 +│ │ ├── src/ +│ │ └── package.json +│ │ +│ ├── common/ 共享工具和类型 +│ │ └── src/ +│ │ ├── index.ts +│ │ └── interfaces/ 业务类型定义 +│ │ +│ └── config/ 运行时配置读取 +│ └── src/ +│ +├── docker-compose.yml +├── docker-compose.development.yml +├── AGENTS.md 本文件 +└── package.json +``` + +## 数据流 + +### Dashboard 数据查询 (oRPC) + +``` +Browser + → Component 调用 orpc.issue.list.useQuery({ projectId }) + → TanStack Query 管理缓存/请求 + → oRPC client POST /api/rpc/issue.list + → TanStack Start server handler + → oRPC router: issue.list procedure + → Zod 校验输入 + → Drizzle 查询 PostgreSQL + → 类型安全的响应返回到组件 +``` + +### SDK 事件上报 (Hono) + +``` +Ohbug SDK + → POST /report { apiKey, type, detail, ... } + → Hono route handler 校验 + → BullMQ: 推入 event 队列 + → Event Worker: + 1. 聚合生成 Issue (md5 去重) + 2. 创建 Event 记录 (Drizzle) + 3. 关联 EventUser + 4. 推入 alert 队列 + → Alert Worker: + 1. 匹配告警规则 (条件 + 过滤器) + 2. 判断静默期 + 3. 发送通知 (Email / Webhook) +``` + +### 认证流 (Better Auth) + +``` +登录: + Browser → signIn.email({ email, password }) 或 signIn.social({ provider: "github" }) + → POST /api/auth/sign-in/email 或 /api/auth/sign-in/social + → Better Auth 处理认证 + 创建 session + → Set cookie → 返回 session + +页面守卫: + TanStack Router _authed layout → beforeLoad + → auth.api.getSession({ headers }) + → 无 session → redirect("/auth/signin") + +oRPC 认证: + oRPC middleware → 从 request headers 获取 session + → 注入 ctx.user → procedure 可访问当前用户 +``` + +## 数据库 Schema + +使用 Drizzle ORM 定义,PostgreSQL 作为数据库。核心表: + +| 表名 | 说明 | +| ------------------------- | --------------------------------- | +| `user` | 系统用户 (Better Auth 管理) | +| `session` | 用户会话 (Better Auth 管理) | +| `account` | OAuth 账号关联 (Better Auth 管理) | +| `verification` | 验证 token (Better Auth 管理) | +| `project` | 监控项目 | +| `users_on_projects` | 用户-项目关联 (多对多) | +| `event` | 错误事件 | +| `issue` | 聚合后的问题 | +| `event_user` | 事件关联的终端用户 | +| `event_users_on_issues` | 终端用户-Issue 关联 | +| `event_users_on_projects` | 终端用户-项目关联 | +| `metric` | Web Vitals 性能指标 | +| `feedback` | 用户反馈 | +| `page_view` | 页面浏览量 | +| `user_view` | 用户访问量 | +| `alert` | 告警规则 | +| `alert_event` | 告警触发记录 | +| `release` | 版本发布 (含 SourceMap) | + +## 关键约定 + +### oRPC Procedures + +- 所有 procedure 定义在 `packages/web/app/server/procedures/` 下,按业务域拆分文件 +- 每个 procedure 使用 Zod 定义输入/输出 schema +- 需要认证的 procedure 使用 `authedProcedure` (内置 session 校验中间件) +- oRPC router 在 `packages/web/app/server/orpc.ts` 中组合 +- 客户端通过 `packages/web/app/lib/orpc.ts` 导出的 hooks 调用 + +### 路由 + +- TanStack Router 文件路由,`_authed` 前缀表示需要登录 +- `$param` 表示动态路由参数 +- 每个路由文件导出 `Route` 对象,包含 `loader` / `component` / `beforeLoad` +- 数据预取在 `loader` 中完成,通过 oRPC server-side 调用 + +### 认证 + +- Better Auth 配置在 `packages/web/app/server/auth.ts` +- 客户端在 `packages/web/app/lib/auth-client.ts` +- 支持 Email/Password 登录 + GitHub OAuth +- 不使用邮箱验证 +- 密码由 Better Auth 内置 bcrypt 哈希(不使用 md5) + +### 状态管理 + +- Zustand 仅用于客户端 UI 状态(如当前选中的项目、排序偏好) +- 服务端数据全部通过 oRPC + TanStack Query 管理 +- 持久化状态使用 Zustand `persist` middleware + +### Hono Server + +- 仅处理来自 Ohbug SDK 的请求,不处理 Dashboard 页面请求 +- 与 Web 应用共享 `packages/db` 访问同一数据库 +- BullMQ 使用 Redis 作为 broker +- Worker 和 HTTP server 在同一进程中运行 + +### 样式 + +- Tailwind CSS utility-first +- 组件基于 Radix UI 原语 + shadcn/ui 模式 +- 暗色/亮色主题通过 CSS 变量切换 + +## 开发 + +```bash +# 启动基础设施 (PostgreSQL + Redis) +docker-compose -f docker-compose.development.yml up -d + +# 安装依赖 +pnpm install + +# 数据库迁移 +pnpm --filter db run migrate + +# 启动开发服务器 (web + server 并行) +pnpm run dev +``` + +## 环境变量 + +```env +# 数据库 +DATABASE_URL=postgresql://ohbug:ohbug@localhost:5432/ohbug + +# Redis +REDIS_URL=redis://localhost:6379 + +# Better Auth +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 + +# GitHub OAuth (可选) +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 8758a22..064deb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,357 +1,323 @@ - - # [0.11.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.10.1...0.11.0) (2023-07-05) - ### Bug Fixes -* fix eslint error ([9ae5eb8](https://github.com/ohbug-org/ohbug-dashboard/commit/9ae5eb805247766dcf096e31cae649de9047b8a7)) -* Update endpoint in defaultConfig to use current location ([e101cfc](https://github.com/ohbug-org/ohbug-dashboard/commit/e101cfc912ad5cfae35b882efff139580731f953)) -* use new link ([baa6c93](https://github.com/ohbug-org/ohbug-dashboard/commit/baa6c93cc84b737ca15ec81dcb1bf216a3395caa)) -* **web:** fix nextjs dayjs utc call exception ([a8c422d](https://github.com/ohbug-org/ohbug-dashboard/commit/a8c422d43cd5da85e1f1aa2586f10a535cc7952f)) - +- fix eslint error ([9ae5eb8](https://github.com/ohbug-org/ohbug-dashboard/commit/9ae5eb805247766dcf096e31cae649de9047b8a7)) +- Update endpoint in defaultConfig to use current location ([e101cfc](https://github.com/ohbug-org/ohbug-dashboard/commit/e101cfc912ad5cfae35b882efff139580731f953)) +- use new link ([baa6c93](https://github.com/ohbug-org/ohbug-dashboard/commit/baa6c93cc84b737ca15ec81dcb1bf216a3395caa)) +- **web:** fix nextjs dayjs utc call exception ([a8c422d](https://github.com/ohbug-org/ohbug-dashboard/commit/a8c422d43cd5da85e1f1aa2586f10a535cc7952f)) ### Features -* **server:** 数据过期时间修改为默认7天并且支持通过配置文件修改 ([14ea185](https://github.com/ohbug-org/ohbug-dashboard/commit/14ea1851d9eef0ba9bc7a81a52c8227d22358983)) -* web migration to app dir ([e282720](https://github.com/ohbug-org/ohbug-dashboard/commit/e2827200db10b3ca437053930c286868ebd8e089)) -* **web:** add mock page ([e82cca2](https://github.com/ohbug-org/ohbug-dashboard/commit/e82cca238f92bb7da51b027853778a438f16ae93)) +- **server:** 数据过期时间修改为默认7天并且支持通过配置文件修改 ([14ea185](https://github.com/ohbug-org/ohbug-dashboard/commit/14ea1851d9eef0ba9bc7a81a52c8227d22358983)) +- web migration to app dir ([e282720](https://github.com/ohbug-org/ohbug-dashboard/commit/e2827200db10b3ca437053930c286868ebd8e089)) +- **web:** add mock page ([e82cca2](https://github.com/ohbug-org/ohbug-dashboard/commit/e82cca238f92bb7da51b027853778a438f16ae93)) ## [0.10.1](https://github.com/ohbug-org/ohbug-dashboard/compare/0.10.0...0.10.1) (2022-09-29) - ### Bug Fixes -* fixed eslint error ([0fe1baa](https://github.com/ohbug-org/ohbug-dashboard/commit/0fe1baa3b92b3b0593f57c66465c21e0eb2088cd)) -* **web:** 修复 relatedMetadata 显示数据异常 ([4fa0728](https://github.com/ohbug-org/ohbug-dashboard/commit/4fa072846c04a8f029f48512169b9a55d1d49a79)) -* **web:** 修复 useInfinite ([096b9f5](https://github.com/ohbug-org/ohbug-dashboard/commit/096b9f50dc33e96ad3d4b656ba849ff3f99fa511)) -* **web:** 修复趋势图时区问题 ([28814ce](https://github.com/ohbug-org/ohbug-dashboard/commit/28814cea57f8b91c568373e1218862168b401ba0)) -* **web:** 登录时数据校验 ([dc20853](https://github.com/ohbug-org/ohbug-dashboard/commit/dc2085394a457f1c48b5f772db56e673f6a09eb0)) +- fixed eslint error ([0fe1baa](https://github.com/ohbug-org/ohbug-dashboard/commit/0fe1baa3b92b3b0593f57c66465c21e0eb2088cd)) +- **web:** 修复 relatedMetadata 显示数据异常 ([4fa0728](https://github.com/ohbug-org/ohbug-dashboard/commit/4fa072846c04a8f029f48512169b9a55d1d49a79)) +- **web:** 修复 useInfinite ([096b9f5](https://github.com/ohbug-org/ohbug-dashboard/commit/096b9f50dc33e96ad3d4b656ba849ff3f99fa511)) +- **web:** 修复趋势图时区问题 ([28814ce](https://github.com/ohbug-org/ohbug-dashboard/commit/28814cea57f8b91c568373e1218862168b401ba0)) +- **web:** 登录时数据校验 ([dc20853](https://github.com/ohbug-org/ohbug-dashboard/commit/dc2085394a457f1c48b5f772db56e673f6a09eb0)) # [0.10.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.9.2...0.10.0) (2022-09-15) - ### Bug Fixes -* **web:** 修复 app 类型 ([561591c](https://github.com/ohbug-org/ohbug-dashboard/commit/561591ccb883fab65a6c18b10e7f361cbfe235ec)) -* **web:** 调整 checkbox 边框色 ([1f2fed2](https://github.com/ohbug-org/ohbug-dashboard/commit/1f2fed29b009f1c8c66368b995971e7c2b0948ee)) - +- **web:** 修复 app 类型 ([561591c](https://github.com/ohbug-org/ohbug-dashboard/commit/561591ccb883fab65a6c18b10e7f361cbfe235ec)) +- **web:** 调整 checkbox 边框色 ([1f2fed2](https://github.com/ohbug-org/ohbug-dashboard/commit/1f2fed29b009f1c8c66368b995971e7c2b0948ee)) ### Features -* **web:** issue 排序选择状态记入缓存 ([2e4d4f6](https://github.com/ohbug-org/ohbug-dashboard/commit/2e4d4f6276198033042db171c5c7095b5758774f)) -* **web:** issue 支持搜索 ([3f1ce81](https://github.com/ohbug-org/ohbug-dashboard/commit/3f1ce813df9d5643c113cae69b1e6bcdad3724ff)) -* **web:** user 支持搜索 ([14bd30f](https://github.com/ohbug-org/ohbug-dashboard/commit/14bd30f457e128127a29ffa70683fcd2fe1e4c4d)) -* **web:** 优化 footer ([72cea37](https://github.com/ohbug-org/ohbug-dashboard/commit/72cea372ab90302fb0d676cbcb90d2b04afaab0f)) +- **web:** issue 排序选择状态记入缓存 ([2e4d4f6](https://github.com/ohbug-org/ohbug-dashboard/commit/2e4d4f6276198033042db171c5c7095b5758774f)) +- **web:** issue 支持搜索 ([3f1ce81](https://github.com/ohbug-org/ohbug-dashboard/commit/3f1ce813df9d5643c113cae69b1e6bcdad3724ff)) +- **web:** user 支持搜索 ([14bd30f](https://github.com/ohbug-org/ohbug-dashboard/commit/14bd30f457e128127a29ffa70683fcd2fe1e4c4d)) +- **web:** 优化 footer ([72cea37](https://github.com/ohbug-org/ohbug-dashboard/commit/72cea372ab90302fb0d676cbcb90d2b04afaab0f)) ## [0.9.2](https://github.com/ohbug-org/ohbug-dashboard/compare/0.9.1...0.9.2) (2022-09-14) - ### Bug Fixes -* **server:** event 上报修复 user 创建问题 ([d778890](https://github.com/ohbug-org/ohbug-dashboard/commit/d778890b361d9c4e92f5e2de1adea90292516b67)) +- **server:** event 上报修复 user 创建问题 ([d778890](https://github.com/ohbug-org/ohbug-dashboard/commit/d778890b361d9c4e92f5e2de1adea90292516b67)) ## [0.9.1](https://github.com/ohbug-org/ohbug-dashboard/compare/0.9.0...0.9.1) (2022-09-14) - ### Bug Fixes -* **nginx:** 修复 nginx 反代导致无法获取真实 IP ([1d1e471](https://github.com/ohbug-org/ohbug-dashboard/commit/1d1e471b5d39851c9dfc6163bd24da3fd9aa523e)) -* project name 修改为可重复字段 ([4a2d1be](https://github.com/ohbug-org/ohbug-dashboard/commit/4a2d1be2f58768d4f67f00f4a8ded51bb43b3a11)) -* **web:** 修复 issue detail 页 active tab 状态问题 ([408d218](https://github.com/ohbug-org/ohbug-dashboard/commit/408d2184cf21e60bc1559da25b104b899e5534d4)) -* **web:** 新增页面标题 ([6983caf](https://github.com/ohbug-org/ohbug-dashboard/commit/6983cafc2725a5d46cf12c2110efda8a0b7347dc)) +- **nginx:** 修复 nginx 反代导致无法获取真实 IP ([1d1e471](https://github.com/ohbug-org/ohbug-dashboard/commit/1d1e471b5d39851c9dfc6163bd24da3fd9aa523e)) +- project name 修改为可重复字段 ([4a2d1be](https://github.com/ohbug-org/ohbug-dashboard/commit/4a2d1be2f58768d4f67f00f4a8ded51bb43b3a11)) +- **web:** 修复 issue detail 页 active tab 状态问题 ([408d218](https://github.com/ohbug-org/ohbug-dashboard/commit/408d2184cf21e60bc1559da25b104b899e5534d4)) +- **web:** 新增页面标题 ([6983caf](https://github.com/ohbug-org/ohbug-dashboard/commit/6983cafc2725a5d46cf12c2110efda8a0b7347dc)) # [0.9.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.8.2...0.9.0) (2022-09-09) - ### Bug Fixes -* **server:** 修复 alert UserFrequencyCondition 异常 ([7f85060](https://github.com/ohbug-org/ohbug-dashboard/commit/7f85060cc2ba3b337e41ebb9a17694e72908030c)) -* **web:** 修复 issues tab 栏异常 ([d4dff96](https://github.com/ohbug-org/ohbug-dashboard/commit/d4dff96d2a3ed4070e7195308dfecc032c52b4fc)) - +- **server:** 修复 alert UserFrequencyCondition 异常 ([7f85060](https://github.com/ohbug-org/ohbug-dashboard/commit/7f85060cc2ba3b337e41ebb9a17694e72908030c)) +- **web:** 修复 issues tab 栏异常 ([d4dff96](https://github.com/ohbug-org/ohbug-dashboard/commit/d4dff96d2a3ed4070e7195308dfecc032c52b4fc)) ### Features -* event metadata 使用 string 类型存储 ([1098116](https://github.com/ohbug-org/ohbug-dashboard/commit/109811683dd33de90e32fed2a7a6b87c06e2980a)) -* **web:** feedback 新增详细信息展示 ([b776057](https://github.com/ohbug-org/ohbug-dashboard/commit/b77605724b130240011e272c870be1b65ca5e7ae)) +- event metadata 使用 string 类型存储 ([1098116](https://github.com/ohbug-org/ohbug-dashboard/commit/109811683dd33de90e32fed2a7a6b87c06e2980a)) +- **web:** feedback 新增详细信息展示 ([b776057](https://github.com/ohbug-org/ohbug-dashboard/commit/b77605724b130240011e272c870be1b65ca5e7ae)) ## [0.8.2](https://github.com/ohbug-org/ohbug-dashboard/compare/0.8.1...0.8.2) (2022-09-09) - ### Bug Fixes -* **server:** 修复 alert message 为空 ([9ef7094](https://github.com/ohbug-org/ohbug-dashboard/commit/9ef7094e55a0c5e2fc26bbc55d34fb7f4f973da0)) -* **server:** 修复 alert 通知 @ 人不生效 ([67bb1f0](https://github.com/ohbug-org/ohbug-dashboard/commit/67bb1f034808662c25cceba7618b2d60c7076352)) -* **server:** 修复 conditionMatch filterMatch 不生效 ([f625ee7](https://github.com/ohbug-org/ohbug-dashboard/commit/f625ee709b199efcc729fb33f0dda73096f595d6)) -* **server:** 修复聚合生成 md5 Object 导致的一致性问题 ([c5ce04b](https://github.com/ohbug-org/ohbug-dashboard/commit/c5ce04becd4ec47c6e22b1cd8b9814e8f9febbae)) -* **web:** issues 页面默认查询最近的 event 记录 ([67282fd](https://github.com/ohbug-org/ohbug-dashboard/commit/67282fd1301eff50cdb0de670da390e5d13acb8d)) -* **web:** 修复 alert 列表编辑没有反应 ([1da7e7b](https://github.com/ohbug-org/ohbug-dashboard/commit/1da7e7b0582eec7de13ec35d38e70be01d759865)) +- **server:** 修复 alert message 为空 ([9ef7094](https://github.com/ohbug-org/ohbug-dashboard/commit/9ef7094e55a0c5e2fc26bbc55d34fb7f4f973da0)) +- **server:** 修复 alert 通知 @ 人不生效 ([67bb1f0](https://github.com/ohbug-org/ohbug-dashboard/commit/67bb1f034808662c25cceba7618b2d60c7076352)) +- **server:** 修复 conditionMatch filterMatch 不生效 ([f625ee7](https://github.com/ohbug-org/ohbug-dashboard/commit/f625ee709b199efcc729fb33f0dda73096f595d6)) +- **server:** 修复聚合生成 md5 Object 导致的一致性问题 ([c5ce04b](https://github.com/ohbug-org/ohbug-dashboard/commit/c5ce04becd4ec47c6e22b1cd8b9814e8f9febbae)) +- **web:** issues 页面默认查询最近的 event 记录 ([67282fd](https://github.com/ohbug-org/ohbug-dashboard/commit/67282fd1301eff50cdb0de670da390e5d13acb8d)) +- **web:** 修复 alert 列表编辑没有反应 ([1da7e7b](https://github.com/ohbug-org/ohbug-dashboard/commit/1da7e7b0582eec7de13ec35d38e70be01d759865)) ## [0.8.1](https://github.com/ohbug-org/ohbug-dashboard/compare/0.8.0...0.8.1) (2022-09-08) - ### Bug Fixes -* 修复 nextAuth 回调地址设置错误 ([0b4935d](https://github.com/ohbug-org/ohbug-dashboard/commit/0b4935d6cbc6952ba52d331f3860952da91e3a03)) +- 修复 nextAuth 回调地址设置错误 ([0b4935d](https://github.com/ohbug-org/ohbug-dashboard/commit/0b4935d6cbc6952ba52d331f3860952da91e3a03)) # [0.8.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.7.2...0.8.0) (2022-09-07) - ### Bug Fixes -* 完善 https 配置 ([3e985b8](https://github.com/ohbug-org/ohbug-dashboard/commit/3e985b883cb8193ff0394abe1cb236dffe1c6a5e)) - +- 完善 https 配置 ([3e985b8](https://github.com/ohbug-org/ohbug-dashboard/commit/3e985b883cb8193ff0394abe1cb236dffe1c6a5e)) ### Features -* 支持 https ([a2f2bfa](https://github.com/ohbug-org/ohbug-dashboard/commit/a2f2bfae0dd9f46c8dcb7aa9558346e3bcee0f14)) +- 支持 https ([a2f2bfa](https://github.com/ohbug-org/ohbug-dashboard/commit/a2f2bfae0dd9f46c8dcb7aa9558346e3bcee0f14)) ## [0.7.2](https://github.com/ohbug-org/ohbug-dashboard/compare/0.7.1...0.7.2) (2022-09-05) - ### Bug Fixes -* adapter sdk ([4b6554d](https://github.com/ohbug-org/ohbug-dashboard/commit/4b6554d71fbbfac9dea7ec44aa9b411da64faf44)) -* **server:** sourceMap 上传使用队列 解决 release 重复创建 ([645cbec](https://github.com/ohbug-org/ohbug-dashboard/commit/645cbec99c1488001cda11f9511d217602a2a430)) -* **server:** 优化 webhook 内容展示 ([31c44be](https://github.com/ohbug-org/ohbug-dashboard/commit/31c44be45f0bca99fea0dbe1f17e4ccfec82d375)) -* **web:** fix issueList link error ([cefdeff](https://github.com/ohbug-org/ohbug-dashboard/commit/cefdeff79f6bf551664d88f649adaac4a6ad24f2)) +- adapter sdk ([4b6554d](https://github.com/ohbug-org/ohbug-dashboard/commit/4b6554d71fbbfac9dea7ec44aa9b411da64faf44)) +- **server:** sourceMap 上传使用队列 解决 release 重复创建 ([645cbec](https://github.com/ohbug-org/ohbug-dashboard/commit/645cbec99c1488001cda11f9511d217602a2a430)) +- **server:** 优化 webhook 内容展示 ([31c44be](https://github.com/ohbug-org/ohbug-dashboard/commit/31c44be45f0bca99fea0dbe1f17e4ccfec82d375)) +- **web:** fix issueList link error ([cefdeff](https://github.com/ohbug-org/ohbug-dashboard/commit/cefdeff79f6bf551664d88f649adaac4a6ad24f2)) ## [0.7.1](https://github.com/ohbug-org/ohbug-dashboard/compare/0.7.0...0.7.1) (2022-09-02) # [0.7.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.6.0...0.7.0) (2022-09-01) - ### Features -* **web:** support delete issues ([b713c46](https://github.com/ohbug-org/ohbug-dashboard/commit/b713c46c26d03d04ce112e23fe5b89ce743c0e93)) -* **web:** support delete release and sourceMaps ([4a42c66](https://github.com/ohbug-org/ohbug-dashboard/commit/4a42c66e6e85c51c907736eda9690d0cbaa77e2f)) +- **web:** support delete issues ([b713c46](https://github.com/ohbug-org/ohbug-dashboard/commit/b713c46c26d03d04ce112e23fe5b89ce743c0e93)) +- **web:** support delete release and sourceMaps ([4a42c66](https://github.com/ohbug-org/ohbug-dashboard/commit/4a42c66e6e85c51c907736eda9690d0cbaa77e2f)) # [0.6.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.5.0...0.6.0) (2022-08-31) - ### Bug Fixes -* **server:** fixed logger problem ([48fadbe](https://github.com/ohbug-org/ohbug-dashboard/commit/48fadbe6f7b3db62eb07e955df2d454820bdb47d)) - +- **server:** fixed logger problem ([48fadbe](https://github.com/ohbug-org/ohbug-dashboard/commit/48fadbe6f7b3db62eb07e955df2d454820bdb47d)) ### Features -* add user page ([573ea2d](https://github.com/ohbug-org/ohbug-dashboard/commit/573ea2d96e5e0a276ab4a77e325279e50c8e02b9)) +- add user page ([573ea2d](https://github.com/ohbug-org/ohbug-dashboard/commit/573ea2d96e5e0a276ab4a77e325279e50c8e02b9)) # [0.5.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.4.1...0.5.0) (2022-08-30) - ### Bug Fixes -* **server:** remove cluster mode ([79ee80e](https://github.com/ohbug-org/ohbug-dashboard/commit/79ee80e153396fadf47817fe7c6270f6d14e25de)) -* **web:** error stack visible problem ([f820135](https://github.com/ohbug-org/ohbug-dashboard/commit/f820135533c00dcd93976451611b808f020923cd)) -* **web:** fix copy error ([c5d1340](https://github.com/ohbug-org/ohbug-dashboard/commit/c5d1340dbb7368cfaa639a193de8e7df52e2b0ce)) - +- **server:** remove cluster mode ([79ee80e](https://github.com/ohbug-org/ohbug-dashboard/commit/79ee80e153396fadf47817fe7c6270f6d14e25de)) +- **web:** error stack visible problem ([f820135](https://github.com/ohbug-org/ohbug-dashboard/commit/f820135533c00dcd93976451611b808f020923cd)) +- **web:** fix copy error ([c5d1340](https://github.com/ohbug-org/ohbug-dashboard/commit/c5d1340dbb7368cfaa639a193de8e7df52e2b0ce)) ### Features -* **server:** delete expiration data regularly every day ([ffc77e6](https://github.com/ohbug-org/ohbug-dashboard/commit/ffc77e68a3079574181132f57e334c6bb466207b)) -* **server:** update http error group fields ([e324e5c](https://github.com/ohbug-org/ohbug-dashboard/commit/e324e5cd23146f3890e79cb653142e04cf3d3d66)) -* **server:** use nestjs-pino ([f8d8fb9](https://github.com/ohbug-org/ohbug-dashboard/commit/f8d8fb9e9624036b23a77659d81409ffec9fb6ae)) -* **web:** issue support order ([e452838](https://github.com/ohbug-org/ohbug-dashboard/commit/e452838e68ef9fc1f2ed9b97513cdd030d808da5)) -* **web:** nextLink passHref ([8666d51](https://github.com/ohbug-org/ohbug-dashboard/commit/8666d5187b75d23b21a66031e4c1032156f75a79)) -* **web:** order findMany results ([562a040](https://github.com/ohbug-org/ohbug-dashboard/commit/562a040107fd5107d5b038ba1f346d8a1ae85aea)) +- **server:** delete expiration data regularly every day ([ffc77e6](https://github.com/ohbug-org/ohbug-dashboard/commit/ffc77e68a3079574181132f57e334c6bb466207b)) +- **server:** update http error group fields ([e324e5c](https://github.com/ohbug-org/ohbug-dashboard/commit/e324e5cd23146f3890e79cb653142e04cf3d3d66)) +- **server:** use nestjs-pino ([f8d8fb9](https://github.com/ohbug-org/ohbug-dashboard/commit/f8d8fb9e9624036b23a77659d81409ffec9fb6ae)) +- **web:** issue support order ([e452838](https://github.com/ohbug-org/ohbug-dashboard/commit/e452838e68ef9fc1f2ed9b97513cdd030d808da5)) +- **web:** nextLink passHref ([8666d51](https://github.com/ohbug-org/ohbug-dashboard/commit/8666d5187b75d23b21a66031e4c1032156f75a79)) +- **web:** order findMany results ([562a040](https://github.com/ohbug-org/ohbug-dashboard/commit/562a040107fd5107d5b038ba1f346d8a1ae85aea)) ## [0.4.1](https://github.com/ohbug-org/ohbug-dashboard/compare/0.4.0...0.4.1) (2022-08-26) - ### Bug Fixes -* **server:** fix server too large body error ([835d493](https://github.com/ohbug-org/ohbug-dashboard/commit/835d49396298494209f8ecd22aa34078103f5c44)) +- **server:** fix server too large body error ([835d493](https://github.com/ohbug-org/ohbug-dashboard/commit/835d49396298494209f8ecd22aa34078103f5c44)) # [0.4.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.3.2...0.4.0) (2022-08-24) - ### Features -* **server:** report api return httpCode 204 ([cdd7b66](https://github.com/ohbug-org/ohbug-dashboard/commit/cdd7b662e2ca940041d7595fa66bae38d95d9de9)) -* **server:** support cluster mode ([cb06054](https://github.com/ohbug-org/ohbug-dashboard/commit/cb06054e835b0641fed967b11e99edaafafc966d)) -* **web:** hide project profile pv data ([e916e49](https://github.com/ohbug-org/ohbug-dashboard/commit/e916e49ea3c47224b7849a2856e0a4ee4b1ddced)) -* **web:** project profile complete i18n ([b162f09](https://github.com/ohbug-org/ohbug-dashboard/commit/b162f09a68525c7a674d9e213ff064386d23b445)) -* **web:** project profile support view path and referrer group data ([bac11ff](https://github.com/ohbug-org/ohbug-dashboard/commit/bac11ff177273b30237d44852f7b44253e2f5569)) -* **web:** support credentials signin ([c2465c8](https://github.com/ohbug-org/ohbug-dashboard/commit/c2465c88e3b8bb36c3d3ae5c9a5ba2ae259eb9ce)) +- **server:** report api return httpCode 204 ([cdd7b66](https://github.com/ohbug-org/ohbug-dashboard/commit/cdd7b662e2ca940041d7595fa66bae38d95d9de9)) +- **server:** support cluster mode ([cb06054](https://github.com/ohbug-org/ohbug-dashboard/commit/cb06054e835b0641fed967b11e99edaafafc966d)) +- **web:** hide project profile pv data ([e916e49](https://github.com/ohbug-org/ohbug-dashboard/commit/e916e49ea3c47224b7849a2856e0a4ee4b1ddced)) +- **web:** project profile complete i18n ([b162f09](https://github.com/ohbug-org/ohbug-dashboard/commit/b162f09a68525c7a674d9e213ff064386d23b445)) +- **web:** project profile support view path and referrer group data ([bac11ff](https://github.com/ohbug-org/ohbug-dashboard/commit/bac11ff177273b30237d44852f7b44253e2f5569)) +- **web:** support credentials signin ([c2465c8](https://github.com/ohbug-org/ohbug-dashboard/commit/c2465c88e3b8bb36c3d3ae5c9a5ba2ae259eb9ce)) ## [0.3.2](https://github.com/ohbug-org/ohbug-dashboard/compare/0.3.1...0.3.2) (2022-08-19) ## [0.3.1](https://github.com/ohbug-org/ohbug-dashboard/compare/0.3.0...0.3.1) (2022-08-18) - ### Bug Fixes -* **web:** user error ([86409f0](https://github.com/ohbug-org/ohbug-dashboard/commit/86409f0d81eefbee15b2c440f5f133b230eaf35e)) +- **web:** user error ([86409f0](https://github.com/ohbug-org/ohbug-dashboard/commit/86409f0d81eefbee15b2c440f5f133b230eaf35e)) # [0.3.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.2.0...0.3.0) (2022-08-18) - ### Features -* **prisma:** drop useless column ([49e586c](https://github.com/ohbug-org/ohbug-dashboard/commit/49e586cf42236a565cfc339effc1d5f20d971c5b)) - +- **prisma:** drop useless column ([49e586c](https://github.com/ohbug-org/ohbug-dashboard/commit/49e586cf42236a565cfc339effc1d5f20d971c5b)) ### Performance Improvements -* change user table ([c697710](https://github.com/ohbug-org/ohbug-dashboard/commit/c6977101e699aadf0d25a69ace339a577cb28b7a)) +- change user table ([c697710](https://github.com/ohbug-org/ohbug-dashboard/commit/c6977101e699aadf0d25a69ace339a577cb28b7a)) # [0.2.0](https://github.com/ohbug-org/ohbug-dashboard/compare/0.1.3...0.2.0) (2022-08-17) - ### Bug Fixes -* **web:** eslint warning ([64881ef](https://github.com/ohbug-org/ohbug-dashboard/commit/64881ef14ba659cdc4262b9dc35dd63f4b14be3f)) -* **web:** reload data after deps changed ([f4d7422](https://github.com/ohbug-org/ohbug-dashboard/commit/f4d742207f4918bc72bcc3b2d6dc313cdfc14b22)) -* **web:** trendChart render ([f8a6c78](https://github.com/ohbug-org/ohbug-dashboard/commit/f8a6c785a0068b1242fb7cd87138f297ebfd2c72)) - +- **web:** eslint warning ([64881ef](https://github.com/ohbug-org/ohbug-dashboard/commit/64881ef14ba659cdc4262b9dc35dd63f4b14be3f)) +- **web:** reload data after deps changed ([f4d7422](https://github.com/ohbug-org/ohbug-dashboard/commit/f4d742207f4918bc72bcc3b2d6dc313cdfc14b22)) +- **web:** trendChart render ([f8a6c78](https://github.com/ohbug-org/ohbug-dashboard/commit/f8a6c785a0068b1242fb7cd87138f297ebfd2c72)) ### Features -* **server:** store view data ([5a44d56](https://github.com/ohbug-org/ohbug-dashboard/commit/5a44d56a039b55255bb499626927d47bfcc0df32)) -* **web:** add integration button ([76d54a6](https://github.com/ohbug-org/ohbug-dashboard/commit/76d54a60c2c612201ceefb81c0e4257cc9e8c87e)) -* **web:** only error event support check raw json data ([12bad61](https://github.com/ohbug-org/ohbug-dashboard/commit/12bad61446fe782e8f0a2529a1d0cd1fa1779db3)) -* **web:** simplify API Route ([82994d5](https://github.com/ohbug-org/ohbug-dashboard/commit/82994d5f29dd2fb9b837bd3d558429e1f7eaaecc)) -* **web:** simplify trends api ([b646a75](https://github.com/ohbug-org/ohbug-dashboard/commit/b646a754ee0bd19d85851fd761e50f2f3bac9894)) -* **web:** support check feedback detail ([de13466](https://github.com/ohbug-org/ohbug-dashboard/commit/de13466c94e6d75a60eea281d2bd68efdff34607)) -* **web:** use nprogress ([3e63544](https://github.com/ohbug-org/ohbug-dashboard/commit/3e635445c8177b933f83003f29448526f943a924)) +- **server:** store view data ([5a44d56](https://github.com/ohbug-org/ohbug-dashboard/commit/5a44d56a039b55255bb499626927d47bfcc0df32)) +- **web:** add integration button ([76d54a6](https://github.com/ohbug-org/ohbug-dashboard/commit/76d54a60c2c612201ceefb81c0e4257cc9e8c87e)) +- **web:** only error event support check raw json data ([12bad61](https://github.com/ohbug-org/ohbug-dashboard/commit/12bad61446fe782e8f0a2529a1d0cd1fa1779db3)) +- **web:** simplify API Route ([82994d5](https://github.com/ohbug-org/ohbug-dashboard/commit/82994d5f29dd2fb9b837bd3d558429e1f7eaaecc)) +- **web:** simplify trends api ([b646a75](https://github.com/ohbug-org/ohbug-dashboard/commit/b646a754ee0bd19d85851fd761e50f2f3bac9894)) +- **web:** support check feedback detail ([de13466](https://github.com/ohbug-org/ohbug-dashboard/commit/de13466c94e6d75a60eea281d2bd68efdff34607)) +- **web:** use nprogress ([3e63544](https://github.com/ohbug-org/ohbug-dashboard/commit/3e635445c8177b933f83003f29448526f943a924)) ## [0.1.3](https://github.com/ohbug-org/ohbug-dashboard/compare/0.1.2...0.1.3) (2022-08-09) - ### Bug Fixes -* **web:** next-auth warning ([cb721d2](https://github.com/ohbug-org/ohbug-dashboard/commit/cb721d20ede836a2b7952e3fff4d5d28a23f1ca9)) +- **web:** next-auth warning ([cb721d2](https://github.com/ohbug-org/ohbug-dashboard/commit/cb721d20ede836a2b7952e3fff4d5d28a23f1ca9)) ## [0.1.2](https://github.com/ohbug-org/ohbug-dashboard/compare/0.1.1...0.1.2) (2022-08-09) - ### Bug Fixes -* **web:** nextjs middleware position error ([74f3bbf](https://github.com/ohbug-org/ohbug-dashboard/commit/74f3bbf219ca9927b37947608bab2d5cded39bb9)) +- **web:** nextjs middleware position error ([74f3bbf](https://github.com/ohbug-org/ohbug-dashboard/commit/74f3bbf219ca9927b37947608bab2d5cded39bb9)) ## [0.1.1](https://github.com/ohbug-org/ohbug-dashboard/compare/0.1.0...0.1.1) (2022-08-09) - ### Bug Fixes -* **web:** ts error ([46e6ed4](https://github.com/ohbug-org/ohbug-dashboard/commit/46e6ed474338805dcb542a355112a49b2ec6e1f6)) +- **web:** ts error ([46e6ed4](https://github.com/ohbug-org/ohbug-dashboard/commit/46e6ed474338805dcb542a355112a49b2ec6e1f6)) # 0.1.0 (2022-08-09) - ### Bug Fixes -* **docker:** docker-compose 环境变量从 .env 读取 ([c2a312b](https://github.com/ohbug-org/ohbug-dashboard/commit/c2a312b8deb9e7057abc774ebeaa8436485b3441)) -* **docker:** 修复 docker arm 架构构建报错 ([d6e7aea](https://github.com/ohbug-org/ohbug-dashboard/commit/d6e7aea47597a3364163e764041aff1eb4ed2447)) -* **docker:** 调整 dockerfile ([6ac4ca8](https://github.com/ohbug-org/ohbug-dashboard/commit/6ac4ca8d7fbf39481b52e58042416810a8ec5320)) -* fixed dockerfile ([7d42019](https://github.com/ohbug-org/ohbug-dashboard/commit/7d4201956825f35806e36a195e46314f8a02266e)) -* **prisma:** 修复 createdAt 数据类型 ([bb7249e](https://github.com/ohbug-org/ohbug-dashboard/commit/bb7249efd002aa2127fdc7081df85b1208e980e9)) -* **server:** 修复 alert interval 生效逻辑问题 ([7e2036a](https://github.com/ohbug-org/ohbug-dashboard/commit/7e2036a64c08a29eff1f6d1f759207a426118258)) -* **web:** get issues trends error ([c995642](https://github.com/ohbug-org/ohbug-dashboard/commit/c995642d8396fc035db19ba3d0bc6583e61cf4fb)) -* **web:** menu list zindex 调整 ([65883f3](https://github.com/ohbug-org/ohbug-dashboard/commit/65883f3f8fe8e828a64fe25f1782d33f25837141)) -* **web:** ohbug.config 加入 url 配置 ([d5cc2e2](https://github.com/ohbug-org/ohbug-dashboard/commit/d5cc2e2531e622fa5da475058889b90487839425)) -* **web:** project profile 默认跳转 ([9912b82](https://github.com/ohbug-org/ohbug-dashboard/commit/9912b822e2eb091273a42cdfb774bd8b97bfa224)) -* **web:** 修复 event 创建 issue updatedAt 不更新 ([cf3b003](https://github.com/ohbug-org/ohbug-dashboard/commit/cf3b00380258761e147d68cb83cb63b17a33f72a)) -* **web:** 修复 nextjs warning ([3d9fa88](https://github.com/ohbug-org/ohbug-dashboard/commit/3d9fa8833b048ec4f77413bb9101d7e434c98196)) -* **web:** 修正 ip 地址显示 ([3308ce8](https://github.com/ohbug-org/ohbug-dashboard/commit/3308ce841b209329c305fe8937e48adc2385a8f6)) - +- **docker:** docker-compose 环境变量从 .env 读取 ([c2a312b](https://github.com/ohbug-org/ohbug-dashboard/commit/c2a312b8deb9e7057abc774ebeaa8436485b3441)) +- **docker:** 修复 docker arm 架构构建报错 ([d6e7aea](https://github.com/ohbug-org/ohbug-dashboard/commit/d6e7aea47597a3364163e764041aff1eb4ed2447)) +- **docker:** 调整 dockerfile ([6ac4ca8](https://github.com/ohbug-org/ohbug-dashboard/commit/6ac4ca8d7fbf39481b52e58042416810a8ec5320)) +- fixed dockerfile ([7d42019](https://github.com/ohbug-org/ohbug-dashboard/commit/7d4201956825f35806e36a195e46314f8a02266e)) +- **prisma:** 修复 createdAt 数据类型 ([bb7249e](https://github.com/ohbug-org/ohbug-dashboard/commit/bb7249efd002aa2127fdc7081df85b1208e980e9)) +- **server:** 修复 alert interval 生效逻辑问题 ([7e2036a](https://github.com/ohbug-org/ohbug-dashboard/commit/7e2036a64c08a29eff1f6d1f759207a426118258)) +- **web:** get issues trends error ([c995642](https://github.com/ohbug-org/ohbug-dashboard/commit/c995642d8396fc035db19ba3d0bc6583e61cf4fb)) +- **web:** menu list zindex 调整 ([65883f3](https://github.com/ohbug-org/ohbug-dashboard/commit/65883f3f8fe8e828a64fe25f1782d33f25837141)) +- **web:** ohbug.config 加入 url 配置 ([d5cc2e2](https://github.com/ohbug-org/ohbug-dashboard/commit/d5cc2e2531e622fa5da475058889b90487839425)) +- **web:** project profile 默认跳转 ([9912b82](https://github.com/ohbug-org/ohbug-dashboard/commit/9912b822e2eb091273a42cdfb774bd8b97bfa224)) +- **web:** 修复 event 创建 issue updatedAt 不更新 ([cf3b003](https://github.com/ohbug-org/ohbug-dashboard/commit/cf3b00380258761e147d68cb83cb63b17a33f72a)) +- **web:** 修复 nextjs warning ([3d9fa88](https://github.com/ohbug-org/ohbug-dashboard/commit/3d9fa8833b048ec4f77413bb9101d7e434c98196)) +- **web:** 修正 ip 地址显示 ([3308ce8](https://github.com/ohbug-org/ohbug-dashboard/commit/3308ce841b209329c305fe8937e48adc2385a8f6)) ### Features -* add package server ([9cd7e14](https://github.com/ohbug-org/ohbug-dashboard/commit/9cd7e14b940016f2058f3145cf3c1c671b3a87e2)) -* **docker:** complete docker deployment ([4752bbe](https://github.com/ohbug-org/ohbug-dashboard/commit/4752bbe365a4cafa022ee0a4f325e37e2c6ead17)) -* **docker:** use default node image ([c41c960](https://github.com/ohbug-org/ohbug-dashboard/commit/c41c960db1fffb091fc034b3e2bb478d19a68b3f)) -* **docker:** 调整启动方式 使用 yml 配置文件 ([1122cb8](https://github.com/ohbug-org/ohbug-dashboard/commit/1122cb874c0acfc1bd4427077c20b0992d70a778)) -* init ([ceaad0b](https://github.com/ohbug-org/ohbug-dashboard/commit/ceaad0b0d6d64edf06dff583cdd63fc44b57805a)) -* **prisma:** create index ([fb75c8d](https://github.com/ohbug-org/ohbug-dashboard/commit/fb75c8d779cd9614b5db84a69a0c8e013ecea8c6)) -* remix 迁移至 next ([3fcf53a](https://github.com/ohbug-org/ohbug-dashboard/commit/3fcf53acfd37faf899b5efb6245f56da92cdb873)) -* **server:** 完成sourceMap上传 ([a0b6f83](https://github.com/ohbug-org/ohbug-dashboard/commit/a0b6f83338bd41fcbc5f76b87ba8322762e261da)) -* **server:** event 上报加入 apiKey 校验 ([223d50b](https://github.com/ohbug-org/ohbug-dashboard/commit/223d50bbfdd1fa4a934d78a7cc0a7c4b3818a5d6)) -* **server:** store metric data ([9a73089](https://github.com/ohbug-org/ohbug-dashboard/commit/9a73089c100a041047b4bc017477e25cb08637d5)) -* **server:** 使用 postgresql 存储 event ([93cd770](https://github.com/ohbug-org/ohbug-dashboard/commit/93cd77068404540167664a82949f14b749f347d5)) -* **server:** 完成 event/issue 存储 ([b856d6d](https://github.com/ohbug-org/ohbug-dashboard/commit/b856d6d9bc7ab9c1ec8b5c982b0a1a1707f92346)) -* **server:** 完成 event/issue/user 表 ([7fe6b2a](https://github.com/ohbug-org/ohbug-dashboard/commit/7fe6b2a22ac4e3824b2ad91cd2148714e7792c40)) -* **server:** 对接 clickhouse ([da409af](https://github.com/ohbug-org/ohbug-dashboard/commit/da409afd985c3346c2c17d696b6e73da9583092f)) -* **server:** 简化 config 获取流程 ([8b568cd](https://github.com/ohbug-org/ohbug-dashboard/commit/8b568cdd758bb120fb89b79d5bf76d434e7d8789)) -* support report and preview feedback ([a8e5114](https://github.com/ohbug-org/ohbug-dashboard/commit/a8e511483b6c740b639981e4596372b2867da845)) -* update deps and adapter prisma4 ([c670382](https://github.com/ohbug-org/ohbug-dashboard/commit/c670382820740ccb9b2b3dc7bf049c34f4c96e09)) -* **web:** add empty issues view ([6e1bfd7](https://github.com/ohbug-org/ohbug-dashboard/commit/6e1bfd76c2e5705c7e4e4cca6dddbfe110fd70df)) -* **web:** add get apikey tips ([b058670](https://github.com/ohbug-org/ohbug-dashboard/commit/b058670994354aaeae0045d00cb4963d98288428)) -* **web:** add metrics integration guide ([a6ba9f2](https://github.com/ohbug-org/ohbug-dashboard/commit/a6ba9f2d5c9e98a862e8c1eb610ef554d1f81515)) -* **web:** add nextauth middleware ([c5bef8d](https://github.com/ohbug-org/ohbug-dashboard/commit/c5bef8d7e2db30db73efab954214d2b27a83296b)) -* **web:** add title to metric page ([2e3ab1c](https://github.com/ohbug-org/ohbug-dashboard/commit/2e3ab1c0882340745516ecb4005ebf7e8c3fdc88)) -* **web:** card 组件 hover default false ([a364944](https://github.com/ohbug-org/ohbug-dashboard/commit/a364944ff7b323599df6c69674bffdb2d1d8f2da)) -* **web:** dashboard support metrics ([a30953e](https://github.com/ohbug-org/ohbug-dashboard/commit/a30953e85f0150b4ba28788fdae57dc2b1d4f249)) -* **web:** finish dark mode ([55186c4](https://github.com/ohbug-org/ohbug-dashboard/commit/55186c4321f80db13214b302f5917d5defe7f966)) -* **web:** finish invite members ([9442246](https://github.com/ohbug-org/ohbug-dashboard/commit/94422469b8ad67bb1c29374b6c458dae0a509f4e)) -* **web:** finish issues search ([bffc457](https://github.com/ohbug-org/ohbug-dashboard/commit/bffc45774ffb8278c25a27d780c9fbdf5a054070)) -* **web:** index 增加 create project 按钮 ([c762ab3](https://github.com/ohbug-org/ohbug-dashboard/commit/c762ab3e31f717ba4ef9be528388d0d16e608b2c)) -* **web:** issue detail trends 样式调整 ([c3a6c69](https://github.com/ohbug-org/ohbug-dashboard/commit/c3a6c697942c9ec799992d109d3ec9ed42d19d75)) -* **web:** issue detail 页面加入 breadcrumb ([22e061e](https://github.com/ohbug-org/ohbug-dashboard/commit/22e061ec318e7d3d2d9ad550ec0fb53578c4ab2e)) -* **web:** issue detail 页面样式调整 ([f934f78](https://github.com/ohbug-org/ohbug-dashboard/commit/f934f7839c9358acd8af4402b3b355fffb0885f9)) -* **web:** issue events 页面样式调整 ([8a35b52](https://github.com/ohbug-org/ohbug-dashboard/commit/8a35b5220f9f9317bf68ab8dd06f48d8ddd83f78)) -* **web:** issues 列表展示 events users 数量 ([0bfd469](https://github.com/ohbug-org/ohbug-dashboard/commit/0bfd469cbf56a5623d38b425ec882285e2cae46b)) -* **web:** navMenu 加入 settings 路由 ([590fed3](https://github.com/ohbug-org/ohbug-dashboard/commit/590fed38e6d1fdea142767bde6bf56d970c39be3)) -* **web:** project 切换同时切换当前路由 ([851ef66](https://github.com/ohbug-org/ohbug-dashboard/commit/851ef66ed5eaefc59358fcc771f4f85907b8105d)) -* **web:** projects 根据 用户查询 ([9700c35](https://github.com/ohbug-org/ohbug-dashboard/commit/9700c357effd58afa766edf59b97840dccf43140)) -* **web:** projects 页面查询 project trends ([6574676](https://github.com/ohbug-org/ohbug-dashboard/commit/6574676c8ac81dbdc4c80072269f1f3da5a64593)) -* **web:** read config from ohbug.config.yml ([dc1610f](https://github.com/ohbug-org/ohbug-dashboard/commit/dc1610f5aa2bf203311fb87b51671d83c356e293)) -* **web:** redirect create-project if user not has project ([e594318](https://github.com/ohbug-org/ohbug-dashboard/commit/e5943189642078f1eb1f50b798c16122d842e7ee)) -* **web:** redirect index if already signin ([0fd6708](https://github.com/ohbug-org/ohbug-dashboard/commit/0fd670839df0fbf857fb8fababf35d8c39d72a46)) -* **web:** redirect to signin when not have authorization ([e7a770d](https://github.com/ohbug-org/ohbug-dashboard/commit/e7a770dc4416e7f55068f27aa36334dbcda9e4be)) -* **web:** scroll navmenu logo click scroll to top ([1997561](https://github.com/ohbug-org/ohbug-dashboard/commit/19975611ffc5552032b84bf9a61e2928836e9e72)) -* **web:** support delete alert ([67d897a](https://github.com/ohbug-org/ohbug-dashboard/commit/67d897a02130c97c3ba258eb64c7c17340ac2a67)) -* **web:** support i18n ([01cde98](https://github.com/ohbug-org/ohbug-dashboard/commit/01cde984a573a7b6c9255fd88f265aa25916ca89)) -* **web:** support preview custom metadata ([ba4249e](https://github.com/ohbug-org/ohbug-dashboard/commit/ba4249e0d3de6188e656b14af6436cbb81771d9d)) -* **web:** support version detection and upgrade prompt ([d31b56e](https://github.com/ohbug-org/ohbug-dashboard/commit/d31b56ee85a9122893a4f5a2bd27077a38cd7268)) -* **web:** themeBox 加入 acrylic props ([5f68b96](https://github.com/ohbug-org/ohbug-dashboard/commit/5f68b967006d26f5a1200f0c05761999f59d2c09)) -* **web:** trendChart 展示 loading 状态 ([bbc1de5](https://github.com/ohbug-org/ohbug-dashboard/commit/bbc1de525a02f1073628144f7f2c3d57eff59425)) -* **web:** 主题样式微调 ([40fa5df](https://github.com/ohbug-org/ohbug-dashboard/commit/40fa5dfb4b9ad529e0719cacdf2a3792bf49b270)) -* **web:** 优化 issue detail 页面 减少非必要查询 ([786532e](https://github.com/ohbug-org/ohbug-dashboard/commit/786532e810f28309d4ba38a565eacd0b9de8ecfe)) -* **web:** 优化 issueList 样式 ([ae787c6](https://github.com/ohbug-org/ohbug-dashboard/commit/ae787c6af3111f15b12635f9636c0da138c60945)) -* **web:** 使用 next auth 登录页面 ([9e0ce8c](https://github.com/ohbug-org/ohbug-dashboard/commit/9e0ce8c2bb6a52bd6a99bb0705ecc99e8b6a5961)) -* **web:** 使用 react-icons 作为图标库 ([a613654](https://github.com/ohbug-org/ohbug-dashboard/commit/a613654b0a0949f25ecb36d800de69a77ec1b08b)) -* **web:** 加入 projects 页面 ([9cc87ea](https://github.com/ohbug-org/ohbug-dashboard/commit/9cc87ead269a080e6140761732a1e0bdad6aab4e)) -* **web:** 加入 searchBar 组件 ([df71220](https://github.com/ohbug-org/ohbug-dashboard/commit/df712201072141ab55a98761a3891ebbbca1d7b7)) -* **web:** 加入权限验证 ([fd62e15](https://github.com/ohbug-org/ohbug-dashboard/commit/fd62e1559e703cc26dd3adbb74e54e7d5f9af828)) -* **web:** 完善 project card 数据展示 ([032d2bc](https://github.com/ohbug-org/ohbug-dashboard/commit/032d2bc7440485c7783367d46ad31a78cf1d8823)) -* **web:** 完成 footer ([0abe255](https://github.com/ohbug-org/ohbug-dashboard/commit/0abe255de49046cbf023fae0c1a9229e8995721f)) -* **web:** 完成 issue detail 更多事件页面 ([e248a90](https://github.com/ohbug-org/ohbug-dashboard/commit/e248a90b02326da7d1b91f0db09fa32331243442)) -* **web:** 完成 issue detail 页面 堆栈信息展示 ([666a341](https://github.com/ohbug-org/ohbug-dashboard/commit/666a34157eb88c26800b068af8e9011867cde425)) -* **web:** 完成 issue detail 页面标签列展示 ([534d0f0](https://github.com/ohbug-org/ohbug-dashboard/commit/534d0f0f6c9c1d4e1a7d541c3da6d5d61fe004c2)) -* **web:** 完成 issue detail 页面趋势信息展示 ([d913d3e](https://github.com/ohbug-org/ohbug-dashboard/commit/d913d3e175441d7ffbef858470f34dd3e99b744b)) -* **web:** 完成 issue list 分页 ([fb502de](https://github.com/ohbug-org/ohbug-dashboard/commit/fb502de812eed29a84fe1b34e7648e1391f17dbd)) -* **web:** 完成 issue trends 数据查询与展示 ([42d55e9](https://github.com/ohbug-org/ohbug-dashboard/commit/42d55e9a40cb8b1276d3bd338ace8cf66a213149)) -* **web:** 完成 navMenu 部分 ([779399d](https://github.com/ohbug-org/ohbug-dashboard/commit/779399d5348b3308559e1f876c7c6c6fbbd750a4)) -* **web:** 完成 project setting apiKey 展示 ([a5e2cf8](https://github.com/ohbug-org/ohbug-dashboard/commit/a5e2cf8f5137769238e3493f1933b16f6b277c99)) -* **web:** 完成 project setting update name ([cdad8bc](https://github.com/ohbug-org/ohbug-dashboard/commit/cdad8bcb688f8b02b8c9c6c05816df0aaa3331bd)) -* **web:** 完成 project trends chart 显示 ([73c263f](https://github.com/ohbug-org/ohbug-dashboard/commit/73c263f6c3c7aedda41ee4f32332a38bb14b8217)) -* **web:** 完成 project 的创建 ([9765988](https://github.com/ohbug-org/ohbug-dashboard/commit/9765988b2a068ed03fa6470f4977c0df5b28baa5)) -* **web:** 完成 project 预览页面 ([ca5900f](https://github.com/ohbug-org/ohbug-dashboard/commit/ca5900f6f3b908d02cc9a1a15bb00ef60161ff7b)) -* **web:** 完成 release list 和 sourceMaps 页面 ([ad881e0](https://github.com/ohbug-org/ohbug-dashboard/commit/ad881e0a82f6ea653e6c117c5c3acdcc9ce410c5)) -* **web:** 完成 user 组件以及 nav 基本布局 ([93f066f](https://github.com/ohbug-org/ohbug-dashboard/commit/93f066f32f7a78471c79b94bb7a879ee341bb9a4)) -* **web:** 完成layout布局 包含 nav navMenu user 等组件 ([353466a](https://github.com/ohbug-org/ohbug-dashboard/commit/353466a7b344edd03f02c9d72985e0c79d9503e1)) -* **web:** 完成授权登录流程 ([d26286d](https://github.com/ohbug-org/ohbug-dashboard/commit/d26286d575a93469ccd6e3cb6327590d95b5a5a0)) -* **web:** 对接 ohbug-extension-rrweb ([6a63f7f](https://github.com/ohbug-org/ohbug-dashboard/commit/6a63f7f56463765cf5e31d2e1fd2fd17cf998bca)) -* **web:** 整体迁移至 chakra-ui ([8a2ff2c](https://github.com/ohbug-org/ohbug-dashboard/commit/8a2ff2c2e5b7a0302713d98c2963b47c4ae5c154)) -* **web:** 新增 alert detail 页面 ([036eff0](https://github.com/ohbug-org/ohbug-dashboard/commit/036eff01b39942b7bec74578641eb86ef0a2be79)) -* **web:** 新增 issues 页面 ([4c4c157](https://github.com/ohbug-org/ohbug-dashboard/commit/4c4c157adf71082ad9786c9f9285a37a5c28e174)) -* **web:** 暂时删除 release detail 页 ([60079f4](https://github.com/ohbug-org/ohbug-dashboard/commit/60079f41d4ad160b0547872174a3b7f4553fced1)) -* **web:** 调整 issue tabs 样式 ([52fc349](https://github.com/ohbug-org/ohbug-dashboard/commit/52fc3499e03b4752d566da4d12b0c5f5c81bcf8f)) -* **web:** 调整 prisma client 获取方式 ([5d7f293](https://github.com/ohbug-org/ohbug-dashboard/commit/5d7f293a99da8ec9df780675858a9a81f057ca41)) -* **web:** 部分页面布局更新 使用 wrapper ([c295cf6](https://github.com/ohbug-org/ohbug-dashboard/commit/c295cf6baaf10f5f6eee28b3e8f1b300f877252e)) -* 准备 issue 趋势图数据 ([d0f8c35](https://github.com/ohbug-org/ohbug-dashboard/commit/d0f8c35f5231f96dd41f73c659b219b7139548ad)) -* 加入 issue 详情页 ([326939a](https://github.com/ohbug-org/ohbug-dashboard/commit/326939ad6f5675d3dedd6e42772afb9368377da0)) -* 完成 issues 列表基本展示 ([b5dc389](https://github.com/ohbug-org/ohbug-dashboard/commit/b5dc389470e9b92dc77ee02c618feb04ab29b9c4)) -* 完成 sourceMap 还原代码 ([8c4bbbd](https://github.com/ohbug-org/ohbug-dashboard/commit/8c4bbbd43fa3009b733043cb2c4c3a508d88681a)) -* 完成基本环境配置、环境变量配置 ([3e36937](https://github.com/ohbug-org/ohbug-dashboard/commit/3e369373fde805ce760ff8837de63f81a402fc37)) -* 完成通知逻辑 ([d4f5a93](https://github.com/ohbug-org/ohbug-dashboard/commit/d4f5a9309d821b7edf83981ae525f2ca7ddc1d80)) -* 新增 email 登录方式 ([e713ca9](https://github.com/ohbug-org/ohbug-dashboard/commit/e713ca9c51d7e607b30ccd8fec15bb2bcb7ead68)) -* 路由调整为以 project 为基础 ([db3d9f2](https://github.com/ohbug-org/ohbug-dashboard/commit/db3d9f28ecf518496ba532d76c33a67821f98be1)) - +- add package server ([9cd7e14](https://github.com/ohbug-org/ohbug-dashboard/commit/9cd7e14b940016f2058f3145cf3c1c671b3a87e2)) +- **docker:** complete docker deployment ([4752bbe](https://github.com/ohbug-org/ohbug-dashboard/commit/4752bbe365a4cafa022ee0a4f325e37e2c6ead17)) +- **docker:** use default node image ([c41c960](https://github.com/ohbug-org/ohbug-dashboard/commit/c41c960db1fffb091fc034b3e2bb478d19a68b3f)) +- **docker:** 调整启动方式 使用 yml 配置文件 ([1122cb8](https://github.com/ohbug-org/ohbug-dashboard/commit/1122cb874c0acfc1bd4427077c20b0992d70a778)) +- init ([ceaad0b](https://github.com/ohbug-org/ohbug-dashboard/commit/ceaad0b0d6d64edf06dff583cdd63fc44b57805a)) +- **prisma:** create index ([fb75c8d](https://github.com/ohbug-org/ohbug-dashboard/commit/fb75c8d779cd9614b5db84a69a0c8e013ecea8c6)) +- remix 迁移至 next ([3fcf53a](https://github.com/ohbug-org/ohbug-dashboard/commit/3fcf53acfd37faf899b5efb6245f56da92cdb873)) +- **server:** 完成sourceMap上传 ([a0b6f83](https://github.com/ohbug-org/ohbug-dashboard/commit/a0b6f83338bd41fcbc5f76b87ba8322762e261da)) +- **server:** event 上报加入 apiKey 校验 ([223d50b](https://github.com/ohbug-org/ohbug-dashboard/commit/223d50bbfdd1fa4a934d78a7cc0a7c4b3818a5d6)) +- **server:** store metric data ([9a73089](https://github.com/ohbug-org/ohbug-dashboard/commit/9a73089c100a041047b4bc017477e25cb08637d5)) +- **server:** 使用 postgresql 存储 event ([93cd770](https://github.com/ohbug-org/ohbug-dashboard/commit/93cd77068404540167664a82949f14b749f347d5)) +- **server:** 完成 event/issue 存储 ([b856d6d](https://github.com/ohbug-org/ohbug-dashboard/commit/b856d6d9bc7ab9c1ec8b5c982b0a1a1707f92346)) +- **server:** 完成 event/issue/user 表 ([7fe6b2a](https://github.com/ohbug-org/ohbug-dashboard/commit/7fe6b2a22ac4e3824b2ad91cd2148714e7792c40)) +- **server:** 对接 clickhouse ([da409af](https://github.com/ohbug-org/ohbug-dashboard/commit/da409afd985c3346c2c17d696b6e73da9583092f)) +- **server:** 简化 config 获取流程 ([8b568cd](https://github.com/ohbug-org/ohbug-dashboard/commit/8b568cdd758bb120fb89b79d5bf76d434e7d8789)) +- support report and preview feedback ([a8e5114](https://github.com/ohbug-org/ohbug-dashboard/commit/a8e511483b6c740b639981e4596372b2867da845)) +- update deps and adapter prisma4 ([c670382](https://github.com/ohbug-org/ohbug-dashboard/commit/c670382820740ccb9b2b3dc7bf049c34f4c96e09)) +- **web:** add empty issues view ([6e1bfd7](https://github.com/ohbug-org/ohbug-dashboard/commit/6e1bfd76c2e5705c7e4e4cca6dddbfe110fd70df)) +- **web:** add get apikey tips ([b058670](https://github.com/ohbug-org/ohbug-dashboard/commit/b058670994354aaeae0045d00cb4963d98288428)) +- **web:** add metrics integration guide ([a6ba9f2](https://github.com/ohbug-org/ohbug-dashboard/commit/a6ba9f2d5c9e98a862e8c1eb610ef554d1f81515)) +- **web:** add nextauth middleware ([c5bef8d](https://github.com/ohbug-org/ohbug-dashboard/commit/c5bef8d7e2db30db73efab954214d2b27a83296b)) +- **web:** add title to metric page ([2e3ab1c](https://github.com/ohbug-org/ohbug-dashboard/commit/2e3ab1c0882340745516ecb4005ebf7e8c3fdc88)) +- **web:** card 组件 hover default false ([a364944](https://github.com/ohbug-org/ohbug-dashboard/commit/a364944ff7b323599df6c69674bffdb2d1d8f2da)) +- **web:** dashboard support metrics ([a30953e](https://github.com/ohbug-org/ohbug-dashboard/commit/a30953e85f0150b4ba28788fdae57dc2b1d4f249)) +- **web:** finish dark mode ([55186c4](https://github.com/ohbug-org/ohbug-dashboard/commit/55186c4321f80db13214b302f5917d5defe7f966)) +- **web:** finish invite members ([9442246](https://github.com/ohbug-org/ohbug-dashboard/commit/94422469b8ad67bb1c29374b6c458dae0a509f4e)) +- **web:** finish issues search ([bffc457](https://github.com/ohbug-org/ohbug-dashboard/commit/bffc45774ffb8278c25a27d780c9fbdf5a054070)) +- **web:** index 增加 create project 按钮 ([c762ab3](https://github.com/ohbug-org/ohbug-dashboard/commit/c762ab3e31f717ba4ef9be528388d0d16e608b2c)) +- **web:** issue detail trends 样式调整 ([c3a6c69](https://github.com/ohbug-org/ohbug-dashboard/commit/c3a6c697942c9ec799992d109d3ec9ed42d19d75)) +- **web:** issue detail 页面加入 breadcrumb ([22e061e](https://github.com/ohbug-org/ohbug-dashboard/commit/22e061ec318e7d3d2d9ad550ec0fb53578c4ab2e)) +- **web:** issue detail 页面样式调整 ([f934f78](https://github.com/ohbug-org/ohbug-dashboard/commit/f934f7839c9358acd8af4402b3b355fffb0885f9)) +- **web:** issue events 页面样式调整 ([8a35b52](https://github.com/ohbug-org/ohbug-dashboard/commit/8a35b5220f9f9317bf68ab8dd06f48d8ddd83f78)) +- **web:** issues 列表展示 events users 数量 ([0bfd469](https://github.com/ohbug-org/ohbug-dashboard/commit/0bfd469cbf56a5623d38b425ec882285e2cae46b)) +- **web:** navMenu 加入 settings 路由 ([590fed3](https://github.com/ohbug-org/ohbug-dashboard/commit/590fed38e6d1fdea142767bde6bf56d970c39be3)) +- **web:** project 切换同时切换当前路由 ([851ef66](https://github.com/ohbug-org/ohbug-dashboard/commit/851ef66ed5eaefc59358fcc771f4f85907b8105d)) +- **web:** projects 根据 用户查询 ([9700c35](https://github.com/ohbug-org/ohbug-dashboard/commit/9700c357effd58afa766edf59b97840dccf43140)) +- **web:** projects 页面查询 project trends ([6574676](https://github.com/ohbug-org/ohbug-dashboard/commit/6574676c8ac81dbdc4c80072269f1f3da5a64593)) +- **web:** read config from ohbug.config.yml ([dc1610f](https://github.com/ohbug-org/ohbug-dashboard/commit/dc1610f5aa2bf203311fb87b51671d83c356e293)) +- **web:** redirect create-project if user not has project ([e594318](https://github.com/ohbug-org/ohbug-dashboard/commit/e5943189642078f1eb1f50b798c16122d842e7ee)) +- **web:** redirect index if already signin ([0fd6708](https://github.com/ohbug-org/ohbug-dashboard/commit/0fd670839df0fbf857fb8fababf35d8c39d72a46)) +- **web:** redirect to signin when not have authorization ([e7a770d](https://github.com/ohbug-org/ohbug-dashboard/commit/e7a770dc4416e7f55068f27aa36334dbcda9e4be)) +- **web:** scroll navmenu logo click scroll to top ([1997561](https://github.com/ohbug-org/ohbug-dashboard/commit/19975611ffc5552032b84bf9a61e2928836e9e72)) +- **web:** support delete alert ([67d897a](https://github.com/ohbug-org/ohbug-dashboard/commit/67d897a02130c97c3ba258eb64c7c17340ac2a67)) +- **web:** support i18n ([01cde98](https://github.com/ohbug-org/ohbug-dashboard/commit/01cde984a573a7b6c9255fd88f265aa25916ca89)) +- **web:** support preview custom metadata ([ba4249e](https://github.com/ohbug-org/ohbug-dashboard/commit/ba4249e0d3de6188e656b14af6436cbb81771d9d)) +- **web:** support version detection and upgrade prompt ([d31b56e](https://github.com/ohbug-org/ohbug-dashboard/commit/d31b56ee85a9122893a4f5a2bd27077a38cd7268)) +- **web:** themeBox 加入 acrylic props ([5f68b96](https://github.com/ohbug-org/ohbug-dashboard/commit/5f68b967006d26f5a1200f0c05761999f59d2c09)) +- **web:** trendChart 展示 loading 状态 ([bbc1de5](https://github.com/ohbug-org/ohbug-dashboard/commit/bbc1de525a02f1073628144f7f2c3d57eff59425)) +- **web:** 主题样式微调 ([40fa5df](https://github.com/ohbug-org/ohbug-dashboard/commit/40fa5dfb4b9ad529e0719cacdf2a3792bf49b270)) +- **web:** 优化 issue detail 页面 减少非必要查询 ([786532e](https://github.com/ohbug-org/ohbug-dashboard/commit/786532e810f28309d4ba38a565eacd0b9de8ecfe)) +- **web:** 优化 issueList 样式 ([ae787c6](https://github.com/ohbug-org/ohbug-dashboard/commit/ae787c6af3111f15b12635f9636c0da138c60945)) +- **web:** 使用 next auth 登录页面 ([9e0ce8c](https://github.com/ohbug-org/ohbug-dashboard/commit/9e0ce8c2bb6a52bd6a99bb0705ecc99e8b6a5961)) +- **web:** 使用 react-icons 作为图标库 ([a613654](https://github.com/ohbug-org/ohbug-dashboard/commit/a613654b0a0949f25ecb36d800de69a77ec1b08b)) +- **web:** 加入 projects 页面 ([9cc87ea](https://github.com/ohbug-org/ohbug-dashboard/commit/9cc87ead269a080e6140761732a1e0bdad6aab4e)) +- **web:** 加入 searchBar 组件 ([df71220](https://github.com/ohbug-org/ohbug-dashboard/commit/df712201072141ab55a98761a3891ebbbca1d7b7)) +- **web:** 加入权限验证 ([fd62e15](https://github.com/ohbug-org/ohbug-dashboard/commit/fd62e1559e703cc26dd3adbb74e54e7d5f9af828)) +- **web:** 完善 project card 数据展示 ([032d2bc](https://github.com/ohbug-org/ohbug-dashboard/commit/032d2bc7440485c7783367d46ad31a78cf1d8823)) +- **web:** 完成 footer ([0abe255](https://github.com/ohbug-org/ohbug-dashboard/commit/0abe255de49046cbf023fae0c1a9229e8995721f)) +- **web:** 完成 issue detail 更多事件页面 ([e248a90](https://github.com/ohbug-org/ohbug-dashboard/commit/e248a90b02326da7d1b91f0db09fa32331243442)) +- **web:** 完成 issue detail 页面 堆栈信息展示 ([666a341](https://github.com/ohbug-org/ohbug-dashboard/commit/666a34157eb88c26800b068af8e9011867cde425)) +- **web:** 完成 issue detail 页面标签列展示 ([534d0f0](https://github.com/ohbug-org/ohbug-dashboard/commit/534d0f0f6c9c1d4e1a7d541c3da6d5d61fe004c2)) +- **web:** 完成 issue detail 页面趋势信息展示 ([d913d3e](https://github.com/ohbug-org/ohbug-dashboard/commit/d913d3e175441d7ffbef858470f34dd3e99b744b)) +- **web:** 完成 issue list 分页 ([fb502de](https://github.com/ohbug-org/ohbug-dashboard/commit/fb502de812eed29a84fe1b34e7648e1391f17dbd)) +- **web:** 完成 issue trends 数据查询与展示 ([42d55e9](https://github.com/ohbug-org/ohbug-dashboard/commit/42d55e9a40cb8b1276d3bd338ace8cf66a213149)) +- **web:** 完成 navMenu 部分 ([779399d](https://github.com/ohbug-org/ohbug-dashboard/commit/779399d5348b3308559e1f876c7c6c6fbbd750a4)) +- **web:** 完成 project setting apiKey 展示 ([a5e2cf8](https://github.com/ohbug-org/ohbug-dashboard/commit/a5e2cf8f5137769238e3493f1933b16f6b277c99)) +- **web:** 完成 project setting update name ([cdad8bc](https://github.com/ohbug-org/ohbug-dashboard/commit/cdad8bcb688f8b02b8c9c6c05816df0aaa3331bd)) +- **web:** 完成 project trends chart 显示 ([73c263f](https://github.com/ohbug-org/ohbug-dashboard/commit/73c263f6c3c7aedda41ee4f32332a38bb14b8217)) +- **web:** 完成 project 的创建 ([9765988](https://github.com/ohbug-org/ohbug-dashboard/commit/9765988b2a068ed03fa6470f4977c0df5b28baa5)) +- **web:** 完成 project 预览页面 ([ca5900f](https://github.com/ohbug-org/ohbug-dashboard/commit/ca5900f6f3b908d02cc9a1a15bb00ef60161ff7b)) +- **web:** 完成 release list 和 sourceMaps 页面 ([ad881e0](https://github.com/ohbug-org/ohbug-dashboard/commit/ad881e0a82f6ea653e6c117c5c3acdcc9ce410c5)) +- **web:** 完成 user 组件以及 nav 基本布局 ([93f066f](https://github.com/ohbug-org/ohbug-dashboard/commit/93f066f32f7a78471c79b94bb7a879ee341bb9a4)) +- **web:** 完成layout布局 包含 nav navMenu user 等组件 ([353466a](https://github.com/ohbug-org/ohbug-dashboard/commit/353466a7b344edd03f02c9d72985e0c79d9503e1)) +- **web:** 完成授权登录流程 ([d26286d](https://github.com/ohbug-org/ohbug-dashboard/commit/d26286d575a93469ccd6e3cb6327590d95b5a5a0)) +- **web:** 对接 ohbug-extension-rrweb ([6a63f7f](https://github.com/ohbug-org/ohbug-dashboard/commit/6a63f7f56463765cf5e31d2e1fd2fd17cf998bca)) +- **web:** 整体迁移至 chakra-ui ([8a2ff2c](https://github.com/ohbug-org/ohbug-dashboard/commit/8a2ff2c2e5b7a0302713d98c2963b47c4ae5c154)) +- **web:** 新增 alert detail 页面 ([036eff0](https://github.com/ohbug-org/ohbug-dashboard/commit/036eff01b39942b7bec74578641eb86ef0a2be79)) +- **web:** 新增 issues 页面 ([4c4c157](https://github.com/ohbug-org/ohbug-dashboard/commit/4c4c157adf71082ad9786c9f9285a37a5c28e174)) +- **web:** 暂时删除 release detail 页 ([60079f4](https://github.com/ohbug-org/ohbug-dashboard/commit/60079f41d4ad160b0547872174a3b7f4553fced1)) +- **web:** 调整 issue tabs 样式 ([52fc349](https://github.com/ohbug-org/ohbug-dashboard/commit/52fc3499e03b4752d566da4d12b0c5f5c81bcf8f)) +- **web:** 调整 prisma client 获取方式 ([5d7f293](https://github.com/ohbug-org/ohbug-dashboard/commit/5d7f293a99da8ec9df780675858a9a81f057ca41)) +- **web:** 部分页面布局更新 使用 wrapper ([c295cf6](https://github.com/ohbug-org/ohbug-dashboard/commit/c295cf6baaf10f5f6eee28b3e8f1b300f877252e)) +- 准备 issue 趋势图数据 ([d0f8c35](https://github.com/ohbug-org/ohbug-dashboard/commit/d0f8c35f5231f96dd41f73c659b219b7139548ad)) +- 加入 issue 详情页 ([326939a](https://github.com/ohbug-org/ohbug-dashboard/commit/326939ad6f5675d3dedd6e42772afb9368377da0)) +- 完成 issues 列表基本展示 ([b5dc389](https://github.com/ohbug-org/ohbug-dashboard/commit/b5dc389470e9b92dc77ee02c618feb04ab29b9c4)) +- 完成 sourceMap 还原代码 ([8c4bbbd](https://github.com/ohbug-org/ohbug-dashboard/commit/8c4bbbd43fa3009b733043cb2c4c3a508d88681a)) +- 完成基本环境配置、环境变量配置 ([3e36937](https://github.com/ohbug-org/ohbug-dashboard/commit/3e369373fde805ce760ff8837de63f81a402fc37)) +- 完成通知逻辑 ([d4f5a93](https://github.com/ohbug-org/ohbug-dashboard/commit/d4f5a9309d821b7edf83981ae525f2ca7ddc1d80)) +- 新增 email 登录方式 ([e713ca9](https://github.com/ohbug-org/ohbug-dashboard/commit/e713ca9c51d7e607b30ccd8fec15bb2bcb7ead68)) +- 路由调整为以 project 为基础 ([db3d9f2](https://github.com/ohbug-org/ohbug-dashboard/commit/db3d9f28ecf518496ba532d76c33a67821f98be1)) ### Performance Improvements -* **server:** 优化 report 接口资源占用 ([f84667b](https://github.com/ohbug-org/ohbug-dashboard/commit/f84667b065f229bfefde3fb661c370d15a36058f)) \ No newline at end of file +- **server:** 优化 report 接口资源占用 ([f84667b](https://github.com/ohbug-org/ohbug-dashboard/commit/f84667b065f229bfefde3fb661c370d15a36058f)) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index be38073..0000000 --- a/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM node:16-slim AS base -RUN apt-get update && apt-get install -y openssl -RUN npm install -g pnpm -ENV NODE_ENV production -WORKDIR /app - -FROM base AS deps -COPY .npmrc . -COPY pnpm-*.yaml . -COPY package.json . -COPY packages/common/package.json ./packages/common/package.json -COPY packages/config/package.json ./packages/config/package.json -COPY packages/server/package.json ./packages/server/package.json -COPY packages/web/package.json ./packages/web/package.json -RUN npm set-script prepare '' -RUN pnpm fetch --prod -RUN pnpm install -r --offline --prod - -FROM base AS builder -COPY --from=deps /app/node_modules ./node_modules -COPY --from=deps /app/packages/server/node_modules ./packages/server/node_modules -COPY --from=deps /app/packages/web/node_modules ./packages/web/node_modules -COPY . . -RUN npm run build - -FROM base AS runner -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/scripts ./scripts -COPY --from=builder /app/prisma ./prisma -# common -COPY --from=builder /app/packages/common/package.json ./packages/common/package.json -COPY --from=builder /app/packages/common/dist ./packages/common/dist -# config -COPY --from=builder /app/packages/config/package.json ./packages/config/package.json -COPY --from=builder /app/packages/config/dist ./packages/config/dist -# server -COPY --from=builder /app/packages/server/node_modules ./packages/server/node_modules -COPY --from=builder /app/packages/server/package.json ./packages/server/package.json -COPY --from=builder /app/packages/server/tsconfig.json ./packages/server/tsconfig.json -COPY --from=builder /app/packages/server/tsconfig.build.json ./packages/server/tsconfig.build.json -COPY --from=builder /app/packages/server/dist ./packages/server/dist -# web -COPY --from=builder /app/packages/web/package.json ./packages/web/package.json -COPY --from=builder /app/packages/web/public ./packages/web/public -COPY --from=builder /app/packages/web/.next/standalone ./packages/web -COPY --from=builder /app/packages/web/.next/static ./packages/web/.next/static -EXPOSE 3000 -EXPOSE 6660 -CMD ["npm", "run", "start"] diff --git a/README.md b/README.md index 0dc177a..973fc69 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ This project exists thanks to all the people who contribute. | Github Issue | 微信群 | | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | [ohbug-org/ohbug/issues](https://github.com/ohbug-org/ohbug/issues) | 关注后回复「ohbug」 | + ## License [Apache-2.0](./LICENSE) License © 2022 [chenyueban](https://github.com/chenyueban) diff --git a/packages/server/README.md b/apps/server/README.md similarity index 100% rename from packages/server/README.md rename to apps/server/README.md diff --git a/apps/server/package.json b/apps/server/package.json new file mode 100644 index 0000000..83d8f2a --- /dev/null +++ b/apps/server/package.json @@ -0,0 +1,32 @@ +{ + "name": "server", + "private": true, + "type": "module", + "scripts": { + "dev": "cross-env NODE_ENV=development tsx watch src/index.ts", + "build": "tsx src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "@hono/node-server": "^1.19.11", + "@ohbug-dashboard/db": "workspace:*", + "@ohbug/core": "^2.2.1", + "@ohbug/types": "^2.2.1", + "bullmq": "^5.71.1", + "common": "workspace:*", + "config": "workspace:*", + "date-fns": "^4.1.0", + "dotenv": "^17.3.1", + "hono": "^4.12.9", + "markdown-it": "^14.1.1", + "nanoid": "^5.1.7", + "node-cron": "^4.2.1", + "nodemailer": "^8.0.4" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.11", + "tsx": "^4.21.0" + } +} diff --git a/apps/server/src/cron/cleanup.ts b/apps/server/src/cron/cleanup.ts new file mode 100644 index 0000000..3a805c6 --- /dev/null +++ b/apps/server/src/cron/cleanup.ts @@ -0,0 +1,42 @@ +import { + feedback as feedbackTable, + getDb, + issue as issueTable, + lte, + metric as metricTable, + pageView as pageViewTable, + userView as userViewTable, +} from "@ohbug-dashboard/db"; +import { getConfig } from "config"; +import { subDays } from "date-fns"; +import cron from "node-cron"; + +export function setupCron() { + // Every day at 3am + cron.schedule("0 3 * * *", async () => { + try { + const db = getDb(); + const dataExpirationTime = getConfig().options?.dataExpirationTime ?? 7; + const threshold = subDays(new Date(), dataExpirationTime); + + await db.delete(metricTable).where(lte(metricTable.createdAt, threshold)); + console.log(`[Cron] Metrics from ${dataExpirationTime} days ago deleted`); + + await db.delete(feedbackTable).where(lte(feedbackTable.createdAt, threshold)); + console.log(`[Cron] Feedbacks from ${dataExpirationTime} days ago deleted`); + + await db.delete(pageViewTable).where(lte(pageViewTable.createdAt, threshold)); + console.log(`[Cron] PageViews from ${dataExpirationTime} days ago deleted`); + + await db.delete(userViewTable).where(lte(userViewTable.createdAt, threshold)); + console.log(`[Cron] UserViews from ${dataExpirationTime} days ago deleted`); + + await db.delete(issueTable).where(lte(issueTable.createdAt, threshold)); + console.log(`[Cron] Issues/Events from ${dataExpirationTime} days ago deleted`); + } catch (error) { + console.error("[Cron] Cleanup error:", error); + } + }); + + console.log("✅ Cron jobs scheduled: daily cleanup at 3am"); +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts new file mode 100644 index 0000000..f4db716 --- /dev/null +++ b/apps/server/src/index.ts @@ -0,0 +1,45 @@ +import { join } from "node:path"; +import { cwd } from "node:process"; + +import { serve } from "@hono/node-server"; +import dotenv from "dotenv"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { logger } from "hono/logger"; + +import { setupCron } from "./cron/cleanup"; +import { reportRoute } from "./routes/report"; +import { sourceMapRoute } from "./routes/source-map"; +import { setupWorkers } from "./workers"; + +dotenv.config({ path: join(cwd(), "../../", ".env") }); + +const app = new Hono(); + +// Middleware +app.use("*", logger()); +app.use("*", cors()); + +// Routes +app.route("/", reportRoute); +app.route("/sourceMap", sourceMapRoute); + +// Health check +app.get("/health", (c) => c.json({ status: "ok" })); + +// Start workers & cron +const workers = setupWorkers(); +setupCron(); + +const port = 6660; + +serve({ fetch: app.fetch, port }, (info) => { + console.log(`🚀 Hono server listening on port ${info.port}`); +}); + +// Graceful shutdown +process.on("SIGTERM", async () => { + console.log("Shutting down..."); + await Promise.all(workers.map((w) => w.close())); + process.exit(0); +}); diff --git a/apps/server/src/lib/alert-core.ts b/apps/server/src/lib/alert-core.ts new file mode 100644 index 0000000..63922c2 --- /dev/null +++ b/apps/server/src/lib/alert-core.ts @@ -0,0 +1,280 @@ +import { + type Db, + alert as alertTable, + count, + eq, + event as eventTable, + eventUser as eventUserTable, + getDb, + gte, + issue as issueTable, +} from "@ohbug-dashboard/db"; +import { + AlertConditionTopic, + AlertFilterTopic, + type ConditionOption, + type FilterOption, + type Interval, +} from "common"; +import { format, isBefore, isEqual, addMilliseconds, subMilliseconds } from "date-fns"; +import markdownIt from "markdown-it"; + +type Alert = typeof alertTable.$inferSelect; +type Event = typeof eventTable.$inferSelect; +type Issue = typeof issueTable.$inferSelect; + +function getIntervalMs(interval: Interval) { + const map: Record = { + "1m": 60 * 1000, + "5m": 5 * 60 * 1000, + "15m": 15 * 60 * 1000, + "1h": 60 * 60 * 1000, + "1d": 24 * 60 * 60 * 1000, + "7d": 7 * 24 * 60 * 60 * 1000, + "30d": 30 * 24 * 60 * 60 * 1000, + }; + return map[interval] ?? 0; +} + +function judgingInterval(a: Alert) { + if (a.recentlyAt) { + const now = new Date(); + const last = new Date(a.recentlyAt); + const interval = getIntervalMs(a.interval as Interval); + if (isBefore(now, addMilliseconds(last, interval))) return true; + } + return false; +} + +async function getEventIntervalCount( + ev: Event, + interval: Interval, + value: number, + a: Alert, + db: Db, +) { + const [result] = await db + .select({ count: count() }) + .from(eventTable) + .where(eq(eventTable.apiKey, ev.apiKey)); + // Simple approach: count events in interval + const eventCount = result?.count ?? 0; + if (eventCount >= value) { + return { + topic: "EventFrequencyCondition" as AlertConditionTopic, + condition: true, + metadata: { count: eventCount, value, interval }, + alert: a, + }; + } + return { topic: "EventFrequencyCondition" as AlertConditionTopic, condition: false }; +} + +async function getUserIntervalCount( + _iss: Issue, + interval: Interval, + value: number, + a: Alert, + db: Db, +) { + const [result] = await db + .select({ count: count() }) + .from(eventUserTable) + .where(gte(eventUserTable.createdAt, subMilliseconds(new Date(), getIntervalMs(interval)))); + const userCount = result?.count ?? 0; + if (userCount >= value) { + return { + topic: "UserFrequencyCondition" as AlertConditionTopic, + condition: true, + metadata: { count: userCount, value, interval }, + alert: a, + }; + } + return { topic: "UserFrequencyCondition" as AlertConditionTopic, condition: false }; +} + +async function judgingCondition(ev: Event, iss: Issue, a: Alert, db: Db) { + const conditions = a.conditions as unknown as ConditionOption[]; + const result: any[] = []; + for (const condition of conditions) { + switch (condition.topic) { + case AlertConditionTopic.EventCapturedCondition: + result.push({ topic: condition.topic, condition: true, alert: a }); + break; + case AlertConditionTopic.FirstSeenEventCondition: + if (isEqual(new Date(iss.createdAt), new Date(ev.createdAt))) { + result.push({ topic: condition.topic, condition: true, alert: a }); + } else { + result.push({ topic: condition.topic, condition: false }); + } + break; + case AlertConditionTopic.EventFrequencyCondition: { + const r = await getEventIntervalCount( + ev, + condition.interval as Interval, + Number.parseInt(condition.value as string), + a, + db, + ); + result.push(r); + break; + } + case AlertConditionTopic.UserFrequencyCondition: { + const r = await getUserIntervalCount( + iss, + condition.interval as Interval, + Number.parseInt(condition.value as string), + a, + db, + ); + result.push(r); + break; + } + } + } + return result; +} + +async function judgingFilter(ev: Event, iss: Issue, issueEventsCount: number, a: Alert, db: Db) { + const filters = a.filters as unknown as FilterOption[]; + if (filters.length === 0) return true; + const result: { topic: string; filter: boolean }[] = []; + + for (const filter of filters) { + switch (filter.topic) { + case AlertFilterTopic.IssueOccurrencesFilter: { + if (issueEventsCount >= Number.parseInt(filter.value as string)) { + result.push({ topic: filter.topic, filter: true }); + } + break; + } + case AlertFilterTopic.EventAttributeFilter: { + const { attribute, match, value } = filter; + const attr = (ev as any)[attribute as string] as string; + const v = value?.toString() ?? ""; + if (attr) { + const checks: Record = { + contains: attr.includes(v), + "starts with": attr.startsWith(v), + "ends with": attr.endsWith(v), + equals: attr === value, + "does not contain": !attr.includes(v), + "does not start with": !attr.startsWith(v), + "does not end with": !attr.endsWith(v), + "does not equal": attr !== value, + }; + if (match && checks[match]) { + result.push({ topic: filter.topic, filter: true }); + } + } + break; + } + case AlertFilterTopic.LatestReleaseFilter: { + const [latestIssue] = await db + .select() + .from(issueTable) + .where(eq(issueTable.apiKey, iss.apiKey)) + .orderBy(issueTable.createdAt) + .limit(1); + if (latestIssue?.id === iss.id) { + result.push({ topic: filter.topic, filter: true }); + } + break; + } + } + } + + if (a.filterMatch === "every") return result.some((item) => item.filter); + if (a.filterMatch === "all") return result.every((item) => item.filter); + return false; +} + +export async function getAlertStatus( + ev: Event, + iss: Issue, + issueEventsCount: number, + alerts: Alert[], +) { + const db = getDb(); + let result: any[] = []; + + for (const a of alerts) { + const [alertNew] = await db.select().from(alertTable).where(eq(alertTable.id, a.id)); + if (!alertNew?.enabled) continue; + if (judgingInterval(alertNew)) continue; + + const filtered = await judgingFilter(ev, iss, issueEventsCount, alertNew, db); + if (!filtered) continue; + + const items = await judgingCondition(ev, iss, alertNew, db); + + if (a.conditionMatch === "every") { + result = result.concat(items.filter((v: any) => v.condition)); + } else if (a.conditionMatch === "all" && items.every((item: any) => item.condition)) { + result = result.concat(items); + } + } + return result; +} + +function switchLevelAndGetText(level: string) { + const map: Record = { + serious: "严重", + warning: "警告", + default: "默认", + }; + return map[level] ?? level; +} + +const md = markdownIt(); + +export function getAlertContent( + ev: Event, + iss: Issue, + issueEventsCount: number, + issueUsersCount: number, + a: Alert, +) { + const url = (ev.device as any)?.url; + const { type, updatedAt } = iss; + const metadata = JSON.parse(iss.metadata || "{}"); + const message = metadata.message || "unknown message"; + const filename = metadata.filename; + const others = metadata.others; + const title = `「Ohbug」[问题通知] [${switchLevelAndGetText(a.level)}] ${type}`; + + const text = ` + ${title} + + Message + ${message} + + Statistics + 总事件数:${issueEventsCount} + 总用户数:${issueUsersCount} + 时间:${format(new Date(updatedAt), "yyyy-MM-dd HH:mm:ss")} + ${url && url !== "undefined" ? `\n Url\n ${url}` : ""} + ${filename && filename !== "undefined" ? `\n File\n ${filename}` : ""} + ${others && others !== "undefined" ? `\n Others\n ${others}` : ""} + `; + + const markdown = ` + # ${title} + + ## Message + - ${message} + + ## Statistics + - 总事件数:${issueEventsCount} + - 总用户数:${issueUsersCount} + - 时间:${format(new Date(updatedAt), "yyyy-MM-dd HH:mm:ss")} + ${url && url !== "undefined" ? `\n Url\n ${url}` : ""} + ${filename && filename !== "undefined" ? `\n File\n ${filename}` : ""} + ${others && others !== "undefined" ? `\n Others\n ${others}` : ""} + `; + + const html = md.render(markdown); + const lite = `总事件数:${issueEventsCount} 总用户数:${issueUsersCount} 时间:${format(new Date(updatedAt), "yyyy-MM-dd HH:mm:ss")}`; + + return { title, text, lite, markdown, html }; +} diff --git a/apps/server/src/lib/email.ts b/apps/server/src/lib/email.ts new file mode 100644 index 0000000..8bc3fa6 --- /dev/null +++ b/apps/server/src/lib/email.ts @@ -0,0 +1,27 @@ +import nodemailer from "nodemailer"; +import type SMTPTransport from "nodemailer/lib/smtp-transport"; + +interface SendMail { + server: SMTPTransport.Options | string; + from: string; + to: string; + title: string; + text: string; + html: string; +} + +export async function sendEmail({ server, from, to, title, text, html }: SendMail) { + const transporter = nodemailer.createTransport(server); + const info = { + from: `Ohbug <${from}>`, + to, + subject: title, + text, + html, + }; + const verify = await transporter.verify(); + if (verify) { + return transporter.sendMail(info); + } + return null; +} diff --git a/packages/server/src/api/report/report.core.ts b/apps/server/src/lib/report-core.ts similarity index 51% rename from packages/server/src/api/report/report.core.ts rename to apps/server/src/lib/report-core.ts index fe78b13..ab3e412 100644 --- a/packages/server/src/api/report/report.core.ts +++ b/apps/server/src/lib/report-core.ts @@ -1,15 +1,25 @@ -import { EventTypes } from '@ohbug/core' -import { md5 } from 'common' -import type { - AggregationDataAndMetaData, - OhbugEventDetail, -} from './report.interface' +import { md5 } from "common"; + +import type { AggregationDataAndMetaData, OhbugEventDetail } from "../types"; + +const EventTypes = { + UNCAUGHT_ERROR: "uncaughtError", + RESOURCE_ERROR: "resourceError", + UNHANDLEDREJECTION_ERROR: "unhandledrejectionError", + AJAX_ERROR: "ajaxError", + FETCH_ERROR: "fetchError", + WEBSOCKET_ERROR: "websocketError", + UNKNOWN_ERROR: "unknownError", + REACT: "react", + VUE: "vue", + MINIAPP_ERROR: "miniappError", + MINIAPP_UNHANDLEDREJECTION_ERROR: "miniappUnhandledrejectionError", + MINIAPP_PAGENOTFOUND_ERROR: "miniappPagenotfoundError", + MINIAPP_MEMORYWARNING_ERROR: "miniappMemorywarningError", +} as const; /** * 根据不同 error detail 返回可用于聚合的字段 - * - * @param type 具体的 error 类型 - * @param detail error detail */ export function switchErrorDetailAndGetAggregationDataAndMetaData( type: string, @@ -17,49 +27,34 @@ export function switchErrorDetailAndGetAggregationDataAndMetaData( ): AggregationDataAndMetaData { switch (type) { case EventTypes.UNCAUGHT_ERROR: { - const filename = detail.filename?.split?.('?')?.[0] + const filename = detail.filename?.split?.("?")?.[0]; return { - agg: [ - detail.name, - detail.message, - filename, - detail.lineno, - detail.colno, - ], + agg: [detail.name, detail.message, filename, detail.lineno, detail.colno], metadata: { type, message: detail.message, filename: detail.filename, others: detail.stack, }, - } + }; } case EventTypes.UNHANDLEDREJECTION_ERROR: { - const filename = detail.filename?.split?.('?')?.[0] + const filename = detail.filename?.split?.("?")?.[0]; return { - agg: [ - detail.name, - detail.message, - filename, - detail.lineno, - detail.colno, - ], + agg: [detail.name, detail.message, filename, detail.lineno, detail.colno], metadata: { type, message: detail.message, filename: detail.filename, others: detail.stack, }, - } + }; } case EventTypes.UNKNOWN_ERROR: return { agg: [detail.message], - metadata: { - type, - message: detail.message, - }, - } + metadata: { type, message: detail.message }, + }; case EventTypes.RESOURCE_ERROR: return { agg: [ @@ -71,52 +66,28 @@ export function switchErrorDetailAndGetAggregationDataAndMetaData( detail.nodeType, detail.selector, ], - metadata: { - type, - message: detail.message, - others: detail.selector, - }, - } + metadata: { type, message: detail.message, others: detail.selector }, + }; case EventTypes.AJAX_ERROR: return { agg: [detail.req?.url, detail.req?.method], - metadata: { - type, - message: detail.req?.url, - others: detail.req?.method, - }, - } + metadata: { type, message: detail.req?.url, others: detail.req?.method }, + }; case EventTypes.FETCH_ERROR: return { agg: [detail.req?.url, detail.req?.method], - metadata: { - type, - message: detail.req?.url, - others: detail.req?.method, - }, - } + metadata: { type, message: detail.req?.url, others: detail.req?.method }, + }; case EventTypes.WEBSOCKET_ERROR: return { agg: [detail.url], - metadata: { - type, - message: detail.url, - }, - } + metadata: { type, message: detail.url }, + }; case EventTypes.REACT: return { - agg: [ - detail.name, - detail.message, - detail.errorInfo, - detail.stack, - ], - metadata: { - type, - message: detail.message, - others: detail.errorInfo, - }, - } + agg: [detail.name, detail.message, detail.errorInfo, detail.stack], + metadata: { type, message: detail.message, others: detail.errorInfo }, + }; case EventTypes.VUE: return { agg: [ @@ -134,64 +105,41 @@ export function switchErrorDetailAndGetAggregationDataAndMetaData( filename: detail.file, others: detail.errorInfo, }, - } + }; case EventTypes.MINIAPP_ERROR: return { agg: detail?.stack ? [detail.message, detail.stack] : [detail.message], - metadata: { - type, - message: detail.message, - stack: detail?.stack, - }, - } + metadata: { type, message: detail.message, stack: detail?.stack }, + }; case EventTypes.MINIAPP_UNHANDLEDREJECTION_ERROR: return { agg: detail?.stack ? [detail.message, detail.stack] : [detail.message], - metadata: { - type, - message: detail.message, - stack: detail?.stack, - }, - } + metadata: { type, message: detail.message, stack: detail?.stack }, + }; case EventTypes.MINIAPP_PAGENOTFOUND_ERROR: return { agg: [detail?.message, detail?.path, detail.query, detail.isEntryPage], - metadata: { - type, - message: detail.message, - path: detail.path, - query: detail.query, - }, - } + metadata: { type, message: detail.message, path: detail.path, query: detail.query }, + }; case EventTypes.MINIAPP_MEMORYWARNING_ERROR: return { agg: [detail?.message, detail?.level], - metadata: { - type, - message: detail.message, - level: detail.level, - }, - } + metadata: { type, message: detail.message, level: detail.level }, + }; default: return { agg: [detail.message], - metadata: { - type, - message: detail.message, - }, - } + metadata: { type, message: detail.message }, + }; } } /** * 对 AggregationData 进行 md5 加密拿到聚合依据 - * - * @param aggregationData */ export function getMd5FromAggregationData(...aggregationData: any[]): string { - const data = aggregationData.map((item) => { - if (typeof item === 'object') return JSON.stringify(item) - return item - }).join(',') - return md5(data) + const data = aggregationData + .map((item) => (typeof item === "object" ? JSON.stringify(item) : item)) + .join(","); + return md5(data); } diff --git a/apps/server/src/lib/webhook.ts b/apps/server/src/lib/webhook.ts new file mode 100644 index 0000000..2d8a874 --- /dev/null +++ b/apps/server/src/lib/webhook.ts @@ -0,0 +1,54 @@ +import type { Action } from "common"; + +interface Content { + title: string; + text: string; + markdown: string; +} + +function formatWebhookPayload({ title, text, markdown }: Content, action: Action) { + const at = action?.at; + const atText = (hasExtra: boolean) => + at + ?.split(",") + .reduce( + (pre: string, cur: string) => + `${pre}\n${hasExtra ? "<" : ""}@${cur}${hasExtra ? ">" : ""} `, + "", + ); + + switch (action?.webhookType) { + case "dingtalk": { + const result: Record = { + msgtype: "markdown", + markdown: { title, text: markdown + atText(false) }, + }; + if (at) { + result.at = { atMobiles: [at], isAtAll: false }; + } + return result; + } + case "wechatWork": { + const result: Record = { + msgtype: "text", + text: { content: text }, + }; + if (at) { + result.text.mentioned_mobile_list = [at]; + } + return result; + } + case "others": + default: + return text; + } +} + +export async function sendWebhook(content: Content, action: Action) { + const payload = formatWebhookPayload(content, action); + await fetch(action.uri, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: typeof payload === "string" ? payload : JSON.stringify(payload), + }); +} diff --git a/apps/server/src/queues.ts b/apps/server/src/queues.ts new file mode 100644 index 0000000..d0ef656 --- /dev/null +++ b/apps/server/src/queues.ts @@ -0,0 +1,8 @@ +import { Queue } from "bullmq"; +import { getConfig } from "config"; + +const connection = getConfig().db.redis; + +export const documentQueue = new Queue("document", { connection }); +export const alertQueue = new Queue("alert", { connection }); +export const sourceMapQueue = new Queue("sourceMap", { connection }); diff --git a/apps/server/src/routes/report.ts b/apps/server/src/routes/report.ts new file mode 100644 index 0000000..30265eb --- /dev/null +++ b/apps/server/src/routes/report.ts @@ -0,0 +1,113 @@ +import type { OhbugEvent } from "@ohbug/types"; +import type { OhbugEventLike } from "common"; +import { Hono } from "hono"; + +import { + getMd5FromAggregationData, + switchErrorDetailAndGetAggregationDataAndMetaData, +} from "../lib/report-core"; +import { documentQueue } from "../queues"; +import type { + CreateEventParams, + CreateFeedbackParams, + CreateMetricParams, + CreateViewParams, +} from "../types"; + +export const reportRoute = new Hono(); + +function filterEvent(event: OhbugEvent): OhbugEvent { + if (!("apiKey" in event && typeof event.apiKey === "string")) { + throw new Error(`Invalid Event: apiKey is ${event.apiKey}`); + } + if ("appVersion" in event && typeof event.appVersion !== "string") { + throw new Error(`Invalid Event: appVersion is ${event.appVersion}`); + } + if ("appType" in event && typeof event.appType !== "string") { + throw new Error(`Invalid Event: appType is ${event.appType}`); + } + if ("releaseStage" in event && typeof event.releaseStage !== "string") { + throw new Error(`Invalid Event: releaseStage is ${event.releaseStage}`); + } + if (!("timestamp" in event && typeof event.timestamp === "string")) { + throw new Error(`Invalid Event: timestamp is ${event.timestamp}`); + } + if ("category" in event && typeof event.category !== "string") { + throw new Error(`Invalid Event: category is ${event.category}`); + } + if (!("type" in event && typeof event.type === "string")) { + throw new Error(`Invalid Event: type is ${event.type}`); + } + if (!("sdk" in event && event.sdk instanceof Object)) { + throw new Error(`Invalid Event: sdk is ${JSON.stringify(event.sdk)}`); + } + if (!("detail" in event)) { + throw new Error("Invalid Event: missing detail"); + } + if (!("device" in event)) { + throw new Error("Invalid Event: missing device"); + } + return event; +} + +function transferEvent(event: OhbugEvent, ipAddress: string): OhbugEventLike { + return Object.assign(event, { + user: { ...event.user, ipAddress }, + }); +} + +function aggregation(event: OhbugEventLike) { + const { type, detail, apiKey } = event; + const { agg, metadata } = switchErrorDetailAndGetAggregationDataAndMetaData(type, detail); + const issueIntro = getMd5FromAggregationData(apiKey, ...agg); + return { issueIntro, metadata }; +} + +reportRoute.post("/", async (c) => { + try { + let body = await c.req.text(); + let event: OhbugEvent = typeof body === "string" ? JSON.parse(body) : body; + + const ip = c.req.header("x-real-ip") ?? c.req.header("x-forwarded-for") ?? "unknown"; + + const filteredEvent = filterEvent(event); + const eventLike = transferEvent(filteredEvent, ip); + const userIntro = getMd5FromAggregationData(eventLike.apiKey, ...Object.values(eventLike.user)); + + const queueOpts = { + delay: 3000, + removeOnComplete: true, + removeOnFail: true, + }; + + if (eventLike.category === "performance") { + const params: CreateMetricParams = { metric: eventLike, userIntro }; + await documentQueue.add("metric", params, queueOpts); + } else if (eventLike.category === "feedback") { + const params: CreateFeedbackParams = { feedback: eventLike, userIntro }; + await documentQueue.add("feedback", params, queueOpts); + } else if (eventLike.category === "view") { + const params: CreateViewParams = { view: eventLike, userIntro }; + if (eventLike.type === "pageView") { + await documentQueue.add("pageView", params, queueOpts); + } + if (eventLike.type === "userView") { + await documentQueue.add("userView", params, queueOpts); + } + } else { + const { issueIntro, metadata } = aggregation(eventLike); + const params: CreateEventParams = { + event: eventLike, + issueIntro, + userIntro, + metadata, + }; + await documentQueue.add("event", params, { ...queueOpts, priority: 1 }); + } + + return c.body(null, 204); + } catch (error) { + console.error("Report error:", error); + return c.json({ error: "Failed to process event" }, 400); + } +}); diff --git a/apps/server/src/routes/source-map.ts b/apps/server/src/routes/source-map.ts new file mode 100644 index 0000000..be5b0c6 --- /dev/null +++ b/apps/server/src/routes/source-map.ts @@ -0,0 +1,62 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { cwd } from "node:process"; + +import { Hono } from "hono"; + +import { sourceMapQueue } from "../queues"; + +const UPLOAD_DEST = join(cwd(), "../../.uploads"); + +export const sourceMapRoute = new Hono(); + +sourceMapRoute.post("/upload", async (c) => { + try { + const formData = await c.req.formData(); + const file = formData.get("file") as File | null; + const apiKey = formData.get("apiKey") as string; + const appVersion = formData.get("appVersion") as string; + const appType = (formData.get("appType") as string) ?? undefined; + + if (!file || !apiKey || !appVersion) { + return c.json({ error: "Missing required fields: file, apiKey, appVersion" }, 400); + } + + // Save file to disk + await mkdir(UPLOAD_DEST, { recursive: true }); + const filename = `${Date.now()}-${file.name}`; + const filepath = join(UPLOAD_DEST, filename); + const buffer = Buffer.from(await file.arrayBuffer()); + const { writeFile } = await import("node:fs/promises"); + await writeFile(filepath, buffer); + + const sourceMapFile = { + fieldname: "file", + originalname: file.name, + encoding: "7bit", + mimetype: file.type, + destination: UPLOAD_DEST, + filename, + path: filepath, + size: buffer.length, + }; + + await sourceMapQueue.add( + "file", + { + sourceMapFile, + body: { apiKey, appVersion, appType }, + }, + { + delay: 3000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + return c.json({ success: true }); + } catch (error) { + console.error("SourceMap upload error:", error); + return c.json({ error: "Failed to process source map" }, 500); + } +}); diff --git a/apps/server/src/types.ts b/apps/server/src/types.ts new file mode 100644 index 0000000..1dbdec8 --- /dev/null +++ b/apps/server/src/types.ts @@ -0,0 +1,76 @@ +import type { alert, event, issue } from "@ohbug-dashboard/db"; +import type { OhbugEventLike } from "common"; + +export interface OhbugEventDetail { + name?: string; + message?: string; + filename?: string; + lineno?: number; + colno?: number; + stack?: string; + src?: string; + tagName?: string; + id?: string; + className?: string; + nodeType?: number; + selector?: string; + errorInfo?: string; + component?: string; + file?: string; + props?: string; + path?: string; + query?: string; + isEntryPage?: boolean; + level?: string; + url?: string; + req?: { url?: string; method?: string }; + CLS?: number; + FCP?: number; + FID?: number; + LCP?: number; + TTFB?: number; + INP?: number; + [key: string]: any; +} + +export interface MetaData { + type: string; + message: string; + filename?: string; + stack?: string; + others?: string; + [key: string]: any; +} + +export interface AggregationDataAndMetaData { + agg: any[]; + metadata: Record; +} + +export interface CreateEventParams { + event: OhbugEventLike; + issueIntro: string; + userIntro: string; + metadata: Record; +} + +export interface CreateMetricParams { + metric: OhbugEventLike; + userIntro: string; +} + +export interface CreateFeedbackParams { + feedback: OhbugEventLike; + userIntro: string; +} + +export interface CreateViewParams { + view: OhbugEventLike; + userIntro: string; +} + +export interface GetAlertStatusParams { + event: typeof event.$inferSelect; + issue: typeof issue.$inferSelect; + alerts: (typeof alert.$inferSelect)[]; +} diff --git a/apps/server/src/workers/alert.worker.ts b/apps/server/src/workers/alert.worker.ts new file mode 100644 index 0000000..be06d60 --- /dev/null +++ b/apps/server/src/workers/alert.worker.ts @@ -0,0 +1,104 @@ +import { + alert as alertTable, + alertEvent as alertEventTable, + count, + eq, + event as eventTable, + eventUsersOnIssues, + getDb, +} from "@ohbug-dashboard/db"; +import { Worker } from "bullmq"; +import type { Action } from "common"; +import { getConfig } from "config"; + +import { getAlertContent, getAlertStatus } from "../lib/alert-core"; +import { sendEmail } from "../lib/email"; +import { sendWebhook } from "../lib/webhook"; +import type { GetAlertStatusParams } from "../types"; + +const connection = getConfig().db.redis; + +export function createAlertWorker() { + return new Worker( + "alert", + async (job) => { + try { + const data = job.data as GetAlertStatusParams; + if (!data) return; + + const db = getDb(); + + const [issueEventsResult] = await db + .select({ count: count() }) + .from(eventTable) + .where(eq(eventTable.issueId, data.issue.id)); + const issueEventsCount = issueEventsResult?.count ?? 0; + + const issueUsers = await db + .select() + .from(eventUsersOnIssues) + .where(eq(eventUsersOnIssues.issueId, data.issue.id)); + const issueUsersCount = issueUsers.length; + + const status = await getAlertStatus(data.event, data.issue, issueEventsCount, data.alerts); + + if (status.length === 0) return; + + for (const item of status) { + if (!item.alert) continue; + + const alertContent = getAlertContent( + data.event, + data.issue, + issueEventsCount, + issueUsersCount, + item.alert, + ); + + for (const action of item.alert.actions as unknown as Action[]) { + if (action.type === "email") { + const config = getConfig(); + if (config.email?.server && config.email?.from) { + await sendEmail({ + server: config.email.server, + from: config.email.from, + to: action.uri, + title: alertContent.title, + text: alertContent.text, + html: alertContent.html, + }); + } + } else if (action.type === "webhook") { + await sendWebhook( + { + title: alertContent.title, + text: alertContent.text, + markdown: alertContent.markdown, + }, + action, + ); + } + } + + // Record alert event + await db.insert(alertEventTable).values({ + alertId: item.alert.id, + eventId: data.event.id, + issueId: data.issue.id, + createdAt: new Date(), + }); + + // Update alert recentlyAt + await db + .update(alertTable) + .set({ recentlyAt: new Date() }) + .where(eq(alertTable.id, item.alert.id)); + } + } catch (error) { + console.error("[AlertWorker] Error:", error); + throw error; + } + }, + { connection }, + ); +} diff --git a/apps/server/src/workers/event.worker.ts b/apps/server/src/workers/event.worker.ts new file mode 100644 index 0000000..053ca2b --- /dev/null +++ b/apps/server/src/workers/event.worker.ts @@ -0,0 +1,337 @@ +import { + and, + eq, + event as eventTable, + eventUser as eventUserTable, + eventUsersOnIssues, + eventUsersOnProjects, + getDb, + issue as issueTable, + project as projectTable, + alert as alertTable, + feedback as feedbackTable, + metric as metricTable, + pageView as pageViewTable, + userView as userViewTable, +} from "@ohbug-dashboard/db"; +import { Worker } from "bullmq"; +import { getConfig } from "config"; + +import { alertQueue } from "../queues"; +import type { + CreateEventParams, + CreateFeedbackParams, + CreateMetricParams, + CreateViewParams, +} from "../types"; + +const connection = getConfig().db.redis; + +async function handleEvent(data: CreateEventParams) { + const db = getDb(); + const { event: ev, issueIntro, userIntro, metadata } = data; + const now = new Date(); + + // 1. Check project exists + const [proj] = await db.select().from(projectTable).where(eq(projectTable.apiKey, ev.apiKey)); + if (!proj) throw new Error(`Project not found for apiKey: ${ev.apiKey}`); + + // 2. Upsert issue + const [existingIssue] = await db.select().from(issueTable).where(eq(issueTable.id, issueIntro)); + + if (!existingIssue) { + await db.insert(issueTable).values({ + id: issueIntro, + apiKey: ev.apiKey, + appVersion: ev.appVersion, + appType: ev.appType, + releaseStage: ev.releaseStage, + type: ev.type, + metadata: JSON.stringify(metadata), + createdAt: now, + updatedAt: now, + }); + } else { + await db.update(issueTable).set({ updatedAt: now }).where(eq(issueTable.id, issueIntro)); + } + + // 3. Upsert event user + const [existingUser] = await db + .select() + .from(eventUserTable) + .where(eq(eventUserTable.id, userIntro)); + + if (!existingUser) { + await db.insert(eventUserTable).values({ + id: userIntro, + ipAddress: ev.user.ipAddress!, + uuid: ev.user.uuid, + email: ev.user.email, + name: ev.user.name, + metadata: ev.user.metadata ? JSON.stringify(ev.user.metadata) : undefined, + createdAt: now, + updatedAt: now, + }); + } else { + await db.update(eventUserTable).set({ updatedAt: now }).where(eq(eventUserTable.id, userIntro)); + } + + // 4. Upsert eventUsersOnProjects + const [existingEUP] = await db + .select() + .from(eventUsersOnProjects) + .where( + and( + eq(eventUsersOnProjects.eventUserId, userIntro), + eq(eventUsersOnProjects.apiKey, ev.apiKey), + ), + ); + if (!existingEUP) { + await db.insert(eventUsersOnProjects).values({ + eventUserId: userIntro, + apiKey: ev.apiKey, + assignedAt: now, + }); + } + + // 5. Create event + const eventId = crypto.randomUUID(); + await db.insert(eventTable).values({ + id: eventId, + apiKey: ev.apiKey, + appVersion: ev.appVersion, + appType: ev.appType, + releaseStage: ev.releaseStage, + timestamp: new Date(ev.timestamp), + category: ev.category, + type: ev.type, + sdk: ev.sdk, + detail: ev.detail, + device: ev.device, + actions: ev.actions, + metadata: ev.metadata ? JSON.stringify(ev.metadata) : undefined, + createdAt: now, + userId: userIntro, + issueId: issueIntro, + }); + + // 6. Upsert eventUsersOnIssues + const [existingEUI] = await db + .select() + .from(eventUsersOnIssues) + .where( + and( + eq(eventUsersOnIssues.issueId, issueIntro), + eq(eventUsersOnIssues.eventUserId, userIntro), + ), + ); + if (!existingEUI) { + await db.insert(eventUsersOnIssues).values({ + issueId: issueIntro, + eventUserId: userIntro, + assignedAt: now, + }); + } + + // 7. Check alerts + const alerts = await db.select().from(alertTable).where(eq(alertTable.projectId, proj.id)); + + if (alerts.length > 0) { + const [createdEvent] = await db.select().from(eventTable).where(eq(eventTable.id, eventId)); + const [iss] = await db.select().from(issueTable).where(eq(issueTable.id, issueIntro)); + + if (createdEvent && iss) { + await alertQueue.add( + "alert", + { event: createdEvent, issue: iss, alerts }, + { delay: 1000, removeOnComplete: true, removeOnFail: true, priority: 2 }, + ); + } + } +} + +async function handleMetric(data: CreateMetricParams) { + const db = getDb(); + const { metric: m, userIntro } = data; + const now = new Date(); + + const [proj] = await db.select().from(projectTable).where(eq(projectTable.apiKey, m.apiKey)); + if (!proj) throw new Error(`Project not found for apiKey: ${m.apiKey}`); + + // Upsert event user + const [existingUser] = await db + .select() + .from(eventUserTable) + .where(eq(eventUserTable.id, userIntro)); + if (!existingUser) { + await db.insert(eventUserTable).values({ + id: userIntro, + ipAddress: m.user.ipAddress!, + uuid: m.user.uuid, + email: m.user.email, + name: m.user.name, + metadata: m.user.metadata ? JSON.stringify(m.user.metadata) : undefined, + createdAt: now, + updatedAt: now, + }); + await db + .insert(eventUsersOnProjects) + .values({ + eventUserId: userIntro, + apiKey: m.apiKey, + assignedAt: now, + }) + .onConflictDoNothing(); + } + + await db.insert(metricTable).values({ + id: crypto.randomUUID(), + apiKey: m.apiKey, + appVersion: m.appVersion, + appType: m.appType, + releaseStage: m.releaseStage, + timestamp: new Date(m.timestamp), + category: m.category, + type: m.type, + sdk: m.sdk, + device: m.device, + cls: m.detail.CLS, + fcp: m.detail.FCP, + fid: m.detail.FID, + lcp: m.detail.LCP, + ttfb: m.detail.TTFB, + inp: m.detail.INP, + createdAt: now, + userId: userIntro, + }); +} + +async function handleFeedback(data: CreateFeedbackParams) { + const db = getDb(); + const { feedback: fb, userIntro } = data; + const now = new Date(); + + const [proj] = await db.select().from(projectTable).where(eq(projectTable.apiKey, fb.apiKey)); + if (!proj) throw new Error(`Project not found for apiKey: ${fb.apiKey}`); + + const [existingUser] = await db + .select() + .from(eventUserTable) + .where(eq(eventUserTable.id, userIntro)); + if (!existingUser) { + await db.insert(eventUserTable).values({ + id: userIntro, + ipAddress: fb.user.ipAddress!, + uuid: fb.user.uuid, + email: fb.user.email, + name: fb.user.name, + metadata: fb.user.metadata ? JSON.stringify(fb.user.metadata) : undefined, + createdAt: now, + updatedAt: now, + }); + await db + .insert(eventUsersOnProjects) + .values({ + eventUserId: userIntro, + apiKey: fb.apiKey, + assignedAt: now, + }) + .onConflictDoNothing(); + } + + await db.insert(feedbackTable).values({ + id: crypto.randomUUID(), + apiKey: fb.apiKey, + appVersion: fb.appVersion, + appType: fb.appType, + releaseStage: fb.releaseStage, + timestamp: new Date(fb.timestamp), + category: fb.category, + type: fb.type, + sdk: fb.sdk, + detail: fb.detail, + device: fb.device, + createdAt: now, + userId: userIntro, + }); +} + +async function handleView(type: "pageView" | "userView", data: CreateViewParams) { + const db = getDb(); + const { view: v, userIntro } = data; + const now = new Date(); + + const [proj] = await db.select().from(projectTable).where(eq(projectTable.apiKey, v.apiKey)); + if (!proj) throw new Error(`Project not found for apiKey: ${v.apiKey}`); + + const [existingUser] = await db + .select() + .from(eventUserTable) + .where(eq(eventUserTable.id, userIntro)); + if (!existingUser) { + await db.insert(eventUserTable).values({ + id: userIntro, + ipAddress: v.user.ipAddress!, + uuid: v.user.uuid, + email: v.user.email, + name: v.user.name, + metadata: v.user.metadata ? JSON.stringify(v.user.metadata) : undefined, + createdAt: now, + updatedAt: now, + }); + await db + .insert(eventUsersOnProjects) + .values({ + eventUserId: userIntro, + apiKey: v.apiKey, + assignedAt: now, + }) + .onConflictDoNothing(); + } + + const table = type === "pageView" ? pageViewTable : userViewTable; + await db.insert(table).values({ + apiKey: v.apiKey, + appVersion: v.appVersion, + appType: v.appType, + releaseStage: v.releaseStage, + timestamp: new Date(v.timestamp), + category: v.category, + type: v.type, + sdk: v.sdk, + device: v.device, + createdAt: now, + userId: userIntro, + }); +} + +export function createEventWorker() { + return new Worker( + "document", + async (job) => { + try { + switch (job.name) { + case "event": + await handleEvent(job.data as CreateEventParams); + break; + case "metric": + await handleMetric(job.data as CreateMetricParams); + break; + case "feedback": + await handleFeedback(job.data as CreateFeedbackParams); + break; + case "pageView": + await handleView("pageView", job.data as CreateViewParams); + break; + case "userView": + await handleView("userView", job.data as CreateViewParams); + break; + } + } catch (error) { + console.error(`[EventWorker] Error processing ${job.name}:`, error); + throw error; + } + }, + { connection }, + ); +} diff --git a/apps/server/src/workers/index.ts b/apps/server/src/workers/index.ts new file mode 100644 index 0000000..8e13c6e --- /dev/null +++ b/apps/server/src/workers/index.ts @@ -0,0 +1,13 @@ +import { createAlertWorker } from "./alert.worker"; +import { createEventWorker } from "./event.worker"; +import { createSourceMapWorker } from "./source-map.worker"; + +export function setupWorkers() { + const eventWorker = createEventWorker(); + const alertWorker = createAlertWorker(); + const sourceMapWorker = createSourceMapWorker(); + + console.log("✅ BullMQ workers started: document, alert, sourceMap"); + + return [eventWorker, alertWorker, sourceMapWorker]; +} diff --git a/apps/server/src/workers/source-map.worker.ts b/apps/server/src/workers/source-map.worker.ts new file mode 100644 index 0000000..2e6c1da --- /dev/null +++ b/apps/server/src/workers/source-map.worker.ts @@ -0,0 +1,99 @@ +import { unlink } from "node:fs/promises"; + +import { eq, getDb, project as projectTable, release as releaseTable } from "@ohbug-dashboard/db"; +import { Worker } from "bullmq"; +import type { ReceiveSourceMapFile, SourceMapData } from "common"; +import { getConfig } from "config"; +import { nanoid } from "nanoid"; + +const connection = getConfig().db.redis; +const maxSourceMap = 5000; + +interface SourceMapJobData { + sourceMapFile: ReceiveSourceMapFile; + body: { + apiKey: string; + appVersion: string; + appType?: string; + }; +} + +export function createSourceMapWorker() { + return new Worker( + "sourceMap", + async (job) => { + try { + if (job.name !== "file") return; + + const data = job.data as SourceMapJobData; + const db = getDb(); + + const [proj] = await db + .select() + .from(projectTable) + .where(eq(projectTable.apiKey, data.body.apiKey)); + if (!proj) throw new Error("Project not found"); + + const releases = await db + .select() + .from(releaseTable) + .where(eq(releaseTable.projectId, proj.id)); + + const sourceMap = { ...data.sourceMapFile, id: nanoid() }; + const { appVersion, appType } = data.body; + const now = new Date(); + + const release = releases.find( + (r: typeof releaseTable.$inferSelect) => r.appVersion === appVersion, + ); + + if (!release) { + // No existing release for this version + await db.insert(releaseTable).values({ + apiKey: data.body.apiKey, + appVersion, + appType, + sourceMaps: [sourceMap], + projectId: proj.id, + createdAt: now, + updatedAt: now, + }); + return; + } + + // Existing release + const sourceMaps = (release.sourceMaps as unknown as SourceMapData) || []; + + if (sourceMaps.length < maxSourceMap) { + await db + .update(releaseTable) + .set({ + sourceMaps: [...sourceMaps, sourceMap], + appType, + updatedAt: now, + }) + .where(eq(releaseTable.id, release.id)); + } else { + // At limit, remove oldest + const deleteSourceMap = sourceMaps.shift(); + if (deleteSourceMap) { + await unlink(deleteSourceMap.path).catch(() => {}); + sourceMaps.push(sourceMap); + await db + .update(releaseTable) + .set({ + sourceMaps, + appType, + updatedAt: now, + }) + .where(eq(releaseTable.id, release.id)); + } + } + } catch (error) { + console.error("[SourceMapWorker] Error:", error); + throw error; + } + }, + { connection }, + ); +} diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json new file mode 100644 index 0000000..74e53f0 --- /dev/null +++ b/apps/server/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2023", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..92c7520 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,6 @@ +# TanStack Start +.output +.nitro + +# Generated route tree +src/routeTree.gen.ts diff --git a/packages/web/README.md b/apps/web/README.md similarity index 100% rename from packages/web/README.md rename to apps/web/README.md diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 0000000..68aeac1 --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + } +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..17d3c0e --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,47 @@ +{ + "name": "web", + "private": true, + "type": "module", + "scripts": { + "dev": "cross-env NODE_ENV=development vite dev --port 3000", + "build": "vite build", + "start": "node .output/server/index.mjs" + }, + "dependencies": { + "@base-ui/react": "^1.3.0", + "@hookform/resolvers": "^5.2.2", + "@ohbug-dashboard/db": "workspace:*", + "@orpc/client": "latest", + "@orpc/react-query": "latest", + "@orpc/server": "latest", + "@tanstack/react-query": "latest", + "@tanstack/react-router": "latest", + "@tanstack/react-start": "latest", + "better-auth": "latest", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "common": "workspace:*", + "config": "workspace:*", + "date-fns": "^4.1.0", + "input-otp": "^1.4.2", + "lucide-react": "^1.7.0", + "next-themes": "^0.4.6", + "react": "^19.2.4", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.4", + "react-hook-form": "^7.72.0", + "source-map-trace": "^0.2.6", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^4.3.6", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "nitro": "^3.0.0", + "tailwindcss": "^4.2.2" + } +} diff --git a/packages/web/public/favicon.ico b/apps/web/public/favicon.ico similarity index 100% rename from packages/web/public/favicon.ico rename to apps/web/public/favicon.ico diff --git a/packages/web/public/logo-white.svg b/apps/web/public/logo-white.svg similarity index 100% rename from packages/web/public/logo-white.svg rename to apps/web/public/logo-white.svg diff --git a/packages/web/public/logo.svg b/apps/web/public/logo.svg similarity index 100% rename from packages/web/public/logo.svg rename to apps/web/public/logo.svg diff --git a/apps/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx new file mode 100644 index 0000000..a8011ca --- /dev/null +++ b/apps/web/src/components/ui/accordion.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; +import { ChevronDownIcon } from "lucide-react"; +import type React from "react"; + +import { cn } from "~/lib/utils"; + +export function Accordion(props: AccordionPrimitive.Root.Props): React.ReactElement { + return ; +} + +export function AccordionItem({ + className, + ...props +}: AccordionPrimitive.Item.Props): React.ReactElement { + return ( + + ); +} + +export function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props): React.ReactElement { + return ( + + + {children} + + + + ); +} + +export function AccordionPanel({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props): React.ReactElement { + return ( + +
{children}
+
+ ); +} + +export { AccordionPrimitive, AccordionPanel as AccordionContent }; diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..3e7596f --- /dev/null +++ b/apps/web/src/components/ui/alert-dialog.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; +import type React from "react"; + +import { cn } from "~/lib/utils"; + +export const AlertDialogCreateHandle: typeof AlertDialogPrimitive.createHandle = + AlertDialogPrimitive.createHandle; + +export const AlertDialog: typeof AlertDialogPrimitive.Root = AlertDialogPrimitive.Root; + +export const AlertDialogPortal: typeof AlertDialogPrimitive.Portal = AlertDialogPrimitive.Portal; + +export function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props): React.ReactElement { + return ; +} + +export function AlertDialogBackdrop({ + className, + ...props +}: AlertDialogPrimitive.Backdrop.Props): React.ReactElement { + return ( + + ); +} + +export function AlertDialogViewport({ + className, + ...props +}: AlertDialogPrimitive.Viewport.Props): React.ReactElement { + return ( + + ); +} + +export function AlertDialogPopup({ + className, + bottomStickOnMobile = true, + ...props +}: AlertDialogPrimitive.Popup.Props & { + bottomStickOnMobile?: boolean; +}): React.ReactElement { + return ( + + + + + + + ); +} + +export function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">): React.ReactElement { + return ( +
+ ); +} + +export function AlertDialogFooter({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & { + variant?: "default" | "bare"; +}): React.ReactElement { + return ( +
+ ); +} + +export function AlertDialogTitle({ + className, + ...props +}: AlertDialogPrimitive.Title.Props): React.ReactElement { + return ( + + ); +} + +export function AlertDialogDescription({ + className, + ...props +}: AlertDialogPrimitive.Description.Props): React.ReactElement { + return ( + + ); +} + +export function AlertDialogClose(props: AlertDialogPrimitive.Close.Props): React.ReactElement { + return ; +} + +export { + AlertDialogPrimitive, + AlertDialogBackdrop as AlertDialogOverlay, + AlertDialogPopup as AlertDialogContent, +}; diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx new file mode 100644 index 0000000..9b06119 --- /dev/null +++ b/apps/web/src/components/ui/alert.tsx @@ -0,0 +1,79 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +const alertVariants = cva( + "relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4", + { + defaultVariants: { + variant: "default", + }, + variants: { + variant: { + default: "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground", + error: "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive", + info: "border-info/32 bg-info/4 [&>svg]:text-info", + success: "border-success/32 bg-success/4 [&>svg]:text-success", + warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning", + }, + }, + }, +); + +export function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps): React.ReactElement { + return ( +
+ ); +} + +export function AlertTitle({ + className, + ...props +}: React.ComponentProps<"div">): React.ReactElement { + return ( +
+ ); +} + +export function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">): React.ReactElement { + return ( +
+ ); +} + +export function AlertAction({ + className, + ...props +}: React.ComponentProps<"div">): React.ReactElement { + return ( +
+ ); +} diff --git a/apps/web/src/components/ui/autocomplete.tsx b/apps/web/src/components/ui/autocomplete.tsx new file mode 100644 index 0000000..4f15897 --- /dev/null +++ b/apps/web/src/components/ui/autocomplete.tsx @@ -0,0 +1,297 @@ +"use client"; + +import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete"; +import { ChevronsUpDownIcon, XIcon } from "lucide-react"; +import type React from "react"; + +import { Input } from "~/components/ui/input"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { cn } from "~/lib/utils"; + +export const Autocomplete: typeof AutocompletePrimitive.Root = AutocompletePrimitive.Root; + +export function AutocompleteInput({ + className, + showTrigger = false, + showClear = false, + startAddon, + size, + triggerProps, + clearProps, + ...props +}: Omit & { + showTrigger?: boolean; + showClear?: boolean; + startAddon?: React.ReactNode; + size?: "sm" | "default" | "lg" | number; + ref?: React.Ref; + triggerProps?: AutocompletePrimitive.Trigger.Props; + clearProps?: AutocompletePrimitive.Clear.Props; +}): React.ReactElement { + const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; + + return ( + + {startAddon && ( + + )} + } + {...props} + /> + {showTrigger && ( + + + + + + )} + {showClear && ( + + + + )} + + ); +} + +export function AutocompletePopup({ + className, + children, + side = "bottom", + sideOffset = 4, + alignOffset, + align = "start", + anchor, + ...props +}: AutocompletePrimitive.Popup.Props & { + align?: AutocompletePrimitive.Positioner.Props["align"]; + sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"]; + alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"]; + side?: AutocompletePrimitive.Positioner.Props["side"]; + anchor?: AutocompletePrimitive.Positioner.Props["anchor"]; +}): React.ReactElement { + return ( + + + + + {children} + + + + + ); +} + +export function AutocompleteItem({ + className, + children, + ...props +}: AutocompletePrimitive.Item.Props): React.ReactElement { + return ( + + {children} + + ); +} + +export function AutocompleteSeparator({ + className, + ...props +}: AutocompletePrimitive.Separator.Props): React.ReactElement { + return ( + + ); +} + +export function AutocompleteGroup({ + className, + ...props +}: AutocompletePrimitive.Group.Props): React.ReactElement { + return ( + + ); +} + +export function AutocompleteGroupLabel({ + className, + ...props +}: AutocompletePrimitive.GroupLabel.Props): React.ReactElement { + return ( + + ); +} + +export function AutocompleteEmpty({ + className, + ...props +}: AutocompletePrimitive.Empty.Props): React.ReactElement { + return ( + + ); +} + +export function AutocompleteRow({ + className, + ...props +}: AutocompletePrimitive.Row.Props): React.ReactElement { + return ( + + ); +} + +export function AutocompleteValue({ + ...props +}: AutocompletePrimitive.Value.Props): React.ReactElement { + return ; +} + +export function AutocompleteList({ + className, + ...props +}: AutocompletePrimitive.List.Props): React.ReactElement { + return ( + + + + ); +} + +export function AutocompleteClear({ + className, + ...props +}: AutocompletePrimitive.Clear.Props): React.ReactElement { + return ( + + + + ); +} + +export function AutocompleteStatus({ + className, + ...props +}: AutocompletePrimitive.Status.Props): React.ReactElement { + return ( + + ); +} + +export function AutocompleteCollection({ + ...props +}: AutocompletePrimitive.Collection.Props): React.ReactElement { + return ; +} + +export function AutocompleteTrigger({ + className, + children, + ...props +}: AutocompletePrimitive.Trigger.Props): React.ReactElement { + return ( + + {children} + + ); +} + +export const useAutocompleteFilter: typeof AutocompletePrimitive.useFilter = + AutocompletePrimitive.useFilter; + +export { AutocompletePrimitive }; diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx new file mode 100644 index 0000000..1900172 --- /dev/null +++ b/apps/web/src/components/ui/avatar.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; +import type React from "react"; + +import { cn } from "~/lib/utils"; + +export function Avatar({ className, ...props }: AvatarPrimitive.Root.Props): React.ReactElement { + return ( + + ); +} + +export function AvatarImage({ + className, + ...props +}: AvatarPrimitive.Image.Props): React.ReactElement { + return ( + + ); +} + +export function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.Fallback.Props): React.ReactElement { + return ( + + ); +} + +export { AvatarPrimitive }; diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..83978bd --- /dev/null +++ b/apps/web/src/components/ui/badge.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; +import { cva, type VariantProps } from "class-variance-authority"; +import type React from "react"; + +import { cn } from "~/lib/utils"; + +export const badgeVariants = cva( + "relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm border border-transparent font-medium outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button&,a&]:cursor-pointer [button&,a&]:pointer-coarse:after:absolute [button&,a&]:pointer-coarse:after:size-full [button&,a&]:pointer-coarse:after:min-h-11 [button&,a&]:pointer-coarse:after:min-w-11", + { + defaultVariants: { + size: "default", + variant: "default", + }, + variants: { + size: { + default: + "h-5.5 min-w-5.5 px-[calc(--spacing(1)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs", + lg: "h-6.5 min-w-6.5 px-[calc(--spacing(1.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm", + sm: "h-5 min-w-5 rounded-[.25rem] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]", + }, + variant: { + default: "bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90", + destructive: "bg-destructive text-white [button&,a&]:hover:bg-destructive/90", + error: "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16", + info: "bg-info/8 text-info-foreground dark:bg-info/16", + outline: + "border-input bg-background text-foreground dark:bg-input/32 [button&,a&]:hover:bg-accent/50 dark:[button&,a&]:hover:bg-input/48", + secondary: "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90", + success: "bg-success/8 text-success-foreground dark:bg-success/16", + warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16", + }, + }, + }, +); + +export interface BadgeProps extends useRender.ComponentProps<"span"> { + variant?: VariantProps["variant"]; + size?: VariantProps["size"]; +} + +export function Badge({ + className, + variant, + size, + render, + ...props +}: BadgeProps): React.ReactElement { + const defaultProps = { + className: cn(badgeVariants({ className, size, variant })), + "data-slot": "badge", + }; + + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">(defaultProps, props), + render, + }); +} diff --git a/apps/web/src/components/ui/breadcrumb.tsx b/apps/web/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..da83610 --- /dev/null +++ b/apps/web/src/components/ui/breadcrumb.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +export function Breadcrumb({ ...props }: React.ComponentProps<"nav">): React.ReactElement { + return