bq-analytics-install
プロジェクトにbq-analyticsをインストールして組み込みます。Phase 1では実行環境を検出してSDKを連携させます。Phase 2ではリポジトリをスキャンして認証・決済・主要機能の箇所を特定し、イベントを提案してtrack/identify/groupをインラインで組み込みます。Phase 3(オプション)ではVercel Edge Configを通じて機能フラグをプロビジョニングします。新規または既存プロジェクトへのアナリティクス導入時に利用できます。
description の原文を見る
Install AND instrument bq-analytics in a project. Phase 1 detects the runtime and wires the SDK. Phase 2 scans the repo for auth / payment / key feature surfaces, suggests events, and instruments track/identify/group inline. Phase 3 (optional) provisions feature flags via Vercel Edge Config. Use when the user wants to add analytics to a new or existing project.
SKILL.md 本文
プロジェクトに bq-analytics を追加する
このスキルには3つのフェーズがあります。フェーズ1と2は必須、フェーズ3はオプションですが低コストです。
- フェーズ1: SDK の準備とワイヤリング (下記のステップ 0–6)
- フェーズ2: auth / payment / 主要イベントの計測 ("Phase 2 — Instrumentation" セクション)
- フェーズ3: Vercel Edge Config 経由でのフィーチャーフラグ ("Phase 3 — Feature flags" セクション)。ユーザーから要求されるか、プロジェクトにフラグが必要な兆候がない限りスキップしてください (PostHog/LD/GrowthBook が依存関係にある、手書きの
if (FEATURE_*)ゲーティングなど)。日々のフラグ操作についてはclaude-skills/flags/SKILL.mdを参照してください。
ステップ 0 — ランタイムを検出する
bq-analytics の第一級の対象サーバーは Next.js App Router on Vercel です。ルートファクトリ (createTrackRoute, createLogDrainRoute) はそれ向けに設計されており、セットアップスクリプトは Vercel 固有の環境変数と Vercel Log Drain をプロビジョニングします。他のサーバーフレームワークも動作しますが、ユーザーが書いたアダプターが必要です (SDK には汎用の Request → Response ハンドラーが付属しており、Hono / Express / Fastify へのマッピングはわずか数行です。README を参照)。
リポジトリを確認して、どのパスに従うかを判断してください。
| シグナル | スタック | 第一級サポート? |
|---|---|---|
next.config.* + next が依存関係 | Next.js (おそらく Vercel 上) | ✅ Yes — 完全なインストールパス |
express, fastify, koa が deps にあり、Next がない | Node server (non-Next) | ⚠️ 部分的 — ルートハンドラーにはアダプターが必要。純粋なサーバーサイド bqTransport はそのまま動作 |
hono が deps にあり、Next がない | Hono (任意のランタイム) | ⚠️ 部分的 — bq-analytics/hono ミドルウェアが動作。c.req.raw でルートファクトリーを直接呼び出せる |
expo / react-native + app.json | Expo / RN (クライアント SDK) | ✅ Yes — httpTransport を使用して別の Next.js サーバーと通信 |
サーバーフレームワークなし、bin / tsx / ts-node スクリプト | Node CLI | ✅ Yes — bqTransport を直接呼び出す。ルート不要 |
pyproject.toml, Gemfile, go.mod | Non-Node | ⚠️ 手動 — あらゆる言語から /api/track に JSON を POST |
<script> だけの HTML ページ | ブラウザのみ | ✅ Yes。Next.js サーバーへの httpTransport 経由 |
プロジェクトに Next.js サーバーがなく、かつ クライアント側の呼び出しがある (ブラウザ/RN) 場合、ユーザーは /api/track をホストするために Next.js (またはその他の Web 標準) サーバーが必要です。進める前にこれを説明してください。共通パターン: 単一の Next.js プロジェクトがルートをホストし、Expo / RN アプリがそれらの httpTransport をそこに指す。
プロジェクトは混在している場合があります (例えば Next.js web + Expo monorepo) — Next.js 側にサーバールートをインストール、Expo 側にクライアント SDK をインストール、両方が同じバックエンドを指すようにしてください。
共通プリフライト (すべてのスタック)
- GCP プロジェクトを確認します。
gcloud projects list --format='value(projectId)'を実行します。gcloud billing projects describe <id>で課金が有効になっていることを確認します — BQ サンドボックス / 無料レベルではストリーミング挿入が禁止されており、SDK は 403 を返します。 - Vercel 上にホストされるスタックの場合:
vercel whoamiとvercel lsを実行します。セットアップのみのため、https://vercel.com/account/tokens からチームスコープのトークンを取得します — コミットしないでください。 - パッケージをインストールします:
pnpm add bq-analytics(npm に公開されています — Vercel CI で動作します。追加のセットアップは不要)。 - Vercel ランタイムのみ:
@vercel/functionsもプロジェクトの依存関係にあることを確認します — bq-analytics はgetVercelOidcToken()を通じてリクエストごとの OIDC トークンを読み込む必要があります (最新の Vercel はVERCEL_OIDC_TOKENを環境変数として公開していません)。ほとんどの Vercel プロジェクトはすでに推移的に持っていますが、そうでない場合は:pnpm add @vercel/functions。
GCP リソースをプロビジョニングする
セットアップスクリプトは BQ データセット + テーブル + IAM を処理します。Vercel 上にホストされるプロジェクトでは、Workload Identity Federation と Log Drain もプロビジョニングします。
Vercel + Next.js — 初回インストール (プロジェクトまだデプロイされていない):
# ステップ1: Log Drain 以外すべてプロビジョニング
VERCEL_TOKEN=<paste> \
./node_modules/bq-analytics/scripts/setup-bq-oidc.sh \
--gcp <GCP_PROJECT_ID> \
--team <VERCEL_TEAM_SLUG> \
--project <VERCEL_PROJECT_NAME> \
--skip-drain
# ステップ2: ルートファイルをワイヤリング (下の "ランタイムをワイヤリング" 参照)
# ステップ3: デプロイ (git push または `vercel --prod`)
# ステップ4: Log Drain を登録 (URL はライブである必要があります)
VERCEL_TOKEN=<paste> \
./node_modules/bq-analytics/scripts/setup-bq-oidc.sh \
--gcp <GCP_PROJECT_ID> \
--team <VERCEL_TEAM_SLUG> \
--project <VERCEL_PROJECT_NAME> \
--domain <PROJECT_DOMAIN> \
--drain-only
Vercel + Next.js — 再実行 / 修復 (プロジェクトすでにデプロイされている):
VERCEL_TOKEN=<paste> \
./node_modules/bq-analytics/scripts/setup-bq-oidc.sh \
--gcp <GCP_PROJECT_ID> \
--team <VERCEL_TEAM_SLUG> \
--project <VERCEL_PROJECT_NAME> \
--domain <PROJECT_DOMAIN>
Non-Vercel (Express, Hono, Render, Fly, Lambda など) — データセット + サービスアカウントのみが必要:
./node_modules/bq-analytics/scripts/setup-bq-oidc.sh \
--gcp <GCP_PROJECT_ID> --skip-vercel
その後、サービスアカウント JSON キーを作成し、ホストで GOOGLE_APPLICATION_CREDENTIALS_JSON 環境変数を通じてインジェクトするか、利用可能であれば Workload Identity (AWS/GCP など) を使用してください。
CLI / ローカル開発 — データセット以外のセットアップは不要です。gcloud auth application-default login で認証します。
スクリプトはべき等で以下を実行します:
events+logsBigQuery データセットを作成し、テーブル DDL を適用します。- (Vercel) WIF プール + 1つの Vercel プロジェクトにスコープされた OIDC プロバイダー。
- (Vercel) 2つのデータセットで
bigquery.dataEditor+ プロジェクトレベルでbigquery.jobUserを持つvercel-bqSA。 - (Vercel) Vercel プリンシパル (production, preview, development) を SA にバインドします。
- (Vercel) 7つの環境変数をすべての3つの Vercel 環境にプッシュします。
- (Vercel)
/api/internal/log-drainを指す Log Drain を作成します。
ランタイムをワイヤリングする — 1つ以上を選択
Next.js (Vercel)
2つのルートファイル + 1つのヘルパー:
src/app/api/track/route.ts
import { createTrackRoute } from "bq-analytics/next";
import { after } from "next/server";
export const POST = createTrackRoute({
projectId: process.env.GCP_PROJECT_ID,
apiKey: process.env.ANALYTICS_API_KEY,
waitUntil: (p) => after(() => p), // ← non-blocking: 200 returns in ~5-15ms
resolveUser: async (_req) => {
// Clerk: const { userId } = await auth(); return userId;
// NextAuth: const s = await getServerSession(); return s?.user?.id;
return null;
},
});
なぜ waitUntil なのか? これがないと、ハンドラーは BQ が挿入を確認するまでブロック (~50-150ms 遅延がすべてのクライアントリクエストに追加されます)。waitUntil を使用すると、BQ 挿入はレスポンス送信後にバックグラウンドで実行されます — クライアントは高速 200s を取得します。ブラウザ/RN SDK は既に localStorage/AsyncStorage 経由でネットワーク障害時に再試行するため、5xx フィードバックの欠落は負担ではありません。
src/app/api/internal/log-drain/route.ts
import { createLogDrainRoute } from "bq-analytics/next";
// MUST export both POST and GET. Vercel's modern drain creation validator
// probes GET with NO incoming headers and expects the response to carry
// `x-vercel-verify: <team-token>`. The setup script auto-fetches the token
// from your team and pushes it as VERCEL_VERIFY_TOKEN — pass it through.
export const { POST, GET } = createLogDrainRoute({
projectId: process.env.GCP_PROJECT_ID,
secret: process.env.LOG_DRAIN_SECRET!,
vercelVerifyToken: process.env.VERCEL_VERIFY_TOKEN,
});
VERCEL_VERIFY_TOKEN が設定されていない場合、セットアップスクリプトは drain 作成時にこれを検出し、Vercel の 422 レスポンスから期待されるトークンをパースし、環境変数としてプッシュ、再デプロイして --drain-only で再実行するよう指示します。
⚠️ POST 内で console.log しないでください — ドレイン済みのラインはそれ自体ドレインされます。無限ループです。console.error は実際のエラーのみです。
Clerk ミドルウェア (src/proxy.ts / src/middleware.ts) を使用している場合、/api/internal/log-drain と /api/track をパブリックルートリストに追加します。
ローカル開発 — 環境変数を引き出す
セットアップの手順1の後 (環境変数が Vercel にプッシュされた)、ローカルで引き出して pnpm dev が実際の BigQuery データセットに書き込めるようにします:
vercel env pull .env.local
Vercel OIDC トークンは約12時間ごとにローテーションします。pnpm dev では、SDK は @vercel/functions/oidc の getVercelOidcToken() を呼び出します。これはリクエストコンテキストから読み込みます — vercel env pull .env.local はサポート環境変数 (GCP_PROJECT_ID など) をポピュレートしますが、OIDC トークン自体はリクエストごとに動的に取得されます。"ID Token … is stale" エラーが表示される場合は、vercel env pull を再実行して開発環境バインディングをリフレッシュしてください。SDK は GCP_PROJECT_ID が見つからない場合はノーオペ トランスポートにフォールバック (vitest ラン と環境変数のないプレビュー デプロイはクラッシュしない) — しかし、pull を忘れるとイベントを静かにドロップすることになります。
Express / Hono / Fastify / Koa / raw Node
import pino from "pino";
import pinoHttp from "pino-http";
import { Analytics, bqTransport } from "bq-analytics";
import { pinoBqTransport } from "bq-analytics/pino";
const a = new Analytics({ transport: bqTransport({ projectId: process.env.GCP_PROJECT_ID! }) });
const logger = pino({}, pinoBqTransport({ projectId: process.env.GCP_PROJECT_ID!, analytics: a }));
app.use(pinoHttp({ logger })); // every request → logs.raw
// inside any handler — no flush() call here, see middleware below
a.track("checkout.started", { plan }, { userId });
// graceful shutdown
process.on("SIGTERM", async () => { await a.flush(); process.exit(0); });
Hono on Vercel / Cloudflare / Bun — フラッシュミドルウェアを一度インストール。その後、ハンドラーはクリーンなままです:
import { Hono } from "hono";
import { honoFlushMiddleware } from "bq-analytics/hono";
import { analytics } from "@/lib/analytics";
const app = new Hono();
app.use("*", honoFlushMiddleware(analytics)); // flushes once per response
これの後、ルートハンドラーは自分自身で analytics().flush() を呼び出すべきではありません。単に track() / identify() / group() を呼び出して返すだけ — ミドルウェアがフラッシュを行います。しないでください 各呼び出しを waitUntil(analytics().flush()) または Promise.resolve(analytics().flush()) でラップする — そはぼイラーストレート冗長です。
Express / Koa / raw Node の場合、同等のレスポンスフックを書いてください:
// Express
app.use((req, res, next) => {
res.on("finish", () => { void analytics().flush(); });
next();
});
Fastify は pino をネイティブに使用します — pino インスタンスを Fastify({ logger }) に渡し、pino-http をスキップしてください。フラッシング用、同じ方法で onResponse フックを使用してください。
ブラウザ
import { Analytics } from "bq-analytics";
import { browserTransport, attachBrowserAutoFlush, attachWindowErrorHandler } from "bq-analytics/browser";
const a = new Analytics({ transport: browserTransport({ url: "/api/track" }) });
attachBrowserAutoFlush(() => a.flush());
attachWindowErrorHandler(a);
Expo / React Native
import { Analytics } from "bq-analytics";
import { reactNativeTransport, attachExpoErrorHandler, attachAppStateFlush } from "bq-analytics/react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { AppState, Platform } from "react-native";
import Constants from "expo-constants";
import * as Updates from "expo-updates";
// Mutable headers ref — the RN transport spreads `config.headers` on every
// fetch, so mutating this object propagates new auth without rebuilding
// the Analytics instance (and losing the retry queue).
const headersRef: Record<string, string> = {};
// Mutable identity ref — the attach helpers below take getter closures
// that re-resolve userId on every event, so it tracks the *current*
// identity instead of whatever was set when the helpers were attached
// (typically null, before SecureStore loads).
let currentDeviceId: string | undefined;
const a = new Analytics({
transport: reactNativeTransport({
url: `${API_URL}/api/track`,
headers: headersRef,
storage: AsyncStorage,
}),
});
// Attach once at module load with getter closures.
attachExpoErrorHandler(a, ErrorUtils, () => ({
platform: Platform.OS,
userId: currentDeviceId,
}));
attachAppStateFlush(a, AppState, () => ({ userId: currentDeviceId }));
// Call this when identity loads / rotates.
export function bindIdentity(identity: { deviceId: string; deviceToken: string } | null) {
if (identity) {
headersRef.authorization = `Bearer ${identity.deviceToken}`;
currentDeviceId = identity.deviceId;
a.identify(identity.deviceId, {
platform: Platform.OS,
app_version: Constants.expoConfig?.version ?? null,
build_number:
Constants.expoConfig?.ios?.buildNumber ??
String(Constants.expoConfig?.android?.versionCode ?? "") || null,
ota_update_id: Updates.updateId, // null = on embedded JS
ota_channel: Updates.channel, // "production" | "preview" | "development"
runtime_version: Updates.runtimeVersion,
});
} else {
delete headersRef.authorization;
currentDeviceId = undefined;
}
}
ゲッター閉包を使う理由、静的オブジェクトではなく。 RN の ID は通常非同期にロード (SecureStore → state → render) されます。ヘルパーが ID がまだ null の間に静的な { userId } で接続される場合、その後のすべての app.state_changed と未検出エラーイベントは user_id: NULL で到着します。ゲッターフォームはイベントごとに再解決します。
OTA / ビルドトレイトを使う理由。 "Just OTA'd!" のトリアージ時、正直な答えは ota_update_id のみです — 他のすべてはユーザーが正確に報告することに依存しています。events.users は deviceId あたり最後の書き込みが勝つため、次の OTA の identify() 呼び出しはその場で行をアップデート; events.users は常に各デバイスの現在のビルドを反映します。これらすべてのイベントにスタンプをしないでください — これは events.raw をクエリの利益なしでブロート。
Node CLI
import { Analytics, bqTransport, httpTransport } from "bq-analytics";
import { attachCliHooks } from "bq-analytics/cli";
// Two transport choices:
const a = new Analytics({ transport: bqTransport({ projectId }) }); // direct to BQ (needs GCP creds)
// OR
const a = new Analytics({ transport: httpTransport({ url: "https://prod/api/track", headers: { "x-api-key": KEY } }) });
attachCliHooks(a, { source: "my-cli" });
a.track("cli.command_run", { command: process.argv[2] });
// ... do work ...
await a.flush(); // CRITICAL: process exits the moment you return
SDK を使用する
サーバーサイドシングルトン (src/lib/analytics.ts):
import { Analytics, bqTransport } from "bq-analytics";
declare global { var __bqa: Analytics | undefined; }
export function analytics() {
if (!globalThis.__bqa) {
globalThis.__bqa = new Analytics({
transport: bqTransport({
projectId: process.env.GCP_PROJECT_ID!,
eventsDataset: process.env.BQ_EVENTS_DATASET,
logsDataset: process.env.BQ_LOGS_DATASET,
}),
});
}
return globalThis.__bqa;
}
任意のルートハンドラー内:
import { after } from "next/server";
import { analytics } from "@/lib/analytics";
export async function POST(req: Request) {
// ... do work ...
analytics().track("foo.bar", { ... }, { userId });
after(() => analytics().flush());
return Response.json({ ok: true });
}
フェーズ2 — 計測 (スキップしないでください)
インストールはパイプを設置します。計測 はデータを有用にする部分です — track(), identify(), group() 呼び出しを正しい場所に置くこと。これをユーザーに任せないでください。仕事をしてください。
順序が重要です: 既に存在するものをまず移行 (安い1対1スワップ)、次に净新計測を追加します。コードベースが他の場所で既に発行しているイベント用の新しい track() 呼び出しを書かないでください。
2-pre-A. 既存の分析ツールを検出して移行
何かを追加する前に、既に使用中の分析 SDK をグレップしてください:
# 探すもの
rg -l 'posthog-js|posthog-node|@posthog' --type ts --type tsx # Clerk
rg -l '@segment/analytics-node|analytics-node|rudder-sdk' --type ts --type tsx # NextAuth
rg -l '@amplitude/analytics-(node|browser)|amplitude-js' --type ts --type tsx # Supabase
rg -l 'mixpanel|@mixpanel' --type ts --type tsx # Lucia
rg -l 'gtag\(|google-analytics' --type ts --type tsx # WorkOS
rg -l 'plausible|umami' --type ts --type tsx
何かがマッチしたら:
- すべての呼び出しサイトをリスト化
.track(,.identify(,.group(,.capture(,.alias(など - API を1対1にマッピング:
posthog.capture('event', props, { distinct_id })→analytics.track('event', props, { userId: distinct_id })posthog.identify(id, traits)→analytics.identify(id, traits)posthog.group(type, key, props)→analytics.group(type, key, props, userId)analytics.track(name, props, ctx)(Segment) → 同じ名前、同じシェイプamplitude.track(eventType, props)→analytics.track(eventType, props, { userId })mixpanel.track(name, props)→analytics.track(name, props, { userId })gtag('event', name, params)→analytics.track(name, params, { userId })
- インラインで置き換え — 同じイベント名 + プロパティシェイプを保持して、履歴分析が実施されるようにしてください。
- すべてのコールサイトが移行されたら古い SDK を削除
package.jsonから。デュアルライトしないでください — それはコストトラップであり、イベント名が漂流します。
移行が部分的である場合 (ユーザーはリプレイのために PostHog を保持したいが、分析 + フラグのために BQ を使用) 、これを明示的にユーザーに伝え、気になるイベントのみをデュアルライトしてください。フィーチャーフラグについては特に、bq-analytics は Vercel Edge Config でサポートされた独自の最小フラグシステムを備えています — Phase 3 below をオプトイン セットアップに参照し、claude-skills/flags/SKILL.md を継続的な操作に参照してください。
2-pre-B. 手書きされたテレメトリエンドポイントを検出して移行
多くのインディープロジェクトは手書きのデバッグエンドポイントを持ちます — /api/debug/log, /api/beacon, /api/event, /api/log, /api/telemetry, /api/track (yours, conflict!)。見つけてください:
fd -t f 'route\.(ts|tsx|js)$' . | xargs rg -l 'debug/log|/beacon|/telemetry|/event/log' 2>/dev/null
rg -l 'app\.(post|get)\(.{1,40}(beacon|telemetry|debug-log|debug/log)' --type ts
rg -l 'console\.log\(`?\[(beacon|telemetry|track|event|user)\]' --type ts --type tsx
各マッチについて、決定してください:
- 純粋なログフォーワーディング (ハンドラーは
console.log(JSON.stringify(body))だけ): ハンドラーを削除します。それは冗長です —bq-analyticsは/api/trackを提供します。呼び出し元を/api/trackに POST するように更新してください。{ records: [{ kind: "log", row: {...} }] }。 - ハンドラー内のカスタムロジック (auth チェック、デバイス検索、構造化フィールド抽出): ルートを保持してください がその終端の
console.logをanalytics.log("info", message, fields, source)に置き換えます。ルートはそのままします。ストレージはログ移動 → BQ に移動。 - クライアント側ビーコンヘルパー (例えば Expo / RN / ブラウザファイル内で
makeBeacon(deviceToken)): ヘルパーをbq-analyticsの SDK からanalytics.log()を呼び出すように書き換えます。同じファイア・アンド・フォーゲット セマンティクス。同じペイロード シェイプ。保守する1つ少ないカスタム関数。
recipes.im パターン (サーバーの /api/debug/log + [beacon] console.log、クライアントの makeBeacon(deviceToken)) は正規ケース — サーバー console.log と クライアント fetch('/api/debug/log', ...) の両方を analytics.log(...) と analytics.track(...) で置き換えます。
2-pre-C. 移行価値がある構造化 console.log 呼び出しを見つける
コードベース内のすべての console.log を移行することはできません — ほとんどはデバッグです。しかし、構造化イベントのように見えるもの (接頭辞が [name]、JSON エンコード、または特に検索可能性のために書かれたもの) は候補です:
rg -n 'console\.log\(`?\[' --type ts --type tsx | head -50
rg -n 'console\.log\(JSON\.stringify' --type ts --type tsx | head -30
リストをユーザーに表示 (~30 にキャップ) し、どれを移行するかを尋ねます。自動移行しないでください — これはしばしばデバッグノイズであり、ユーザーは BQ で実際に欲しくないかもしれません。
2a-pre. ノーオース アプリ: デバイスをユーザーとして扱う
いくつかのアプリにはユーザー auth がありません — デバイスとグループのみ (例えば Expo アプリ。各インストールが deviceId を取得して householdId に参加する、または CLI ツールマシンでキーに付けられる)。これら:
userId= 永続的なデバイス/インストール識別子。 app ローンチ全体で安定しています。それはidentify()が必要なすべてです。identify(deviceId, traits)—traitsはデバイスレベル:platform,app_version,device_label,created_at。group("household", householdId, traits, deviceId)— グループは org 相当です。4番目の引数はこのデバイスを世帯に付けます。- イベント:
track(event, props, { userId: deviceId })—userId: householdIdは決して。
一般的な間違いを回避してください: 世帯 / org / チーム / ワークスペースをユーザーとして識別すること。それはグループです。group() を使用してください。
アプリが後で実際の auth を追加する場合、新しいパターンは identify(realUserId, traits) とエイリアスマッピング (alias(deviceId → realUserId)) — しかし、その時まで、device-as-user は Segment/PostHog/Amplitude が匿名ユーザーを扱う方法とまったく同じように動作します (安定した UUID = distinct_id)。
2a. Auth サーフェスを見つけて計測
auth プロバイダーのリポジトリをグレップしてください:
# 探すもの
rg -l 'clerk|@clerk' --type ts --type tsx # Clerk
rg -l 'next-auth|getServerSession' --type ts --type tsx # NextAuth
rg -l '@supabase/auth' --type ts --type tsx # Supabase
rg -l 'lucia-auth|@lucia-auth' --type ts --type tsx # Lucia
rg -l '@workos-inc/authkit' --type ts --type tsx # WorkOS
その後、identify() で以下のタッチポイントを計測します:
| Auth プロバイダー | ここを計測します | 呼び出し |
|---|---|---|
| Clerk | user.created の webhook ハンドラー (検索: rg 'user\.created' app/api/webhooks/clerk) | identify(userId, { email, signup_country, signup_source }) |
| Clerk | user.updated の webhook ハンドラー | identify(userId, { email, ...changedTraits }) |
| NextAuth | auth.ts / [...nextauth].ts 内の events.signIn コールバック | identify(userId, { email, provider }) |
| Supabase | サインアップルート + INSERT ON public.users トリガー consumer | identify(userId, { email }) |
| カスタム | DB でユーザー行を作成する場所 | identify(userId, traits) |
リクエストごとにフラッシュしてください。呼び出しごとではなく。 フラッシュミドルウェア (Hono honoFlushMiddleware, Next after(() => flush()) ハンドラーの最後に一度、Express res.on("finish", flush)) を使用している場合、ルートは自身のフラッシュを必要としません。すべての track/identify 呼び出しに waitUntil(analytics().flush()) を散らすことは — それはボイラープレートアンチパターンです。ミドルウェアを使用していない場合は、レスポンスを返す前に await analytics().flush() を一度だけ実行してください。
2b. Payment / Billing サーフェスを見つけて計測
rg -l 'stripe|@stripe/stripe' --type ts # Stripe
rg -l 'polar\.sh|@polar-sh' --type ts # Polar
rg -l 'paddle|@paddle' --type ts # Paddle
rg -l 'lemonsqueezy|@lemonsqueezy' --type ts # Lemon Squeezy
rg -l 'app/api/webhooks/stripe' --type ts # Stripe webhook route
支払い webhook ハンドラーで計測する標準イベント:
| イベント | トリガー | コード |
|---|---|---|
subscription.created | customer.subscription.created | track("subscription.created", { plan, period, price_cents }, { userId }) |
subscription.upgraded | customer.subscription.updated with previous_attributes.items containing higher tier | track("subscription.upgraded", { from_plan, to_plan }, { userId }) |
subscription.canceled | customer.subscription.deleted | track("subscription.canceled", { plan, reason }, { userId }) |
payment.failed | invoice.payment_failed | track("payment.failed", { amount_cents, attempt_count }, { userId }) |
payment.recovered | invoice.paid after a failed attempt | track("payment.recovered", {...}, { userId }) |
また すべてのサブスクリプション状態変更で identify() を呼び出して、plan / is_pro トレイトをリフレッシュします:
identify(userId, {
plan: subscription.plan.id,
plan_period: subscription.recurring.interval,
is_pro: subscription.status === "active",
current_period_end: subscription.current_period_end,
});
2c. ルートサーフェスからイベントを提案
アプリのルートをリストして、可能性のあるイベントサーフェスを見つけます:
# Next.js App Router
fd 'page\.tsx?' src/app | grep -v node_modules
fd 'route\.tsx?' src/app | grep -v node_modules
# Express / Hono — grep for HTTP methods
rg -E '\b(app|router|honoApp)\.(get|post|put|delete|patch)\(' --type ts
各カタログマッチを下のカタログと照合し、3〜7個のイベント を提案して始めましょう — 一度に 30 個の計測を試みないでください。ユーザーにどれを配線するかを尋ねます。イベント単位では、正しいコード位置を見つけ、インラインで track() 呼び出しを書きます。
共通イベントカタログ
リポジトリ内の実際のコードにマッピングするもの。存在しない機能のイベントを作らないでください。
買収 / アクティベーション
pageview(Log Drain を代わりに検討してください — すべてのリクエストを自動的にキャプチャします)signup.started— サインアップフォームマウント、または最初のフィールド相互作用時signup.completed— Clerkuser.createdwebhook が発火した後 (server-side, authoritative)onboarding.completed— ユーザーがウェルカムフロー完了後referral.invited/referral.accepted— 紹介システムがある場合
エンゲージメント / コア機能使用
<feature>.used— ルート内の動詞の後で命名してください。例えばtranslation.started,transcription.completed,recipe.imported,chord.detected。実際に API ルートが何をしているかを読んで見つけてください。search.performed— 検索ボックスがある場合export.downloaded— ダウンロードボタンがある場合share.created— 共有/招待リンクがある場合<resource>.created/<resource>.deleted— 主要な名詞 (プロジェクト、レシピ、ビデオなど)
マネタイゼーション
pricing.viewed—/pricingページのレンダーまたは最初の相互作用で計測checkout.started— ユーザーが "Subscribe" / "Buy" をクリック時checkout.completed— Stripe webhook (2b を参照)subscription.*— Stripe webhook (2b を参照)paywall.viewed/paywall.dismissed— インアプリアップセルがある場合
リテンション / リスク
login.completed— Clerk ダッシュボード vs BQ で DAU/MAU メトリクスを希望する場合に便利account.deletedsupport.contacted— サポートフォーム / Intercom / 同様な場合error.encountered— 重要な既知の回復可能エラー (支払い失敗、アップロード却下など)
B2B / マルチテナント のみ
team.created,team.member_invited,team.member_joined—group("team", id, traits)とgroup("team", id, traits, userId)を伴ってメンバーシップを配線
2d. 製品固有のコンテキストを読む
イベントを提案する前に、スキャンしてください:
CLAUDE.md/seo/CLAUDE.md/docs/— ユーザーの独自の製品/マーケティングノートはしばしば重要なメトリクスをリストしますREADME.md— 製品の UVP は測定すべきことをヒント- env vars またはコード内の価格 / プラン ID —
planトレイト値を駆動 - 既存の分析呼び出し (PostHog, Segment, Amplitude) — 連続性を保つために1対1で置き換え
ユーザーが project_uvps.md のようなメモリノートを持っている場合、それはしばしば明示的にケアしていることをステートメントします。それを尊重してください。
2e. すべてのイベントのエンリッチをワイヤアップ
createTrackRoute ファクトリーは enrich フックを受け入れます。これをセット リクエストレベル フィールド (ip, ua) をアタッチし、任意の hot トレイト events.users から参加したくない:
export const POST = createTrackRoute({
...,
enrich: (req, record) => {
if (record.kind !== "event") return record;
const props = JSON.parse(record.row.properties || "{}") as Record<string, unknown>;
props.ip ??= req.headers.get("x-forwarded-for");
props.ua ??= req.headers.get("user-agent");
return { ...record, row: { ...record.row, properties: JSON.stringify(props) } };
},
});
注: enrich はレコードごとに1回実行されるため、N イベントのバッチは N 回プロパティをパース+文字列化します。これは通常のペイロード向けの低コストですが、ここに高価な検索をしないでください — リクエストスコープのキャッシュから引く、またはそれを必要としないイベントはスキップしてください。
2f. 製品フィードバック取込をワイヤアップ (オプション)
analytics.feedback({ kind, subject, message, severity, url, properties }, attrs) は SDK の一部です — track() と同じバッファ/フラッシュ ライフサイクル。events.feedback に着地し、events.users と events.raw へ user_id で参加可能。ユーザーが X を言った後。実際にどうなっていたんですか?
kind ∈ "bug" | "request" | "general" (任意の文字列受け入れ)。1つのメソッドは3つすべての意図をカバーします — 主要なパターンに一致します (Sentry, Featurebase)。匿名送信は受け入れられます。
スキップしてください ユーザーが持っていない限り (a) インアプリフィードバック / コンタクトフォーム、または (b) Claude がフルセッションコンテキストでユーザーが報告した問題を調査することを望む。さもなければ後で opt-in するのに任せます。
検索するサーフェスとワイヤアップ:
# インアップフィードバック / コンタクト / サポートボタン
rg -l 'feedback|contact|support|report.{0,20}bug' --type ts --type tsx | head
# エラーバウンダリー / クラッシュレポーター。フィードバックプロンプトを付ける場合があります
rg -l 'ErrorBoundary|componentDidCatch|window\.onerror' --type ts --type tsx
# existing helpdesk integrationsミラーリング
rg -l 'intercom|crisp|plain\.com|@plain/sdk|usepylon' --type ts --type tsx
典型的なワイヤアップ — 任意の fetch('/api/contact', ...) / Intercom 送信を analytics.feedback() で置き換えます。同じ /api/track ハンドラーは新しい kind: "feedback" レコードを受け入れます、新しいルート不要:
// クライアントウィジェット
analytics.feedback(
{ kind: "bug", message, url: location.pathname, properties: { app_version } },
{ userId },
);
await analytics.flush(); // ブラウザautoflush も pagehide で検出
// サーバー (例えば /api/contact 実際のチケットシステムをラップする)
analytics().feedback({ kind: "general", subject, message }, { userId });
after(() => analytics().flush());
それを oversell しないでください — bq-analytics はインボックス、返信、またはチケットステータスを提供しません。プロジェクトがそれらを必要とする場合は、Linear/Plain/Pylon にミラーリングすることを提案し、feedback() を純粋にエージェント調査のためのウェアハウスミラーとして使用してください。README の "Product feedback" セクションにはフレーミングがあります。
2g. 計測後: スモークコミットをシップ
ユーザーが再デプロイする前に:
- 計測したイベントのリストを印刷します (ファイル:行 ごと)。
- 2〜3の例
bq querySQL 文字列。ユーザーは最初の実際のセッション後に各サーフェスが正しく発火したことを確認するために実行できます。 - フォローアップを提案してください: "1日の実際のトラフィック後、
/bq-analytics-query show me event volume last 24hを実行して流れるものを見てください。"
フェーズ3 — フィーチャーフラグ (オプション)
ユーザーがフラグを要求、PostHog/LaunchDarkly/GrowthBook から移行、または if (FEATURE_FOO), process.env.NEXT_PUBLIC_FLAG_*, または手書きのゲーティングのような信号がある限りスキップしてください。
フラグは分析アイデンティティ (userId) を共有し、同じ events.raw テーブルに $flag_called エクスポージャーを発行します — だからインパクト分析は単なる BigQuery です。
ステップ1 — Edge Config ストアをプロビジョニング
./node_modules/bq-analytics/scripts/setup-edge-config.sh
それはするもの (べき等):
- 既に
vercel linkされていない場合はvercel link - Edge Config (
bq-analytics-flags) を作成またはリユース flagsキーを{}として初期化- 読み取りトークンを鋳造し、
EDGE_CONFIGを Vercel Production に設定 vercel env pull .env.local --environment production
Preview / development 環境は自動ポピュレートされません (Vercel CLI の git_branch_required 詐欺 on preview)。必要に応じてダッシュボードまたは REST ヘルパーで追加します。
ステップ2 — SDK をワイヤアップ
サーバー (Next.js / Hono / Node):
// src/lib/flags.ts
import { Flags } from "bq-analytics";
import { edgeConfigSource } from "bq-analytics/edge-config";
import { analytics } from "./analytics";
declare global { var __bqf: Flags | undefined; }
export function flags() {
return globalThis.__bqf ??= new Flags({
source: edgeConfigSource(),
analytics: analytics(),
refreshIntervalMs: 60_000,
});
}
// 任意のルートハンドラー内
await flags().ready();
if (flags().isOn("new-checkout", userId)) { /* new flow */ }
ブラウザ / RN — Edge Config トークンを決して expose しないでください:
// src/app/api/flags/route.ts
// Subpath import — `bq-analytics/next/flags` は `@vercel/edge-config` を引く唯一のエントリーです
// (オプションピア)。/api/track など lean を保ちます。
import { createFlagsRoute } from "bq-analytics/next/flags";
export const GET = createFlagsRoute({
resolveUser: async (req) => /* /api/track と同じ auth */ null,
filter: (flags) => Object.fromEntries( // allowlists をストリップ
Object.entries(flags).map(([k, v]) => [k, { ...v, users: undefined }]),
),
});
// ブラウザ / RN クライアント
import { Flags, httpSource } from "bq-analytics";
const f = new Flags({ source: httpSource({ url: "/api/flags" }) });
await f.ready();
f.isOn("new-checkout", userId);
ステップ3 — bq-flags CLI で操作
pnpm exec bq-flags on new-checkout --rollout 25%
pnpm exec bq-flags allow ai-suggestions u_alice u_bob
pnpm exec bq-flags rollout new-checkout 100%
pnpm exec bq-flags off kill-old-flow
pnpm exec bq-flags eval new-checkout --outcome subscription.started
ユーザーに claude-skills/flags/SKILL.md を渡してください。コホートマテリアライゼーション from BigQuery を含むフル ops のために。
ステップ4 — 検証
pnpm exec bq-flags on smoke-test --rollout 100%
pnpm exec bq-flags list # should show smoke-test
pnpm exec bq-flags delete smoke-test
その後、コード内でフラグをゲート、デプロイ、および確認します $flag_called イベントは events.raw に表示:
bq query --nouse_legacy_sql --format=pretty \
"SELECT * FROM \`<gcp>.events.raw\` WHERE event_name='\$flag_called' ORDER BY ts DESC LIMIT 5"
エンドツーエンドを確認
再デプロイ後:
# CLI からテストイベントを送信
curl -X POST https://<domain>/api/track \
-H "x-api-key: $ANALYTICS_API_KEY" \
-H "content-type: application/json" \
-d '{"records":[{"kind":"event","row":{"event_id":"test-1","ts":"2026-01-01T00:00:00Z","event_name":"smoke","user_id":"cli","anonymous_id":null,"session_id":null,"properties":"{}"}}]}'
# ランドを確認 (ストリーミングバッファのため ~10s 待つ)
bq query --nouse_legacy_sql --format=pretty \
"SELECT ts, event_name FROM \`<gcp>.events.raw\` WHERE event_name = 'smoke' ORDER BY ts DESC LIMIT 1"
ログドレイン検証用、console.logs するルートを any Hit し、~10–60s 後に logs.raw をチェック — Vercel はドレイン配信をバッチ処理します。
トラブルシューティング
"Streaming insert is not allowed in the free tier" — GCP プロジェクトは BQ Sandbox にあります。console.cloud.google.com/billing または別のプロジェクトを選択を介して有効化してください。
"Permission 'bigquery.tables.updateData' denied" — WIF バインディングはまだ伝播していないか、env vars は Vercel で古くなっています。30秒待機して再試行、またはセットアップスクリプトを再実行します。
Drain ハンドラー 502s — LOG_DRAIN_SECRET Vercel が x-drain-secret で送信する値と一致しません。--skip-vercel でセットアップを再実行...実際には、セットアップを再実行するだけです。シークレットを再生成します。
匿名イベントがドロップされている — resolveUser はデフォルトで必須です。匿名イベントが必要な場合は、resolveUser で受け入れます (null を返す) rejectAnonymous を設定しないでください。
Tear down
GCP_PROJECT_ID=<id> ./node_modules/bq-analytics/scripts/teardown.sh
各破壊の前にプロンプト。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- johnkueh
- ライセンス
- MIT
- 最終更新
- 2026/5/8
Source: https://github.com/johnkueh/bq-analytics / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。