workload-analysis

Par posthog · skills

Générez des visualisations complètes d'analyse de charge de travail pour les comptes clients PostHog. À utiliser lorsque l'utilisateur demande une analyse de compte, une décomposition de la charge de travail, une analyse des SDK, une répartition des dépenses ou une évaluation des opportunités d'expansion. Les déclencheurs incluent « analyser [compte] », « analyse de charge de travail pour [compte] », « décomposition des SDK pour [compte] », « montrez-moi comment [compte] utilise PostHog », ou toute demande visant à comprendre les habitudes d'utilisation d'un client sur l'ensemble des produits et plateformes.

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

Compétence Analyse des Charges de Travail

Générez des visualisations React interactives montrant comment la dépense client s'écoule des produits → charges de travail → équipes, avec décompositions SDK, identification des opportunités et évaluation des risques.

Workflow

Étape 1 : Collecter les Données du Compte

  1. Trouver le compte dans Vitally :

    vitally:find_account_by_name → get externalId (organization_id)
    vitally:get_account_full with filterUnnecessaryFields=false
  2. Interroger les données de facturation depuis PostHog :

    SELECT date, report 
    FROM postgres.prod.billing_usagereport
    WHERE organization_id = '[externalId]'
    ORDER BY date DESC LIMIT 3
  3. Extraire les champs clés (voir references/data-mapping.md pour la liste complète des champs) :

    • Vitally : MRR, forecasted_mrr, diff_dollars, champs de dépense produit
    • Facturation : comptages d'événements SDK, événements IA, enregistrements, exceptions, demandes de feature flag

Étape 2 : Calculer les Proportions SDK

⚠️ CRITIQUE : Vérifier TOUS les SDK, pas seulement les courants. Flutter/React Native sont souvent le SDK dominant pour les entreprises mobile-first.

À partir des données de facturation, extraire la décomposition COMPLÈTE des 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 (NE PAS OUBLIER CEUX-CI !)
  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);

// Vérifier : le total SDK devrait ≈ event_count_in_period
// Si grand écart, vous manquez un SDK !

Étape 3 : Définir les Charges de Travail

Créez des charges de travail basées sur les proportions SDK et les modèles d'utilisation des produits :

Modèle Type de Charge Exemples
Web >80% Application Web (primaire) Tableau de bord SaaS
Node/Python/Go/Ruby/PHP/Java >80% API Backend (primaire) Produit API-first
iOS + Android >20% Applications Mobile Native (primaire) iOS/Android grand public
Flutter >20% Application Mobile Flutter (primaire) Mobile cross-platform
React Native >20% Application Mobile React Native (primaire) Mobile cross-platform
AI events >0 Plateforme LLM/IA Entreprise IA-first
Schémas externes >0 Plateforme Data Utilisation data warehouse

Priorité Mobile : Si Flutter ou React Native domine, c'est la charge de travail mobile primaire (pas native iOS/Android).

Étape 4 : Allouer la Dépense aux Charges de Travail

Appliquer les règles d'allocation depuis references/spend-allocation.md :

Produit Règle d'Allocation
Analyse Produit Par proportion d'événement SDK
Événements Identifiés Par proportion d'événement SDK
Session Replay (Web) 100% à la charge Web
Mobile Replay Répartir par ratio événements iOS/Android
Feature Flags 100% au SDK consommateur primaire
Error Tracking Par source d'exception (web vs node)
LLM Analytics 100% à la charge Plateforme LLM
Data Warehouse 100% à la charge Plateforme Data
Group Analytics 100% à la charge primaire
Teams/Scale Plateforme globale (séparé)

Étape 5 : Identifier les Opportunités et Risques

Appliquer les frameworks depuis references/opportunity-framework.md :

Opportunités (vérifier dans l'ordre) :

  1. Conversion mensuel → annuel (si pas annuel)
  2. Lacunes produit (inscrit mais $0 de dépense)
  3. Cross-sell (produits manquants pour leur profil)
  4. Expansion d'utilisation (approche des limites)

Risques (signaler si présents) :

  1. MRR en baisse (projeté < courant)
  2. Pas de contrat annuel (>$3K MRR mensuel)
  3. Score santé faible (<6)
  4. Dépendance produit unique (>50% de la dépense)

Étape 6 : Générer la Visualisation React

Utiliser le modèle depuis assets/workload-template.jsx. TOUS les composants ci-dessous sont REQUIS :

Composants OBLIGATOIRES (inclure TOUS) :

  1. SummaryCards - MRR, score santé, type de contrat, alertes
  2. SdkBreakdownBar - Barre visuelle de proportion SDK
  3. WorkloadCards - Cartes pour chaque charge avec produits
  4. OpportunitiesTable - Opportunités classées par confiance
  5. RisksTable - Si risques présents, classés par sévérité
  6. TreeView - REQUIS : Vue hiérarchique montrant Compte → Charges → Produits → Équipes avec lignes de connexion
  7. SankeyView - REQUIS : Diagramme flux revenu SVG montrant Produits (barres gauche) s'écoulant vers Charges (barres droite) avec chemins courbes
  8. MatrixView - Grille Produits × Charges avec cellules de dépense

Onglets Vue (DOIT inclure tous les 5) :

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

⚠️ NE PAS IGNORER TreeView ou SankeyView. Ce sont les visualisations les plus précieuses pour comprendre la structure du compte.

Composant Requis : TreeView

Copier ce composant exactement :

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>
  );
};

Composant Requis : SankeyView

Copier ce composant exactement :

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>
  );
};

Format de Sortie

Générer un fichier composant React complet nommé {account-name}-workload-analysis.jsx qui :

  • Contient toutes les données brutes dans des variables (à titre de référence)
  • Calcule les proportions et allocations
  • Définit les charges avec tableau de produits
  • Inclut tous les composants de visualisation
  • A des onglets vue pour différentes perspectives

Référence Rapide

Seuils de Statut

  • Significatif : ≥$500/mo (vert)
  • Adopté : $100-499/mo (bleu)
  • Expérimentation : $1-99/mo (ambre)
  • Opportunité : $0/mo (gris)

Champs SQL Clés

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

Champs Dépense 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

Liste de Vérification Pré-Livraison

Avant de présenter l'artifact React, vérifier que TOUS les éléments suivants sont inclus :

  • [ ] Composant SummaryCards avec MRR, santé, type de contrat
  • [ ] SdkBreakdownBar montrant TOUS les SDK (y compris Flutter/React Native si présents)
  • [ ] WorkloadCards pour chaque charge identifiée
  • [ ] OpportunitiesTable avec opportunités classées
  • [ ] RisksTable (si des risques identifiés)
  • [ ] Composant TreeView - hiérarchie Compte → Charges → Produits → Équipes
  • [ ] Composant SankeyView - SVG avec chemins flux courbes de Produits à Charges
  • [ ] MatrixView avec grille Produits × Charges
  • [ ] Onglets vue incluant : Overview, Risks, 🌳 Tree View, 💰 Revenue Flow, Matrix
  • [ ] Section de référence données brutes (repliable)

Skills similaires