writing-streamlit-apps について
PostHog のサンドボックスでデータアプリを作る開発者向けの Streamlit コーディングガイドです。サンドボックス経由の PostHog クエリに加え、再実行、安全性、レイアウト、実行環境の制約を扱います。
主な機能
- 01HogQL による PostHog データのクエリ
- 02Streamlit 再実行時のクエリキャッシュ
- 03session state による UI 状態の保持
- 04レイアウト、テーブル、チャートの作成
- 05サンドボックスの依存関係と安全性の制約
活用シーン
- 01PostHog イベントを使ったダッシュボード作成
- 02クエリアクセスを公開しないフィルターの追加
- 03サンドボックスアプリでの指標とチャートの表示
MCP 対応
MCP set-source フローによるアプリソースの送信に言及していますが、MCP サーバーなどの機能は説明されていません。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ posthog/ ai-plugin --skill writing-streamlit-appsインストール後、Claude Code で/writing-streamlit-appsを入力して読み込まれたか確認します。
詳細
PostHog サンドボックス向け Streamlit アプリの作成
作成したソースは、サンドボックス化された Streamlit 1.31 ランタイムのルートにある app.py になります。
デプロイの仕組み(作成/起動/共有)については managing-streamlit-apps スキルを参照してください。このスキルではコードを扱います。
PostHog のデータを読み取る:posthog_apps.query()
データにアクセスする唯一の手段は、サンドボックス内のブリッジです。
import posthog_apps
df = posthog_apps.query("SELECT event, count() FROM events GROUP BY event LIMIT 10")
- HogQL 文字列を受け取り、pandas DataFrame を返します。
- 失敗すると
RuntimeErrorが発生します。メッセージは意図的に一般化されています(「Query execution failed」)。ブリッジはクエリの内部情報をサンドボックスに返さないため、アプリ内から不正なクエリの原因を特定することはできません。例外をキャッチし、st.error(str(e))で表示して、閲覧者にスタックトレースではなくメッセージを表示してください。また、実際のエラーを確認できる SQL エディターでクエリをテストしてください。 - サンドボックスには
import posthogはありません。モジュール名はposthog_appsで、posthog-python SDK とは意図的に別の名前になっています。 - ブリッジはアプリのプロジェクトに対して事前認証されています。ユーザーコードがトークンを参照することはなく、設定も不要です。
- クエリにはサーバー側の上限(実行時間 30 秒、メモリ 256 MB)が適用され、サンドボックスのサイズを大きくしても上限は変わりません。そのため、時間範囲を制限し、結果に
LIMITを指定し、生のイベントを pandas に取り込むのではなく HogQL で集計してください。
Streamlit の再実行モデルに合わせた設計
Streamlit は、ウィジェットを操作するたびにスクリプト全体を先頭から再実行します。 そのため、次の 2 点が重要です。
-
ブリッジの呼び出しはすべてキャッシュしてください。キャッシュしないと、スライダーを 1 回動かすだけで、すべてのクエリが再実行されます。
@st.cache_data(ttl=300, show_spinner="Running query...") def run_query(hogql: str) -> pd.DataFrame: return posthog_apps.query(hogql) -
再実行後も保持する必要があるデータには
st.session_stateを使ってください。累積した選択内容、ページネーションのカーソル、「最終更新」時刻などが該当します。モジュールレベルの変数は操作のたびにリセットされます。
ウィジェットを使うとパラメーターを自然に設定できますが、自由入力のウィジェット値を HogQL に直接埋め込まないでください。
ブリッジはバージョンの作成者のデータアクセス権限でクエリを実行し、アプリを閲覧できる人なら誰でもウィジェットを操作できます。生の st.text_input の値をクエリに埋め込むと、閲覧者に作成者のアクセス権限を使って任意のクエリを書かせることになります。
代わりに、入力を制限してください。管理された固定リストから選ばせる(既知の値を指定した st.selectbox を使う)、または SQL を含められない型に変換する(int(days) や st.date_input の date など)方法があります。さらに、クエリに渡す前に値を検証してください。
レイアウトとグラフ
- 最初に
st.set_page_config(page_title=..., layout="wide")を設定してください。既定の狭いレイアウトでは、データアプリの画面の大部分が無駄になります。 - 横並びの指標には
st.columns、表示を切り替えるタブにはst.tabs、詳細セクションにはst.expanderを使って構成し、見出しとなる数値にはst.metricを使ってください。 - グラフには plotly express と併せた
st.plotly_chart(fig, use_container_width=True)が安定した既定の選択肢です。表にはst.dataframe(df, use_container_width=True)を使ってください。matplotlib と seaborn もst.pyplot経由で利用できます。
インストール済みのライブラリ
イメージには Python 3.11 と、次のライブラリが含まれています:streamlit 1.31、pandas、numpy、polars、plotly、matplotlib、seaborn、scipy、scikit-learn、pyarrow、duckdb、requests、beautifulsoup4、lxml、sqlalchemy、aiohttp。
依存関係を追加する方法はありません。サンドボックスでは pip が実行されず(起動時に任意のパッケージコードを実行させないための、意図的なセキュリティ対策です)、アップロードした zip に requirements.txt を含めても無視されます。上記のライブラリだけをインポートしてください。
構成と実行時の制約
app.pyがエントリーポイントです。 MCP の set-source フローでは、ソースはapp.py自体です。ヘルパーモジュールとデータファイルは、files(テキスト)とassets(base64)のマップを通じて同じ場所に配置できます。Streamlit はアプリのディレクトリをsys.pathに追加するため、import utilsは機能しますが、プロセスはそのディレクトリ内で実行されるわけではありません。そのため、データファイルを開くときはPath(__file__).parent / "data/events.parquet"を使い、相対パスを直接指定しないでください。バンドルするデータは小さく保ってください。set-source の呼び出しごとに、JSON としてインラインで送信されます。- サンドボックスは一時的な環境です。停止または再起動すると、ディスクに書き込んだものはすべて消えます。ファイルに状態を保存せず、クエリから再計算する(キャッシュを使用する)か、
st.session_stateに保持してください。 - コードは権限のないユーザーとして実行されます。posthog SDK はなく、アプリに独自の環境変数やシークレットを渡す方法もなく、一般的なネットワークへの外部接続も想定されていません。
os.environを読み込まないでください。そこにあるものはすべてアプリではなく、サンドボックスのランタイムに属します。データソースとしてposthog_apps.query()を利用するように設計してください。
最小限の、適切な構成のアプリ
import pandas as pd
import plotly.express as px
import posthog_apps
import streamlit as st
st.set_page_config(page_title="Events overview", layout="wide")
st.title("Events overview")
@st.cache_data(ttl=300, show_spinner="Running query...")
def run_query(hogql: str) -> pd.DataFrame:
return posthog_apps.query(hogql)
# A slider is bounded and coerced to int, so it is safe to interpolate.
days = int(st.slider("Days to show", 1, 30, 7))
try:
daily = run_query(
f"""
SELECT toDate(timestamp) AS day, count() AS events
FROM events
WHERE timestamp >= now() - INTERVAL {days} DAY
GROUP BY day ORDER BY day
"""
)
st.plotly_chart(px.bar(daily, x="day", y="events"), use_container_width=True)
except RuntimeError as e:
st.error(str(e))