Design System Data Driven: como coletar métricas e transformar código em decisões

V

Victor Assis

Guest
Um Design System é reconhecido por suas vantagens na construção de aplicações front-end. Entre elas, a padronização visual é, na prática, o benefício mais relevante; os ganhos de velocidade acabam surgindo como consequência. No entanto, para além da padronização, há um aspecto cada vez mais estratégico: a coleta e análise de dados sobre o uso do DS.
Neste artigo, discuto a importância de tornar um Design System orientado por dados, apresento quais métricas podem ser monitoradas e como essas informações apoiam decisões no ciclo de vida do software, além de como podemos coletar na prática essas informações.

Por que (e quais) dados coletar?​


Assim como em qualquer produto digital, os dados são fundamentais para guiar decisões e avaliar impacto. No caso de um Design System, eles permitem entender como a solução é utilizada, mensurar seu valor nas aplicações e reduzir riscos em processos de atualização.
Embora aqui o foco esteja em um contexto web (HTML, CSS, JS/TS), os conceitos são aplicáveis a outros ambientes — de Android (Kotlin) a iOS (Swift), multiplataforma com Flutter ou até soluções de infraestrutura. Para este exercício, consideramos um cenário empresarial com um DS proprietário.
Os principais dados a serem analisados incluem:

Linguagem: tecnologias adotadas pelo app.

Framework/biblioteca e versões: por exemplo, Angular, React ou Vue; essas informações permitem prever impactos em atualizações.

Bibliotecas instaladas: identificação de outros DS ou soluções que convivem no mesmo projeto.

Componentes do DS: número de instâncias e propriedades utilizadas, principal métrica de adoção.

Componentes internos: elementos criados fora do DS, úteis para detectar gaps e necessidades de evolução.

Componentes desconhecidos: padrões emergentes que podem inspirar a inclusão de novas peças no DS.

Como coletar os dados​


Tendo em mente quais informações queremos extrair, precisamos avaliar onde e como a coleta deve acontecer. Ferramentas de mercado como Google Analytics, FullStory ou Microsoft Clarity podem parecer opções viáveis, mas não atendem bem ao nosso objetivo. Isso porque elas são voltadas à análise de comportamento do usuário e, mesmo com customizações, não oferecem dados realmente confiáveis para medir o uso de um Design System. Os principais problemas são:


  • Execução após o build: em aplicações baseadas em JSX/TSX, referências a componentes do DS são removidas quando não se trata de tags customizadas. Após o build, perdemos o vínculo direto com o código-fonte.


  • Excesso de dados: a coleta em tempo de execução dispara registros a cada acesso de usuário, gerando redundância e necessidade de filtros.


  • Alto custo: muitas dessas ferramentas cobram por requisição ou volume de dados. Para nosso caso, em que basta coletar uma instância consolidada, esse custo é desnecessário.

Por esses motivos, a coleta deve ocorrer entre o desenvolvimento e a publicação, ou seja, durante o processo de CI/CD. Nesse estágio, conseguimos analisar o código-fonte de forma íntegra, sem ruído de execução em produção e sem custos adicionais. No nosso caso, a estratégia será implementar a coleta diretamente em pipelines de build — por exemplo, no GitHub Actions.

Implementação em Código​


Existem diferentes estratégias para coletar os dados de um Design System no código-fonte. Uma abordagem simples seria converter tudo para string e usar expressões regulares para buscar prefixos e listar componentes, ou até manter uma allowlist com os nomes de cada componente a serem analisados.
Entretanto, essas soluções não escalam bem. Um caminho mais robusto é utilizar Abstract Syntax Tree (AST) ou mesmo ferramentas como o ANTLR (ANother Tool for Language Recognition). O AST é uma estrutura de dados hierárquica que representa o código em forma de árvore: cada nó corresponde a uma construção da linguagem (função, variável, chamada de componente etc.). Isso nos permite inspecionar o código de forma confiável e precisa.

Na imagem, vemos um exemplo de um código do lado esquerdo, e no lado direito sua referencia em ast
Prefixos
Por convenção, muitos Design Systems usam prefixos para diferenciar seus componentes, como mat-, ng-, cbs-. Para este artigo, adotaremos o prefixo fictício abs-.

Componentes internos​


Antes de mapear os componentes do DS, precisamos identificar os componentes criados internamente na aplicação. Isso depende do framework:


  • Angular: podemos ler o angular.json e, nos arquivos, inspecionar o decorator @Component, extraindo o valor do selector.


  • React: cada componente é uma função ou classe exportada; logo, precisamos mapear todas as declarações de função que representam componentes.


  • Outros frameworks: cada biblioteca pode ter sua própria convenção, mas o objetivo é sempre identificar os elementos internos para diferenciá-los do DS.

Com esses dados, conseguimos traçar um paralelo: quais componentes vêm do DS, quais são internos e quais são externos/desconhecidos.

Parser HTML​


Nos arquivos HTML, queremos identificar tags que representam componentes do DS. Para isso podemos usar a biblioteca htmlparser2 junto com css-select.


  • Tags customizadas: normalmente seguem o padrão prefixo-nome, como abs-input.


  • Diretivas e atributos: em Angular, por exemplo, precisamos capturar absButton, absMask e outras diretivas.


  • Lógica de contagem: percorremos todas as tags, filtramos as que começam com o prefixo definido (abs-) e incrementamos um contador em um objeto. Assim, cada chave representa o nome do componente e o valor, o número de instâncias detectadas.

Além disso, também podemos capturar classes CSS e atributos para enriquecer as métricas.


Code:
import { parseDocument } from "htmlparser2";
import { selectAll } from "css-select";

/**
 * @param {string} html - conteúdo HTML
 * @param {string[]} dsPrefixes - ex: ['abs']
 * @param {Set<string>} discoveredAngularSelectorsSet - Set of discovered Angular selectors.
 * @returns {{
 *   components: Record<string, number>,
 *   propValues: Record<string, Record<string, string[]>>,
 *   directives: Record<string, number>, // Corrected type from previous subtasks
 *   outsideComponents: Record<string, number>,
 *   internalComponents: Record<string, number>
 * }}
 */
export function extractHtmlUsage(
  html,
  dsPrefixes = [],
  discoveredAngularSelectorsSet = new Set()
) {
  const tagPrefixes = dsPrefixes.map((p) => p + "-");
  const htmlDirectivePrefixes = dsPrefixes.map((p) => p.toLowerCase());


  const result = {
    components: {},
    propValues: {},
    directives: {},
    outsideComponents: {},
    internalComponents: {},
    classes: {},
  };


  const doc = parseDocument(html);
  const elements = selectAll("*", doc);


  for (const el of elements) {
    if (!el.name || !el.attribs) continue;
    const tag = el.name;
    const isCustomElement = tag.includes("-");
    const isDSComponent = tagPrefixes.some((prefix) => tag.startsWith(prefix));


    if (isDSComponent) {
      result.components[tag] = (result.components[tag] || 0) + 1;
      if (!result.propValues[tag]) result.propValues[tag] = {};
      for (const [attr, val] of Object.entries(el.attribs)) {
        const cleanAttr = attr.replace(/[\[\]\(\)\*]/g, "");
        if (typeof val === "string") {
          if (!result.propValues[tag][cleanAttr])
            result.propValues[tag][cleanAttr] = [];
          if (!result.propValues[tag][cleanAttr].includes(val)) {
            result.propValues[tag][cleanAttr].push(val);
          }
        }
      }
    } else if (discoveredAngularSelectorsSet.has(tag)) {
      result.internalComponents[tag] =
        (result.internalComponents[tag] || 0) + 1;
    } else if (isCustomElement) {
      result.outsideComponents[tag] = (result.outsideComponents[tag] || 0) + 1;
    }


    const classAttr = el.attribs.class;
    if (classAttr) {
      const classes = classAttr.split(/\s+/);
      for (const cls of classes) {
        if (dsPrefixes.some((prefix) => cls.startsWith(prefix + "-"))) {
          result.classes[cls] = (result.classes[cls] || 0) + 1;
        }
      }
    }


    for (const attr of Object.keys(el.attribs)) {
      const cleanAttr = attr.replace(/[\[\]\(\)\*]/g, "");
      if (
        htmlDirectivePrefixes.some(
          (p) => cleanAttr.startsWith(p) && cleanAttr.length > p.length
        )
      ) {
        result.directives[cleanAttr] = (result.directives[cleanAttr] || 0) + 1;
      }
    }
  }


  return result;
}

O parser varre o DOM, incrementa contadores por componente DS, coleta props/atributos, contabiliza diretivas com prefixo, registra classes do DS e separa componentes internos (já descobertos) de externos.

Parser JS/TS​


Nos arquivos JavaScript e TypeScript, a ideia é percorrer a árvore AST (com ferramentas como @babel) e identificar:
Instâncias de componentes do DS.


  • Diretivas ou props aplicadas.


  • Componentes internos.


  • Qualquer outro elemento não mapeado, tratado como componente externo.

Code:
import fs from "fs";
import * as babelParser from "@babel/parser";


import traverseModule from "@babel/traverse";
const traverse = traverseModule.default;


/**
 * Extrai componentes, props, diretivas e componentes internos de um arquivo JSX/TSX.
 * @param {string} filePath - Caminho do arquivo.
 * @param {string[]} dsPrefixes - Prefixos dos componentes e diretivas do DS, ex: ['abs']. (Note: para diretivas, o prefixo pode ser minúsculo ex: 'abs')
 * @param {Set<string>} discoveredJsxInternalNamesSet - Set of discovered JSX internal component names.
 * @returns {{
 *   components: Record<string, number>,
 *   propValues: Record<string, Record<string, (string | number | boolean)[]>>,
 *   directives: Record<string, number>,
 *   internalComponents: Record<string, number>,
 *   outsideComponents: Record<string, number>,
 *   classes: Record<string, number>
 * }}
 */
export function extractJsxUsage(
  filePath,
  dsPrefixes = [],
  discoveredJsxInternalNamesSet = new Set()
) {
  const code = fs.readFileSync(filePath, "utf8");
  const ast = babelParser.parse(code, {
    sourceType: "module",
    plugins: [
      "jsx",
      "typescript",
      "decorators-legacy",
      "deprecatedImportAssert",
    ],
  });


  const result = {
    components: {},
    propValues: {},
    directives: {},
    internalComponents: {},
    outsideComponents: {},
    classes: {},
  };


  const dsClassPrefixes = dsPrefixes.map((p) => `${p.toLowerCase()}-`);
  const directiveAttributePrefixes = dsPrefixes.map((p) => p.toLowerCase());


  traverse(ast, {
    JSXOpeningElement(path) {
      const nameNode = path.node.name;
      if (!nameNode || !nameNode.name) return;


      const tagName = nameNode.name;
      const isDSComponent = dsPrefixes.some((p) => tagName.startsWith(p));


      if (isDSComponent) {
        result.components[tagName] = (result.components[tagName] || 0) + 1;
        if (!result.propValues[tagName]) result.propValues[tagName] = {};


        for (const attr of path.node.attributes) {
          if (attr.type !== "JSXAttribute" || !attr.name) continue;


          const propName = attr.name.name;
          if (!propName) continue;


          let value;


          if (attr.value === null) {
            value = true;
          } else if (attr.value.type === "StringLiteral") {
            value = attr.value.value;
          } else if (attr.value.type === "JSXExpressionContainer") {
            const expression = attr.value.expression;
            if (
              expression.type === "StringLiteral" ||
              expression.type === "NumericLiteral" ||
              expression.type === "BooleanLiteral"
            ) {
              value = expression.value;
            }
          }


          if (value !== undefined) {
            if (!result.propValues[tagName][propName]) {
              result.propValues[tagName][propName] = [];
            }
            if (!result.propValues[tagName][propName].includes(value)) {
              result.propValues[tagName][propName].push(value);
            }
          }
        }
      } else if (discoveredJsxInternalNamesSet.has(tagName)) {
        result.internalComponents[tagName] =
          (result.internalComponents[tagName] || 0) + 1;
      } else {
        if (/^[A-Z]/.test(tagName)) {
          result.outsideComponents[tagName] =
            (result.outsideComponents[tagName] || 0) + 1;
        }
      }


      for (const attr of path.node.attributes) {
        if (attr.type === "JSXAttribute" && attr.name && attr.name.name) {
          const propName = attr.name.name;
          if (
            directiveAttributePrefixes.some(
              (prefix) =>
                propName.toLowerCase().startsWith(prefix) &&
                propName.length > prefix.length
            )
          ) {
            result.directives[propName] =
              (result.directives[propName] || 0) + 1;
          }
        }
      }


      for (const attr of path.node.attributes) {
        if (
          attr.type === "JSXAttribute" &&
          attr.name &&
          (attr.name.name === "className" || attr.name.name === "class")
        ) {
          const processClasses = (classString) => {
            if (typeof classString !== "string") return;
            const classes = classString.split(/\s+/).filter(Boolean);
            for (const cls of classes) {
              if (dsClassPrefixes.some((p) => cls.startsWith(p))) {
                result.classes[cls] = (result.classes[cls] || 0) + 1;
              }
            }
          };


          if (attr.value) {
            if (attr.value.type === "StringLiteral") {
              processClasses(attr.value.value);
            } else if (attr.value.type === "JSXExpressionContainer") {
              const expression = attr.value.expression;
              if (expression.type === "StringLiteral") {
                processClasses(expression.value);
              } else if (expression.type === "TemplateLiteral") {
                expression.quasis.forEach((quasi) => {
                  processClasses(quasi.value.cooked);
                });
              }
            }
          }
          break;
        }
      }
    },
  });


  return result;
}

Extra – Parser CSS/SCSS​


Nos Design Systems modernos, tokens também são elementos importantes a monitorar. Podemos buscar ocorrências de:


  • Custom Properties: var(--prefix-*)


  • Variáveis SCSS: $prefix-*

Essas métricas ajudam a entender a adoção dos tokens de estilo.


Code:
/**
 * Analisa conteúdo CSS/SCSS para detectar tokens do DS por prefixo
 * @param {string} content - Conteúdo do arquivo
 * @param {string} prefix - Prefixo do DS (ex: 'abs')
 * @returns {{
 *   customProperties: string[],
 *   scssVariables: string[]
 * }}
 */
export function extractCssTokens(content, prefix) {
  const customProperties = new Set();
  const scssVariables = new Set();
  const classes = new Set();


  const varRegex = new RegExp(
    `var\\(\\s*(--${prefix}-[a-z0-9-_]+)\\s*\\)`,
    "gi"
  );
  for (const match of content.matchAll(varRegex)) {
    customProperties.add(match[1]);
  }


  const scssRegex = new RegExp(`\\$${prefix}-[a-z0-9-_]+`, "gi");
  for (const match of content.matchAll(scssRegex)) {
    scssVariables.add(match[0]);
  }


  const classSelectorRegex = new RegExp(`\\.(${prefix}-[a-zA-Z0-9-_]+)`, "g");
  for (const match of content.matchAll(classSelectorRegex)) {
    if (match[1]) {
      classes.add(match[1]);
    }
  }


  return {
    customProperties: [...customProperties],
    scssVariables: [...scssVariables],
    classes: [...classes],
  };
}

O parser lista custom properties, variáveis SCSS e classes utilitárias usadas, revelando a maturidade dos tokens (cores, spacing, tipografia) e pontos de inconsistência.

GitHub Action​


A execução desse código na pipeline é bem simples, depois de centralizar o projeto, podemos subir um ambiente nodejs, e executar quando tivermos uma pr para a branch principal:


Code:
name: Reusable DS Usage Analyzer

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '18'


jobs:
  analyze-ds-usage:
    runs-on: ubuntu-latest


    steps:
      - name: Checkout target repository
        uses: actions/checkout@v4


      - name: Checkout ds-usage-analyzer logic
        uses: actions/checkout@v4
        with:
          repository: victor-assis/Design-System-Metrics
          path: analyzer


      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}


      - name: Install analyzer dependencies
        run: cd analyzer && npm i


      - name: Run Web Scanner
        run: node analyzer/scripts/scan-web.js


      - name: Generate Final Report and Commit
        run: node analyzer/scripts/generate-report.js


      - name: Upload JSON Report
        uses: actions/upload-artifact@v4
        with:
          name: ds-usage-report-json
          path: analyzer/reports/final-report.json


      - name: Upload Markdown Report
        uses: actions/upload-artifact@v4
        with:
          name: ds-usage-report-md
          path: analyzer/reports/final-report.md


      - name: Comment on PR with report
        if: github.event_name == 'pull_request'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: node analyzer/scripts/comment-pr.js

Abaixo, o passo a passo dos steps do seu workflow:


  1. Checkout do repositório alvo -
    Obtém o código da aplicação que será analisada (a “fonte” onde mediremos o uso do DS).


  2. Checkout da lógica do analisador -
    Baixa o projeto do analisador (scripts scan-web.js, generate-report.js, etc.) em analyzer/, separando app e ferramenta.


  3. Setup Node -
    Define a versão do Node para rodar os scripts de análise com previsibilidade.


  4. Instalação de dependências do analisador -
    Executa npm i dentro de analyzer/ para preparar parsers (Babel, htmlparser2, etc.).


  5. Execução do Web Scanner -
    Roda scan-web.js para vasculhar HTML/JS/TS/CSS/SCSS, identificar componentes DS, internos, externos e tokens, e gerar resultados intermediários (ex.: JSON em analyzer/reports/).


  6. Geração do relatório final -
    generate-report.js consolida tudo em JSON e Markdown (ex.: final-report.json e final-report.md), prontos para stakeholders.


  7. Upload do relatório JSON (artifact) -
    Publica o JSON como artefato do workflow (útil para integrações ou ingestão posterior em dashboards).


  8. Upload do relatório Markdown (artifact) -
    Publica a versão legível (MD) para leitura rápida.


  9. Comentário no PR com o relatório (condicional a pull_request) -
    Usa GITHUB_TOKEN para postar um resumo do relatório diretamente na discussão do PR — visibilidade imediata para devs, design e produto.

Resumo da integração: a análise roda antes do merge, dando uma “fotografia” confiável da adoção do DS no código-fonte (sem ruído de runtime), com custo zero adicional e alto sinal para tomada de decisão.
Exemplo do relatório legível como comentário na PR:

na imagem, vemos um comentário em uma pr no github, com algumas informações da coleta das métricas, exemplo, nome do componente mais suas quantidade

Análise dos dados​


Com as informações coletadas, conseguimos extrair insights relevantes para a evolução do Design System. Por exemplo, se semanticamente uma das propriedades de um componente precisar mudar, conseguimos mensurar o impacto dessa atualização a partir dos números. Outro caso é o do nosso componente de ícones: com uma folha próxima de 300 ícones, podemos identificar quais não estão em uso e, assim, reduzir o tamanho do sprite e otimizar o bundle dos aplicativos.

Além disso, as métricas permitem calcular a economia aproximada de tempo proporcionada pelo uso de um Design System. Nesse caso, é importante refletir que cada componente não é recriado do zero a cada instância — ele é construído uma vez e depois reaproveitado. Assim, podemos modelar o cálculo em duas partes: o custo do primeiro uso e o custo marginal de reuso.

Uma fórmula possível para estimar esse ganho é:


ΔT=∑i=1N(Pi⋅Si−(bi+(Pi−1)⋅ri)) \Delta T = \sum_{i=1}^{N} \Big(P_i \cdot S_i - (b_i + (P_i - 1) \cdot r_i)\Big) ΔT=i=1∑N(Pi⋅Si−(bi+(Pi−1)⋅ri))

Onde:

  • PiP_iPi = número total de instâncias do componente no projeto.
  • SiS_iSi = tempo para construir do zero um componente de complexidade CiC_iCi .
  • bib_ibi = tempo do primeiro uso do componente no app (integração inicial).
  • rir_iri = tempo de reuso marginal por instância adicional.

Para parametrizar:

  • Si=Ci⋅VbuildS_i = C_i \cdot V_{\text{build}}Si=Ci⋅Vbuild
  • bi=k1⋅Sib_i = k_1 \cdot S_ibi=k1⋅Si (ex.: 30% do esforço de build)
  • ri=k2⋅Sir_i = k_2 \cdot S_iri=k2⋅Si (ex.: 5% do esforço de build)

Tabela-guia de complexidade:

ComplexidadeExemplos CiC_iCi SiS_iSi (semana base)
BaixaButton, Tag, Badge1~0.5
MédiaModal, Tabs, FormField2~1.0
AltaDataTable, DatePicker, RichEditor3~1.5

Exemplo prático (com k₁=0,3 e k₂=0,05):


  • Botão (P=20, C=1) → Sem DS = 20 × 0,5 = 10 semanas
    Com DS = 0,15 + 19 × 0,025 = 0,625 semanas
    Economia ≈ 9,4 semanas


  • Tabela (P=5, C=3) → Sem DS = 5 × 1,5 = 7,5 semanas
    Com DS = 0,45 + 4 × 0,075 = 0,75 semanas
    Economia ≈ 6,75 semanas

Total estimado = ~16,1 semanas economizadas.

Conclusão​


Um Design System vai além da padronização visual e da velocidade de entrega. Quando orientado por dados, ele se torna uma ferramenta estratégica, capaz de revelar como é realmente utilizado nas aplicações e de apoiar decisões fundamentadas para sua evolução.

Ao longo deste artigo, vimos que a coleta de métricas não precisa depender de ferramentas de mercado voltadas ao comportamento do usuário. Pelo contrário, a análise pode (e deve) acontecer no ciclo de desenvolvimento, aproveitando o código-fonte cru em pipelines de CI/CD. Essa abordagem garante dados consistentes, evita custos desnecessários e oferece uma visão precisa da adoção do DS.

A implementação prática mostrou como usar técnicas como AST, parsers de HTML/JS/SCSS e integração com GitHub Actions para extrair informações relevantes: quais componentes estão em uso, quais foram criados internamente, quais tokens de estilo foram aplicados e até mesmo quais padrões emergentes podem inspirar novos elementos no DS.
Mais do que medir, a chave está em transformar os dados em decisões: identificar gaps, priorizar melhorias, avaliar impacto de mudanças e, principalmente, alinhar times de design, produto e tecnologia em torno de um mesmo objetivo.

Um Design System data-driven não é apenas um repositório de componentes — é um mecanismo de feedback contínuo que fortalece a consistência, reduz riscos e potencializa a colaboração. Em outras palavras, é quando o Design System deixa de ser apenas uma “caixa de ferramentas” e se consolida como um ativo estratégico da organização.

Referências​


Este projeto foi fruto de um estudo sobre como coletar métricas de um Design System, o código não está perfeito, mas pode ajudar times a evoluir o assunto.

GitHub do projeto: https://github.com/victor-assis/Design-System-Metrics

Exemplo de report em comentario: https://github.com/victor-assis/Design-System-Metrics/wiki/Exemplo-de-comentário-da-analise

Continue reading...
 


Join 𝕋𝕄𝕋 on Telegram
Channel PREVIEW:
Back
Top