15 KiB
todos:
- id: "fix-fbc-format" content: "FbService 增加 resolveFbcForCapi:raw fbclid 格式化为 fbc;creationTime 优先从 ad_fbp 解析" status: pending
- id: "fix-purchase-event-id" content: "purchaseSelf 恢复使用 orderId 作为 event_id 保证幂等" status: pending
- id: "hash-external-id" content: "(可选)registerSelf/purchaseSelf 对 external_id 做 SHA256 哈希,对齐 TikTokDot" status: pending
- id: "align-user-data" content: "registerSelf 使用 array_filter;解析 4xx 响应 body 写日志;可选异常上抛" status: pending
- id: "fix-400-debug" content: "记录 Meta error JSON(不含 token);日志脱敏 access_token;建议轮换已泄露 token" status: pending
- id: "verify-meta-test-events" content: "用 test_event_code 在 Meta Test Events 验证 fbc 格式与 Purchase 去重" status: pending isProject: false
FbService 像素打点问题分析与修复计划
结论:有问题,且会影响 FB 广告归因;400 很可能就是 fbc 格式错误导致
当前 slot_console/app/service/dot/FbService.php 的 registerSelf / purchaseSelf 在重构为 Guzzle 直调 Graph API 后,把原始 fbclid 当作 user_data.fbc 上报。Meta CAPI 对 fbc 有严格格式校验,不符合 fb.1.{ms}.{fbclid} 时会返回 HTTP 400(Graph API error code 100,Invalid parameter)。
关于「URL 上传来的 fbc/fbclid 没有点」——这是正常的
对照 Meta 官方文档(ClickID and the fbp and fbc Parameters,Updated: Jan 9, 2026):
| 阶段 | 字段 | 格式 | 是否带点 |
|---|---|---|---|
| 广告落地 URL | fbclid 查询参数 |
原始 ClickID,如 IwAR2F4-dbP0l7Mn1IawQQGCINEz7PYXQvwjNwB_qa2ofrHyiLjcbCRxTDMgk |
否 |
浏览器 _fbc cookie / CAPI user_data.fbc |
格式化 ClickID | fb.{subdomainIndex}.{creationTimeMs}.{fbclid} |
是(3 个点分隔 4 段) |
浏览器 _fbp cookie / CAPI user_data.fbp |
Browser ID | fb.{subdomainIndex}.{creationTimeMs}.{random} |
是 |
官方示例:
- URL:
https://example.com/?fbclid=IwAR2F4-dbP0l7Mn1IawQQGCINEz7PYXQvwjNwB_qa2ofrHyiLjcbCRxTDMgk - CAPI payload:
"fbc": "fb.1.1554763741205.IwAR2F4-dbP0l7Mn1IawQQGCINEz7PYXQvwjNwB_qa2ofrHyiLjcbCRxTDMgk"
官方明确说明:
If the
_fbccookie is not available ... it is still possible to send thefbcevent parameter ... if anfbclidquery parameter is in the URL ... The formatted ClickID value must be of the formversion.subdomainIndex.creationTime.fbclid.
creationTime ... If you don't save the
_fbccookie, use the timestamp when you first observed or received this fbclid value.
因此:
- 落地页 / 注册侧存 raw fbclid(无点)是对的,与你们
ImeiRegisterService注释一致。 - 问题不在采集,而在 CAPI 发送前缺少格式化:
FbService应把 raw fbclid 组装成fb.1.{ms}.{fbclid}再写入user_data.fbc,而不是原样上报。 fbp来自浏览器_fbpcookie,本身带点(如fb.1.1769311259905.2389032359262323),当前做法正确。
creationTime 补充:官方要求用「首次观察到 fbclid 的时间」,不是转化事件发送时间。同一次落地页访问里,fbp 已含首访毫秒时间戳,可从中解析作为 fbc 的 creationTime(见下方真实样例)。
真实生产样例(SiteController 日志)
来源:SiteController::fb 第 28 行日志。
| 字段 | 实际值 | 说明 |
|---|---|---|
post.fbp |
fb.1.1782099747655.910276663728351279 |
浏览器 _fbp cookie,已带点、格式正确,可直接作为 CAPI fbp |
post.fbc |
IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRjBmFwcF9pZAo2NjI4NTY4Mzc5AAEe0aCSkd3i8J9sLoIGWHNcZF4YJuNXtOMXsFspgYX_76nQTzSUrDDtOzE4qJY_aem_gZtG64LnGIXu3gNiyhejkQ |
URL 上的 raw fbclid,无点,命名虽叫 fbc 但不是 CAPI 最终格式 |
post.ua |
...[FBAN/FBIOS;...] |
Facebook iOS 内置浏览器 |
注册后写入用户:ad_fbc = 上面 raw fbclid,ad_fbp = 上面 fbp。
当前 FbService 错误上报(原样发送 ad_fbc):
"fbc": "IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRjBmFwcF9pZAo2NjI4NTY4Mzc5AAEe0aCSkd3i8J9sLoIGWHNcZF4YJuNXtOMXsFspgYX_76nQTzSUrDDtOzE4qJY_aem_gZtG64LnGIXu3gNiyhejkQ"
修复后应上报(从 fbp 解析 creationTime=1782099747655,拼接 raw fbclid):
"fbc": "fb.1.1782099747655.IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRjBmFwcF9pZAo2NjI4NTY4Mzc5AAEe0aCSkd3i8J9sLoIGWHNcZF4YJuNXtOMXsFspgYX_76nQTzSUrDDtOzE4qJY_aem_gZtG64LnGIXu3gNiyhejkQ",
"fbp": "fb.1.1782099747655.910276663728351279"
creationTime 策略(推荐):
- 优先从
entity.fbp/ad_fbp解析第 3 段毫秒时间戳(同会话首访时间,符合 Meta 要求)。 - 解析失败时 fallback 到
generateFBC()当前毫秒时间。 - 禁止对 fbclid 做大小写变换。
附带发现(可选后续):SiteController 第 33 行要求 UA 含 FB_IAB,但 iOS Facebook 内置浏览器 UA 为 FBAN/FBIOS,该条日志在 info 之后可能被 PARAMS_ERROR 拒绝、未写入 Redis。若 iOS 用户依赖落地页 Redis 回退取归因,需放宽 UA 校验(如同时接受 FB_IAB / FBAN/)。客户端直传 fbclid 注册路径不受影响。
HTTP 400 原因分析(结合当前代码)
生产报错确认(2026-06-22)
实际日志(FbService::registerSelf,uid 3895162):
Client error: POST https://graph.facebook.com/v24.0/211064574998002/events?access_token=...
resulted in a `400 Bad Request` response:
可确认:
- 失败方法:
registerSelf(注册打点 CompleteRegistration) - Pixel ID:
211064574998002 - API:
v24.0/events - 响应 body 被 Guzzle 截断/未记录,日志里看不到 Meta 的
error.message(通常为Invalid parameter/fbc相关) - 安全:
access_token完整出现在 ERROR 日志中(Guzzle 异常 message 含 query string),应在 Meta 后台 立即轮换 token,并修复日志脱敏(禁止记录含 token 的 URL)
结合同一用户链路(落地页 raw fbclid + 合法 fbp),400 与未格式化的 user_data.fbc 高度吻合。
最可能根因:user_data.fbc 格式非法(与真实样例直接相关)
当前发送(错误):
"fbc": "IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRj..."
Meta 要求(Customer Information Parameters,Jan 9, 2026):
The format is:
fb.${subdomain_index}.${creation_time}.${fbclid}.
未格式化的 raw fbclid 不以 fb. 开头,会被判定为 invalid parameter → HTTP 400。这与「采集端无点是对的、发送端必须带点」完全一致。
修复后应发送:
"fbc": "fb.1.1782099747655.IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRj..."
为什么日志里可能看不清 Meta 具体报错
Guzzle 默认 http_errors=true,收到 400 会抛 ClientException,当前代码:
} catch (\Throwable $e) {
LoggerService::error(__METHOD__, $e->getMessage());
}
- 成功分支的
LoggerService::info($body)不会执行(400 时进 catch)。 - 只记
$e->getMessage(),通常不含 Meta 返回的完整 JSON(error.message、error_user_msg、error_subcode)。 - 修复时应从
$e->getResponse()->getBody()解析并记录,便于确认是否为Invalid parameter: fbc。
其它可能导致 400 的次要因素
| 因素 | 当前代码 | 风险 |
|---|---|---|
registerSelf 未 array_filter |
可能发送 "fbc":"" / "fbp":"" |
空字符串也可能触发参数校验失败 |
external_id 类型不一致 |
register 传 [1689380](整数),purchase 传 ["1689380"](字符串) |
部分 Graph 版本对类型敏感,应统一为字符串数组 |
client_user_agent 为空 |
action_source=website 时 Meta 标注为 required |
若 ad_ua 为空仍发送 "",可能 400 |
Test.php 测试 |
fbRegister 未设置 entity.fbc/fbp |
本地测试必 400(空 fbc) |
| access_token / pixel_id 错误 | 配置问题 | 多为 401/403 或 OAuth error,不一定是 400 |
不太可能是 400 的项:fbp 格式(你的样例 fb.1.1782099747655.910276663728351279 合法)、currency 大小写、event_time 秒级时间戳。
数据流(当前)
sequenceDiagram
participant LP as LandingPage
participant User as slot_user注册
participant Dot as FbDot
participant FB as FbService
participant Meta as Meta_CAPI
LP->>User: POST fbp + fbc(实为URL fbclid)
User->>User: ad_fbc = 原始fbclid
Dot->>FB: entity.fbc = ad_fbc
FB->>Meta: user_data.fbc = 原始fbclid
Note over FB,Meta: 错误:应为 fb.1.{ms}.{fbclid}
已确认的问题
1. 【严重】fbc 格式错误(根因)
- 用户侧
slot_user/app/service/register/ImeiRegisterService.php明确注释:落地页fbc实际是 URL 上的fbclid,直接写入ad_fbc。 UserInfoEntity::$ad_fbc注释也是「FB点击id」,不是_fbccookie。FbDot.php把ad_fbc赋给$entity->fbc后,FbService原样上报:
$fbc = $entity->fbc;// $this->generateFBC();
$fbp = $entity->fbp ;// $this->generateFPB();
// ...
"user_data" => [
// ...
"fbc" => $fbc,
"fbp" => $fbp
]
- Meta 要求
fbc格式:fb.{subdomainIndex}.{creationTimeMs}.{fbclid}。 - 类内已有正确实现
generateFBC()(旧 SDK 代码也在用),但新代码里被注释掉未调用:
public function generateFBC()
{
if (!$this->fbclid) return null;
return "fb.1.$creationTime.$this->fbclid";
}
- 构造函数已接收 fbclid(
FbDot传的是ad_fbc),但registerSelf/purchaseSelf完全没用$this->fbclid和generateFBC()。
2. 【中等】Purchase 的 event_id 丢失幂等
- 旧 SDK 实现使用
$entity->orderId作为event_id(可去重、可重试)。 - 新
purchaseSelf改为uniqid('purchase_', true),同一订单重复打点会被 Meta 视为不同事件,无法幂等。
$eventId = uniqid('purchase_', true);
// 旧代码: ->setEventId($entity->orderId)
3. 【低】external_id 未 SHA256 哈希(官方为 recommended,非 required)
- 直调 CAPI 当前发送明文 uid:
"external_id" => [$entity->uid]/["{$entity->uid}"]。 - Customer Information Parameters(Jan 9, 2026)对
external_id写的是 Hashing recommended,不是必须。 - 旧 Facebook SDK 的
UserData::setExternalId()会自动哈希;同项目TikTokDot也已哈希。建议对齐,但优先级低于 fbc 格式化。
4. 【低】其它可改进点
| 项 | 说明 |
|---|---|
registerSelf 未 array_filter |
purchaseSelf 会过滤空字段,registerSelf 可能上报空 fbc/fbp |
| 响应未校验 | 仅 LoggerService::info 打 body,不检查 events_received / error |
| 异常被吞 | catch 只记日志不抛出,上层无法感知失败 |
| 死代码 | 大段注释 SDK 代码 + 未使用的 FacebookAds import |
FbDot 命名误导 |
$fbclid = $userInfo->ad_fbc 逻辑对,但 $entity->fbc = $fbclid 易让人误以为已是 _fbc 格式 |
fbp 侧目前是正确的:来自浏览器 cookie(ad_fbp),不应服务端 generateFPB()。
修复方案(建议最小改动)
A. 在 FbService 内统一解析 fbc(核心)
新增私有方法,例如 resolveFbcForCapi(string $rawFbclid, string $fbp = ''): ?string:
- 若
$rawFbclid已匹配^fb\.\d+\.\d+\.→ 直接使用(兼容未来若存完整_fbccookie)。 - 否则取 raw fbclid(优先
$rawFbclid,fallback$this->fbclid)→ 格式化为fb.1.{creationTimeMs}.{fbclid}。creationTimeMs优先从$fbp解析(正则^fb\.\d+\.(\d+)\.),与真实样例1782099747655对齐 Meta「首访观察时间」。- 解析失败时 fallback 到
round(microtime(true) * 1000)。 - 服务端生成时
subdomainIndex固定1(官方推荐)。
- 无 fbclid 时返回
null,配合array_filter不上报空字段。 - 禁止修改 fbclid 大小写(官方:ClickID is case sensitive)。
registerSelf / purchaseSelf 均改为:
$fbc = $this->resolveFbcForCapi($entity->fbc, $entity->fbp);
不再直接把 ad_fbc 当 fbc 上报。
B. 恢复 Purchase 幂等
"event_id" => $entity->orderId !== '' ? $entity->orderId : uniqid('purchase_', true),
优先订单号;仅测试/缺单号时 fallback。
C. 哈希 external_id
"external_id" => [hash('sha256', (string) $entity->uid)],
register / purchase 保持一致。
D. 对齐 registerSelf 与 purchaseSelf + 改善 400 排障
registerSelf的user_data也使用array_filter(空 fbc/fbp/ua 不上报)。external_id统一为字符串数组:[(string) $entity->uid]。- Guzzle 请求增加
'http_errors' => false或 catchClientException时读取 response body;日志禁止输出含 access_token 的完整 URL(只记 pixel_id、status、body):LoggerService::error(__METHOD__, [ 'status' => $resp->getStatusCode(), 'body' => (string) $resp->getBody(), 'pixel_id' => $this->pixelId, 'event_name' => 'CompleteRegistration', ]); - 可选:
events_received < 1或 HTTP >= 400 时抛业务异常,让 FbDot 感知失败。
E. FbDot 小清理(可选)
- 变量改名:
$fbclid = $userInfo->ad_fbc保留语义。 $entity->fbc仍可传 raw fbclid(由 Service 格式化),或只依赖构造函数 fbclid、entity 不再设 fbc——二选一,避免双份来源;推荐只传 raw fbclid 给构造函数,entity.fbc 可选。
验证方式
- 用
Test.php的fbRegister/fbPurchase(带test_event_code)在 Meta Events Manager → Test Events 查看:fbc应为fb.1.{13位毫秒}.{fbclid}格式fbp保持fb.1.{ms}.{random}- Purchase 重复同一
orderId不应产生 duplicate 事件
- 对比修复前后 payload 日志(
registerSelf/purchaseSelf已有 body 日志)。 - 跑 slot 后端 completion report(改动 PHP 后必跑)。
改动范围
| 文件 | 改动 |
|---|---|
slot_console/app/service/dot/FbService.php |
核心:resolve fbc、event_id、external_id 哈希、array_filter、可选响应校验 |
slot_console/app/command/dot/FbDot.php |
可选:变量命名/clarity,无业务逻辑必须改 |
不需要改 slot_user 注册写入逻辑(ad_fbc 存 raw fbclid 是合理设计,格式化应在 CAPI 发送层完成)。