Cal.com 集成 Fathom Analytics:为预约事件接入隐私友好的网站分析
Fathom Analytics 是一款以“简单、隐私优先”为核心理念的网站分析服务,作为 GDPR 合规的 Google Analytics 替代方案,它在 Cal.com 中以内置应用(App)的形式存在,让你无需额外埋点即可跟踪预约(Booking)页面的访问与转化数据。本文以仓库中的 fathom 应用目录 为线索,完整讲解该应用的定位、安装机制、Tracking ID 配置方式,以及脚本注入到预约页面的底层渲染链路,读完你即可在自托管或云端 Cal.com 中独立完成配置并理解其实现原理。
Fathom 应用是什么
官方文档 DESCRIPTION.md 对该应用给出了精确定位:
Fathom Analytics provides simple, privacy-focused website analytics. We're a GDPR-compliant, Google Analytics alternative. Use the Fathom app to track analytics of your bookings.
翻译过来即:Fathom Analytics 提供简单、隐私优先的网站分析能力,是 GDPR 合规的 Google Analytics 替代方案;在 Cal.com 中使用 Fathom 应用,可以直接跟踪预约事件的埋点数据。这一定位同时回答了“为什么选 Fathom”——相比传统分析工具,它不依赖 Cookie 追踪,契合 GDPR 合规诉求;以及“在 Cal.com 中怎么用”——它不是独立部署的服务,而是作为事件类型(EventType)的一个扩展能力嵌入预约流程。
从包结构看,packages/app-store/fathom/ 是一个完整、自洽的应用模块,包含配置(config.json)、安装处理器(api/)、数据校验(zod.ts)与 UI 组件(components/),这使它能够无缝接入 Cal.com 的应用商店体系。
应用元数据与定位:config.json 详解
Fathom 应用的所有商店级元数据都定义在 config.json 中,这也是 Cal.com 应用商店识别、归类该应用的唯一依据:
| 配置字段 | 值 | 含义 |
|---|---|---|
name |
Fathom |
应用在商店中的显示名称 |
slug |
fathom |
应用唯一标识,官方注释明确要求“Don't modify slug”,如需改动须通过 CLI 编辑命令完成 |
type |
fathom_analytics |
应用类型,用于在数据库中标识凭据类型 |
variant |
analytics |
应用变体,归入分析类 |
categories |
["analytics"] |
应用分类,用于商店浏览与筛选 |
publisher |
Cal.com, Inc. |
发布方 |
extendsFeature |
EventType |
关键字段:该应用扩展的是“事件类型”功能,即按预约事件粒度生效 |
isOAuth |
false |
非 OAuth 应用,无需授权码流程 |
appData.tag.scripts |
Fathom 官方脚本 | 注入到预约页面的脚本定义(见下文) |
其中 extendsFeature: "EventType" 决定了该应用的集成深度:它不像连接器类应用那样同步日历或视频会议,而是给每个事件类型(EventType)追加一段可开关的分析脚本。type 与 slug 的组合则是运行时将应用与其存储数据关联起来的关键。
appData.tag:脚本注入的声明式定义
config.json 中最核心的部分是 appData.tag:
"appData": {
"tag": {
"scripts": [
{
"src": "https://cdn.usefathom.com/script.js",
"attrs": {
"data-site": "{TRACKING_ID}"
}
}
]
}
}
它声明:当该应用在某个事件类型上启用时,预约页面需要加载 https://cdn.usefathom.com/script.js,并携带 data-site 属性。属性值使用了 {TRACKING_ID} 占位符——这个占位符会在渲染时被替换为管理员在事件类型设置里填写的真实 Tracking ID(替换机制见下文“渲染链路”小节)。data-site 正是 Fathom 官方脚本识别站点 ID 的标准属性,与 Fathom 控制台中创建站点后生成的 ID 一一对应。
这种“配置驱动脚本注入”的设计意味着 Fathom 应用本身不包含任何服务器端埋点逻辑,完全依赖前端脚本标签完成数据采集,这是它与 Cal.com 中其他分析类应用共有的声明式(Declarative)模式。
安装机制:声明式处理器与默认凭据
Fathom 应用的安装逻辑位于 api/add.ts,整个文件只有十几行,体现了 Cal.com “声明式应用”(declarative app)的极简风格:
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import { createDefaultInstallation } from "../../_utils/installation";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
};
export default handler;
关键点如下:
handlerType: "add"表示这是安装型处理器,由 api/index.ts 统一导出。supportsMultipleInstalls: false表明每个用户/团队只允许安装一次——分析应用通常一个站点对应一个 ID,无需重复安装。createCredential调用createDefaultInstallation(定义于packages/app-store/_utils/installation目录)创建一条空凭据记录(key: {})。Fathom 不需要服务端密钥或 OAuth Token,因为真正的接入凭据是每个事件类型上独立的 Tracking ID,由用户在 UI 中填写,而非安装在应用级凭据里。这从源码结构看是“应用级空凭据 + 事件级 Tracking ID”的双层设计。
数据模型:trackingId 的 Zod 校验
事件类型上保存的 Fathom 配置由 zod.ts 定义:
import { z } from "zod";
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
export const appDataSchema = eventTypeAppCardZod.merge(
z.object({
trackingId: z.string().default("").optional(),
})
);
export const appKeysSchema = z.object({});
appDataSchema在共享的 eventTypeAppCardZod.ts(包含enabled、credentialId、appCategories三个通用字段)之上合并出trackingId: z.string().default("").optional()——即每个事件类型可存一个字符串类型的 Tracking ID,默认为空字符串,可选。appKeysSchema为空对象,再次印证应用级密钥为空,全部配置都落在事件级别的appData中。
这段代码是“Tracking ID 存哪里、存什么类型”的唯一事实来源:它被写入事件类型的 metadata.apps 下对应 slug 的字段中,并在 UI 与页面渲染两侧共用同一 schema 保证类型安全。
事件类型设置界面:在哪里填写 Tracking ID
配置入口由两个组件组成:
- EventTypeAppCardInterface.tsx:事件类型编辑页中 Fathom 应用的卡片外壳,通过
useAppContextWithSchema读写 appData,通过useIsAppEnabled控制开关(switchOnClick切换enabled状态)。 - EventTypeAppSettingsInterface.tsx:卡片内的设置表单,只有一个输入框:
const trackingId = getAppData("trackingId");
return (
<TextField
dataTestid={slug}
name="Tracking ID"
value={trackingId}
disabled={disabled}
onChange={(e) => {
setAppData("trackingId", e.target.value);
}}
/>
);
也就是说,你只需要在事件类型的 Fathom 卡片中填入 Fathom 控制台生成的站点 ID(Tracking ID),其余全部由系统接管。dataTestid={slug} 也便于 Playwright 等测试工具按 fathom 定位该输入框。
渲染链路:脚本如何出现在预约页面上
从应用配置到预约页面实际加载脚本,中间的关键枢纽是 BookingPageTagManager.tsx。从该文件的源码结构可以还原整条链路:
- BookingPageTagManager 遍历事件类型已启用的应用,读取每个应用的
app.appData.tag(注释明确说明“AnalyticApp has appData.tag always set”,即分析类应用必然携带tag)。 - 取出
tag.scripts数组,与可能的推送事件脚本合并后逐个渲染为<script>标签(对应tag.scripts.concat(...)与parsedAttributes的属性解析逻辑)。 - 渲染前会把脚本
attrs中的{TRACKING_ID}占位符替换为当前事件类型 appData 中保存的trackingId值——这正是config.json里"data-site": "{TRACKING_ID}"与zod.ts里trackingId字段对接的落点。
因此完整的数据流是:
config.json (声明 script 模板)
│
▼
zod.ts (定义 trackingId 字段并校验)
│
▼
事件类型设置页 (EventTypeAppSettingsInterface 填写 Tracking ID)
│
▼
BookingPageTagManager (读取 appData.tag,替换 {TRACKING_ID},注入 <script>)
│
▼
预约页面 → 加载 cdn.usefathom.com/script.js → Fathom 采集数据
从源码结构看,这一“声明式 tag 渲染器”是通用的,其他分析类应用(同样通过 appData.tag.scripts 声明脚本)也复用同一条链路。
安装与使用步骤
- 安装应用:在 Cal.com 的应用商店中搜索并安装 Fathom(需具有事件类型管理权限)。由于
supportsMultipleInstalls: false,每个账户仅可安装一次。 - 创建 Fathom 站点:在 Fathom Analytics 控制台创建一个新站点,获取站点 Tracking ID(对应脚本中的
data-site值)。 - 打开事件类型设置:进入任意事件类型的编辑页面,找到 Fathom 应用卡片,打开启用开关(触发
enabled字段写入)。 - 填写 Tracking ID:在 “Tracking ID” 输入框中粘贴第 2 步获取的 ID,保存设置。
- 验证埋点:访问该事件类型的公开预约页面,在浏览器开发者工具中确认
https://cdn.usefathom.com/script.js已加载,且data-site属性值为你的真实 ID;随后即可在 Fathom 控制台看到来自预约页面的实时访问数据。
总结与扩展阅读
Fathom 应用是 Cal.com 声明式应用架构的一个典型样例:配置文件声明脚本模板,Zod schema 定义数据结构,极简的 add 处理器完成空凭据安装,通用 Tag 渲染器负责把脚本注入预约页面。理解了它,也就理解了 Cal.com 应用商店中所有分析类应用的通用接入范式。
进一步探索可参考以下仓库文件:
- 应用定义与元数据:config.json、DESCRIPTION.md
- 安装与数据校验:api/add.ts、zod.ts
- UI 组件:EventTypeAppCardInterface.tsx、EventTypeAppSettingsInterface.tsx
- 脚本注入枢纽:BookingPageTagManager.tsx
- 共享 schema:eventTypeAppCardZod.ts
- 应用商店中其他分析类应用可对照
packages/app-store/下的ga4、gtm、plausible、matomo、umami、posthog等目录,它们共享相同的variant: "analytics"与appData.tag注入模式。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00