Início/Docs/Métricas personalizadas
Contrato de métricas personalizadas
As métricas personalizadas permitem que o próprio serviço da sua extensão exponha os números que importam para ela (conversões, receita, saúde do backend, funis) no mesmo painel que já acompanha as instalações e a saúde na Chrome Web Store. O contrato é orientado por metadados e tem superfície dupla: cada métrica declara onde e como aparece tanto no painel quanto no status do Telegram, de modo que o painel continua sendo um renderizador genérico, sem código específico de produto.
1. Como funciona (superfície dupla)
Seu serviço expõe um endpoint de métricas. O Ext Ops Panel é o cliente: ele consulta esse endpoint (nunca o contrário) e renderiza a resposta. A resposta alimenta duas superfícies ao mesmo tempo:
- Abas do painel na visualização Detalhe da extensão: abas, seções e tipos de renderização por métrica.
- Status no Telegram: linhas lógicas compostas pelas mesmas métricas, filtradas e formatadas por métrica.
O painel mescla suas métricas com as métricas básicas do próprio painel (instalações / desinstalações / reinstalações / feedback / Chrome Web Store / saúde), que ficam na aba reservada base e na linha do Telegram de mesmo nome.
2. O endpoint e a autenticação
O painel chama o seu endpoint com GET e espera o payload v2 como resposta 200. Os dois modos de pull têm o mesmo formato:
- Sob demanda: acionado quando um usuário abre o Detalhe. Limitado a no máximo 1 requisição por minuto por fonte, em todos os planos, incluindo o Free.
- Monitoramento: de forma agendada nos planos que incluem monitoramento, monta o status do Telegram e os alertas de mudança de estado.
A autenticação usa uma das opções nenhum, bearer ou api_key_header. Os segredos ficam criptografados em repouso no backend do painel e nunca são entregues à extensão do painel. Se um pull falhar, o painel se degrada de forma controlada (último snapshot ou bloco oculto) sem quebrar as métricas básicas.
3. Estrutura do payload
O payload de nível superior traz o layout (abas, seções, linhas do Telegram) e o array de métricas.
| Campo | Tipo | Notas |
|---|---|---|
schema_version | string | Obrigatório. Deve ser "2.0". |
generated_at | string (date-time) | Quando o payload foi composto (detecção de atualidade / inatividade). |
tabs | Tab[] | Abas do painel. Consulte a seção 4. |
sections | Section[] | Grupos de métricas dentro de uma aba (tab, id, title, order). |
telegram | TelegramMeta | Template header + lines[]. Consulte a seção 7. |
metrics | Metric[] | As próprias métricas. Consulte as seções 5 e 6. |
4. Abas e ícones de abas
Uma aba é { id, title, order, icon? }. A aba reservada
base pertence ao próprio painel (icon="layout-dashboard"); o seu serviço adiciona as demais. Quando há muitas abas abertas, a aba ativa mostra ícone + título e as outras se recolhem para apenas o ícone (o título fica no tooltip e no aria-label).
Dois ids de aba são reservados ao painel: base (a aba Basic) e monitoring (a aba Monitoring integrada que os relatórios de heartbeat preenchem, veja o
contrato de monitoramento). Não os declare em tabs: uma declaração é ignorada e o painel define título, ícone e ordem. As métricas continuam apontando para eles via dashboard.tab.
O valor icon é uma de duas coisas:
- Um nome de ícone Lucide em kebab-case ASCII (corresponde a
^[a-z][a-z0-9-]*$), por exemplotrending-up. Um nome bem formado, mas não suportado, cai no ícone padrão (square), então a barra nunca quebra. - Um emoji único, por exemplo 💰 ou 🚀, renderizado como texto.
O painel traz um catálogo selecionado de 60 nomes Lucide (leve para o pacote MV3). O mesmo catálogo serve para ícones de abas e ícones de metas (seção 5.3):
- layout-dashboard
- activity
- gauge
- layers
- plug
- dollar-sign
- banknote
- wallet
- credit-card
- receipt
- bar-chart-3
- line-chart
- pie-chart
- trending-up
- trending-down
- users
- user
- alert-triangle
- bell
- clock
- git-branch
- package
- box
- database
- server
- cpu
- zap
- globe
- pencil
- puzzle
- funnel
- filter
- square
- circle
- hash
- percent
- tag
- flag
- user-plus
- log-in
- plus-square
- shopping-cart
- check-circle
- send
- download
- upload
- star
- heart
- eye
- mouse-pointer-click
- share-2
- message-square
- target
- rocket
- file-text
- bookmark
- calendar
- key
- play
5. Métrica: valor + dois blocos de posicionamento
Uma métrica traz um valor tipado mais dois blocos independentes: dashboard (onde e como na extensão) e telegram (onde e como na mensagem). Qualquer um dos dois pode ser omitido ou ocultado com show:false, então uma métrica pode existir em uma única superfície.
| Campo | Notas |
|---|---|
id | Id de métrica estável, único no payload. |
kind | Tipo de dado (seção 5.1). Define quais campos de valor se aplicam. |
label | Rótulo canônico legível para pessoas. |
value e campos do tipo | Os campos do payload dependem de kind (seção 5.1). |
severity | Um de ok, info, warn, crit (define a cor / o emoji). |
shareable | Booleano opcional, padrão true. Quando false, a métrica é exclusiva do dono: em um projeto compartilhado como somente leitura, ela é removida no servidor e nunca chega a um observador / visualização pública (use para métricas financeiras). Controla o acesso, não o posicionamento. |
dashboard | { show, tab, section, order, name, render, options } (seção 5.2). |
telegram | { show, line, pos, format, sep, when } (seções 6 e 8). |
5.1. Tipos (tipo de dado + payload)
Os tipos formam um conjunto aberto: um tipo desconhecido é ignorado pelo renderizador (compatível com versões futuras).
| kind | Campos do payload | Exemplo de uso |
|---|---|---|
number | value (+ opcional delta, unit) | contagens de instalações / feedback |
delta | value, delta (renderiza 1200 (+72)) | total (+hoje) |
percent | value (0 a 100) | taxa de sucesso do motor |
duration | seconds ou minutes | p50/p95/média, atualidade |
bytes | bytes ou kb | tamanho da saída (~522KB) |
rating | value, count | 5.0★ (1) |
money | value, currency, opcional delta | MRR, gastos |
status | state + severity | semáforo de saúde |
series | points:[{date,count}], opcionalmente vários series:[{name,points}], empilhadas com stack: true | instalações diárias, usuários ativos |
intraday | series:[{name,intraday:[{minute,value}]}], opcional step_min (padrão 15), tz | hoje em intervalos de 15 minutos |
hour_histogram | { weekdays:[...], start, end, tz } | conversões por hora |
funnel | steps:[{key,label,value}] | da tabela de preços ao checkout |
breakdown | items:[{label,value,unit?,sub?/money?,country?,parts?}], opcional ranges:{today,week,month,all} | assinaturas por plano, por motor, por superfície; visitas por origem ou país |
list | items:[{text,count?,severity?}] no nível superior da métrica, não dentro de value | erros recentes |
table | items:[{…}], um objeto por linha; colunas de dashboard.options.columns, senão as chaves da primeira linha | filas, envios recentes |
text | value (string) | texto livre |
Uma métrica table:
{ "id": "queues", "kind": "table", "label": "Mail queues",
"items": [ { "queue": "outbound", "size": 3, "oldest": "2m" },
{ "queue": "deferred", "size": 0, "oldest": "-" } ],
"dashboard": { "show": true, "tab": "backend", "section": "engines", "order": 5, "render": "table",
"options": { "columns": ["queue", "size", "oldest"] } } }5.2. Renderização (como exibir no painel)
dashboard.render escolhe o visual (geralmente coincide com kind, mas pode diferir, por exemplo, exibir um percent como badge). Também é um conjunto aberto.
| render | Aparência | Opções (dashboard.options) |
|---|---|---|
kpi | Cartão: nome + valor grande + delta; com stack, as partes em uma fileira abaixo do valor | tone, stack |
badge | Pílula de status / pictograma por severidade | – |
chart | Gráfico de linhas (Mês/Todo o período, média móvel de 7 dias, hover); com stack: true, colunas empilhadas por dia com uma linha Total | series, defaultMode, stack |
intraday | Hoje por intervalos (96 por dia UTC): barras empilhadas ou linhas suavizadas, eixo de tempo HH:MM, leitura ao passar o mouse em todas as séries | mode (bars | lines, padrão lines) |
hour_histogram | Barras por hora, agrupadas em Dias úteis / Fim de semana / Por dia da semana | series |
funnel | Etapas com a conversão entre elas | – |
breakdown | Barras categóricas horizontais com valor + participação no total. Com ranges, um seletor hoje / 7 dias / 30 dias / tudo; com map: countries, um mapa-múndi acima da lista; com stack, barras divididas em partes alternáveis | items, defaultRange, map, stack |
table | Tabela (por exemplo, feedback) | columns |
text | Parágrafo / inline | – |
status | Estado de semáforo por severidade | – |
list | Lista de itens com contagem / severidade opcional | – |
intraday vs. chart. O eixo do chart é diário (points[].date = YYYY-MM-DD), e a visualização Mês dele mostra só os últimos 30 pontos, então os 96 intervalos de hoje não cabem nela. intraday é o gráfico Hoje do próprio painel, exposto como tipo do contrato: a mesma convenção em que minute conta os minutos desde o início do dia UTC (0, 15, …, 1425), mas as séries e os dados são declarados pela fonte. Envie apenas os intervalos não vazios; o painel preenche o resto.
Um intervalo vazio é null, não 0. Para uma média (por exemplo, o tempo médio de conversão), um zero seria uma mentira, então a linha se interrompe
em vez de despencar para zero. Um intervalo ausente e value: null são equivalentes: a linha se interrompe, nenhuma barra é desenhada e a leitura ao passar o mouse mostra um traço.
Empilhado: o total e suas partes em um gráfico e uma lista. Quando uma métrica é composta de partes (por exemplo, as visualizações de página de dois sites), envie o total e as partes como UMA métrica: o painel desenha as partes umas sobre as outras e a legenda liga e desliga cada uma, sem abas extras nem filtros suspensos. As duas extensões são aditivas: um renderizador que não as conhece desenha a mesma métrica de antes.
Gráfico: kind: "series" com dashboard.options.stack: true. As séries se empilham na ordem do array: a primeira é a camada de baixo. O painel desenha uma coluna por dia (em todo o período, por janela de 7 dias) com um segmento por série visível, então o topo de uma coluna é a soma exata das séries visíveis; um dia em que todas são zero não tem coluna. Clicar em um item da legenda oculta uma camada (a última visível continua ativa); o resto é redesenhado a partir de zero e o eixo de valores acompanha o total visível. Ao passar o mouse sobre uma coluna, ou chegar a ela com as setas, a leitura mostra cada camada visível e uma linha Total. Todo o período empilha a média móvel de 7 dias de cada série, marcada com ~. Qualquer outro valor de stack mantém as linhas sobrepostas.
Lista: kind: "breakdown" com dashboard.options.stack definido como lista ordenada de partes [{key, label}], e parts: {"key": number} em cada item, em items e em cada janela de ranges. value deve ser igual à soma de todas as partes. O painel mostra um chip por parte que a liga ou desliga para o cartão inteiro, em todas as janelas. Uma linha mostra a soma de suas partes visíveis, linhas com zero ficam ocultas, a lista é reordenada por essa soma e a barra é dividida em um segmento por parte visível. Com map: "countries", os círculos acompanham as somas visíveis. Um item sem parts conta inteiro, não importa o que esteja desligado.
{ "id": "site_sources", "kind": "breakdown", "label": "Sources",
"items": [ { "label": "Search engines", "value": 812, "parts": { "makcards": 790, "journalingapp": 22 } } ],
"ranges": { "today": [ { "label": "Search engines", "value": 30, "parts": { "makcards": 29, "journalingapp": 1 } } ],
"month": [ { "label": "Search engines", "value": 812, "parts": { "makcards": 790, "journalingapp": 22 } } ] },
"dashboard": { "show": true, "tab": "site", "section": "site_sources", "render": "breakdown",
"options": { "defaultRange": "month",
"stack": [ { "key": "makcards", "label": "makcards.online" },
{ "key": "journalingapp", "label": "journalingapp.app" } ] } } }
Bloco: um bloco kpi do tipo number ou delta aceita a mesma lista dashboard.options.stack e parts: {"key": number} na própria métrica. O valor grande continua sendo value, que deve ser igual à soma de todas as partes. Abaixo dele, o painel mostra as partes em uma fileira na ordem da pilha (num bloco estreito a fileira quebra), com a cor que o mesmo nome tem em um gráfico empilhado da aba; uma parte zerada aparece esmaecida. O bloco não tem interruptores. Os cabeçalhos da lista de projetos e o Telegram continuam lendo value.
{ "id": "site_views_today", "kind": "number", "label": "Page views", "value": 42,
"parts": { "portal": 30, "cabinet": 4, "backoffice": 8 },
"dashboard": { "show": true, "tab": "site", "section": "site_today", "render": "kpi",
"options": { "stack": [ { "key": "portal", "label": "zakazindia.ru" },
{ "key": "cabinet", "label": "Client cabinet" },
{ "key": "backoffice", "label": "Backoffice" } ] } } }5.3. A lista de projetos: usuários e metas da aba Básica
A lista de projetos mostra alguns números de um serviço web sem abri-lo. O painel os lê apenas da aba base e apenas quando a métrica tem um value numérico. A primeira correspondência vence.
| Métrica | Como o painel a encontra | Onde é exibida |
|---|---|---|
| DAU, WAU, MAU | O id, o label ou o nome no dashboard é exatamente dau, wau ou mau (sem diferenciar maiúsculas). | WAU e MAU na coluna Usuários, DAU em Hoje. |
| Metas 1 a 3 | O id é exatamente goal_1, goal_2 ou goal_3. | Hoje, na mesma linha que DAU: um ícone e um número. |
Uma meta é uma ação-alvo dos seus usuários: cadastros, compras, formulários enviados. Você escolhe a janela do número e o painel o mostra como está. Recomendamos contar o dia de hoje e nomear a janela no label (por exemplo, Cadastros hoje). O label é o tooltip, até 80 caracteres. O campo icon aceita um nome do catálogo da seção 4; emoji não são aceitos para metas. Sem ícone, ou com um nome desconhecido, o painel desenha target. Um zero aparece como um traço atenuado.
{ "id": "goal_1", "kind": "number", "label": "Sign-ups today", "value": 12, "icon": "user-plus",
"dashboard": { "show": true, "tab": "base", "section": "goals", "render": "kpi" } }O painel guarda apenas os valores mais recentes, como no DAU. Cada consulta bem-sucedida os substitui, e uma meta que não está mais na resposta desaparece da lista.
Várias superfícies
Uma fonte de métricas personalizadas pertence a uma superfície do seu serviço, e o painel guarda os números por superfície. A lista de projetos mostra a soma; a página do projeto mostra cada superfície separadamente.
- Várias fontes de uma superfície contam como a última consultada. Fontes sem superfície formam mais um grupo.
- Só contam as superfícies de produção. Uma superfície em ambiente de staging, teste ou desenvolvimento fica de fora.
- Uma superfície cujos últimos números estão mais de 48 horas atrás dos mais recentes fica de fora.
- Cada janela é somada entre as superfícies que a enviam, e as metas são somadas por id, então uma meta deve significar o mesmo em todas as superfícies.
- A soma é um limite superior: uma pessoa que usa duas superfícies é contada duas vezes.
Extensões do Chrome
Uma extensão segue a mesma convenção. Quando o seu feed envia dau e metas na aba base, a lista de projetos mostra-os numa linha a mais abaixo dos contadores de instalações, desinstalações, feedback e reinstalações, no mesmo formato da linha Hoje de um serviço web.
- O WAU e o MAU de uma extensão não aparecem na lista; a sua coluna Usuários é o número de usuários da Chrome Web Store.
- Conte o DAU e as metas do dia UTC atual: os contadores da mesma linha usam dias UTC.
- Várias fontes de uma extensão somam-se pelas regras acima. Não há variação em relação a ontem.
- Só o autor vê a linha. O botão de atualizar da linha também consulta as métricas personalizadas.
5.4. Limites (warn e crit)
Uma métrica numérica pode trazer thresholds. Nesse caso, o painel calcula sozinho a severidade da métrica, a cada consulta e a cada recebimento de heartbeat, e mostra os limites ao lado do valor: 41 (threshold 20/100), amarelo entre warn e crit, vermelho a partir de crit. A severidade que o seu serviço envia para essa métrica é substituída pela calculada.
| Campo | Significado |
|---|---|
direction | above: pior quando mais alto (fila, CPU). below: pior quando mais baixo (disco livre, dias restantes). |
warn, crit | Números. Com above, um valor igual ou acima de warn é amarelo e igual ou acima de crit é vermelho; below faz o inverso. Com above, warn não pode passar de crit; com below, não pode ficar abaixo. |
unit | Rótulo opcional de até 16 bytes para a legenda: threshold 80/95%. |
recover | Histerese opcional: uma métrica que estava em warn ou crit só volta para ok quando o valor passa de recover; entre os dois ela continua em warn. |
{
"id": "queue",
"kind": "number",
"label": "Mail queue",
"value": 41,
"thresholds": { "direction": "above", "warn": 20, "crit": 100, "recover": 15 },
"dashboard": { "show": true, "tab": "backend", "section": "mail", "order": 1, "render": "kpi" },
"telegram": { "show": true, "line": "mail", "pos": 10, "format": "queue {value}", "when": "severity>=warn" }
}- Os limites comparam
value; emdurationcomparamseconds(senãominutes), embytescomparambytes(senãokb). Kinds não numéricos comostatus,listoutablenão podem ter limites. - Sem número desta vez significa
idle: cinza, e não conta para nenhuma luz. - Um bloco malformado é recusado em um relatório de heartbeat (
422 invalid_report). Em uma resposta consultada, ele é descartado, a métrica é desenhada como foi enviada e o motivo aparece como último erro da fonte. - Em um relatório de heartbeat, a luz do heartbeat nunca fica abaixo de um limite ultrapassado (veja o contrato de monitoramento).
Consulta agendada. Em um serviço web, você pode ativar a consulta agendada de uma fonte nas configurações dela. O painel passa a consultar o endpoint em um intervalo que vai do mínimo do seu plano (5 minutos no Trial e no Indie, 1 minuto no Studio e no Fleet) até 24 horas, com a mesma requisição de qualquer consulta: um User-Agent honesto e no máximo uma requisição por minuto. Ela ocupa uma unidade de monitoramento, por isso não está disponível no Free. Um limite ultrapassado abre um incidente para essa métrica e avisa você; uma nova ultrapassagem não avisa de novo. O incidente se fecha depois de uma consulta sem ultrapassagem se o intervalo for de 1 hora ou mais, e depois de duas consultas seguidas sem ultrapassagem se for menor. Uma consulta com falha não muda nada e mostra o motivo como último erro. Até 20 métricas com limites por fonte geram incidentes.
6. A paleta pertence ao painel
As cores de gráficos e séries são atribuídas pelo painel a partir dos tokens da marca, não pela fonte. options.series[] carrega apenas a identidade ({key,label,order}); um campo color no payload é apenas indicativo e ignorado. Chaves semânticas são mapeadas para tokens fixos (paid para destaque, free para neutro suave, failed/error para perigo, ready/success para sucesso); as demais chaves recebem a paleta categórica da marca por posição. Preenchimentos e linhas das séries mantêm contraste de pelo menos 3:1 nos dois temas (WCAG 1.4.11), e texto e rótulos de pelo menos 4,5:1. Isso protege a marca de cores hex externas arbitrárias.
7. Templates do Telegram
telegram.header é o cabeçalho da mensagem; telegram.lines[] são linhas lógicas ({ id, order, prefix, when }). O compositor reúne todas as métricas com um dado telegram.line, filtra por when, ordena por pos, renderiza cada uma pelo template format e as une com o sep de cada métrica. O prefix da linha é impresso primeiro. Tudo é texto simples.
| Placeholder | Saída |
|---|---|
{value} | Conforme o tipo (número 1,200; percentual 99%) |
{value:k} | 1.2k / 3.0M (compacto) |
{delta:+} | +72 / -3 |
{value:pct} | 99% |
{seconds:dur} | 28s / 5m 47s |
{minutes:age} | 45m / 2h 30m / never |
{bytes:size} | ~522KB / 1.4MB |
{value}★ ({count}) | 5.0★ (1) |
{sev} | Emoji de severidade: 🟢 ok, 🟡 warn, 🔴 crit, ⚪ idle |
{threshold} | O limite da métrica: threshold 20/100. Sem ele no formato, uma métrica com limites recebe (threshold 20/100) no final. |
{trend} | ↗ / ↘ (±5pp vs 7d) |
8. Condições de visibilidade (when)
when controla se um campo de métrica (ou uma linha inteira do Telegram) é exibido. Valores:
| when | Mostra o campo |
|---|---|
always | Sempre (padrão). |
nonzero | Somente quando o valor não for 0/vazio. |
severity>=warn | Somente quando a severidade é warn ou crit. |
changed | Somente quando mudou desde o snapshot anterior. |
Valores desconhecidos de when se comportam como always (compatível com versões futuras).
9. Exemplo de payload
{
"schema_version": "2.0",
"generated_at": "2026-07-02T23:17:30Z",
"tabs": [
{ "id": "conv", "title": "Conversions", "order": 1, "icon": "trending-up" }
],
"sections": [
{ "tab": "conv", "id": "by_hour", "title": "By hour", "order": 0 }
],
"telegram": {
"header": "{name} • {cws.users} • {cws.rating}★ ({cws.rating_count})",
"lines": [
{ "id": "base", "order": 0 },
{ "id": "conv", "order": 1, "prefix": "{sev} Conv " }
]
},
"metrics": [
{
"id": "conv_today",
"kind": "number",
"label": "Conversions today",
"value": 254,
"severity": "ok",
"shareable": true,
"dashboard": {
"show": true,
"tab": "conv", "section": "by_hour", "order": 10,
"name": "Today", "render": "kpi", "options": {}
},
"telegram": {
"show": true,
"line": "conv", "pos": 20,
"format": "today {value}", "sep": " • ", "when": "always"
}
}
]
}10. O contrato legível por máquina
A descrição formal e importável deste endpoint é o arquivo OpenAPI 3.1. Ele traz a mesma versão de contrato (2.1.0) e é o único artefato canônico para máquinas, compartilhado entre os idiomas.