327 lines
15 KiB
Markdown
327 lines
15 KiB
Markdown
<!-- 46ed22aa-9d18-40e3-a7fb-ed92e69d49a2 -->
|
||
---
|
||
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`](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](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 发送层完成)。
|