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 で:

[[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

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 で SQL を直接実行できる。

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

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 ランキング

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 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)
↗ Source markdown (analytics-engine.md)