# @playcraft/ads-tracking

可玩广告埋点追踪库，支持 Bigabid、InMobi、AppLovin、Mintegral、social_media 等多渠道的标准化埋点上报。

版本：`1.1.2` · 产物格式：`esm` / `cjs` / `iife`（由 tsup 构建，`legacyOutput` 保留 `dist/index.js` 入口）。

---

## 安装与构建

```bash
npm install
npm run build      # npx tsup → 输出 dist/（esm/cjs/iife + d.ts）
npm run deploy     # 构建并发布到 npm（--access public）
```

发布的 `files` 仅包含 `dist`、`LICENSE`、`defines.d.ts`。

---

## 渠道选择

渠道通过全局常量 `AD_NETWORK` 决定（`defines.d.ts` 中声明，由打包工具注入，如 `playable-scripts` 的 `--network`）。

| AD_NETWORK 值 | 渠道 | 追踪方式 |
|---------------|------|----------|
| `bigabid` | Bigabid | Image Pixel（从 `window.BIGABID_BIDTIMEMACROS` 取 URL） |
| `inmobi` | InMobi | Image Pixel（从 `window.INMOBI_DSPMACROS` 取 URL）+ 停留时长定时器 |
| `applovin` | AppLovin | Analytics API（`window.ALPlayableAnalytics.trackEvent()`） |
| `mintegral` | Mintegral | HttpAPI（`window.HttpAPI.sendPoint()`，对应 `action.json` 的 action1~action5） |
| `social_media` | AiX WebSDK | `aix_track_web_sdk_v2.js` 的 `collectInfo` API |

> 未匹配上述任一值时，所有事件静默跳过（`getCurrentAdNetwork` 返回 `null`）。
>
> ⚠️ `defines.d.ts` 中列出的 `AD_NETWORK` 联合类型比埋点实际支持的 5 个渠道更宽（含 `unity`、`google`、`pangle` 等）。这些未支持的渠道当前只做透传、不会上报任何埋点，接入前请确认已配置对应宏/SDK。

---

## 统一 API

所有埋点通过单一 `tracking` 对象暴露，导入方式：

```ts
import { tracking } from '@playcraft/ads-tracking';

// 各事件在内部按 AD_NETWORK 自动路由到对应渠道实现
tracking.onPlaycraftTrackingInit();        // SDK 初始化开始（同时初始化进度里程碑）
tracking.onPlaycraftTrackingLoading();     // 资源加载中
tracking.onPlaycraftTrackingLoaded();      // 资源加载完成、主场景就绪
tracking.onPlaycraftChallengeStart();      // 首次用户交互 / 挑战开始
tracking.onPlaycraftChallengeProgress(75); // 上报进度（0-100），内部触发里程碑
tracking.onPlaycraftChallengeSuccess();    // 挑战成功（全局只上报一次）
tracking.onPlaycraftChallengeFinish();     // 游戏结束
tracking.onPlaycraftInstall();             // 点击安装 / CTA
tracking.onPlaycraftRetry();               // 重试
tracking.onPlaycraftChallengeFailed();     // 挑战失败
```

每个方法均可传入可选的 `adNetwork` 参数覆盖全局 `AD_NETWORK`（用于多实例场景）。

### 各渠道事件映射

| tracking 方法 | bigabid | inmobi | applovin | mintegral | social_media |
|---------------|---------|--------|----------|-----------|--------------|
| `onPlaycraftTrackingInit` | `mraid_viewable` | `Ad_Load_Start` | `LOADING` | `action1` | 加载 AiX SDK + 开始记录停留时长 |
| `onPlaycraftTrackingLoading` | `game_viewable` | `Ad_Viewable` + 启动停留时长定时器 | `LOADED` | — | — |
| `onPlaycraftTrackingLoaded` | — | — | `DISPLAYED` | — | `playcraftTrackingLoaded` |
| `onPlaycraftChallengeStart` | `engagement` | `First_Engagement` | `CHALLENGE_STARTED` | `action2` | — |
| `onPlaycraftChallengeSuccess` | `complete`（去重） | `Gameplay_Complete` | `CHALLENGE_SOLVED` | `action3` | `playcraftChallengeSuccess` |
| `onPlaycraftChallengeFinish` | `complete` | `Gameplay_Complete` + 清除定时器 | `ENDCARD_SHOWN` | `action5` | — |
| `onPlaycraftInstall` | `click` | `DSP_Click` + 清除定时器 | `CTA_CLICKED` | `action4` | `std_clicked_download_patch` |
| `onPlaycraftRetry` | — | — | `CHALLENGE_RETRY` | — | — |
| `onPlaycraftChallengeFailed` | — | — | `CHALLENGE_FAILED` | — | — |

说明：
- **Bigabid** `complete` 事件通过 `bigabidCompleteTriggered` 标志去重，避免 repeat。
- **InMobi** 在 `Ad_Viewable` 后启动 5/10/15/20/25/30 秒定时器上报 `Spent_{n}_Seconds`，在离开/下载时由 `clearInMobiDurationTracking()` 清理。
- **AppLovin / Bigabid** 支持进度里程碑（见下）。

### 进度里程碑系统

`tracking.onPlaycraftChallengeProgress(percent)` 接收 0-100 的进度值，按渠道自动触发里程碑（不重复上报）：

| 渠道 | 里程碑 | 触发动作 |
|------|--------|----------|
| `applovin` | 25% / 50% / 75% | `CHALLENGE_PASS_25` / `_50` / `_75` |
| `applovin` | 100% | 转调 `onPlaycraftChallengeSuccess()` |
| `bigabid` | 100% | 转调 `onPlaycraftChallengeSuccess()` |

InMobi 不监控进度百分比。里程碑在 `onPlaycraftTrackingInit()` 时按当前渠道初始化。

---

## social_media 渠道（AiX WebSDK）

### 初始化

在 `index.ts` 中通过 `sdk.playcraftInit` 的 `tracker` 选项传入配置；该配置会通过 `setSocialTrackerConfig()` 注册，**必须在 `onPlaycraftTrackingInit()` 之前调用**：

```ts
sdk.playcraftInit(
  (width, height) => { StartGame("game-container"); },
  {
    tracker: {
      game_code: "arrows",    // 由 AiX 团队提供
      skey: "",               // 由 AiX 团队提供
      track_id: "",           // 由 AiX 团队提供
      page_type: "landing",   // 页面类型，如 "share"、"landing"
    },
  },
);
```

配置也可通过 `theme/index.ts` 的 `ThemeVariantConfig` 读取：

```ts
export default {
  config: {
    levels: [level],
    share: true,
    game_code: "arrows",
    skey: "",
    track_id: "",
    page_type: "landing",
  } as ThemeVariantConfig,
};
```

### 构建命令

```bash
npm run dev:wp social_media           # 简写
npm run dev:wp -- --network social_media  # 完整参数
```

### 上报事件

| 事件名 | 触发时机 | 来源 |
|--------|----------|------|
| `std_page_scan` | 页面浏览 | AiX SDK 自动上报 |
| `stay_duration` | 页面关闭/切后台 | `startStayDuration()` + `visibilitychange`/`beforeunload`/`pagehide`，附带 `bounce_rate` / `stay_seconds` |
| `std_clicked_download_patch` | 点击下载按钮 | `tracking.onPlaycraftInstall()` → `reportDownload()` |
| `std_share` | 点击分享按钮 | `tracking.onShareClick()` / `reportShare()`（playable-share 内部亦会自上报） |
| `std_share_channel` | 点击分享渠道 | `tracking.onShareChannelClick()` / `reportShareChannel()`（`event_value` = 渠道 id） |
| `playcraftTrackingLoaded` | 游戏加载完成 | `reportLoaded()`，附带 `real_channel` / `to_c` |
| `playcraftChallengeSuccess` | 游戏通关 | `reportChallengeSuccess()` |

### 上报字段

所有 `collectInfo` 调用统一携带以下字段：

| 字段 | 来源 | 说明 |
|------|------|------|
| `event_name` | 调用方传入 | 事件名称 |
| `event_value` | 调用方传入 | 事件值 |
| `user_id` | `sessionStorage.user_id` 或本地持久化匿名 ID（`localStorage.anon_uid`） | 用户标识 |
| `login_channel` | `sessionStorage.login_channel` | 登录渠道 |
| `page_type` | `setSocialTrackerConfig` 配置 | 页面类型（一级字段） |
| `extend.src` | URL `src` 参数或 `detectEnvChannel()` | 首次来源渠道（不随分享覆盖） |
| `extend.cur_s` | `detectEnvChannel()` 实时取值 | 当前链接所在渠道 |
| `extend.page_index` | `window.location.href` | 当前页面地址 |
| `extend.to_c` | URL `to_c` / 调用方传入 | 目标分享渠道 |
| `extend.real_channel` | `detectEnvChannel()` | 实际运行环境（loading 事件） |

### 环境识别（env.ts）

`detectEnvChannel()` 是渠道 UA 特征表的**唯一权威来源**，用于 `src` / `cur_s` / `real_channel` 归因：

- in-app WebView 返回渠道 id：`messenger` / `facebook` / `instagram` / `tiktok` / `x` / `linkedin` / `whatsapp` / `line` / `telegram` / `snapchat` / `pinterest` / `reddit` / `youtube` / `wechat` / `kakao` / `zalo`。
- 官方浏览器（非 App 内置 WebView）返回 `off_br`（常量 `OFFICIAL_BROWSER`）。
- ⚠️ 顺序敏感：Messenger 的 UA 同时含 Facebook 特征，已排在 facebook 之前。
- 该函数为无状态纯函数，可安全被多份打包复制；`collectInfo` / `reportShare` 等依赖单例的方法通过 `window.playableAdsTracking` 桥接暴露（见下）。

### 全局桥接

`window.playableAdsTracking` 在模块加载时挂载，供 `playable-share` 等零依赖包复用本包封装：

```ts
window.playableAdsTracking = {
  collectInfo,          // 统一上报封装
  reportShare,          // 分享按钮点击
  reportShareChannel,   // 分享渠道点击
  detectEnvChannel,     // 环境识别
  isOfficialBrowser,     // 是否官方浏览器
};
```

---

## 架构

```
index.ts (入口，导出 tracking / detectEnvChannel / isOfficialBrowser)
  ├─ tracking.ts           渠道路由 + 进度里程碑
  │    ├─ getCurrentAdNetwork()       全局 AD_NETWORK 解析 + 类型保护
  │    ├─ TRACKING_EVENTS 分发表        各渠道事件 → 具体上报函数
  │    ├─ onPlaycraft*() 方法          对外 API，按渠道调用分发表
  │    ├─ initProgressMilestones()    按渠道注册进度里程碑
  │    └─ onPlaycraftChallengeProgress() 进度值 → 里程碑触发（去重）
  ├─ social-share.ts       social_media 渠道专用（AiX WebSDK）
  │    ├─ loadAiXSDK()             加载 + 注入 SDK 参数
  │    ├─ collectInfo()            统一上报封装（extend 归因）
  │    ├─ startStayDuration()/reportStayDuration()  停留时长/跳出率
  │    ├─ reportLoaded()/reportChallengeSuccess()/reportDownload()
  │    ├─ reportShare()/reportShareChannel()       分享上报
  │    ├─ autoReportShare()/autoReportShareChannel()  SDK 存在时自上报
  │    └─ getOrCreateAnonymousId()  本地持久化匿名 ID（兜底 user_id）
  └─ env.ts               环境识别（渠道 UA 特征表，纯函数）
```

### 文件结构

```
src/
├── index.ts        # 入口，导出 tracking 与 env 工具
├── tracking.ts      # 渠道路由 + 进度里程碑系统
├── social-share.ts  # social_media 渠道 AiX WebSDK 埋点
└── env.ts           # 运行环境识别（渠道 UA 特征表）
```

---

## 注意事项

1. **调用顺序**：`social_media` 渠道必须先 `setSocialTrackerConfig()`（由 `playcraftInit` 的 `tracker` 注入）再调用 `onPlaycraftTrackingInit()`，否则 SDK 不会加载。
2. **挑战成功去重**：`onPlaycraftChallengeSuccess()` 全局仅上报一次（`challengeSuccessReported`），重复调用会被忽略。
3. **匿名 ID 兜底**：早期用公网 IP 做兜底 user_id，存在首报必空、移动网络易超时、NAT 下不准确等问题，现已改为 `localStorage.anon_uid` 本地持久化随机 ID（同步、不依赖网络）；隐私模式不可用时退化为会话内临时 ID（`temp-` 前缀）。
4. **Mintegral 宏**：依赖 `window.HttpAPI.sendPoint()`，由 Mintegral 容器注入；`action` 值对应 `action.json` 的 action1~action5。
5. **跨包复用**：`playable-share` 保持零依赖，通过 `window.playableAdsTracking` 桥接调用，而非直接 import 本包。
