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

详细介绍

工作负载分析技能

生成交互式 React 可视化图表,展示客户支出如何从产品流向工作负载,再流向团队,并提供 SDK 明细、机会识别和风险评估。

工作流

第 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 优先型公司
外部 schema >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% 分配给主要工作负载
团队/规模覆盖整个平台(单独列出)
第 5 步:识别机会与风险

应用 references/opportunity-framework.md 中的框架:

机会(按顺序检查):

  1. 月付转年付(如果当前不是年付)
  2. 产品缺口(已启用但支出为 $0)
  3. 交叉销售(客户画像对应的产品尚未使用)
  4. 用量扩展(即将达到限制)

风险(如存在则标记):

  1. MRR 下降(预测值低于当前值)
  2. 没有年度合同(月付且 MRR >$3K)
  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>
  );
};
输出格式

生成一个名为 {account-name}-workload-analysis.jsx 的完整 React 组件文件,其中:

  • 以变量形式包含所有原始数据(供参考)
  • 计算比例和分配情况
  • 定义包含 products 数组的工作负载
  • 包含所有可视化组件
  • 提供不同视角的视图标签页

快速参考

状态阈值
  • 显著:≥$500/mo(绿色)
  • 已采用:$100-499/mo(蓝色)
  • 试用中:$1-99/mo(琥珀色)
  • 机会:$0/mo(灰色)
关键 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 产物前,确认以下各项均已包含:

  • SummaryCards 组件,包含 MRR、健康状况、合同类型
  • SdkBreakdownBar 显示所有 SDK(包括存在的 Flutter/React Native)
  • 为每个已识别的工作负载提供 WorkloadCards
  • OpportunitiesTable,列出按排名排序的机会
  • RisksTable(若识别出任何风险)
  • TreeView 组件——层级结构:Account → Workloads → Products → Teams
  • SankeyView 组件——使用 SVG 绘制从 Products 到 Workloads 的弯曲流向路径
  • MatrixView,展示 Products × Workloads 网格
  • 视图标签页包括:Overview、Risks、🌳 Tree View、💰 Revenue Flow、Matrix
  • 原始数据参考部分(可折叠)

评论

支持 Markdown