This commit is contained in:
ray zhou
2026-06-29 14:51:55 +08:00
parent 225fb2bd28
commit 2dd9f17da9
319 changed files with 29461 additions and 9412 deletions

View File

@@ -0,0 +1,326 @@
<!-- 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 发送层完成)。