PostHog 适配器
PostHog 是一个开源的产品分析平台。evlog 的 PostHog 适配器通过标准 OTLP 格式将你的广泛事件发送到 PostHog Logs,为你提供一个带有过滤、搜索和尾部模式的专用日志查看器,并可利用现有的 PostHog API 密钥。
添加 PostHog 排水适配器
安装
PostHog 适配器已与 evlog 捆绑在一起:
import { createPostHogDrain } from 'evlog/posthog'
快速开始
1. 获取 PostHog 项目 API 密钥
- 登录你的 PostHog 控制面板
- 进入 设置 > 项目 > 项目 API 密钥
- 复制密钥(以
phc_开头)
2. 设置环境变量
POSTHOG_API_KEY=phc_your-project-api-key
3. 将排水管道连接到你的框架
// server/plugins/evlog-drain.ts
import { createPostHogDrain } from 'evlog/posthog'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
})
// lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createPostHogDrain } from 'evlog/posthog'
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'my-app',
drain: createPostHogDrain(),
})
import { createPostHogDrain } from 'evlog/posthog'
app.use(evlog({ drain: createPostHogDrain() }))
import { createPostHogDrain } from 'evlog/posthog'
app.use(evlog({ drain: createPostHogDrain() }))
import { createPostHogDrain } from 'evlog/posthog'
await app.register(evlog, { drain: createPostHogDrain() })
import { createPostHogDrain } from 'evlog/posthog'
app.use(evlog({ drain: createPostHogDrain() }))
import { createPostHogDrain } from 'evlog/posthog'
EvlogModule.forRoot({ drain: createPostHogDrain() })
import { createPostHogDrain } from 'evlog/posthog'
initLogger({ drain: createPostHogDrain() })
就这样!现在你的广泛事件将以完整的 OTLP 结构(包括严重级别、跟踪上下文和结构化属性)出现在 PostHog 日志中。
配置
适配器从多个来源读取配置(优先级从高到低):
- 通过
createPostHogDrain()传入的覆盖项 - 位于
runtimeConfig.posthog的运行时配置(仅限 Nuxt/Nitro) - 环境变量(
POSTHOG_*)
环境变量
| 变量 | 说明 |
|---|---|
POSTHOG_API_KEY | 项目 API 密钥(以 phc_ 开头) |
POSTHOG_HOST | PostHog 主机 URL(适用于 EU 或自托管) |
运行时配置(仅限 Nuxt)
通过 nuxt.config.ts 进行配置,以实现类型安全的配置:
export default defineNuxtConfig({
runtimeConfig: {
posthog: {
apiKey: '', // 通过 POSTHOG_API_KEY 设置
host: '', // 通过 POSTHOG_HOST 设置
},
},
})
覆盖选项
直接传递选项以覆盖任何配置:
const drain = createPostHogDrain({
host: 'https://eu.i.posthog.com',
timeout: 10000,
})
完整配置参考
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
apiKey | string | - | 项目 API 密钥(必填) |
host | string | https://us.i.posthog.com | PostHog 主机 URL |
distinctId | string | - | 每个事件使用的静态用户标识符 |
distinctIdField | string | userId | 保存用户标识符的事件字段(点号路径) |
sessionIdField | string | sessionId | 保存 PostHog 会话 ID 的事件字段(点号路径) |
recordShape | 'json' | 'compact' | 'json' | 日志记录形状,参见下文 |
timeout | number | 5000 | 请求超时时间,单位为毫秒 |
retries | number | 2 | 临时故障时的重试次数 |
日志与 PostHog 之间发生了什么
在底层,createPostHogDrain() 使用 PostHog 特定的默认值包装 OTLP 适配器的 sendBatchToOTLP():
- 端点:
{host}/i/v1/logs(PostHog 的 OTLP 日志摄取端点) - 身份验证:
Authorization: Bearer {apiKey}请求头 - 格式:标准 OTLP
ExportLogsServiceRequest,包含严重级别、跟踪上下文和结构化属性 - 身份标识:
posthogDistinctId和sessionId属性,PostHog 通过它们将日志与项目中的其他数据关联起来
选择记录形状
PostHog 将日志属性视为分面:你可以按这些属性进行过滤、细分和设置警报。使用默认的 json 形状时,嵌套字段会作为一个序列化属性到达(ai = {"calls":2,"costUsd":0.0012}),PostHog 可以显示它,但无法据此绘制图表。
compact 会将这些字段扁平化为 ai.calls 和 ai.costUsd,并使用一行摘要替代正文,从而避免重复整个事件:
const drain = createPostHogDrain({ recordShape: 'compact' })
这是 PostHog 推荐的形状。它还会减少发送的数据量:Logs 按摄取的数据量计费,而默认形状会将每个字段传输两次:一次位于正文中,一次位于属性中。
compact 将成为默认值。现在就在新项目中设置它;之后切换意味着需要重写基于 json 形状构建的已保存视图和警报。将日志关联到用户和会话回放
携带用户标识符的日志会显示在该用户的 PostHog 个人资料中的 日志 选项卡下,无需通过猜测服务名称来查找特定用户访问过的内容。如果同时携带会话 ID,日志还会关联到该用户的会话回放。
PostHog 从日志属性中读取这两个值:使用 posthogDistinctId 表示用户,使用 sessionId 表示回放。适配器会从你的广泛事件中填充它们,因此只要事件携带这些值,就可以直接使用:
const log = useLogger(event)
log.set({
userId: user.id, // → posthogDistinctId,关联到该用户
sessionId: body.sessionId, // → sessionId,关联到会话回放
})
会话 ID 来自前端。使用 posthog.get_session_id() 读取它,并将其随请求一起发送:
import posthog from 'posthog-js'
await $fetch('/api/checkout', {
method: 'POST',
body: { ...payload, sessionId: posthog.get_session_id() },
})
指定你自己的字段
如果用户身份位于事件中的其他位置,可以让适配器指向该字段。两个选项都接受点号路径:
const drain = createPostHogDrain({
distinctIdField: 'user.id',
sessionIdField: 'session.id',
})
对于 eve agent,调用方主体就是 eve 用于路由的身份:
const drain = createPostHogDrain({ distinctIdField: 'eve.caller.principalId' })
静态 distinctId 会完全覆盖字段查找。对于作为单一身份运行而非代表用户运行的后端,可以使用它。
distinct_id 进行匹配,因此该用户的任一标识符都可以使用。属性键可以在每个项目的 设置 > 日志 中配置。将其保留为默认值 posthogDistinctId,即可直接使用。区域
PostHog 提供美国和欧盟云托管。设置 host 以匹配你的区域:
| 区域 | 主机 |
|---|---|
| 美国(默认) | https://us.i.posthog.com |
| 欧盟 | https://eu.i.posthog.com |
| 自托管 | 你的实例 URL |
# 欧盟区域
POSTHOG_API_KEY=phc_xxx
POSTHOG_HOST=https://eu.i.posthog.com
在 PostHog 中查询日志
一旦日志开始流动,请在 PostHog 的 日志 选项卡中查询它们:
- 进入 日志,并按服务、严重级别或任何结构化属性进行过滤
- 使用搜索栏查找特定日志条目
- 点击日志条目以查看所有结构化属性
将自定义事件与日志一起发送
如果你更喜欢将日志作为 PostHog 自定义事件发送(例如用于产品分析、群组或漏斗),请使用带有 mode: 'events' 的 createPostHogDrain():
import { createPostHogDrain } from 'evlog/posthog'
const drain = createPostHogDrain({
mode: 'events',
eventName: 'server_request',
distinctId: 'my-backend-service',
})
然后像默认日志排水管道一样,将 drain 传递给你的框架(参见上面的快速开始)。
createPostHogDrain())成本要低得多。createPostHogEventsDrain() 已弃用,并会重定向到 createPostHogDrain({ mode: 'events' })。它将在下一个主要版本中移除。配置要发送的事件
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
apiKey | string | - | 项目 API 密钥(必填) |
host | string | https://us.i.posthog.com | PostHog 主机 URL |
eventName | string | evlog_wide_event | PostHog 事件名称 |
distinctId | string | - | 所有事件使用的静态 distinct_id |
distinctIdField | string | userId | 保存 distinct_id 的事件字段(点号路径) |
recordShape | 'json' | 'compact' | 'json' | 事件属性形状:compact 会扁平化嵌套字段(参见下文) |
timeout | number | 5000 | 请求超时时间,单位为毫秒 |
事件格式
evlog 将广泛事件映射到 PostHog 事件:
| evlog 字段 | PostHog 字段 |
|---|---|
config.distinctId 或 userId 或 service | distinct_id(回退链) |
timestamp | timestamp |
level | properties.level |
service | properties.service |
environment | properties.environment |
| 所有其他字段 | properties.* |
使用 recordShape: 'compact' 时,其他字段会以与日志模式相同的方式扁平化为点号键:嵌套的 ai 对象会作为 properties.ai.costUsd 和 properties.ai.calls 到达,而不是作为一个不透明的 properties.ai 值。PostHog 可以对这些单独的属性进行过滤和细分;默认的 json 形状会将嵌套对象保留为单个序列化属性。有关其中的权衡,以及它如何影响已保存的视图和警报,请参见选择记录形状。
distinct ID 的解析方式
distinct_id 遵循回退链:
config.distinctId—createPostHogDrain({ mode: 'events' })中的显式覆盖项event.userId— 或distinctIdField指向的字段(当其值为字符串或数字时)event.service— 最终回退值,以匿名事件发送
已识别事件和匿名事件
解析为真实用户的事件属于已识别事件:PostHog 会为其创建用户个人资料并附加用户属性。
当没有解析出标识符时,事件会作为匿名事件发送($process_person_profile: false),而不是将每个请求都归入一个以服务命名的“用户”。PostHog 对匿名事件的收费更低,并且不会将其纳入用户个人资料。
// 已识别:携带 `userId` 的事件会创建并更新用户个人资料
const drain = createPostHogDrain({ mode: 'events' })
// 将其识别为单一后端身份,无论请求来自何处
const service = createPostHogDrain({ mode: 'events', distinctId: 'checkout-worker' })
何时使用日志,何时使用事件
createPostHogDrain() | createPostHogDrain({ mode: 'events' }) | |
|---|---|---|
| 格式 | OTLP 日志(/i/v1/logs) | PostHog 事件(/batch/) |
| PostHog UI | 日志查看器 | 事件探索器 |
| 成本 | 较低(专用的日志管道) | 较高(计入事件) |
| 最佳用途 | 调试、日志搜索、监控 | 产品分析、群组、漏斗 |
你可以同时使用两种排水管道以获得最佳效果:
import { createPostHogDrain } from 'evlog/posthog'
const logs = createPostHogDrain()
const events = createPostHogDrain({ mode: 'events' })
const drain = async (ctx) => {
await Promise.allSettled([logs(ctx), events(ctx)])
}
故障排除
缺少 API 密钥错误
[evlog/posthog] 缺少 apiKey。设置 POSTHOG_API_KEY 环境变量或传递给 createPostHogDrain()
确保环境变量已设置,并且在添加后重新启动了服务器。
事件未出现
PostHog 异步处理事件。事件可能需要短暂延迟(通常不到 1 分钟)才能在仪表板中显示。
- 检查服务器控制台是否有
[evlog/posthog]错误信息 - 验证 API 密钥是否正确并以
phc_开头 - 确认
host与你的 PostHog 区域(美国或欧盟)匹配
区域错误
如果你在 PostHog EU 上但使用了默认的美国主机,事件交付将失败,并且适配器会在服务器控制台记录错误(例如 [evlog/posthog])。设置正确的主机:
POSTHOG_HOST=https://eu.i.posthog.com
不使用框架调用排水管道
对于高级用例,你可以使用更低级别的函数:
import { sendToPostHog, sendBatchToPostHog } from 'evlog/posthog'
// 发送单个事件到 PostHog 日志(OTLP)
await sendToPostHog(event, {
apiKey: 'phc_xxx',
})
// 批量发送多个事件
await sendBatchToPostHog(events, {
apiKey: 'phc_xxx',
})
对于自定义事件,请使用专用的事件函数:
import { sendToPostHogEvents, sendBatchToPostHogEvents, toPostHogEvent } from 'evlog/posthog'
// 发送单个自定义事件
await sendToPostHogEvents(event, {
apiKey: 'phc_xxx',
})
// 批量发送多个自定义事件
await sendBatchToPostHogEvents(events, {
apiKey: 'phc_xxx',
})
// 转换为 PostHog 格式(用于检查)
const posthogEvent = toPostHogEvent(event, { apiKey: 'phc_xxx' })