在 Next.js 应用中接入@thedaviddias/analytics:OpenPanel 统一埋点方案实战指南
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
@thedaviddias/analytics是 Front-End-Checklist 仓库(packages/analytics)中面向OpenPanel的统一分析包,它把页面浏览量、外链点击追踪和用户身份同步开箱即用地封装成 Next.js 组件与 SDK 单例。读完本文,你将掌握如何在根布局中注入 nonce 感知的 OpenPanel 脚本、通过自有域名代理请求以规避广告拦截器、在 API 路由与 Server Actions 中完成服务端事件上报,以及如何在严格的 CSP(内容安全策略)和 Turborepo 缓存体系下正确配置与运行它。
一、包概览:四个导出,覆盖客户端到服务端
包入口定义在 index.tsx,完整导出关系记录在 package.json 的exports字段中,共有四个导入路径:
| 导入路径 | 导出内容 | 说明 |
|---|---|---|
@thedaviddias/analytics | AnalyticsProvider | 渲染带 nonce 感知能力的 OpenPanel 脚本(loader + init) |
@thedaviddias/analytics/head | AnalyticsHead | 兼容旧代码的包装组件,内部转调AnalyticsProvider |
@thedaviddias/analytics/server | opServer | 服务端 OpenPanel SDK 单例,另导出openPanelApiUrl与hasOpenPanelServerConfig |
@thedaviddias/analytics/providers/openpanel-identify | OpenPanelIdentify | 客户端组件,把认证用户同步到 OpenPanel |
其中AnalyticsHead的实现在 head.tsx:它只是AnalyticsProvider的向后兼容包装(同时重导出OpenPanelIdentify),如果你的项目代码库中还残留着旧式AnalyticsHead用法,可以直接替换为AnalyticsProvider,两者接收相同的clientId与nonce属性。
从AnalyticsProvider的源码可以看到一个关键防御逻辑:当clientId为空或全空白时组件直接返回null,不渲染任何脚本(index.tsx)。这意味着即使环境变量缺失,也不会破坏页面渲染或抛出异常,非常适合"分析能力按需开启"的渐进式接入。
二、环境变量:三枚键的正确打开方式
根据 README 中的环境变量表,接入 OpenPanel 共涉及三个变量:
| 变量 | 是否必需 | 作用域 | 说明 |
|---|---|---|---|
NEXT_PUBLIC_OPENPANEL_CLIENT_ID | OpenPanel 场景必需 | Client + Server | 自托管 OpenPanel 控制台签发的客户端 ID |
OPENPANEL_CLIENT_SECRET | 仅服务端追踪时需要 | Server only | 客户端密钥,严禁暴露给浏览器 |
OPENPANEL_API_URL | 否 | Server only | 自托管 API 基址,默认https://stats.daviddias.digital/api,供代理与 SDK 使用 |
这三个变量在服务端单例中的真实读取逻辑见 server.ts:
const clientId = process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID?.trim() ?? '' const clientSecret = process.env.OPENPANEL_CLIENT_SECRET?.trim() ?? '' export const openPanelApiUrl = process.env.OPENPANEL_API_URL?.trim() || 'https://stats.daviddias.digital/api' export const hasOpenPanelServerConfig = Boolean(clientId && clientSecret) export const opServer = new OpenPanel({ apiUrl: openPanelApiUrl, clientId, clientSecret })值得注意的细节:
server.ts首行引入server-only,从模块层面保证这些代码永远不会被打进客户端 bundle,这是密钥安全的第一道防线;hasOpenPanelServerConfig作为配置就绪标志,供上层判断"服务端追踪是否真正可用";opServer是模块级单例,@openpanel/sdk的OpenPanel实例在整个进程生命周期内只创建一次。
Turborepo 缓存失效问题
README 特别强调:三个变量必须加入turbo.json→tasks.build.env,否则 Turborepo 在变量变化时不会使构建缓存失效。当前仓库根目录的 turbo.json 已经按此配置,其中build任务的env数组中包含:
"NEXT_PUBLIC_OPENPANEL_CLIENT_ID", "OPENPANEL_API_URL", "OPENPANEL_CLIENT_SECRET"(此外还包含OPENPANEL_SECRET_ID等其他部署相关变量。)在你自己接入该包时,务必同步维护这一配置,否则在 CI 或本地切换环境变量后可能拿到"过期"的构建产物。
三、安装与接入:五步完成整套埋点
1. 安装包
pnpm add @thedaviddias/analytics该包依赖@openpanel/sdk、@openpanel/web、@repo/auth(用于会话)、next与react,且被标记为"private": true、"sideEffects": false(见 package.json),便于 tree-shaking。
2. 在根布局注入 Provider
将AnalyticsProvider与OpenPanelIdentify放进<body>内(README 原示例):
// app/layout.tsx import { AnalyticsProvider } from '@thedaviddias/analytics' import { OpenPanelIdentify } from '@thedaviddias/analytics/providers/openpanel-identify' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <AnalyticsProvider clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID} nonce={nonce} /> <OpenPanelIdentify /> {children} </body> </html> ) }本仓库 apps/web/app/layout.tsx 的实际用法是:先从环境变量读取openPanelClientId,只有存在时才渲染AnalyticsProvider,并且用条件表达式{openPanelClientId ? <OpenPanelIdentify /> : null}控制身份同步组件——这比无脑渲染更稳妥,可避免在未配置 OpenPanel 的环境中出现无意义的空组件。
nonce属性是可选的。若你的站点使用严格 nonce 型 CSP,请把请求级 nonce 传进来;否则可省略。
3. 创建 OpenPanel 代理路由
为了让分析请求走自有域名(规避广告拦截器),需要创建app/api/op/[...path]/route.ts:
import { createRouteHandler } from '@openpanel/nextjs/server' import { openPanelApiUrl } from '@thedaviddias/analytics/server' export const { GET, POST } = createRouteHandler({ apiUrl: openPanelApiUrl })仓库内的真实实现完全一致,见 apps/web/app/api/op/[...path]/route.ts。apiUrl取自openPanelApiUrl,因此代理的目标地址可通过OPENPANEL_API_URL环境变量灵活切换(自托管地址或默认的https://stats.daviddias.digital/api)。
4. 更新中间件(Middleware)
代理路由就绪后,还需要在你的 middleware 中处理四点:
- 公开路由放行:加入
'/api/op/(.*)',让代理无需认证即可访问; - CSP script-src:将
https://openpanel.dev作为兜底来源加入 CSP 白名单; - 限流排除:把
/api/op/路径从速率限制中剔除(避免埋点请求被限流丢弃); - HTTP 方法允许:为
/api/op/显式放行POST。
Front-End-Checklist 本身把中间件逻辑收敛在 apps/web/proxy.ts,其 matcher 已排除静态资源与 favicon 等路径;接入时注意把代理路径纳入同样的排除/放行规则即可。
5. 将环境变量加入 turbo.json
按第二节所述,在turbo.json → tasks.build.env中补齐三个变量(当前仓库已配置,见 turbo.json)。
四、服务端事件追踪:不阻塞响应路径
对于 API 路由与 Server Actions,README 推荐使用trackServerEvent辅助函数配合 Vercel 的waitUntil,让埋点上报异步执行、不拖慢响应:
// lib/openpanel-server.ts import { waitUntil } from '@vercel/functions' import { opServer } from '@thedaviddias/analytics/server' export function trackServerEvent(event: string, properties?: Record<string, unknown>) { if (process.env.NODE_ENV !== 'production') return waitUntil(opServer.track(event, properties ?? {})) }这需要应用安装@vercel/functions,并设置OPENPANEL_CLIENT_SECRET。
本仓库在 apps/web/lib/telemetry-server.ts 中的生产级实现比 README 示例更严谨,可作参考模板:
export function trackServerEvent( event: TelemetryEventName, properties: ServerTelemetryProperties = {} ) { if (process.env.NODE_ENV !== 'production' || !hasOpenPanelServerConfig) { return } void opServer.track(event, properties).catch(error => { if (!sentryDsn) { return } Sentry.captureException(error, { tags: { 'app.feature': 'telemetry', 'app.telemetry_event': event }, extra: properties }) }) }这里增加了两个工程化细节:
- 双保险守卫:除
NODE_ENV !== 'production'外,还校验hasOpenPanelServerConfig(即 clientId 与 clientSecret 同时存在),避免在配置缺失时盲目调用 SDK; - 失败兜底:
.catch中把上报失败镜像到 Sentry,并附带app.feature: telemetry标签与事件名、属性等上下文,方便定位埋点链路故障。
该trackServerEvent被广泛用于业务侧,例如 apps/web/actions/checklist-actions.ts 中的checklistCreated/checklistUpdated等事件、apps/web/app/api/checklists/route.ts 等 API 路由,并在 apps/web/app/api/checklists/tests/route.test.ts 等测试中以 mock 形式验证调用次数与载荷。
五、用户身份同步:OpenPanelIdentify的内部机制
OpenPanelIdentify是'use client'客户端组件(providers/openpanel-identify.tsx),它借助 Better Auth 的authClient.useSession()钩子自动同步认证用户:
- 登录时:调用
window.op.identify(...),携带profileId(用户 ID)与name、email属性; - 登出时:调用
window.op.clear()清除身份; - 守卫:
window不存在(SSR)或window.op未挂载时直接返回;NODE_ENV !== 'production'时同样提前返回。
useEffect(() => { if (typeof window === 'undefined' || !window.op) return if (process.env.NODE_ENV !== 'production') return if (user) { window.op.identify({ profileId: user.id, properties: { name: user.name ?? undefined, email: user.email ?? undefined } }) } else { window.op.clear() } }, [user])它依赖@repo/auth的authClient(workspace 依赖,见 package.json),因此使用前请确保认证包已正确安装与初始化。
六、CSP 兼容性:为什么不用@openpanel/nextjs注入脚本
README 明确说明了一个设计取舍:AnalyticsProvider刻意不用@openpanel/nextjs做脚本注入,因为该库会以afterInteractive方式输出内联 init 脚本且不支持 nonce——在严格 nonce 型 CSP 下这类内联脚本会被直接拦截。
取而代之的方案在 providers/openpanel.tsx 中清晰可见:在服务端渲染脚本标签,并给每个<script>都带上nonce属性。该组件组装出完整的 init 代码:
const initOptions = { apiUrl: '/api/op', clientId, disabled: process.env.NODE_ENV !== 'production', sdk: 'nextjs', sdkVersion: '1.3.0', trackAttributes: true, trackOutgoingLinks: true, trackScreenViews: true } const globalProperties = { environment: process.env.NODE_ENV ?? 'development' } const initScript = `${getInitSnippet()} window.op('init', ${JSON.stringify(initOptions)}); window.op('setGlobalProperties', ${JSON.stringify(globalProperties)});` return ( <> <script async={true} defer={true} nonce={nonce} src="/api/op/op1.js" /> <script dangerouslySetInnerHTML={{ __html: initScript }} id="openpanel-init" nonce={nonce} /> </> )几个值得展开的默认行为(来自源码,属于实现事实):
apiUrl: '/api/op':客户端 SDK 一律走自有域名的代理路径,对应第三节创建的 Route Handler,这也是防广告拦截的关键;trackScreenViews/trackOutgoingLinks/trackAttributes默认全开:分别对应页面浏览、外链点击与 DOM 属性追踪,README 所说的"out of the box"即源于此;globalProperties.environment:把构建环境(development/production等)作为全局属性附加到每次事件,方便在 OpenPanel 控制台按环境筛选数据;disabled在非生产环境为true:与 README 的"Production guards"呼应——开发环境不会产生脏数据。
七、工作流程与生产守卫
数据流全景
README 给出的架构示意可结合源码验证:
AnalyticsProvider └─ OpenPanelAnalyticsComponent → loads op1.js via /api/op proxy └─ globalProperties: { environment } OpenPanelIdentify (body) └─ authClient.useSession() → window.op.identify() / window.op.clear() opServer.track() (server) └─ OpenPanel SDK → direct API call with clientSecret- 客户端链路:
AnalyticsProvider→OpenPanelAnalyticsComponent(providers/openpanel.tsx)→ 通过/api/op代理加载op1.js与 init 脚本; - 身份链路:
OpenPanelIdentify(body 内)订阅会话 →window.op.identify()/clear(); - 服务端链路:
opServer.track()→ OpenPanel SDK 直接携带clientSecret调 API(绕过浏览器,不经过代理)。
双重生产守卫
OpenPanel 追踪在生产环境之外被双重关闭:
- 客户端脚本:
OpenPanelAnalyticsComponent在非生产环境渲染时带disabled={true}(见 providers/openpanel.tsx); - 用户识别:
OpenPanelIdentify在非生产环境提前返回(见 providers/openpanel-identify.tsx)。
再叠加服务端的NODE_ENV与hasOpenPanelServerConfig双守卫(apps/web/lib/telemetry-server.ts),整条链路在开发、测试环境默认保持静默,只在生产环境产生真实数据。
八、接入检查清单
完成上述全部步骤后,建议逐项自查:
pnpm add @thedaviddias/analytics已完成,且@openpanel/sdk、@openpanel/web依赖正常解析;- 根布局
<body>内已放置AnalyticsProvider(可传nonce)与OpenPanelIdentify; - 已创建
app/api/op/[...path]/route.ts代理,且中间件对/api/op/(.*)放行(含POST); - 服务端追踪封装里同时校验
NODE_ENV === 'production'与hasOpenPanelServerConfig; NEXT_PUBLIC_OPENPANEL_CLIENT_ID、OPENPANEL_CLIENT_SECRET、OPENPANEL_API_URL均已加入turbo.json的tasks.build.env,且密钥未出现在客户端 bundle;- 本地
pnpm dev时页面不报错、控制台无 OpenPanel 脚本(生产守卫生效),部署到生产后页面浏览/外链点击/登录身份均能在 OpenPanel 控制台看到对应事件。
至此,你便获得了一套与 Front-End-Checklist 同源的、可复用的 OpenPanel 分析接入方案:既满足严格 CSP,又能抗广告拦截,还兼顾了客户端与服务端的完整埋点能力。
【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考