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

327 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 46ed22aa-9d18-40e3-a7fb-ed92e69d49a2 -->
---
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.php`](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 100Invalid parameter
---
## 关于「URL 上传来的 fbc/fbclid 没有点」——这是正常的
对照 Meta 官方文档([ClickID and the fbp and fbc Parameters](https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc/)**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 `_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`](slot_user/app/service/register/ImeiRegisterService.php) 注释一致。
- **问题不在采集,而在 CAPI 发送前缺少格式化**`FbService` 应把 raw fbclid 组装成 `fb.1.{ms}.{fbclid}` 再写入 `user_data.fbc`,而不是原样上报。
- `fbp` 来自浏览器 `_fbp` cookie本身带点`fb.1.1769311259905.2389032359262323`),当前做法正确。
**creationTime 补充**:官方要求用「首次观察到 fbclid 的时间」,不是转化事件发送时间。同一次落地页访问里,`fbp` 已含首访毫秒时间戳,可从中解析作为 `fbc``creationTime`(见下方真实样例)。
---
## 真实生产样例SiteController 日志)
来源:[`SiteController::fb`](slot_user/app/api/controller/SiteController.php) 第 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`
```json
"fbc": "IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRjBmFwcF9pZAo2NjI4NTY4Mzc5AAEe0aCSkd3i8J9sLoIGWHNcZF4YJuNXtOMXsFspgYX_76nQTzSUrDDtOzE4qJY_aem_gZtG64LnGIXu3gNiyhejkQ"
```
**修复后应上报**(从 `fbp` 解析 `creationTime=1782099747655`,拼接 raw fbclid
```json
"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::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` 格式非法(与真实样例直接相关)
当前发送(错误):
```json
"fbc": "IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRj..."
```
Meta 要求([Customer Information Parameters](https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters/)Jan 9, 2026
> The format is: `fb.${subdomain_index}.${creation_time}.${fbclid}`.
未格式化的 raw fbclid **不以 `fb.` 开头**,会被判定为 invalid parameter → **HTTP 400**。这与「采集端无点是对的、发送端必须带点」完全一致。
修复后应发送:
```json
"fbc": "fb.1.1782099747655.IwZXh0bgNhZW0BMABhZGlkAAAvy_DRTLVzcnRj..."
```
### 为什么日志里可能看不清 Meta 具体报错
Guzzle 默认 `http_errors=true`,收到 400 会抛 `ClientException`,当前代码:
```php
} 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` 秒级时间戳。
---
## 数据流(当前)
```mermaid
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`](slot_user/app/service/register/ImeiRegisterService.php) 明确注释:**落地页 `fbc` 实际是 URL 上的 `fbclid`,直接写入 `ad_fbc`**。
- [`UserInfoEntity::$ad_fbc`](slot_lib/src/entity/user/UserInfoEntity.php) 注释也是「FB点击id」不是 `_fbc` cookie。
- [`FbDot.php`](slot_console/app/command/dot/FbDot.php) 把 `ad_fbc` 赋给 `$entity->fbc` 后,`FbService` 原样上报:
```82:98:slot_console/app/service/dot/FbService.php
$fbc = $entity->fbc;// $this->generateFBC();
$fbp = $entity->fbp ;// $this->generateFPB();
// ...
"user_data" => [
// ...
"fbc" => $fbc,
"fbp" => $fbp
]
```
- Meta 要求 `fbc` 格式:`fb.{subdomainIndex}.{creationTimeMs}.{fbclid}`。
- 类内已有正确实现 [`generateFBC()`](slot_console/app/service/dot/FbService.php)(旧 SDK 代码也在用),但新代码里被注释掉未调用:
```235:244:slot_console/app/service/dot/FbService.php
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 视为不同事件,**无法幂等**。
```174:182:slot_console/app/service/dot/FbService.php
$eventId = uniqid('purchase_', true);
// 旧代码: ->setEventId($entity->orderId)
```
### 3. 【低】`external_id` 未 SHA256 哈希(官方为 recommended非 required
- 直调 CAPI 当前发送明文 uid`"external_id" => [$entity->uid]` / `["{$entity->uid}"]`。
- [Customer Information Parameters](https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters/)Jan 9, 2026对 `external_id` 写的是 **Hashing recommended**,不是必须。
- 旧 Facebook SDK 的 `UserData::setExternalId()` 会自动哈希;同项目 [`TikTokDot`](slot_console/app/command/dot/TikTokDot.php) 也已哈希。建议对齐,但优先级低于 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`
1. 若 `$rawFbclid` 已匹配 `^fb\.\d+\.\d+\.` → 直接使用(兼容未来若存完整 `_fbc` cookie
2. 否则取 raw fbclid优先 `$rawFbclid`fallback `$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` 均改为:
```php
$fbc = $this->resolveFbcForCapi($entity->fbc, $entity->fbp);
```
不再直接把 `ad_fbc` 当 `fbc` 上报。
### B. 恢复 Purchase 幂等
```php
"event_id" => $entity->orderId !== '' ? $entity->orderId : uniqid('purchase_', true),
```
优先订单号;仅测试/缺单号时 fallback。
### C. 哈希 `external_id`
```php
"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` **或** catch `ClientException` 时读取 response body**日志禁止输出含 access_token 的完整 URL**(只记 pixel_id、status、body
```php
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.php`](slot_console/app/command/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 事件
2. 对比修复前后 payload 日志(`registerSelf` / `purchaseSelf` 已有 body 日志)。
3. 跑 slot 后端 completion report改动 PHP 后必跑)。
---
## 改动范围
| 文件 | 改动 |
|---|---|
| [`slot_console/app/service/dot/FbService.php`](slot_console/app/service/dot/FbService.php) | 核心resolve fbc、event_id、external_id 哈希、array_filter、可选响应校验 |
| [`slot_console/app/command/dot/FbDot.php`](slot_console/app/command/dot/FbDot.php) | 可选:变量命名/clarity无业务逻辑必须改 |
**不需要改** `slot_user` 注册写入逻辑(`ad_fbc` 存 raw fbclid 是合理设计,格式化应在 CAPI 发送层完成)。