1. なぜワークフローを測るのか
端的に言えば、アナリティクスがなければ「たぶんこうだ」という思い込みで動き、「知っている」状態にはなれません。
このモジュールの前の講義では、シナリオをステップに分解し、GPT・ウィジェット・MCPに役割を割り当て、tool‑gatingやステップ間の状態保持を議論しました。ここでは同じ構造をアナリティクスの視点で見直します——それは意図どおり動いているのか、そしてユーザーが本当はどこで詰まっているのか。
一般的なWebでは、ファネルはおなじみです。どれだけの人がランディングに来て、何人が商品をカートに入れ、どれだけが決済まで行ったか。ChatGPT Appでも本質は同じですが、ページの代わりにワークフローのステップがあり、「購入ボタンのクリック」の代わりにユーザーの発話・ツール呼び出し・ウィジェットとのインタラクションの組み合わせがあります。
メトリクスなしで複雑なシナリオを作ると、以下が見えません。
- どのステップで最もドロップオフが起きているか;
- どこで滞留して1分以上読んでいるのか(あるいはお茶を取りに行って戻ってこないのか);
- まったく価値を生まず、ただイライラさせているステップはどれか;
- プロンプトやtool gatingの変更が行動にどう影響するか。
ステップ別アナリティクスの目的はシンプルです。完了したシナリオの割合を高め、結果に到達するまでの時間を短縮し、エラーとサポートへの問い合わせを減らすこと。
ここから、ワークフローは単なるアーキテクチャのオブジェクトでもUXのクエストでもありません。数値で測れる対象でもあります。
2. ChatGPT App におけるシナリオのファネル
クラシックなWebのファネルは直線的です: Landing → Product → Cart → Checkout。 ChatGPT App では少し賑やかです。ユーザーは「決済に飛ばして」や「まず高い順で見せて」と書いてステップを言葉で飛ばすことがあり、モデルがステップを省くこともあり、ウィジェットと対話テキストが非同期になることもあります。
それでも基本の考えは同じです。連続するステップがあり、それぞれで一部のユーザーは先に進み、別の一部は進みません。
例として GiftGenius を使いましょう。
- collect_recipient — ChatGPT とウィジェットが受取人の基本情報(性別、年齢、関係性、興味)を集める。
- collect_budget — 予算と通貨を確認する。
- suggest_ideas — MCP/エージェントがアイデアを選定し、ウィジェットにギフトカードを返す。
- review_selection — ユーザーがアイデアにいいね/非表示をつけ、1〜2個のお気に入りを選ぶ。
- checkout — commerce intent を作成し、注文を確定する。
ファネルとして描くと次のようになります。
flowchart TD
A[ワークフロー開始] --> B["1\. 受取人"]
B --> C["2\. 予算"]
C --> D["3\. ギフトのアイデア"]
D --> E["4\. ギフトの選定"]
E --> F["5\. Checkout"]
ただし重要なのは、ユーザーがチャットで「すぐに決済へ」とか「まず高いものを見せて」などと言えば、モデルが一部のステップをスキップすることもある、という点です。したがってChatGPT App のステップ分析はUI画面だけでなく、モデルの振る舞いも対象にします。実際にどのステップをどの順で通過したか、そして誰が遷移を開始したか——ユーザーか、ウィジェットか、GPTか。
3. ステップ別の基本メトリクス
まずはプロダクトアナリティクスの定番を、ChatGPT App向けに少し調整して使います。
各ワークフローについて、少なくとも4つの基本指標が必要です。
分かりやすいよう、表にまとめます。
| 指標 | 意味 | 典型的な問い |
|---|---|---|
| Start rate | シナリオを開始したユーザーの数 | うちのAppはそもそも誰かに表示されている? |
| Completion rate | 最後まで到達したユーザーの数 | このシナリオは結果にどれだけ到達させている? |
| Conversion per step | ステップNからN+1へ進んだユーザーの割合 | どのステップが「穴だらけ」? |
| Drop-off per step | ステップNで離脱したユーザーの割合 | どのステップで最も離脱が多い? |
これらに加え、負荷を示すメトリクスをほぼ必ず追加します。
- ステップの平均滞在時間(どこで「詰まっている」か);
- ステップでのインタラクション数(必要だったメッセージ/クリックの回数);
- エラーで終了、または再試行を要したステップの割合。
LLMシナリオの文脈ではさらに、モデルのツール選択精度や、特定ステップでの「ハルシネーション」回答の割合など、より専門的な項目が出てきます。これは上級編の内容で、終盤のモジュールで扱います。
コマース系シナリオでは、ステップの上にビジネスメトリクスを重ねます。
- ワークフロースタートからの決済コンバージョン;
- 特定ステップ(例:「アイデア選定」)からの決済コンバージョン;
- 平均客単価;
- キャンセル/返品率。
重要なのは、これらの数字は独立して存在しているわけではなく、因果の物語があることです。ドロップオフが大きいステップが常に悪いとは限りません。そこで不適合なユーザーをふるい落とし、その後に進むのは本当に価値を得るユーザーだけ、ということもあります。ゆえにアナリティクスとは、単に「割合を数える」ことではなく、データでストーリーを語ることなのです。
これらの割合やファネルを計算するには、まずは生のイベントが必要です。誰が、いつ、どのステップを通過した(あるいは通過しなかった)か。次の章でイベントの形式を取り決めます。
4. アナリティクスのイベント設計
コードを書く前に、ウィジェットとバックエンドから送る「イベント(event)」のフォーマットを決めておきましょう。
一般的にアナリティクスイベントは次を含みます。
- 誰:ユーザーID、または少なくともセッションID;
- どのワークフローか、そのバージョン;
- どのステップか;
- 何が起きたか(イベントタイプ);
- 成功/失敗、所要時間;
- 少しのメタデータ(ロケール、デバイス等)。
ワークフロー用に単純化したイベントスキーマは次のように記述できます。
export type WorkflowEventType =
| "workflow_started"
| "workflow_finished"
| "step_started"
| "step_completed"
| "step_failed";
export interface WorkflowAnalyticsEvent {
eventId: string; // uuid
timestamp: string; // ISO文字列
userId?: string; // 可能なら非匿名化ID
conversationId?: string; // ChatGPT の会話ID(利用可なら)
workflowId: string; // 内部ID
workflowType: "gift_selection";
workflowVersion: string; // 例: "1.2.0" または "1.2.0-A"
stepName?: string; // collect_budget, suggest_ideas など
eventType: WorkflowEventType;
toolName?: string; // tool-call に紐づく場合
success?: boolean;
errorCode?: string | null;
durationMs?: number;
metadata?: Record<string, unknown>;
}
いくつか補足です。
まず、workflowVersion はA/Bテストを行うなら極めて重要です。これがなければ、どのバリアントがより良い数値を出しているのか永遠に分かりません。
次に、conversationId などの相関IDは、イベントを結び付けるのに役立ちます。ウィジェットのステップ、MCPでのツール呼び出し、テキスト対話を紐づけられます。後続モジュールでトレーシングやオブザーバビリティについても扱いますが、最初から「エンドツーエンドのID」を意識する習慣はとても有益です。
最後に、イベントへ何でも詰め込む必要はありません。メッセージ本文、e-mail、住所などのPIIは避けるか、厳格に匿名化しましょう——この点は後半でも取り上げます。
5. ウィジェットでの計測(Next.js + Apps SDK)
では本題です。GiftGenius ウィジェットが、ユーザーのシナリオ進行に合わせて静かにステップをレポートするにはどうするか。
前の講義で、すでに次のような実装をしているとしましょう。
// components/GiftWizard.tsx
type StepId = "recipient" | "budget" | "ideas" | "review" | "checkout";
export function GiftWizard() {
const [currentStep, setCurrentStep] = useState<StepId>("recipient");
const [workflowId] = useState(() => crypto.randomUUID());
// ... ここで各ステップのUIをレンダリング
}
ここに小さな「アナリティクス層」をフックとして追加します。
フック useWorkflowAnalytics
workflowId と workflowVersion を把握し、Next.js の API ルート /api/workflow-analytics にイベントを送れるラッパーを作ります。
// lib/useWorkflowAnalytics.ts
import { useCallback } from "react";
import type { WorkflowAnalyticsEvent, WorkflowEventType } from "./types";
const WORKFLOW_VERSION = "1.0.0";
export function useWorkflowAnalytics(
workflowId: string,
workflowType: WorkflowAnalyticsEvent["workflowType"] = "gift_selection"
) {
const sendEvent = useCallback(
async (payload: Omit<WorkflowAnalyticsEvent, "eventId" | "timestamp" | "workflowType" | "workflowVersion" | "workflowId">) => {
const event: WorkflowAnalyticsEvent = {
eventId: crypto.randomUUID(),
timestamp: new Date().toISOString(),
workflowId,
workflowType,
workflowVersion: WORKFLOW_VERSION,
...payload,
};
// 単純なAPI送信。プロダクションではバッファ/デバウンスを追加可能
await fetch("/api/workflow-analytics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
});
},
[workflowId, workflowType]
);
const trackStepEvent = useCallback(
async (stepName: string, eventType: WorkflowEventType, extra?: Partial<WorkflowAnalyticsEvent>) => {
await sendEvent({ stepName, eventType, ...extra });
},
[sendEvent]
);
return { sendEvent, trackStepEvent };
}
このフックは特定のUIステップに依存しない点が重要です。stepName と eventType を知っているだけで、具体的なコンポーネントが「ステップを開始した」「完了した」などを伝えます。
workflow_started と workflow_finished を送る
コンポーネント GiftWizard では、マウント時とアンマウント時にシナリオの開始と終了を記録できます。
// components/GiftWizard.tsx
export function GiftWizard() {
const [currentStep, setCurrentStep] = useState<StepId>("recipient");
const [workflowId] = useState(() => crypto.randomUUID());
const { sendEvent } = useWorkflowAnalytics(workflowId);
useEffect(() => {
void sendEvent({ eventType: "workflow_started" });
return () => {
void sendEvent({ eventType: "workflow_finished" });
};
}, [sendEvent]);
// ...
}
もちろん、アンマウントでの終了記録は粗い近似です。ユーザーはチャットを畳んだり別の対話に移動するかもしれません。それでも、こうした大まかなメトリクスだけでも、どれくらいのシナリオが「どこかまで到達したか」の感触が得られます。
ステップごとのイベントを追跡する
各ステップが自らアナリティクスに通知するようにしましょう。まずシンプルなラッパーを追加します。
interface StepProps {
stepId: StepId;
onNext: () => void;
trackStepEvent: (stepName: string, eventType: WorkflowEventType, extra?: Partial<WorkflowAnalyticsEvent>) => Promise<void>;
}
function StepRecipient({ stepId, onNext, trackStepEvent }: StepProps) {
useEffect(() => {
void trackStepEvent(stepId, "step_started");
}, [stepId, trackStepEvent]);
const handleSubmit = async () => {
// ... バリデーション、widgetState への保存
await trackStepEvent(stepId, "step_completed");
onNext();
};
return (
<div>
{/* 受取人フォームのフィールド */}
<button onClick={handleSubmit}>次へ</button>
</div>
);
}
GiftWizard では trackStepEvent を渡します。
export function GiftWizard() {
// ...
const { trackStepEvent } = useWorkflowAnalytics(workflowId);
const goToNext = () => {
setCurrentStep((prev) => NEXT_STEP[prev]);
};
if (currentStep === "recipient") {
return (
<StepRecipient
stepId="recipient"
onNext={goToNext}
trackStepEvent={trackStepEvent}
/>
);
}
// 他のステップ...
}
同様に、エラーの可能性があるステップ(たとえば suggest_ideas での外部API呼び出し)では、失敗時に "step_failed" と errorCode を送信し、候補の読み込みが成功したら "step_completed" を送れます。
これにより次が得られます。
- ステップの開始と終了の明確なイベント一覧;
- "step_started" と "step_completed" の差から、ステップ所要時間を計算する機能;
- どのステップで "step_failed" が多いかの可視化。
6. バックエンド/MCPでの計測
クライアント側のアナリティクスは有用ですが、ウィジェットは不安定な世界(ユーザーのブラウザ、iframe、サンドボックス制約など)で動きます。並行してサーバー側——MCPツールやAppのバックエンドAPI——でもイベントを記録するのがおすすめです。
たとえば suggest_gifts ツールは実務の重い作業を担います。プロダクトフィードへアクセスし、フィルタを適用し、ギフトを返す。ツール内部でビジネスロジックとアナリティクスイベントの両方を記録できます。
TypeScriptの仮のMCPツールハンドラは次のようになります。
// mcp/tools/suggestGifts.ts
import type { SuggestGiftsArgs } from "../schemas";
import { logWorkflowEvent } from "../analytics/log";
export async function handleSuggestGifts(args: SuggestGiftsArgs, context: { workflowId: string; stepName: string }) {
const startedAt = Date.now();
try {
// ... アイデア選定の主要ロジック
await logWorkflowEvent({
workflowId: context.workflowId,
workflowType: "gift_selection",
workflowVersion: "1.0.0",
stepName: context.stepName,
eventType: "step_completed",
toolName: "suggest_gifts",
success: true,
durationMs: Date.now() - startedAt,
});
return {
content: [{ type: "text", text: "ギフトのアイデアを5件見つけました。" }],
_meta: {
// ウィジェット用の生データ
},
};
} catch (e) {
await logWorkflowEvent({
workflowId: context.workflowId,
workflowType: "gift_selection",
workflowVersion: "1.0.0",
stepName: context.stepName,
eventType: "step_failed",
toolName: "suggest_gifts",
success: false,
errorCode: "SUGGEST_FAILED",
durationMs: Date.now() - startedAt,
});
throw e;
}
}
logWorkflowEvent は、フロントからのイベントと同じテーブル/ストレージへ書き込めます。違いは "source" に "backend" と印を付ける程度です。
なぜサーバー側アナリティクスが頼れるのか
第一に、ツール呼び出しは起きたか否かの事実であり、「ユーザーがたぶんボタンを押した」というヒューリスティクスではありません。
第二に、サーバーでは集計が容易です。各ツールの呼び出し回数、平均 durationMs、エラーで終わる割合を計算できます。
第三に、UXの問題(ユーザーがツールを呼ぶステップまで到達しない)と技術的問題(到達するがツールがよく落ちる)を区別できます。
7. データの読み方:ボトルネックを探す
ウィジェットとMCPから "workflow_started"、"step_started"、"step_completed"、"step_failed" を送るように実装し、十分なデータが蓄積されたとします。GiftGenius で集めたデータから、ステップごとの集計が得られたと仮定しましょう。以下は、1000件のワークフロー開始に対する仮の数値です。
| ステップ | ステップ開始 | ステップ完了 | ステップでのドロップオフ | 平均時間(秒) |
|---|---|---|---|---|
| recipient | 1000 | 950 | 5% | 12 |
| budget | 950 | 700 | 26% | 35 |
| ideas | 700 | 680 | 3% | 8 |
| review | 680 | 500 | 26% | 40 |
| checkout | 500 | 420 | 16% | 20 |
ここで注目すべき点:
第一に、budget ステップは明らかなボトルネックです。ドロップオフ(26%)が高く、平均時間も長い。通貨/税の聞き方が多すぎる、文言が分かりづらい、あるいはユーザーが予算に自信がないのかもしれません。ステップの簡素化、2つのサブステップへの分割、質問文の変更などの候補になります。
第二に、review でも離脱が大きい。ギフトカードのUIが煩雑か、ユーザーが「いいね」の意味を理解しにくいのかもしれません。モデルが候補を出しすぎ、ウィジェットが無限リストのように見える可能性もあります。ここは数字だけでなく、スクリーンショット/セッション録画(実施していれば)や、ユーザーになりきって手動でシナリオを辿ることも大切です。
第三に、checkout では16%を失っています。コマースのシナリオとしては大きな損失です。ただし、どこで失っているかを見極める必要があります。注文確定の過程か、決済プロバイダのエラーか、単純にユーザーが心変わりしたのか。これは純粋なUX問題というより、UXとビジネス制約の複合問題です。
UIの問題とモデルの問題を見分けることが重要です。
- ユーザーが前のステップに戻って回答をしばしば変えるなら、質問が分かりにくい・悪い聞き方であるサインです。
- ステップは素早く完了するが、そのステップでのツール呼び出しがよく落ちるなら、backend/MCP の問題です。
- ステップが長引くのに、エラーも戻りもない場合、ユーザーは不要な長文を読まされているのかもしれません。
8. 実験とワークフローのA/Bテスト
数字を眺めるだけでは何も改善しません。アナリティクスを有効にするには、実験を設計し、ステップを変更して「良くなったか」を比較できる必要があります。
ChatGPT App の文脈では、典型的な実験はステップやステップ順の2バリアント比較です。
- 複数のシンプルな画面を持つ長いウィザード vs. 1つの複雑なフォーム;
- 質問文の違い;
- ステップ順の違い(たとえば予算を先に聞くか後にするか);
- tool gating の戦略の違い(最初のステップではツール少なめ、次で多め、など)。
良い習慣は、workflowVersion にシナリオのバージョンを固定し、必要なら実験IDを付けることです。たとえば "1.3.0-A" と "1.3.0-B"。
最も簡単なウィジェット側のA/Bスプリット
実運用ではユーザーやセッション単位で安定したアサインメント(バックエンド経由)を望むでしょうが、学習用の例としては乱択でも十分です。
// lib/useWorkflowVariant.ts
import { useMemo } from "react";
export type WorkflowVariant = "A" | "B";
export function useWorkflowVariant(): WorkflowVariant {
return useMemo(() => {
return Math.random() < 0.5 ? "A" : "B";
}, []);
}
GiftWizard でバリアントを決め、アナリティクスに渡します。
export function GiftWizard() {
const [workflowId] = useState(() => crypto.randomUUID());
const variant = useWorkflowVariant();
const { sendEvent, trackStepEvent } = useWorkflowAnalytics(
workflowId,
"gift_selection"
);
useEffect(() => {
void sendEvent({
eventType: "workflow_started",
metadata: { variant },
});
}, [sendEvent, variant]);
// 以降、variant に応じて文言/ステップ構造を切り替え可能
}
サーバー側では固定の "1.0.0" を "1.1.0-A" や "1.1.0-B" のように置き換えるか、単に metadata.variant を記録して、分析時にそれでグルーピングしてもよいでしょう。
A/Bテストの肝は、あらかじめ目標指標を決めることです。例:「シナリオのcompletion rate を42%から50%へ上げる」あるいは「budget ステップの時間を20%短縮する」。目標指標がないと、ワークフローの改修は「棚の位置を変えて見た目が良くなった気がする」程度にしかなりません。
9. プライバシーとデータ倫理
以前、metadata やアナリティクスイベントにPIIをそのまま持ち込むべきではないと軽く触れました。メトリクスの話をしていると、何でもログに取りたくなりがちですが、ここはChatGPTの中で動くという前提を忘れてはいけません。ユーザーは自分の個人的なメッセージが外部アナリティクスへ生データで送られていないと合理的に期待するはずです。
セキュリティやStoreに関するモジュールに進む前に、今の段階で守るべき簡単なルールを挙げておきます。
- 第一に、ユーザーのメッセージ本文をログに残さないこと。代わりに、メッセージ長、回答タイプ(数値/はい・いいえ/選択式)、あるいは「空/不完全/変更あり」といった匿名化した特徴量を保存できます。
- 第二に、ビジネスロジック上不要なら、明確に個人を特定できる情報(PII)——e-mail、電話、住所、氏名——をログに残さないこと。不可避な場合は別の保護された領域に保管し、厳格にアクセス制御してください。
- 第三に、対話のコンテキストの扱いには注意を。conversationId を保存するなら、正当な理由と法的根拠なく、個別の対話を「巨大なプロファイル」に結合しようとしないこと。
- 第四に、OpenAI のポリシーと Store の要件(公開とセキュリティのモジュールで詳述)に注意を払いましょう。どのデータをChatGPTから外に出せるか、出せないかが明記されています。アナリティクス設計段階から匿名化とデータ最小化を織り込んでおくと、後でシステムを作り直さずに済みます。
最後に、UXアナリティクスは全体監視でも、Big Brother 的なサーベイランスでもありません。目的は、シナリオを改善し、ユーザーのフラストレーションを減らすことです。「誰が2:37にcheckoutまで行かなかったか」を監視する Big Brother ダッシュボードを作ることではありません。
10. ワークフローUXアナリティクスのよくある誤り
誤り №1: 「まだ本番じゃない。メトリクスは後で」。
開発者は、Appが実ユーザーに使われ始めてからアナリティクスを考え始めることがよくあります。その結果、イベントは「事後付け」になり、データは断片的で、旧版と新版のシナリオをほぼ比較できません。最小限のファネル("workflow_started"、"step_started"、"step_completed"、"workflow_finished")は、コードがまだ比較的シンプルなうちに組み込んでおくのがベストです。
誤り №2: 成功だけをログし、エラーを無視する。
ログには "step_completed" だけがあり、"step_failed" は誰も書かない。「落ちるはずがないから」という理由で。こうなると、あるステップに到達する人が少ないのは見えても、それがユーザーの任意離脱なのか、エラーで弾かれたのか分かりません。常に成功と失敗の両方をログし、少なくとも大まかな errorCode を付けましょう。
誤り №3: ワークフローバージョンへの完全な無頓着。
文言を変え、ステップ順を変え、tool gating を導入しても、イベントには常に workflowVersion が "1.0.0" のまま。1か月後にグラフを見ても、どれが変更前でどれが変更後か分かりません。シナリオのバージョン固定と、必要に応じたA/Bバリアントの識別は、アナリティクスの必須要素です。
誤り №4: 根拠のない過度に詳細なアナリティクス。
反対の極端は、「完璧」なイベントスキーマをいきなり50フィールドで作り、ピクセルごとのクリックや一文字ごとのタイプをログすること。第一に、プライバシー侵害になり得ます。第二に、分析が難しく、ノイズに溺れます。実際に特定のプロダクトの問いに答える、少数のイベントとメトリクスから始め、必要に応じて拡張する方が賢明です。
誤り №5: ステップ名・シナリオ名の不整合。
コードではステップ名が budget、アナリティクスでは collect_budget、レポートでは「お金の質問のステップ」。数週間もすれば、誰も対応関係を覚えていません。ワークフロー設計段階で、安定したステップ識別子(stepName)に合意し、UI・ログ・レポートのすべてで使うと良いでしょう。
誤り №6: 誰も使わないメトリクスの存在。
最も悲しいパターンです。大量のデータを丁寧に集め、ウィジェットとMCPからのイベント送信も整えたのに、誰もダッシュボードを開かず、意思決定もしない。アナリティクスのためのアナリティクスは不要です。常に自問しましょう。「このメトリクスに基づいて、私はどんな意思決定ができるか?」。答えがなければ、そのメトリクスは今は不要です。
GO TO FULL VERSION