Files
cursor/plans/FbService-46ed22aa.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

15 KiB
Raw Blame History


todos:

  • id: "fix-fbc-format" content: "FbService 增加 resolveFbcForCapiraw fbclid 格式化为 fbccreationTime 优先从 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.phpregisterSelf / purchaseSelf 在重构为 Guzzle 直调 Graph API 后,把原始 fbclid 当作 user_data.fbc 上报。Meta CAPI 对 fbc 有严格格式校验,不符合 fb.1.{ms}.{fbclid} 时会返回 HTTP 400Graph API error code 100Invalid parameter


关于「URL 上传来的 fbc/fbclid 没有点」——这是正常的

对照 Meta 官方文档(ClickID and the fbp and fbc ParametersUpdated: Jan 9, 2026

阶段 字段 格式 是否带点
广告落地 URL fbclid 查询参数 原始 ClickIDIwAR2F4-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}

官方示例:

  • URLhttps://example.com/?fbclid=IwAR2F4-dbP0l7Mn1IawQQGCINEz7PYXQvwjNwB_qa2ofrHyiLjcbCRxTDMgk
  • CAPI payload"fbc": "fb.1.1554763741205.IwAR2F4-dbP0l7Mn1IawQQGCINEz7PYXQvwjNwB_qa2ofrHyiLjcbCRxTDMgk"

官方明确说明:

If the _fbc cookie is not available ... it is still possible to send the fbc event parameter ... if an fbclid query parameter is in the URL ... The formatted ClickID value must be of the form version.subdomainIndex.creationTime.fbclid.

creationTime ... If you don't save the _fbc cookie, 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 来自浏览器 _fbp cookie本身带点fb.1.1769311259905.2389032359262323),当前做法正确。

creationTime 补充:官方要求用「首次观察到 fbclid 的时间」,不是转化事件发送时间。同一次落地页访问里,fbp 已含首访毫秒时间戳,可从中解析作为 fbccreationTime(见下方真实样例)。


真实生产样例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 fbclidad_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 策略(推荐)

  1. 优先从 entity.fbp / ad_fbp 解析第 3 段毫秒时间戳(同会话首访时间,符合 Meta 要求)。
  2. 解析失败时 fallback 到 generateFBC() 当前毫秒时间。
  3. 禁止对 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::registerSelfuid 3895162

Client error: POST https://graph.facebook.com/v24.0/211064574998002/events?access_token=... 
resulted in a `400 Bad Request` response:

可确认:

  • 失败方法:registerSelf(注册打点 CompleteRegistration
  • Pixel ID211064574998002
  • APIv24.0/events
  • 响应 body 被 Guzzle 截断/未记录,日志里看不到 Meta 的 error.message(通常为 Invalid parameter / fbc 相关)
  • 安全access_token 完整出现在 ERROR 日志中Guzzle 异常 message 含 query string应在 Meta 后台 立即轮换 token,并修复日志脱敏(禁止记录含 token 的 URL

结合同一用户链路(落地页 raw fbclid + 合法 fbp400 与未格式化的 user_data.fbc 高度吻合

最可能根因:user_data.fbc 格式非法(与真实样例直接相关)

当前发送(错误):

"fbc": "IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRj..."

Meta 要求(Customer Information ParametersJan 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 返回的完整 JSONerror.messageerror_user_msgerror_subcode)。
  • 修复时应从 $e->getResponse()->getBody() 解析并记录,便于确认是否为 Invalid parameter: fbc

其它可能导致 400 的次要因素

因素 当前代码 风险
registerSelfarray_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 格式错误(根因)

$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";
}
  • 构造函数已接收 fbclidFbDot 传的是 ad_fbc),但 registerSelf/purchaseSelf 完全没用 $this->fbclidgenerateFBC()

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 ParametersJan 9, 2026external_id 写的是 Hashing recommended,不是必须。
  • 旧 Facebook SDK 的 UserData::setExternalId() 会自动哈希;同项目 TikTokDot 也已哈希。建议对齐,但优先级低于 fbc 格式化。

4. 【低】其它可改进点

说明
registerSelfarray_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 侧目前是正确的:来自浏览器 cookiead_fbp),不应服务端 generateFPB()


修复方案(建议最小改动)

A. 在 FbService 内统一解析 fbc(核心)

新增私有方法,例如 resolveFbcForCapi(string $rawFbclid, string $fbp = ''): ?string

  1. $rawFbclid 已匹配 ^fb\.\d+\.\d+\. → 直接使用(兼容未来若存完整 _fbc cookie
  2. 否则取 raw fbclid优先 $rawFbclidfallback $this->fbclid)→ 格式化为 fb.1.{creationTimeMs}.{fbclid}
    • creationTimeMs 优先从 $fbp 解析(正则 ^fb\.\d+\.(\d+)\.),与真实样例 1782099747655 对齐 Meta「首访观察时间」。
    • 解析失败时 fallback 到 round(microtime(true) * 1000)
    • 服务端生成时 subdomainIndex 固定 1(官方推荐)。
  3. 无 fbclid 时返回 null,配合 array_filter 不上报空字段。
  4. 禁止修改 fbclid 大小写官方ClickID is case sensitive

registerSelf / purchaseSelf 均改为:

$fbc = $this->resolveFbcForCapi($entity->fbc, $entity->fbp);

不再直接把 ad_fbcfbc 上报。

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. 对齐 registerSelfpurchaseSelf + 改善 400 排障

  • registerSelfuser_data 也使用 array_filter(空 fbc/fbp/ua 不上报)。
  • external_id 统一为字符串数组:[(string) $entity->uid]
  • Guzzle 请求增加 'http_errors' => false catch ClientException 时读取 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 可选

验证方式

  1. Test.phpfbRegister / fbPurchase(带 test_event_code)在 Meta Events Manager → Test Events 查看:
    • fbc 应为 fb.1.{13位毫秒}.{fbclid} 格式
    • fbp 保持 fb.1.{ms}.{random}
    • Purchase 重复同一 orderId 不应产生 duplicate 事件
  2. 对比修复前后 payload 日志(registerSelf / purchaseSelf 已有 body 日志)。
  3. 跑 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 发送层完成)。