@evumi/tracker
v0.2.14
Published
Plausible-compatible analytics with shared route templates and framework adapters
Readme
@evumi/tracker
English · 简体中文
从公共 npm 安装,无需令牌:
npm install @evumi/tracker
# pnpm add @evumi/tracker
# yarn add @evumi/tracker
# bun add @evumi/trackerReact / Next.js / TanStack / Vue / Electron 共用此包的不同入口。原有私有 GitHub Packages 镜像继续发布;使用 npm 时删除 .npmrc 中旧的 @evumi:registry=https://npm.pkg.github.com,或仅将该 scope 改为 https://registry.npmjs.org,保留其他配置。可用 @evumi/tracker@<version> 固定版本;站点设置仍提供当前实例 .tgz 下载作为备用。
domain 是在 Evumi 中创建的被统计站点,apiHost 是你的 Evumi 实例 origin(如 https://stats.example.com,也支持端口)。SDK 自动追加 /api/event,不要在 apiHost 中填路径、查询参数或凭据。所有框架和 Electron 共用此配置,支持自部署。生产使用 HTTPS,并在 CSP connect-src 允许该地址;浏览器代码不需要 npm 或 Evumi 管理密钥。
沿用校验过的 Plausible v3.2.1 MIT tracker;路由适配器处理模板和重复导航。tracker 打包进引入 SDK 的模块,首个事件前不再额外加载 chunk。SDK 不占用 window.plausible,可与 Plausible 官方脚本或 npm SDK 共存;仅当页面已有向同一 Evumi 服务器上报的脚本标签时拒绝启动,避免重复计数。
Electron(渲染进程)
// src/analytics.ts,在 renderer 中使用;不要在 main 或 preload 发送。
import { createElectronTracker } from '@evumi/tracker/electron'
export const analytics = createElectronTracker({
domain: 'desktop.example.com', // 先在 Evumi 创建这个站点
apiHost: 'https://stats.example.com',
routePatterns: ['/projects/:id'],
})普通 Hash 路由:
import { installElectronHashRouter } from '@evumi/tracker/electron'
import { analytics } from './analytics'
const stop = installElectronHashRouter(analytics)
window.addEventListener('beforeunload', stop, { once: true })
// 业务成功回调,不在初始化时调用
export function onProjectCreated() {
void analytics.track('Project created', { props: { plan: 'pro' } })
}React / TanStack / Vue 桌面应用将这个 analytics 传入对应框架适配器即可,不再同时安装 Hash 监听。手动导航使用 analytics.pageview({ url: '/projects/123' })。无 Router 的不同窗口可传各自的应用路径,例如 /settings。每个渲染窗口独立初始化,统计仍按服务器的匿名会话规则归并,默认不新增永久设备 ID。
| 渲染 URL | 上报 URL(配置上述模板) |
| --- | --- |
| file:///Users/alice/app/index.html | https://desktop.example.com/ |
| file:///…/index.html#/projects/123 | https://desktop.example.com/projects/:id |
| app://renderer/projects/123 | https://desktop.example.com/projects/:id |
| http://localhost:5173/#/projects/123 | https://desktop.example.com/projects/:id |
默认解析 #/ 和 #!/,可用 hashRouting: false 关闭。不自动发送文件路径、文件 URL 的查询参数和 referrer;路由中的 query 继续使用公共白名单规则。显式传入的属性、URL 和事件名由接入方选择,勿传文件目录或用户身份数据。Electron 入口允许本地 renderer 发送,开发环境同样会计数,可按环境决定是否挂载。自动外链/文件下载/表单捕获关闭;使用明确的业务事件记录桌面操作。主进程直接调用会返回 skipped,没有实现主进程 Node SDK 或离线持久队列。
无需 Electron 依赖或 preload/IPC 桥;保留 contextIsolation: true、nodeIntegration: false、sandbox: true 和默认 webSecurity,在已有 CSP connect-src 中添加 Evumi 的 HTTPS origin。普通 file:// 的跨域请求使用 /api/event 的公开 CORS,不需要关闭安全设置。参见 Electron 安全指南 和 上下文隔离。
公共配置
import { createTracker } from '@evumi/tracker'
export const analytics = createTracker({
domain: 'example.com',
apiHost: 'https://stats.example.com',
routePatterns: ['/users/new', '/users/:id', '/products/:id/reviews'],
outboundLinks: true,
fileDownloads: true,
})
await analytics.pageview({ routeTemplate: '/users/:id' })
await analytics.track('Signup', { props: { plan: 'pro' } })
await analytics.track('Purchase', { revenue: { amount: 29, currency: 'USD' } })/users/123、/users/456 会归到 /users/:id,但两次实际导航仍产生两次 pageview;静态规则 /users/new 优先于动态规则。未命中显式规则的路径原样上报,不通过数字或 UUID 猜测路由。支持 :id、Next [id]/[...slug]/[[...slug]]、TanStack $id/$/{-$id}、Vue :pathMatch(.*)*;报表统一使用 :name。不支持的复杂路由正则会报错,应提供简单模板。
默认只保留 UTM、ref 和 source 参数,不发送 hash;其他参数需要 includeSearchParams: ['sort'] 显式允许。浏览器默认不统计 localhost,开发验证需要 captureOnLocalhost: true。沿用 Plausible localStorage.plausible_ignore 排除行为,默认不创建持久访客 ID 或 Cookie。
pageview 和 track 返回 { status: 'sent' | 'skipped' | 'error' }。sent 表示接收端返回成功 HTTP 状态,不代表统计已入库。可通过 onError 接错误;框架适配器不会把网络失败抛到页面。SSR 调用为 skipped,不访问浏览器对象。
React
import { EvumiProvider, EvumiPageview, useEvumi } from '@evumi/tracker/react'
import { analytics } from './analytics'
// 接收你实际使用的路由器状态;不另行 patch History API。
function App({ pathname, search }: { pathname: string; search: string }) {
return <EvumiProvider tracker={analytics}>
<EvumiPageview url={pathname + search} />
<YourRoutes />
</EvumiProvider>
}
// 后代组件内:const analytics = useEvumi(); analytics.track('Signup')pageview 与 Plausible 的 SPA 规则一致:仅在 pathname 变化时发送,因此仅 query/hash 变化、重复通知及 React StrictMode effect 重放会合并,真实的 A→B→A 导航和连续两次自定义事件保留。props 变化本身不触发重复 pageview。navigationId 已弃用并被忽略。与 Plausible 相同,隐藏或预渲染页面的首个 pageview 会等到页面可见再发送,从往返缓存(bfcache)恢复的页面会再次发送 pageview。
Next.js App Router
// app/analytics.tsx
'use client'
import { createTracker } from '@evumi/tracker'
import { EvumiNext } from '@evumi/tracker/nextjs'
const tracker = createTracker({
domain: 'example.com', apiHost: 'https://stats.example.com',
routePatterns: ['/products/[id]', '/users/[id]'],
})
export default function Analytics() { return <EvumiNext tracker={tracker} /> }
// 在 app/layout.tsx 的 body 内只挂载一次 <Analytics />App Router 的 usePathname() 只返回真实路径,不提供文件路由模板,因此需显式配置 routePatterns 或给 EvumiNext 传 routeTemplate。组件内置 Suspense 边界;重写路径同样由你显式映射。
Next.js Pages Router
// pages/_app.tsx
import { EvumiNextPages } from '@evumi/tracker/nextjs/pages'
import { analytics } from '../analytics'
export default function App({ Component, pageProps }) {
return <><EvumiNextPages tracker={analytics} /><Component {...pageProps} /></>
}读取 router.pathname(如 /products/[id]),等待 isReady 后上报。App/Pages 两个适配器按实际路由架构选用,不在同一页面重复挂载。
TanStack Router / Start
import { EvumiTanStack } from '@evumi/tracker/tanstack'
import { Outlet } from '@tanstack/react-router'
import { analytics } from './analytics'
// 放在 root route 组件内,位于 RouterProvider 下。
export function Root() { return <><EvumiTanStack tracker={analytics} /><Outlet /></> }导航成功且匹配完成后,读取最深层 match 的 fullPath,把 /users/$id 转为 /users/:id;不统计预加载、pending 或失败的导航。
Vue 3 / Vue Router
import { createEvumiPlugin } from '@evumi/tracker/vue'
import { analytics } from './analytics'
app.use(router)
app.use(createEvumiPlugin({ tracker: analytics, router }))
app.mount('#app')导航成功后读取最深层匹配记录的 path,初始路由就绪后补发首次浏览;取消/失败导航不统计。组件内 useEvumi() 可发送事件。也可以手动调用 installVueRouter(analytics, router),返回取消订阅函数。应用卸载会移除路由监听;上游 engagement 监听器按页面生命周期保留。
包边界和参考
只导入 core 不会引入 React、Vue 或 Next。框架是可选 peer dependency;浏览器 runtime 按需动态加载,并复用每页单例,多个框架入口不会重复注册 tracker。不同配置在同页初始化会返回错误。
可选浏览器匿名 ID
默认保持 Plausible 无 Cookie 模式。需要跨标签页、刷新和日期保持同一匿名访客时,在站点基本设置开启「浏览器匿名 ID」,并在共享 SDK 配置中添加 identity: 'browser'。所有框架和 Electron 入口共用该实现。
SDK 通过 IndexedDB 事务按当前 origin 与站点创建 UUID,避免多个标签页同时首次打开各自产生 ID。服务端按站点再次哈希,原始 ID 不进入队列和 DuckDB。存储不可用时回退默认模式;清理站点数据、切浏览器配置或设备会产生新 ID。同一浏览器配置多人共用无法识别人,未实现跨域 linker 或账号 identify。原版 Plausible JS 保持无 Cookie 模式。客户端收入需在站点设置允许后才接收。
Transient delivery failures
The SDK retries network errors, timeouts, 408, 429 and 5xx for up to five minutes. It keeps at most 64 events / 256 KB in first-party sessionStorage, falling back to memory when storage is unavailable. Per-event IDs prevent retry duplicates on the matching Evumi collector. Upgrade collector and analytics before SDK consumers. Closing the tab, clearing storage or exceeding limits can still lose events. See delivery guarantees.
