querying-canvas-data について
ホスト提供の `ph` ブリッジを通じて PostHog のデータを取得・表示するキャンバスの構築ガイドです。クエリの選択、結果の検証と処理、段階的な読み込み、キャンバス状態を扱い、データ駆動型キャンバスの開発者を対象としています。
主な機能
- 01保存済みインサイトの読み込みや型付きクエリの実行
- 02HogQLを明示された最終手段として利用
- 03表示する指標を検証可能にする
- 04クエリ結果とエラーを明示的に処理
- 05セクションごとに段階的に読み込み
- 06ユーザー別または共有のキャンバス状態を保存
活用シーン
- 01PostHog インサイトへのリンク付き指標キャンバスの構築
- 02データセクションが個別に読み込まれるステータスボードの作成
- 03システムテーブルからライブタスクデータを表示
- 04キャンバスの下書きやチーム共有設定を保持
MCP 対応
PostHog MCP のインサイトツールでインサイトを作成・保存してから、キャンバスで読み込む方法を推奨します。キャンバス自体は `ph` ブリッジ経由で PostHog にアクセスします。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ posthog/ ai-plugin --skill querying-canvas-dataインストール後、Claude Code で/querying-canvas-dataを入力して読み込まれたか確認します。
同梱ファイルreferences/canvas-sdk.d.ts
詳細
翻訳後に原文が更新されています。最新の内容は原文をご確認ください。
キャンバスデータのクエリ
ph ブリッジは、キャンバスが PostHog と通信する唯一の手段です。import { ph } from "@posthog/canvas-sdk" でインポートします。プラットフォームから提供されるモジュールのため、dependencies への追加は不要です。同じオブジェクトは window.ph グローバルとしてもインストールされており、既存のキャンバスで使用されています。新しいコードではインポートを優先してください。型定義されたインターフェースは
references/canvas-sdk.d.ts です。初期化はしないでください。認証情報はホスト内に保持され、サンドボックスから PostHog にアクセスするために fetch()、posthog-js、手書きのクライアントを使用することはできません。外部リクエストとリソースを使用するには、PostHog 以外のオリジンを capabilities.network.origins で宣言する必要があります。また、動作するのは公開済みのキャンバスのみです。編集モードのプレビューでは、すべての直接ネットワークアクセスがブロックされます。外部スタイルシートも対象です。リモートスクリプトは引き続きブロックされます。
データ階層 — すべての指標の根拠として保存済みインサイトを使用する
- 推奨 — インサイトを保存し、参照から読み込む。 PostHog MCP のインサイトツールを使い、インサイトクエリタイプ(TrendsQuery、FunnelsQuery、RetentionQuery、PathsQuery、または WebOverviewQuery / WebStatsTableQuery などのウェブ分析用クエリタイプ)で指標を計算するインサイトを作成・保存します(生の SQL は使用しません)。数値を確認して
short_idを記録し、await ph.loadInsight(shortId, { dateRange })で表示します。これらは検証済みのクエリであり、数値は PostHog UI と完全に一致します(セッション化、ユニークユーザー、内訳、直帰率)。クエリを捏造したり、イベント名やプロパティ名を推測したりしないでください。まず MCP を使って見つけ、保存してください。 - 次善策 — アドホックな型付きノード:インサイトを保存する方法が本当に適さない場合は、
ph.query({ kind: "TrendsQuery", series: [...], dateRange: {...} })を使用します。 - 最後の手段 — インライン HogQL:どのインサイトタイプでも指標を表現できない場合に限り、
ph.query("SELECT …")を使用します。その場合、SQL と日付範囲は自分で管理します。
特にウェブ分析ボードでは、ウェブ分析用のクエリタイプを使用してください。生の HogQL では、直帰率、セッション化、チャネルアトリビューション、ユニーク訪問者数が微妙に誤った値になります。
どの階層を使用する場合も、公開前にプロジェクトの capabilities で宣言してください。capabilities.posthog.insights にすべての ph.loadInsight の short id を、captureEvents にすべての ph.capture のイベント名を追加し、ph.query を使用する場合は inlineQueries: true を設定します。宣言されていない呼び出しは実行時にホストによって拒否され、宣言されていないリテラルがあると検証に失敗します。
検証可能性 — すべての主張を PostHog で確認できること
閲覧者が検証できない数値は、信頼できない数値です。キャンバスに表示される、データに基づくすべての数値 — KPI、チャート、テーブル、明示された結論 — には、該当する階層に応じた検証手段を用意する必要があります。
- インサイトに基づくメトリクスから、PostHog の実際のインサイトにリンクします。 保存済みのインサイトから読み込んだメトリクス(推奨される階層)には、クリック時に
ph.openExternal(insightUrl)を呼び出す「PostHog で表示」手段を表示します。作成時にgenerate-app-urlMCP ツール(パステンプレート/insights/{id}とインサイトの短い ID を使用)で URL を生成し、返された URL をソースにそのまま埋め込みます。自分で URL を組み立ててはいけません。ph.openExternalが開くのはhttps://*.posthog.comの URL のみで、ユーザー操作からしか呼び出せません。そのため、ボタンまたはリンクに割り当て、読み込み時やレンダリング時には呼び出さないでください。また、インサイトの保存済みクエリテキストをソースに埋め込んではいけません。キャンバスのソースはすべてのキャンバス閲覧者が読めますが、インサイトへのアクセスは PostHog によって制御されます。閲覧者が自身の権限でクエリを確認するための場所が、このリンクです。 - アドホッククエリでは、実行された正確なクエリを開示し、その場で確認できるようにします。
ph.query(型付きノードまたはインライン HogQL)で計算された数値については、その根拠となるクエリ — HogQL テキスト、または JSON として整形した型付きクエリノード — をカードに関連付けたモーダルまたは折りたたみ式の開示領域に表示します(React キャンバスでは Quill のDialogまたはCollapsible、HTML キャンバスでは<details>要素を使用します)。表示するクエリは、ph.queryに渡すものと同じ定数またはビルダーから生成してください。そうすれば、表示されるクエリと実行されるクエリが食い違うことはありません。これは閲覧者がすでに実行している内容を開示するだけです。ph.queryは、サインイン中の閲覧者として実行されます。
これらは任意の仕上げではありません。これらを備えていない、PostHog のデータを提示するキャンバスは未完成です。表示手段はコンパクトにしてください。インサイトに基づくカードごとに小さなリンクアイコンを置く、アドホックカードごとに「クエリを表示」の開示領域を設ける、またはキャンバスで実行されるすべてのアドホッククエリを、根拠となる数値と対応付けて一覧表示する共通モーダルを設けます。
ステータスボードでは、refresh にキャッシュの有効期間を秒単位で設定します。30 から 86400(1 日)までの整数を指定してください。この範囲外の値や小数を指定すると、実行時にエラーになります。
await ph.loadInsight(shortId, { refresh: 30 })
await ph.query(queryNode, {}, { refresh: 30 })
結果の形式 — 正しく読み取らないと、すべての値が 0 として表示されます
- トレンド形式の結果(インサイトのクエリタイプ。
ph.loadInsightまたは型付きノード経由):resultsは行ではなく、系列オブジェクトの配列です。各系列には、data: number[](期間ごとの値)、days: string[](ISO 形式)、labels: string[]、count(合計)、aggregated_value(単一値の合計)、label、および任意のcompare_label: "current" | "previous"が含まれます。KPI の合計にはresults[0].count(または.aggregated_value)を使用します。折れ線グラフではresults[0].dataをresults[0].daysに対してプロットします。countは期間ごとの値を合計するため、複数の日にアクティブだったユーザーを重複して数えます(math: "dau"のユニークユーザー系列の場合)。期間内のユニーク数を KPI に表示するには、クエリにtrendsFilter: { display: "BoldNumber" }を設定し、代わりにaggregated_valueを読み取ってください。比較期間がある場合は、compare_label === "previous"で前の系列を検索してください。インデックスで検索してはいけません。この場合、columnsは空です。 - SQL の結果:
{ columns: string[], results: rows[][] }— 各行はcolumnsの順に並んだセル値の配列です。
段階的に読み込む — 各セクションのデータが届いたら、そのセクションを描画します
PostHog のクエリはそれぞれ数秒かかることがあり、ボードでは通常、複数のクエリを実行します。すべてのクエリが完了するまで描画を待たせないでください。
- マウント時に独立したクエリを並行して実行し、無関係なクエリを逐次的な
awaitで連鎖させないでください。ホストは一度に 8 件のデータリクエストを実行し、残りは最大 32 件のキューで待機するため、セクション数が実行枠を超えるボードでも、セクションごとに読み込まれます。キューよりも幅広いボードでは、追加リクエストはエラーメッセージに理由を添えて拒否されます。ランタイムは、諦める前にバックオフ後に各リクエストを再送します。この規模のボードでは、クエリを統合してください(すべての行を返すクエリを 1 つ実行し、クライアント側で分割します)。状態はセクションごとに 1 つずつ維持してください。 - 各クエリに独自の
{ loading, error, data }状態を持たせ、結果が届いたらすぐに、各カード、グラフ、テーブルのスケルトンをデータに切り替えてください。loadingフラグを共有したり、独立したクエリ全体を 1 つのPromise.allでまとめたりすると、最も速い指標も最も遅い指標を待つことになります。キャンバスは一度にすべて表示するのではなく、段階的に埋まっていく必要があります。 - 静的な枠組み(見出し、日付ピッカー、内部にスケルトンを表示したカード枠)はすぐに描画してください。各セクション内の値だけを、そのクエリの結果が届くまで待たせます。
- 初回描画に不要なクエリは後回しにしてください。タブの背後にあるコンテンツ、折りたたまれたセクション、ドリルダウンのクエリは、マウント時ではなく、ユーザーが表示したときに実行します。
useState とともに useEffect でデータを読み込み、クエリ内で集計してください。生のイベントデータを一括取得してはいけません。クエリの失敗と空の結果は、異なる状態として扱ってください。.catch でエラー状態を設定し、目に見える形で描画してください(メッセージと再試行)。ゼロ、空のグラフ、「データなし」のメッセージに置き換えてはいけません。エラーを握りつぶすと、テーブルの欠落や認証の失敗といった実際の問題が、データの欠落に見えてしまいます。空の状態は、クエリが成功し、行が返されなかった場合にのみ使用してください。
日付範囲
- キャンバスの日付ピッカーの範囲を、そのまま
dateRangeに渡してください。ph.loadInsight(shortId, { dateRange: { date_from: win.start.toISOString(), date_to: win.end.toISOString() } })— 保存済みインサイトは、時間に関する SQL なしでその範囲に再スコープされます。型付きノードにも同じdateRangeを指定します。範囲が変わったら、すべてのクエリを再実行してください。 - 保存済みの SQL インサイトは
dateRangeを無視することがあります(範囲は SQL 内に記述されています)。これも、インサイトクエリタイプを優先する理由の 1 つです。範囲が{variables.…}プレースホルダーに由来する場合は、代わりにvariables(以下を参照)を通じて指定してください。dateRangeはそこには反映されません。 - インライン HogQL は、他に手段がない場合に限って使用してください。
now()やハードコードされた INTERVAL を埋め込んではいけません。Unix 時刻の境界値(Math.floor(win.start.getTime() / 1000))を計算し、半開区間のtimestamp >= toDateTime(fromUnix) AND timestamp < toDateTime(toUnix)を記述してください。前期間は直前の同じ長さの期間とし、toStartOfDay/toStartOfHourでバケット化します。
SQL 変数
HogQL に {variables.name} プレースホルダーを含む保存済み SQL インサイトでは、各呼び出し時に変数の値を指定します。キーには変数の UUID ではなく、コード名を使用します。
await ph.loadInsight(shortId, { variables: { product: 'surveys', month: '2026-07-01' } })
こうすることで、保存済みインサイトを製品ごとに一度ずつ読み込む同じ製品別インサイトとして使い、ボード全体を 1 つの保存済みインサイトで埋められます。各タイルでインサイトに保存されたデフォルト値を解決する必要はありません。
- まず、インサイトのクエリからコード名を確認してください(MCP 経由の
insight-get)。ホストは、インサイトで使われていない変数や、値が反映されなかった変数を拒否します。保存済みの値に黙ってフォールバックすることはありません。そのため、変数の不一致は、別の製品の数値としてではなく、目に見えるエラーとして表れます。 - 変数は読み取りキャッシュキーの一部であるため、製品が N 個なら読み込みも N 回になります。同じインサイトを N 回読み込むより、すべての製品を行として返すインサイトを1 つ使い、クライアント側で分割してください。
- 値の型は PostHog での変数定義(String/Number/Boolean/Date/List)によって決まります。インサイトが期待する形式で渡し、複数選択の List 変数には配列を指定してください。
ライブタスクデータ
タスクの受信トレイ、キュー、またはステータスボードを作成する場合は、ph.query を通じて
system.tasks と system.task_runs をクエリしてください。作成時に posthog:tasks-list を呼び出してその応答を埋め込まないでください。スナップショットが生成される一方、システムテーブルを使えば、レンダリングされたキャンバスをライブ状態に保てます。
テーブルはサインイン済みの閲覧者として実行されます。プロジェクト単位でスコープが設定され、Tasks リソースへのアクセスが必要です。system.tasks に含まれるのは、公開中のスペースで登録された内部向けではないタスクのみです。プライベート、個人用、未登録、内部向けのタスクは含まれません。ソフト削除されたタスクは必ず明示的に除外してください。
キャンバスで最新のステータスが必要な場合は、タスクを最新の実行に結合します。
const data = await ph.query(`
SELECT
t.id,
t.task_number,
t.title,
t.repository,
t.created_by_id,
t.created_at,
t.updated_at,
latest.status AS latest_run_status
FROM system.tasks AS t
LEFT JOIN (
SELECT
task_id,
argMax(status, tuple(created_at, id)) AS status
FROM system.task_runs
GROUP BY task_id
) AS latest ON latest.task_id = t.id
WHERE t.deleted = 0
ORDER BY t.updated_at DESC
LIMIT 100
`)
これはインライン HogQL なので、capabilities.posthog.inlineQueries: true を宣言してください。リンクの表示には、タスク URL を組み立てるのではなく、ph.navigate.toTask(id) を使ってください。
テーブルで表現できないフィルターを適用できると約束しないでください。channel_id はクエリできないため、キャンバスのクエリを現在のスペースに限定することはできません。また、現在の閲覧者に絞り込むには、既知の数値ユーザー ID が必要ですが、キャンバスのランタイムではその ID は渡されません。「このスペース」や「自分のタスク」に関する依頼でこの制限が関係する場合は、プロジェクト全体の公開タスクを黙って表示するのではなく、制限を明示してください。
ランタイムメモリ — ph.state
キャンバスごとの永続的なキー・バリューストレージです。使用するすべてのスコープ(["user"]、["shared"]、またはその両方)を capabilities.posthog.state で宣言してください。宣言されていないスコープは検証に失敗し、ホストはランタイムでの使用を拒否します。スコープを渡さない場合のデフォルトである "user" は各閲覧者専用です。"shared" はキャンバスごとに 1 つの値を持ち、チーム全体から参照できます。
const draft = await ph.state.get('draft') // user scope by default; null when unset
await ph.state.set('draft', { text }) // JSON value, capped at 64 KB serialized
await ph.state.set('draft', null) // null deletes the key
await ph.state.set('board', { columns }, { scope: 'shared' }) // team-visible
const entries = await ph.state.list({ scope: 'shared' }) // [{ scope, key, value, updatedAt }]
- マウント時にエフェクト内で状態を読み込み、解決するまではスケルトンを表示してください。書き込みは後勝ちになるため、再読み込みするか、自分の書き込みを信頼してください。マージは行わないでください。
- スコープあたり 256 個のキーを使用できます。大きなデータは PostHog(インサイト、ウェアハウス)に保存し、そこへの参照を保持してください。
- 状態はチーム全体に見えるアプリケーションデータです。シークレットや閲覧者の個人識別情報(PII)は決して保存しないでください。
ユーザーがキャンバスの現在の進捗や設定について尋ねた場合、ソースだけから推測しないでください。ソースを読んだ後、キャンバス ID を指定して canvas-state-retrieve を呼び出してください。これは、公開チャンネルまたはユーザーの個人チャンネルにあるキャンバスについて、共有状態と、認証済みユーザー自身のユーザースコープの状態を返します。ユーザーが値の変更を求めた場合は canvas-state-set を使ってください。最初に読み込み、無関係なキーを保持したうえで、キャンバスのソースが想定するスコープを使用してください。
キャンバスのディスカッションには汎用コメントツールを使用します。scope=canvas とキャンバス ID で絞り込んで comments-list を呼び出し、コメントを読み取ってください。同じスコープとアイテム ID を指定して comments-create を呼び出し、ルートコメントまたは返信を作成します。スレッドはキャンバスに紐づくため、タスク ID は省略可能です。そのタスクがキャンバスを生成または公開した場合に限り、item_context.taskId にタスク ID を指定してください。それ以外のタスクは API に拒否されます。アクセス権はキャンバスのスペースによって決まります。そのスペースを閲覧できるユーザーは、そのコメントの読み取りと書き込みができます。
PostHog への書き込み — ph.actions
ph.actions.invoke(verb, payload) は、閲覧者として PostHog に書き込みます。すべての verb を
capabilities.posthog.actions で宣言してください。宣言されていない、または登録されていない verb は検証に失敗し、ホストによって実行時に拒否されます。呼び出しは明示的なユーザー操作(閲覧者がクリックするボタン)に接続する必要があります。ホストは読み込み時またはレンダリング時の呼び出しを拒否します。
結果または発生したエラーを目に見える形で表示し、呼び出し中はボタンを無効にしてください。呼び出しはすべて実際に PostHog へ書き込みます。
verb の正確な情報源はレジストリです。接続する前に、canvases-actions-retrieve ツールで一覧を取得してください。各項目には verb、summary、destructive、usage が含まれています。つまり、ペイロードと結果の形式、呼び出しによって実際に行われること、必要な確認メッセージです。成功メッセージで何を伝えてよいかも含め、verb の usage に厳密に従ってください。対応する製品の MCP ツールやスキルから verb のペイロードを推測しないでください。MCP ツールの呼び出し(あなたが今、自分の認証情報を使って行うもの)とキャンバスの verb(公開済みキャンバスで後から閲覧者が行うもの)では、ペイロードの形式、認証、動作が異なります。呼び出し方法は次のとおりです。
const { result } = await ph.actions.invoke('tasks.create', { title, description })
サードパーティーの最新データ — ph.connectors
ph.connectors.call(provider, tool, args, { refresh? }) は、表示時に閲覧者自身の接続を使ってサードパーティーサービスからデータを読み取ります。人ごとに最新の状態を保つ必要があるデータ(未完了のプルリクエスト、今日の会議、担当するイシューなど)にはこれを使用してください。作成時に自分でデータを取得してソースに埋め込まないでください。そのスナップショットは公開した時点で古くなり、すべての閲覧者に作成者のデータが表示されます。
- プロバイダーは
github(閲覧者個人の GitHub 接続を使うネイティブプロバイダー)か、MCP ストアで閲覧者が接続した任意のサーバーのmcp:<server host>(例:mcp:mcp.calendly.com)です。canvas-connectors-retrieveツールでプロバイダー、ツール、引数スキーマ、ツールごとのusageを確認してください。現在のユーザーが接続していないサーバーを調べる場合は、mcp_hostsを渡してください。カタログ項目のis_read_only: trueのツールのみを呼び出してください。MCP ツールには、明示的な読み取り専用ヒントと、ローカルの読み取り用 verb 判定を通過する名前が必要です。 - すべてのプロバイダーとツールを
capabilities.connectorsで宣言してください。例:[{ "provider": "github", "tools": ["list_pull_requests"] }]。宣言されていないph.connectors.callのリテラルは検証で拒否され、宣言されていない呼び出しは実行時にもホストによって拒否されます。 - 呼び出しは
{ status, result, detail, truncated, connect_path }を返します。statusに応じて処理してください。ok—resultにツールの出力が格納されます。ネイティブツールはドキュメントに記載された形式を返し、MCP ツールは{ content, structured_content, is_error }(MCP のコンテンツブロック)を返します。not_connected/needs_reauth— この閲覧者には使用可能な接続がありません。クリック時にph.connectors.connect(provider)を呼び出す「Connect GitHub」(またはサーバー名)のボタンを表示してください。ホストが適切な設定ページを開きます。これを空のデータとして扱わないでください。blocked、write_blocked、tool_missing、upstream_error—detailを再試行できる形で表示してください。
truncated: trueは、結果が 256 KB を超え、プレビューまで切り詰められたことを示します。クライアント側でページ分割せず、limitを小さくする、リポジトリを1つに絞るなどして呼び出しの対象を限定してください。- マウント時に呼び出しを開始できますが、ホストはコネクタのデータを読み取る前に閲覧者にアクセス許可を求めます。同意はキャンバスのバージョン、プロバイダー、ツールの組み合わせごとに適用されます。拒否された場合は呼び出しが失敗するため、エラーと再試行ボタンを表示してください。
- 結果はキャンバスのバージョンごとに
refresh秒間キャッシュされます(デフォルトは 60 秒、範囲は 30~86400 秒)。アカウント、組織、プロジェクトを変更すると、結果と同意の両方が消去されます。 - コネクタの結果は、コンポーネントの状態、またはスコープが
"user"のph.stateに保持してください。コネクタを使うキャンバスでは共有状態を宣言できません。この組み合わせは検証と API の両方で拒否されます。
const [prs, setPrs] = useState<{ loading: boolean; status?: string; rows?: PullRequest[] }>({ loading: true })
useEffect(() => {
ph.connectors
.call('github', 'list_pull_requests', { repository: 'example/app', state: 'open' }, { refresh: 60 })
.then((res) => setPrs({ loading: false, status: res.status, rows: res.result?.pull_requests ?? [] }))
.catch((error) => setPrs({ loading: false, status: 'error', rows: [] }))
}, [])
副作用
ph.capture(event, properties?, distinctId?)— インタラクションに関する分析イベントを送信します (呼び出し後は結果を待ちません)。セッションリプレイ、$session_id、人物へのアトリビューションはホストが自動的に処理します。記録の初期化やセッション ID の設定、自前でのキャプチャは行わないでください。ph.openExternal(url)— ユーザーのクリックに応じて、PostHog の HTTPS URL とhttps://github.com/<owner>/<repo>/pull/<number>のリンクを開きます。GitHub の PR リンクは確認ダイアログなしで開きます。ファイル、コミット、チェック、フラグメントリンクは許可されますが、認証情報、カスタムポート、クエリ文字列、その他のドメインは許可されません。 サンドボックス化されたtarget="_blank"によるナビゲーションはブロックされるため、ブラウザーへのフォールバックではなくブリッジを使用してください。ph.navigate.toTask(id)/.toNewTask()/.toCanvas(id)/.toNewCanvas()— キャンバス独自のチャネル内でアプリ内を移動します。ph.navigate.toNewTask({ prompt, repository })— ユーザーのクリックに応じて、新しいタブでプロンプトが事前入力されたタスクフォームを開きます。promptは最大 16,000 文字で、repositoryはowner/repo形式の名前です。どちらも省略できます。リポジトリを設定すると、このタスクのクラウドモードが選択されますが、スペースは変更されません。 ビューアーがプロンプトを確認して送信します。フォームを開いただけでは実行は開始されません。以前に公開したアーティファクトにph.navigateがあるか確認し、見当たらない場合はデプロイ後に再ビルドするよう依頼してください。