关于 fbc-guard
fbc-guard 对处理 Meta 点击 ID 和 Cookie 的 HTML 与 JavaScript 进行静态检查,适用于审查落地页跟踪代码的广告主和开发者。它也可作为 CLI 用于 CI;扫描时不执行代码,也不向 Meta 发送数据。
主要功能
- 01检测点击 ID 处理风险
- 02检查 HTML 和 JavaScript 源码
- 03按 URL 获取页面及同源脚本
- 04报告通过、失败、未知和警告结果
- 05在 CI 中通过 CLI 运行检查
- 06检查前运行负向测试
适用场景
- 01发布前审查 Meta 跟踪代码
- 02调查 fbclid 被修改的诊断提示
- 03将跟踪检查加入 CI
- 04扫描落地页及其脚本
MCP 相关
提供 MCP 工具,用于检查传入代码,或获取公开页面及其同源脚本进行分析。代码检查工具不发起网络请求;URL 检查会获取页面和脚本。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
2收录的工具列表 · 更新于 6 天前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | string | 是 | 要检查的 HTML 或 JavaScript 源码。请拼接所有涉及 fbclid、_fbc 或 _fbp 的文件。 |
| kind | enum | 否 | 如何读取 `code`。"html":分析内联 <script> 代码块。"js":将其作为 JavaScript 分析。"auto":如果以 "<" 开头则按 HTML 分析,否则按 JavaScript 分析。auto | html | js |
详细介绍
fbc-guard
用于检查存储 Meta 点击 ID 的落地页代码的静态检查工具。
文档和背景:adsandcode.com/en/open-source/fbc-guard
fbc-guard 会读取将 fbclid URL 参数转换为 _fbc Cookie 的 HTML 和 JavaScript(如果你自行编写代码,也会检查 _fbp Cookie),并标记代码中已知的、会更改或丢失点击 ID 的问题。
它以 MCP 服务的形式运行,因此 AI 助手可以在编写或审查页面时检查跟踪代码;它也可以作为命令行工具用于 CI。
它解决的问题
有人点击 Meta 广告时,落地页 URL 会携带 fbclid 参数。你的页面或 Meta Pixel 会将其以 fb.<subdomainIndex>.<creationTime>.<fbclid> 的形式存储在第一方 _fbc Cookie 中,后续事件会在 fbc 参数中将该值发送给 Meta。
手写这类代码时,常会出现几种特定问题:
- 回访访客通过另一则广告进入时,代码仍保留旧的点击 ID,导致转化归因到更早的广告。
- 点击 ID 在传递过程中被更改:再次解码、改变大小写,或截断内容。
creationTime使用秒而非毫秒,或将subdomainIndex硬编码。
广告主反映,他们在 Events Manager 中看到了关于 fbc 参数中的 fbclid 被修改的诊断信息。Meta 的文档没有列出导致此问题的原因。上面前两种错误会在 URL 到事件的传递过程中更改或替换点击 ID,因此出现该诊断信息时,值得排查这些问题。
fbc-guard 会在代码发布前找出其中的这类模式。它不会运行代码,也不会向 Meta 发送任何内容。
安装
MCP 服务
将以下内容添加到 MCP 客户端配置中(Claude Desktop、Cursor 以及其他读取 mcpServers 区块的客户端):
{
"mcpServers": {
"fbc-guard": {
"command": "npx",
"args": ["-y", "mcp-server-fbc-guard"]
}
}
}
Claude Code:
claude mcp add fbc-guard -- npx -y mcp-server-fbc-guard
需要 Node.js 18 或更高版本。
命令行
npx -y -p mcp-server-fbc-guard fbc-guard https://your-landing-page.example/
# or install it
npm install -g mcp-server-fbc-guard
fbc-guard ./dist/index.html ./dist/js/tracking.js
作为库使用
import { checkCode } from "mcp-server-fbc-guard";
const result = checkCode(htmlOrJs); // { status, failed, unknown, warnings, rules }
该包的入口点仅包含规则模块。导入它不会启动 MCP 服务、读取文件或访问网络。
MCP 工具
| 工具 | 输入 | 功能 |
|---|---|---|
check_fbc_code | code:HTML 或 JavaScript;可选的 kind:auto(默认)、html 或 js | 对传入的源代码运行所有规则。不访问网络。 |
check_fbc_url | url:http 或 https URL | 获取页面及其加载的所有同源 <script src>,然后对全部内容运行所有规则。仅允许公共主机,除非服务以 FBC_GUARD_ALLOW_PRIVATE=1 运行(参见网络安全)。还会返回 final_url、scanned_scripts 和 skipped_scripts。 |
两者都会返回 JSON:
{
"status": "fail",
"failed": ["FBC-01"],
"unknown": [],
"warnings": ["FBC-06"],
"rules": [
{
"id": "FBC-01",
"title": "A new fbclid must overwrite an older _fbc cookie",
"status": "fail",
"evidence": "indexOf('_fbc=')<0",
"protects": "If the cookie is only written when it is missing, ...",
"basis": "official",
"severity": "error",
"reference": "https://developers.facebook.com/documentation/ads-commerce/conversions-api/parameters/fbp-and-fbc"
}
]
}
每条规则的 status 都是 pass、fail、unknown 或 warn。warn 仅由警告级规则(FBC-06)产生,会列在 warnings 下,且不会改变整体状态或退出代码。
整体 status 为 pass、fail、unknown 或 not_applicable。
unknown表示没有规则失败,但至少有一条规则无法作出判断。目前只有 FBC-01 会返回此状态,原因是无法确定哪个变量保存了来自 URL 的fbclid。规则的evidence会说明无法解析的内容。not_applicable表示扫描内容中完全没有出现字符串_fbc,因此没有评估任何规则。相关代码可能位于扫描无法覆盖的位置。
这两种状态都不代表通过。
在 check_fbc_code 中,kind: "auto" 会在输入以 < 开头时将其视为 HTML,并分析其中的内联 <script> 内容;否则会将其视为 JavaScript。
服务会在启动时运行自己的负向测试。如果某条规则未能对其应捕获的代码触发失败,服务就会拒绝启动。
命令行
fbc-guard <url|file> [<url|file> ...] check pages (http/https) or local files
fbc-guard --json <url|file> [...] print JSON instead of text
fbc-guard --allow-private <url> [...] also allow localhost and private-network URLs
fbc-guard --selftest run only the negative tests
fbc-guard --help | --version
每个目标都会单独判定。URL 会与其同源脚本一起获取。本地文件会按原样扫描,不会跟随 <script src>,因此也要列出 JavaScript 文件。扩展名为 .js、.mjs、.cjs、.jsx、.ts、.mts、.cts 或 .tsx 的本地文件会作为 JavaScript 读取;扩展名为 .html、.htm、.xhtml、.shtml 或 .php 的文件会作为 HTML 读取;其他文件则根据其第一个字符读取,如 check_fbc_code 中所示。
判定任何目标前,CLI 都会运行与服务端相同的负向测试。如果测试失败,它会打印失败的测试,并以代码 2 退出,不给出判定结果。
| 退出代码 | 含义 |
|---|---|
| 0 | 所有目标均未触发错误级规则。其中包括只有警告的目标(FBC-06)、unknown(规则无法判定)或 not_applicable(没有 _fbc 代码);输出会标记这三种情况 |
| 1 | 至少有一个目标触发了至少一条规则 |
| 2 | 负向测试失败、参数无效(包括 --selftest 与其他参数一起使用),或无法读取或获取某个目标 |
在 CI 中:
npx -y -p mcp-server-fbc-guard fbc-guard ./public/index.html ./public/js/tracking.js
规则
每条规则都引用 Meta 的参考页面
ClickID and the fbp and fbc Parameters
(旧 URL /docs/marketing-api/conversions-api/parameters/fbp-and-fbc/ 会重定向至该页面)。依据列说明该页面对规则的支持程度:
- 官方:页面明确说明了该要求。
- 推导:页面说明了一般要求;将其应用于此特定代码模式是我们的解读。
- 实践观察:Meta 未作说明;是在实际实现中发现的错误。
| ID | 检查内容 | 严重性 | 依据 |
|---|---|---|---|
| FBC-01 | 仅在 _fbc Cookie 不存在时才写入,或从未将 URL 中的点击 ID 与除字面值以外的任何内容进行比较 | error | 官方 |
| FBC-02 | decodeURIComponent(fbclid) | error | 推导 |
| FBC-03 | fbclid.toLowerCase() / fbclid.toUpperCase() | error | 官方 |
| FBC-04 | fbclid.slice(、.substring(、.substr( | error | 推导 |
| FBC-05 | 构造 _fbc=fb. 值的语句中使用了秒级时间戳 | error | 官方 |
| FBC-06 | _fbc Cookie 值中的 subdomainIndex 是数字字面量 | warning | 实践观察 |
| FBP-01 | 与 FBC-05 相同,但针对 _fbp=fb. | error | 官方 |
触发错误级规则会导致检查失败(退出代码 1)。触发警告级规则时会以状态 warn 报告,不会改变结果或退出代码。
规则通过一个小型扫描器读取源代码,而非使用 JavaScript 解析器。JavaScript 注释中的内容不会计入,HTML 注释中的标记也不会计入。
- FBC-02、FBC-03 和 FBC-04 只读取 JavaScript(内联
<script>正文和获取的脚本文件),并且会忽略字符串内容,因此像'do not call decodeURIComponent(fbclid)'这样的消息不会触发这些规则。 - FBC-06 和 FBC-01 中的“仅在不存在时写入”检查会查找字符串字面量内的文本,因此它们会读取移除注释、保留字符串内容后的整个文档。
- FBC-01、FBC-05 和 FBP-01 也会使用扫描器查找语句的起止位置。
FBC-01:新的 fbclid 必须覆盖较旧的 _fbc cookie
只在没有 cookie 时设置 cookie 的代码会永远保留第一个点击 ID。访客通过另一则广告再次访问时,仍会携带旧的点击 ID,转化也会被归因给较早的广告。Meta 的页面列出了一种应设置 cookie 的情况:URL 中的 fbclid“不等于 _fbc cookie 值中的对应值”。
规则的判定方式:
- 查找保存 URL 点击 ID 的变量:通过
.get('fbclid')、['fbclid']或.fbclid赋值;从包含fbclid=的文本中调用match、split、exec或类似方法;或从上述变量复制而来(var f = fbclid || '')。字面名称为fbclid的变量或参数也计入。 - 查找上述变量与非字面量之间的
!==、!=、===或==比较。id !== f和if (stored === clickId) return;计入;f !== ''和f != null不计入。 - 存在比较时,规则通过。这也包括“在 cookie 缺失或值不同时设置”的写法,例如
indexOf('_fbc=') < 0 || id !== f。 - 如果没有比较,但找到仅在 cookie 缺失时写入的模式
indexOf('_fbc=') < 0,或找到点击 ID 变量,则规则失败。 - 如果既未找到点击 ID 变量,也未找到该模式,则状态为
unknown,并附带说明。退出代码为 0。
FBC-02:不要再次对 fbclid 进行 URL 解码
URLSearchParams.get() 已经会返回解码后的值。再次解码要么不会改变它,要么会改变它;而 decodeURIComponent 遇到单独的 % 时会抛出异常,从而中止脚本的其余部分。Meta 的页面说明“使用前不要对点击 ID 应用任何修改”。其中没有明确提到解码;将二次解码视为一种修改,是我们的理解。
FBC-03:不要更改 fbclid 的大小写
Meta 的页面指出“ClickID 值区分大小写”,并将转为小写或大写列为应避免的修改。
FBC-04:不要截断 fbclid
缩短点击 ID 就是修改点击 ID。Meta 的页面说明“使用前不要对它应用任何修改”;其中没有明确提到截断。
FBC-05:_fbc 的 creationTime 必须以毫秒为单位
Meta 的页面对此有明确说明:“creationTime 是自 UNIX 纪元起经过的毫秒数”。Date.now() 已返回毫秒;除以 1000 会写入秒数。
规则会检查每条包含 _fbc=fb. 字符串或模板字面量的语句;如果该语句将毫秒时钟除以 1000,则判定失败:
- 时钟:
Date.now()、new Date()、(new Date)、new Date().getTime()、(new Date).getTime()、new Date().valueOf()、+new Date,以及变量上的d.getTime()/d.valueOf()(由参数构造的new Date(...)表示固定时刻,而非时钟,不会匹配); - 除数:
/1000或/1e3,无论是否有空格,也无论外层包装是什么(Math.floor、Math.round、Math.trunc、parseInt、|0、~~); - 无论是直接写在拼接或模板字面量中,还是先存入变量再拼接。对于变量,以该语句之前的最后一次赋值为准。
仅有 Date.now() 时不会触发;其他语句中的 /1000(例如文件其他位置的倒计时器)不会触发;注释掉的行不会触发;时间差(例如 (end - Date.now()) / 1000)也不会触发,因为它表示的是类似 max-age 的持续时长,而非时间戳。
FBC-06(警告):检查 _fbc Cookie 中硬编码的 subdomainIndex
Meta 的页面称,“subdomainIndex 是 Cookie 所定义的域名”,其中 com 为 0,example.com 为 1,www.example.com 为 2。只有当 Cookie 始终设置在同一级别的域名上时,固定数字才是正确的。该页面还称,如果在服务器上生成值但不保存 Cookie,应使用 1;页面上的示例也使用 fb.1.,并未说明不匹配的值会被拒绝。因此,固定数字本身并没有错,这条规则是警告,而非失败:它指出这个值值得根据 Cookie 实际设置的域名进行检查。
FBP-01:_fbp 的 creationTime 必须以毫秒为单位
同一页面的 fbp 部分采用了相同的定义:“creationTime 是自 Unix 纪元以来的时间,单位为毫秒”。这只适用于你自己的代码写入 _fbp 的情况;通常是 Meta Pixel 写入。检测方式与 FBC-05 相同,检查包含 _fbp=fb. 的语句。
通过所有规则的代码
这是自测会用来逐条检查规则的代码片段,发布检查会拒绝发布与之不同的 README。
<script>
(function () {
// URLSearchParams.get() already returns the decoded value. Use it as is.
var fbclid = new URLSearchParams(location.search).get('fbclid');
if (!fbclid) return;
// The click ID in an existing _fbc cookie is its fourth field:
// fb.<subdomainIndex>.<creationTime>.<fbclid>
var current = (document.cookie.match(/(?:^|;\s*)_fbc=([^;]*)/) || [])[1] || '';
var storedId = current ? current.split('.')[3] : '';
// A different click replaces the stored one. The same click keeps it.
if (storedId !== fbclid) {
// Use the widest domain the browser accepts for a cookie, and derive
// subdomainIndex from it: example.com is 1, www.example.com is 2.
var host = location.hostname, parts = host.split('.');
var domain = host, domainAttr = '';
for (var i = parts.length - 2; i >= 0; i--) {
var candidate = parts.slice(i).join('.');
document.cookie = 'cookie_domain_probe=1;path=/;domain=' + candidate;
if (document.cookie.indexOf('cookie_domain_probe=1') >= 0) {
document.cookie = 'cookie_domain_probe=;path=/;domain=' + candidate + ';max-age=0';
domain = candidate;
domainAttr = ';domain=' + candidate;
break;
}
}
var subdomainIndex = domain.split('.').length - 1;
// creationTime is Date.now(): milliseconds, not seconds. 90 days = 7776000 s.
document.cookie = '_fbc=fb.' + subdomainIndex + '.' + Date.now() + '.' + fbclid +
';path=/' + domainAttr + ';max-age=7776000;SameSite=Lax';
}
})();
</script>
示例说明:
- Cookie 的哪一部分是点击 ID。 示例取点分隔的第四个部分。Meta 的 fbc 页面则称,点击 ID 是 Cookie 值中最后一个点之后的部分。使用 Meta 的
Parameter Builder library
写入 Cookie 时,两者并不相同:该库文档中的格式为
fb.${subdomain_index}.${creation_time}.${fbclid}.${appendix},因此最后一个点之后的部分是附加内容,而不是点击 ID。在两种格式中,第四个部分都是点击 ID。 - 浏览器会拒绝设置在
com或com.tw等公共后缀上的 Cookie,因此循环最终会选定可注册域名(example.com、example.com.tw)。 - 优先使用服务器设置的 Cookie。 Meta 的 fbc 页面建议在请求携带
fbclid时,通过响应标头设置_fbcHTTP Cookie,并将其有效期设为 90 天。示例使用document.cookie写入 Cookie,这是静态页面可以采用的做法,但浏览器对此有限制:WebKit 的 Intelligent Tracking Prevention 2.2 称,当某个被其归类为跨站跟踪器的域名引导访客访问落地页,且落地 URL 带有查询字符串或片段时,“通过 document.cookie 设置的持久 Cookie 最多只能存储一天”。广告点击始终会在查询字符串中携带fbclid,因此,只要 ITP 将引荐网站归为此类,该脚本设置的 90 天有效期在 Safari 中就会变成一天。如果你能控制服务器,请在服务器上设置 Cookie。 - 如果安装了 Meta Pixel,它会自行管理
_fbc和_fbp。这类代码适用于未安装 Pixel、但需要捕获点击 ID 的页面,例如将其转发到 Conversions API。
与原规则集的变化
此软件包最初是一个内部脚本,使用相同的规则 ID。公开发布时,我们重写了几条规则,因为它们会漏掉实际错误,或误报正确代码。
- FBC-05 和 FBP-01 以前只会在与
_fbc=fb./_fbp=fb.位于同一字符串或模板字面量中的情况下,捕获Date.now()/1000。常见的拼接写法'_fbc=fb.' + i + '.' + Math.floor(Date.now()/1000)不会被检查到。现在,它们会按上文所述检查整个语句,以及存储在变量中的秒数,并忽略注释。 - FBC-01 以前只接受紧邻字面命名为
fbclid的变量的!==或!=,因此if (id !== f)或if (stored === fbclid) return;这样的正确代码会检查失败。现在,它会解析哪个变量存储了点击 ID,接受全部四种比较运算符;无法解析变量时,则报告unknown,而不是检查失败。 - FBC-06 以前会导致检查失败。由于 Meta 自己在示例和服务器生成的值中使用
fb.1.,现在它会发出警告,不再改变退出代码。 - FBC-02、FBC-03、FBC-04 和 FBC-06 以前会在文本的任何位置匹配,包括注释,因此警告错误写法的注释也会触发规则。现在它们会跳过注释,FBC-02 至 FBC-04 还会跳过字符串内容。
在原有测试样例中,每条规则给出的结果都与之前相同,唯一例外是 FBC-06 现在报告 warn,而之前报告的是 fail。
局限性
这是对源文本进行的静态分析,不会执行代码。通过检查应理解为“未发现此工具可识别的形式中的已知错误”,而不是“此代码正确”。
- 漏报。 经压缩、打包或混淆的代码,以及运行时组装的代码,都可能隐藏错误。FBC-02、FBC-03 和 FBC-04 只检查字面名称为
fbclid的变量,因此不会捕获f.toLowerCase()。FBC-05 和 FBP-01 要求_fbc=fb./_fbp=fb.文本位于同一个字符串或模板字面量中;以'_fbc=' + value或join('.')组装的值不会被检查,也不会追踪由辅助函数返回的秒数。变量跟踪基于文本:它会采用语句之前的最后一次赋值,不会跟踪作用域或分支。 - 误报。 FBC-01 接受点击 ID 与非字面值之间的任何比较,因此无法判断另一侧是否确实为已存储的 Cookie 值。因此,它更可能造成误通过,而不是误失败。若秒数变量在分支内被重新赋值,判断可能会依据错误的赋值。
- HTML 或 JavaScript。 FBC-01 至 FBC-05 或 FBP-01 不会分析内联事件处理程序属性(
onclick="...")。以<开头的 JavaScript 文件会被当作 HTML 读取,除非传入kind: "js"或文件扩展名为.js。 - 不适用不等于通过。 如果扫描内容中没有
_fbc,就不会进行判断。标签管理器容器、跨源脚本或服务器端集成中的跟踪代码对此扫描不可见。 - URL 扫描限制。 只会获取同源脚本,最多 20 个;每个脚本解压后最多 2 MB;每个请求超时为 30 秒;最多跟随 5 次重定向,并使用移动版 Safari 用户代理。绝不会获取跨源脚本;这些脚本会列在
skipped_scripts下。重定向到其他源的同源脚本不会继续跟随。只有在 JavaScript 运行后才提供的标记不会被发现。不会使用代理环境变量。 - 不进行运行时检查。 静态分析无法判断实际发送的值。要确认结果,请在浏览器中加载页面,并确保已设置旧的
_fbcCookie,且 URL 中包含真实的fbclid,然后逐字符比较代码发送的内容与 URL 值。测试时请阻止向 Meta 发送请求,以免测试用点击 ID 被报告为真实事件。
网络安全
check_fbc_url 和 CLI 的 URL 模式会获取传入的任意 URL,AI Agent 也可能会选择该 URL。为避免借此访问云元数据端点、本地主机服务或私有网络,默认仅允许获取公共主机:
- 仅接受
http和httpsURL。 - 如果主机本身或解析到的地址属于环回地址、私有地址(
10/8、172.16/12、192.168/16、fc00::/7)、链路本地地址(169.254/16、fe80::/10)、运营商级 NAT(100.64/10)、0.0.0.0/8、组播、保留或未指定地址,则拒绝连接;这些地址写在 IPv6 地址内部时也会被拒绝(::ffff:127.0.0.1、NAT64、6to4)。如果某个名称解析到的任一地址被拒绝,则拒绝该名称。 - 手动跟随重定向,最多 5 次;每一跳都以相同方式检查,同源脚本也一样。
- 被拒绝的地址、DNS 故障、被拒绝或重置的连接、TLS 错误和超时都会返回相同的错误文本,因此无法通过工具的消息判断端口是开放还是关闭。
若要检查自己机器或内网中的页面,请选择启用:在命令行中使用 --allow-private,或在 MCP 服务的环境中设置 FBC_GUARD_ALLOW_PRIVATE=1:
{
"mcpServers": {
"fbc-guard": {
"command": "npx",
"args": ["-y", "mcp-server-fbc-guard"],
"env": { "FBC_GUARD_ALLOW_PRIVATE": "1" }
}
}
}
启用后,不会拒绝任何地址。
残余风险:
- DNS 重绑定。 主机名在检查时可能解析到公共地址,而在连接时解析到私有地址。地址检查会在连接自身的 DNS 查询过程中,对套接字即将使用的地址执行;每次重定向和脚本也都会重新解析并检查,因此变化的解析结果无法在检查与连接之间钻空子。这依赖 Node.js 在每次连接时都执行该查询;对于主机名,它确实如此;IP 字面量则会在连接前检查。应将其视为缓解措施,而非保证。
- 时序。 被拒绝的地址会立即失败,而无法访问的公共主机可能需要等待最长 30 秒才超时,因此尽管错误消息相同,时序仍会泄露一些信息。
- 可访问内部资源的公共主机。 公共 URL 对应的服务器若能代你获取内部资源(例如开放代理),则不在地址检查的检测范围内。
开发
npm ci
npm run build
npm run selftest # negative tests: every rule must fail on its own fixture
node scripts/smoke.mjs # talks to the built server over stdio, including a local HTTP check
node scripts/check-publish-ready.mjs # package and MCP Registry metadata checks
许可证
MIT。详见 LICENSE。
维护者
由 Ads & Code 构建并维护。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。