mcpField
W
workload-analysis

workload-analysis

github.com/posthogデータ・データベース76183 回インストール今日更新
リポジトリ

workload-analysis について

workload-analysisは、顧客アカウント分析向けのスキルです。Vitallyのアカウント情報とPostHogの請求データを組み合わせ、ワークロード別の支出配分や機会・リスクを分析し、インタラクティブなReact可視化を生成します。

主な機能

  1. 01アカウント情報と請求データの統合
  2. 02全SDKの利用割合を分析
  3. 03定義済みルールによる製品支出配分
  4. 04拡大機会とアカウントリスクの特定
  5. 05複数ビューのReactコンポーネント生成

活用シーン

  1. 01ワークロード別のアカウント支出の把握
  2. 02クロスセルや利用拡大の機会発見
  3. 03収益減少や製品依存リスクの評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/posthog/skills --skill workload-analysis

インストール後、Claude Code で/workload-analysisを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
assets/
workload-template.jsx
references/
data-mapping.md
opportunity-framework.md
spend-allocation.md

詳細

ワークロード分析スキル

顧客の支出がプロダクト → ワークロード → チームへどのように流れるかを示す、SDK の内訳、機会の特定、リスク評価を含むインタラクティブな React ビジュアライゼーションを生成します。

ワークフロー

ステップ 1:アカウントデータを収集する
  1. Vitally でアカウントを検索します:

    vitally:find_account_by_name → get externalId (organization_id)
    vitally:get_account_full with filterUnnecessaryFields=false
    
  2. PostHog から請求データをクエリします:

    SELECT date, report 
    FROM postgres.prod.billing_usagereport
    WHERE organization_id = '[externalId]'
    ORDER BY date DESC LIMIT 3
    
  3. 主要なフィールドを抽出します(フィールドの一覧は references/data-mapping.md を参照してください):

    • Vitally:MRR、forecasted_mrr、diff_dollars、プロダクトの支出フィールド
    • 請求:SDK イベント数、AI イベント、録画、例外、フィーチャーフラグのリクエスト
ステップ 2:SDK の割合を計算する

⚠️ 重要:一般的な SDK だけでなく、すべての SDK を確認してください。Flutter/React Native は、モバイルファーストの企業で最も多く使われている SDK であることがよくあります。

請求データから、SDK の内訳を漏れなく抽出します:

const sdkBreakdown = {
  // Web
  web: report.web_events_count_in_period || 0,
  // Backend
  node: report.node_events_count_in_period || 0,
  python: report.python_events_count_in_period || 0,
  go: report.go_events_count_in_period || 0,
  ruby: report.ruby_events_count_in_period || 0,
  php: report.php_events_count_in_period || 0,
  java: report.java_events_count_in_period || 0,
  // Mobile Native
  ios: report.ios_events_count_in_period || 0,
  android: report.android_events_count_in_period || 0,
  // Mobile Cross-Platform (DON'T MISS THESE!)
  flutter: report.flutter_events_count_in_period || 0,
  react_native: report.react_native_events_count_in_period || 0,
};
const total = Object.values(sdkBreakdown).reduce((a, b) => a + b, 0);

// Verify: SDK total should ≈ event_count_in_period
// If big gap, you're missing an SDK!
ステップ 3:ワークロードを定義する

SDK の割合とプロダクトの利用パターンに基づいて、ワークロードを作成します:

パターンワークロードタイプ例
Web >80%Web アプリケーション(プライマリ)SaaS ダッシュボード
Node/Python/Go/Ruby/PHP/Java >80%バックエンド API(プライマリ)API ファーストのプロダクト
iOS + Android >20%ネイティブモバイルアプリ(プライマリ)コンシューマー向け iOS/Android
Flutter >20%Flutter モバイルアプリ(プライマリ)クロスプラットフォームのモバイル
React Native >20%React Native アプリ(プライマリ)クロスプラットフォームのモバイル
AI イベント >0LLM/AI プラットフォームAI ファーストの企業
外部スキーマ >0データプラットフォームデータウェアハウスの利用

モバイルの優先順位: Flutter または React Native が主流の場合、それがプライマリのモバイルワークロードです(iOS/Android のネイティブアプリではありません)。

ステップ 4:ワークロードに支出を割り当てる

references/spend-allocation.md の割り当てルールを適用します:

プロダクト割り当てルール
プロダクト分析SDK イベントの割合に基づく
識別済みイベントSDK イベントの割合に基づく
セッションリプレイ(Web)100% を Web ワークロードに割り当て
モバイルリプレイiOS/Android のイベント比率で分割
フィーチャーフラグ100% をプライマリ SDK の利用者に割り当て
エラートラッキング例外の発生元(Web/Node)に基づく
LLM 分析100% を LLM プラットフォームのワークロードに割り当て
データウェアハウス100% をデータプラットフォームのワークロードに割り当て
グループ分析100% をプライマリワークロードに割り当て
Teams/Scaleプラットフォーム全体(個別に扱う)
ステップ 5:機会とリスクを特定する

references/opportunity-framework.md のフレームワークを適用します:

機会(次の順に確認):

  1. 月次から年次への切り替え(年次契約でない場合)
  2. プロダクトの不足(利用登録済みだが支出が 0 ドル)
  3. クロスセル(顧客のプロファイルに必要なプロダクトが不足)
  4. 利用拡大(上限に近づいている)

リスク(該当する場合にフラグを付ける):

  1. MRR の減少(forecasted が current を下回る)
  2. 年次契約なし(月次 MRR が 3,000 ドル超)
  3. ヘルススコアが低い(6 未満)
  4. 単一プロダクトへの依存(支出の 50% 超)
ステップ 6:React ビジュアライゼーションを生成する

assets/workload-template.jsx のテンプレートを使用します。以下のすべてのコンポーネントが必須です:

必須コンポーネント(すべて含めてください):

  1. SummaryCards - MRR、ヘルススコア、契約タイプ、アラート
  2. SdkBreakdownBar - SDK の割合を示すビジュアルバー
  3. WorkloadCards - 各ワークロードとプロダクトのカード
  4. OpportunitiesTable - 信頼度付きの優先順位順の機会
  5. RisksTable - リスクがある場合に、深刻度順で表示
  6. TreeView - 必須:アカウント → ワークロード → プロダクト → チームを接続線で示す階層ビュー
  7. SankeyView - 必須:左側のプロダクトのバーから右側のワークロードのバーへ流れる収益を、曲線で示す SVG フロー図
  8. MatrixView - プロダクト × ワークロードのグリッドと支出セル

ビュータブ(以下の 5 つすべてを含めてください):

const tabs = [
  { id: 'overview', label: '📊 Overview' },
  { id: 'risks', label: '🚨 Risks' },      // Show if risks.length > 0
  { id: 'tree', label: '🌳 Tree View' },   // REQUIRED
  { id: 'sankey', label: '💰 Revenue Flow' }, // REQUIRED
  { id: 'matrix', label: '📋 Matrix' },
];

⚠️ TreeView や SankeyView を省略しないでください。これらはアカウント構造を理解するうえで最も価値のあるビジュアライゼーションです。

必須コンポーネント:TreeView

このコンポーネントをそのままコピーしてください:

const TreeView = () => {
  const activeWorkloads = workloadTotals.filter(w => w.type !== 'inactive');
  return (
    <div className="bg-white p-4 rounded-lg shadow border mb-4">
      <div className="text-sm font-bold text-gray-700 mb-4">Hierarchical View: Account → Workloads → Products → Teams</div>
      <div className="flex flex-col items-start">
        <div className="bg-gray-900 text-white px-4 py-2 rounded font-bold mb-4">
          {accountSummary.name} (${(accountSummary.arr/1000).toFixed(0)}k ARR)
          <span className="text-gray-400 text-sm ml-2">| {accountSummary.employees} employees | {accountSummary.fundingStage}</span>
        </div>
        <div className="ml-8 border-l-2 border-gray-300">
          {activeWorkloads.map((workload, wIdx) => (
            <div key={wIdx} className="ml-4 mb-6">
              <div className="flex items-center">
                <div className="w-6 h-px bg-gray-300 mr-2"></div>
                <div className="px-3 py-2 rounded text-sm font-medium"
                  style={{ backgroundColor: workload.type === 'primary' ? '#dbeafe' : '#f3e8ff',
                    color: workload.type === 'primary' ? '#1e40af' : '#6b21a8',
                    borderLeft: `4px solid ${workloadTypeColors[workload.type]}` }}>
                  <div className="font-bold">{workload.name}</div>
                  <div className="text-xs opacity-75">{workload.sdk} • {formatNumber(workload.dailyEvents)} events/day • {formatCurrency(workload.totalSpend)}/mo</div>
                </div>
              </div>
              <div className="ml-10 mt-2 border-l border-gray-200">
                {workload.products.map((product, pIdx) => (
                  <div key={pIdx} className="ml-4 mb-2 flex items-center">
                    <div className="w-4 h-px bg-gray-200 mr-2"></div>
                    <div className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: statusColors[product.status] }}></div>
                    <span className={`text-sm ${product.status === 'opportunity' ? 'text-gray-400' : 'text-gray-700'}`}>
                      <span className="font-medium">{product.name}</span>
                      {product.spend > 0 && <span className="text-green-600 ml-2">({formatCurrency(product.spend)}/mo)</span>}
                    </span>
                  </div>
                ))}
              </div>
              <div className="ml-10 mt-2 flex items-center gap-2">
                <div className="w-4 h-px bg-gray-200"></div>
                <span className="text-xs text-gray-500">Teams:</span>
                {workload.teams.map((team, tIdx) => (
                  <span key={tIdx} className="bg-purple-100 text-purple-700 px-2 py-0.5 rounded text-xs">{team}</span>
                ))}
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};
必須コンポーネント:SankeyView

このコンポーネントをそのままコピーしてください:

const SankeyView = () => {
  const productsWithSpend = [];
  const productSpendMap = {};
  workloadTotals.forEach(w => {
    w.products.forEach(p => {
      if (p.spend > 0) {
        productSpendMap[p.name] = (productSpendMap[p.name] || 0) + p.spend;
      }
    });
  });
  Object.entries(productSpendMap).forEach(([name, amount]) => {
    productsWithSpend.push({ name, amount });
  });
  productsWithSpend.sort((a, b) => b.amount - a.amount);
  
  const totalSpend = productsWithSpend.reduce((s, p) => s + p.amount, 0) || 1;
  const activeWorkloads = workloadTotals.filter(w => w.type !== 'inactive' && w.totalSpend > 0);
  
  const productColors = ['#3b82f6', '#8b5cf6', '#22c55e', '#f59e0b', '#ec4899', '#06b6d4'];
  productsWithSpend.forEach((p, i) => { p.color = productColors[i % productColors.length]; });
  activeWorkloads.forEach((w, i) => { w.color = productColors[i % productColors.length]; });

  const svgWidth = 800;
  const svgHeight = Math.max(300, Math.max(productsWithSpend.length, activeWorkloads.length) * 60 + 60);
  const leftX = 140, rightX = svgWidth - 200, barWidth = 30, gap = 8;
  
  let leftY = 40;
  const leftBars = productsWithSpend.map(p => {
    const height = Math.max((p.amount / totalSpend) * (svgHeight - 80), 25);
    const bar = { ...p, x: leftX, y: leftY, width: barWidth, height };
    leftY += height + gap;
    return bar;
  });
  
  let rightY = 40;
  const rightBars = activeWorkloads.map(w => {
    const height = Math.max((w.totalSpend / totalSpend) * (svgHeight - 80), 25);
    const bar = { ...w, amount: w.totalSpend, x: rightX, y: rightY, width: barWidth, height };
    rightY += height + gap;
    return bar;
  });

  const flows = [];
  const leftOffsets = {}, rightOffsets = {};
  leftBars.forEach(b => { leftOffsets[b.name] = b.y; });
  rightBars.forEach(b => { rightOffsets[b.name] = b.y; });
  
  activeWorkloads.forEach(workload => {
    workload.products.forEach(product => {
      if (product.spend > 0) {
        flows.push({ from: product.name, to: workload.name, amount: product.spend, 
          color: leftBars.find(b => b.name === product.name)?.color || '#888' });
      }
    });
  });

  return (
    <div className="bg-white p-4 rounded-lg shadow border mb-4">
      <div className="text-sm font-bold text-gray-700 mb-4">Revenue Flow: Products → Workloads</div>
      <svg width={svgWidth} height={svgHeight} className="mx-auto">
        {flows.map((flow, idx) => {
          const fromBar = leftBars.find(b => b.name === flow.from);
          const toBar = rightBars.find(b => b.name === flow.to);
          if (!fromBar || !toBar) return null;
          const flowHeight = Math.max((flow.amount / totalSpend) * (svgHeight - 80), 3);
          const startY = leftOffsets[flow.from], endY = rightOffsets[flow.to];
          leftOffsets[flow.from] += flowHeight;
          rightOffsets[flow.to] += flowHeight;
          const path = `M ${fromBar.x + barWidth} ${startY} C ${fromBar.x + barWidth + 100} ${startY}, ${toBar.x - 100} ${endY}, ${toBar.x} ${endY} L ${toBar.x} ${endY + flowHeight} C ${toBar.x - 100} ${endY + flowHeight}, ${fromBar.x + barWidth + 100} ${startY + flowHeight}, ${fromBar.x + barWidth} ${startY + flowHeight} Z`;
          return <path key={idx} d={path} fill={flow.color} fillOpacity={0.3} stroke={flow.color} strokeWidth={0.5} />;
        })}
        {leftBars.map((bar, idx) => (
          <g key={`left-${idx}`}>
            <rect x={bar.x} y={bar.y} width={bar.width} height={bar.height} fill={bar.color} rx={4} />
            <text x={bar.x - 10} y={bar.y + bar.height / 2} textAnchor="end" dominantBaseline="middle" fontSize={11} fill="#374151" fontWeight="500">{bar.name}</text>
            <text x={bar.x - 10} y={bar.y + bar.height / 2 + 13} textAnchor="end" dominantBaseline="middle" fontSize={10} fill="#6b7280">{formatCurrency(bar.amount)}</text>
          </g>
        ))}
        {rightBars.map((bar, idx) => (
          <g key={`right-${idx}`}>
            <rect x={bar.x} y={bar.y} width={bar.width} height={bar.height} fill={bar.color} rx={4} />
            <text x={bar.x + bar.width + 10} y={bar.y + bar.height / 2} textAnchor="start" dominantBaseline="middle" fontSize={11} fill="#374151" fontWeight="500">{bar.name}</text>
            <text x={bar.x + bar.width + 10} y={bar.y + bar.height / 2 + 13} textAnchor="start" dominantBaseline="middle" fontSize={10} fill="#6b7280">{formatCurrency(bar.amount)} ({((bar.amount / totalSpend) * 100).toFixed(0)}%)</text>
          </g>
        ))}
        <text x={leftX + barWidth/2} y={20} textAnchor="middle" fontSize={11} fill="#6b7280" fontWeight="bold">PRODUCTS</text>
        <text x={rightX + barWidth/2} y={20} textAnchor="middle" fontSize={11} fill="#6b7280" fontWeight="bold">WORKLOADS</text>
      </svg>
      <div className="text-xs text-gray-500 text-center mt-2">Flow width represents dollar allocation</div>
    </div>
  );
};
出力形式

以下を含む、完全な React コンポーネントファイル {account-name}-workload-analysis.jsx を生成します。

  • 参照用のすべての生データを変数に格納します
  • 比率と割り当てを計算します
  • products 配列を含むワークロードを定義します
  • すべての可視化コンポーネントを含めます
  • さまざまな視点で確認できるビュータブを設けます

クイックリファレンス

ステータスのしきい値
  • 重要:月額 ≥$500(緑)
  • 導入済み:月額 $100-499(青)
  • 試験中:月額 $1-99(アンバー)
  • 機会:月額 $0(グレー)
主な SQL フィールド
event_count_in_period, recording_count_in_period, 
ai_event_count_in_period, exceptions_captured_in_period,
billable_feature_flag_requests_count_in_period,
enhanced_persons_event_count_in_period
Vitally の支出フィールド
product_analytics_forecasted_mrr, session_replay_forecasted_mrr,
feature_flags_forecasted_mrr, llm_analytics_forecasted_mrr,
enhanced_persons_forecasted_mrr, error_tracking_forecasted_mrr,
data_warehouse_forecasted_mrr, surveys_forecasted_mrr

提出前チェックリスト

React アーティファクトを提示する前に、以下の項目がすべて含まれていることを確認してください。

  • MRR、健全性、契約タイプを含む SummaryCards コンポーネント
  • すべての SDK(該当する場合は Flutter/React Native を含む)を表示する SdkBreakdownBar
  • 特定した各ワークロードの WorkloadCards
  • 優先順位を付けた機会を表示する OpportunitiesTable
  • (リスクが特定された場合)RisksTable
  • TreeView コンポーネント - アカウント → ワークロード → 製品 → チームの階層構造
  • SankeyView コンポーネント - 製品からワークロードへの流れを曲線で示す SVG
  • 製品 × ワークロードのグリッドを表示する MatrixView
  • 次の項目を含むビュータブ:概要、リスク、🌳 ツリービュー、💰 収益フロー、マトリックス
  • 生データの参照セクション(折りたたみ可能)

コメント