# Workers Analytics Engine 運用

業務イベント (検索 / 閲覧 / コンバージョン) を Cloudflare Workers Analytics Engine (CAE) に書き出して、SQL API で集計する。

## なぜ CAE か

| 用途 | Workers Logs | OTel | Sentry | **CAE** |
|---|---|---|---|---|
| 検索クエリの集計 | △ | × | × | ◎ |
| LCP / レイテンシ階層 | × | ◎ | × | ○ |
| 5xx の即時通知 | △ | × | ◎ | × |
| 業務 KPI ダッシュボード | × | × | × | ◎ |
| 90 日無料保管 | × (3d) | 課金 | 課金 | ◎ |

CAE は「サンプル不要の業務指標」と「SQL で柔軟に集計したい」要件に最適。

## binding

`wrangler.toml` の各 env / 各 split file で:
```toml
[[env.dev.analytics_engine_datasets]]
binding = "ANALYTICS"
dataset = "parky_business_events"

[[env.prod.analytics_engine_datasets]]
binding = "ANALYTICS"
dataset = "parky_business_events_prod"
```

dataset 名は dev / stg / prod で分離（混入防止）。binding 未設定でも helper は no-op で動くので開発用 wrangler dev で安全。

## helper

[parky/api/src/lib/analytics-engine.ts](../../api/src/lib/analytics-engine.ts)

### 主要 API

```ts
import {
  recordSearchEvent,
  recordViewEvent,
  recordActionEvent,
  writeBusinessEvent,
} from "../lib/analytics-engine";

// 検索イベント
recordSearchEvent(c.env, {
  kind: "ai",                // "ai" / "lot" / "media"
  query: body.message,
  resultCount: 1,
  channel: "mobile",
  userId,
  latencyMs: 230,
});

// 閲覧イベント
recordViewEvent(c.env, {
  resource: "lot",
  resourceId: lot.code,
  channel: "web",
  userId,
});

// 行動イベント (お気に入り / 経路選択 / 通知 ON)
recordActionEvent(c.env, {
  action: "favorite.add",
  channel: "mobile",
  userId,
  metadata: { lot_code: "shibuya-001" },
});

// 任意イベント
writeBusinessEvent(c.env, {
  event: "sponsor.click",
  channel: "web",
  tags: { campaign_id: "spring-2026" },
  metrics: { count: 1, revenue_yen: 500 },
  indexKey: userId,
});
```

### スキーマ

CAE の各 datapoint は次の 3 配列で構成される。helper は内部で詰める:

| Slot | 用途 | 上限 |
|---|---|---|
| `blobs[0]` | event 名 (例 `search.ai`) | 必須 |
| `blobs[1]` | channel | — |
| `blobs[2]` | locale | — |
| `blobs[3..19]` | tags ("key=value" 形式) | 17 個 |
| `doubles[0]` | count (default 1) | — |
| `doubles[1..19]` | sub metric | 19 個 |
| `indexes[0]` | 高 cardinality 索引 (user_id 等) | 1 個 / 96 文字 |

## 集計クエリ

[Cloudflare Dashboard](https://dash.cloudflare.com/analytics/sql) で SQL を直接実行できる。

### 例: 直近 24h の AI 検索 latency P95

```sql
SELECT
  blob1 AS channel,
  quantileTDigest(0.95)(double2) AS p95_latency_ms,
  count() AS hits
FROM parky_business_events
WHERE timestamp > NOW() - INTERVAL '1' DAY
  AND blob1 = 'mobile'
  AND blob0 = 'search.ai'
GROUP BY blob1
ORDER BY hits DESC;
```

### 例: 駐車場 detail view ランキング

```sql
SELECT
  index1 AS lot_code,
  count() AS views,
  uniqExact(index1) AS unique_users
FROM parky_business_events
WHERE timestamp > NOW() - INTERVAL '7' DAY
  AND blob0 = 'view.lot'
GROUP BY index1
ORDER BY views DESC
LIMIT 50;
```

## コスト

- 90 日まで無料保管
- write: 100 万 datapoint / 月まで無料、超過後 $0.25 / 100 万
- read (SQL API): query 1 回 = 100 万行スキャン まで無料、超過後 $1 / 100 万 行

Parky の現在の write rate (検索 + 閲覧) で月 50 万 datapoint 程度の見込み。

## 既存導入箇所

| 場所 | event |
|---|---|
| [bff/web/search.ts](../../api/src/bff/web/search.ts) | `search.ai` |

順次拡張: lot detail view / favorite / directions / sponsor click / push opt-in 等。

## トラブルシュート

### datapoint が出ない
- binding 未設定 → wrangler.toml を確認 (`grep ANALYTICS api/wrangler*.toml`)
- helper を呼んでない → grep `recordSearchEvent\|writeBusinessEvent`
- Cloudflare Account のプラン制限 → Workers Paid plan が必要 (Free でも binding は使えるが SQL API が無効)

### SQL に出てこない
- 反映まで数秒のラグあり (CAE は eventually consistent)
- dataset 名間違い → `parky_business_events` (dev) / `parky_business_events_prod` (prod)
