mcpField
F
fbc-guard

fbc-guard

本地服务
adsandcode.com商业与营销01 周前更新

关于 fbc-guard

fbc-guard 对处理 Meta 点击 ID 和 Cookie 的 HTML 与 JavaScript 进行静态检查,适用于审查落地页跟踪代码的广告主和开发者。它也可作为 CLI 用于 CI;扫描时不执行代码,也不向 Meta 发送数据。

主要功能

  1. 01检测点击 ID 处理风险
  2. 02检查 HTML 和 JavaScript 源码
  3. 03按 URL 获取页面及同源脚本
  4. 04报告通过、失败、未知和警告结果
  5. 05在 CI 中通过 CLI 运行检查
  6. 06检查前运行负向测试

适用场景

  1. 01发布前审查 Meta 跟踪代码
  2. 02调查 fbclid 被修改的诊断提示
  3. 03将跟踪检查加入 CI
  4. 04扫描落地页及其脚本

MCP 相关

提供 MCP 工具,用于检查传入代码,或获取公开页面及其同源脚本进行分析。代码检查工具不发起网络请求;URL 检查会获取页面和脚本。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"adsandcode-fbc-guard": {
"command": "npx",
"args": [
"-y",
"mcp-server-fbc-guard"
]
}
}
}
启动命令npx -y mcp-server-fbc-guard

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmmcp-server-fbc-guard0.1.0

工具

2

收录的工具列表 · 更新于 6 天前

参数类型必填说明
codestring是要检查的 HTML 或 JavaScript 源码。请拼接所有涉及 fbclid、_fbc 或 _fbp 的文件。
kindenum否如何读取 `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_codecode:HTML 或 JavaScript;可选的 kind:auto(默认)、html 或 js对传入的源代码运行所有规则。不访问网络。
check_fbc_urlurl: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-02decodeURIComponent(fbclid)error推导
FBC-03fbclid.toLowerCase() / fbclid.toUpperCase()error官方
FBC-04fbclid.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 值中的对应值”。

规则的判定方式:

  1. 查找保存 URL 点击 ID 的变量:通过 .get('fbclid')、['fbclid'] 或 .fbclid 赋值;从包含 fbclid= 的文本中调用 match、split、exec 或类似方法;或从上述变量复制而来(var f = fbclid || '')。字面名称为 fbclid 的变量或参数也计入。
  2. 查找上述变量与非字面量之间的 !==、!=、=== 或 == 比较。id !== f 和 if (stored === clickId) return; 计入;f !== '' 和 f != null 不计入。
  3. 存在比较时,规则通过。这也包括“在 cookie 缺失或值不同时设置”的写法,例如 indexOf('_fbc=') < 0 || id !== f。
  4. 如果没有比较,但找到仅在 cookie 缺失时写入的模式 indexOf('_fbc=') < 0,或找到点击 ID 变量,则规则失败。
  5. 如果既未找到点击 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 时,通过响应标头设置 _fbc HTTP 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 运行后才提供的标记不会被发现。不会使用代理环境变量。
  • 不进行运行时检查。 静态分析无法判断实际发送的值。要确认结果,请在浏览器中加载页面,并确保已设置旧的 _fbc Cookie,且 URL 中包含真实的 fbclid,然后逐字符比较代码发送的内容与 URL 值。测试时请阻止向 Meta 发送请求,以免测试用点击 ID 被报告为真实事件。

网络安全

check_fbc_url 和 CLI 的 URL 模式会获取传入的任意 URL,AI Agent 也可能会选择该 URL。为避免借此访问云元数据端点、本地主机服务或私有网络,默认仅允许获取公共主机:

  • 仅接受 http 和 https URL。
  • 如果主机本身或解析到的地址属于环回地址、私有地址(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 构建并维护。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。