Главная/Документация/Кастом-метрики
Контракт кастом-метрик
Кастом-метрики позволяют сервису вашего расширения слать любые важные для него числа (конверсии, выручку, здоровье бэкенда, воронки) в ту же панель, которая уже отслеживает установки и здоровье Chrome Web Store. Контракт metadata-driven и dual-surface: каждая метрика объявляет, где и как она появляется и на дашборде, и в Telegram-статусе, поэтому панель остаётся generic-рендерером без продуктового кода.
1. Как это работает (dual-surface)
Ваш сервис отдаёт один эндпоинт метрик. Ext Ops Panel – клиент: он пуллит этот эндпоинт (никогда наоборот) и рендерит ответ. Ответ управляет двумя поверхностями сразу:
- Табы дашборда в Detail-виде расширения: табы, секции и типы рендера по метрикам.
- Telegram-статус: логические строки, собранные из тех же метрик, с фильтрацией и форматированием по метрике.
Панель мержит ваши метрики с базовыми first-party метриками (установки / удаления / возвраты / фидбек / Chrome Web Store / здоровье), которые живут в зарезервированном табе base и строке Telegram.
2. Эндпоинт и авторизация
Панель вызывает ваш эндпоинт методом GET и ждёт payload v2 в ответе 200. Два режима пула используют одинаковую структуру:
- On-demand: срабатывает, когда пользователь открывает Detail. Лимит – не чаще 1 запроса в минуту на источник, на всех тарифах включая Free.
- Monitoring: по расписанию на тарифах с мониторингом, компонует Telegram-статус и edge-алерты.
Авторизация – одна из none, bearer или api_key_header. Секреты хранятся зашифрованными at rest на бэкенде панели и никогда не передаются дашборд-расширению. Если пул падает, панель деградирует gracefully (last snapshot или скрыто), не ломая базовые метрики.
3. Структура payload
Верхнеуровневый payload несёт раскладку (табы, секции, строки Telegram) и массив метрик.
| Поле | Тип | Заметки |
|---|---|---|
schema_version | string | Обязательно. Должно быть "2.0". |
generated_at | string (date-time) | Когда payload собран (свежесть / idle-детект). |
tabs | Tab[] | Табы дашборда. См. раздел 4. |
sections | Section[] | Группы метрик внутри таба (tab, id, title, order). |
telegram | TelegramMeta | Шаблон header + lines[]. См. раздел 7. |
metrics | Metric[] | Сами метрики. См. разделы 5 и 6. |
4. Табы и иконки табов
Tab – это { id, title, order, icon? }. Зарезервированный таб
base – наш
(icon="layout-dashboard"); ваш сервис добавляет остальные. Когда
табов много, активный таб показывает иконку + название, а остальные схлопнуты
до иконки (название остаётся в tooltip и aria-label).
Два id табов зарезервированы за панелью: base (вкладка Basic) и
monitoring (встроенная вкладка Monitoring, которую заполняют отчёты heartbeat, см.
контракт мониторинга). Объявлять их в tabs не нужно:
объявление игнорируется, заголовок, иконку и порядок задаёт панель. Метрики адресуются в них
через dashboard.tab как обычно.
Значение icon – одно из двух:
- Имя иконки Lucide в kebab-case ASCII (матчит
^[a-z][a-z0-9-]*$), напримерtrending-up. Корректное по форме, но неподдержанное имя деградирует к дефолтной иконке (square), так что бар не ломается. - Один эмодзи, например 💰 или 🚀, рендерится как текст.
Панель везёт каталог из 60 имён Lucide (держим MV3-бандл лёгким). Тот же каталог служит иконкам табов и пиктограммам целей (раздел 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. Метрика: значение + два блока размещения
Метрика несёт типизированное значение плюс два независимых блока:
dashboard (где и как в расширении) и telegram (где и
как в сообщении). Любой можно опустить или скрыть через show:false,
так что метрика может жить только на одной поверхности.
| Поле | Заметки |
|---|---|
id | Стабильный id метрики, уникальный в payload. |
kind | Тип данных (раздел 5.1). Определяет, какие поля значения применяются. |
label | Каноническое человекочитаемое имя. |
value и поля по kind | Поля payload зависят от kind (раздел 5.1). |
severity | Одно из ok, info, warn, crit (маппится в цвет / эмодзи). |
shareable | Необяз. boolean, default true. При false метрика owner-only: на расшаренном read-only проекте отсекается на сервере и не доходит до наблюдателя / public (для финансовых метрик). Управляет доступом, не размещением. |
dashboard | { show, tab, section, order, name, render, options } (раздел 5.2). |
telegram | { show, line, pos, format, sep, when } (разделы 6 и 8). |
5.1. Kinds (тип данных + payload)
Kinds – открытое множество: неизвестный kind рендерер игнорирует (forward-compat).
| kind | Поля payload | Пример использования |
|---|---|---|
number | value (+ опц. delta, unit) | счётчики установок / фидбека |
delta | value, delta (рендер 1200 (+72)) | total (+today) |
percent | value (0 до 100) | engine success rate |
duration | seconds или minutes | p50/p95/avg, свежесть |
bytes | bytes или kb | размер вывода (~522KB) |
rating | value, count | 5.0★ (1) |
money | value, currency, опц. delta | MRR, расход |
status | state + severity | health-светофор |
series | points:[{date,count}], опц. мульти series:[{name,points}], стеком при stack: true | daily installs, active users |
intraday | series:[{name,intraday:[{minute,value}]}], опц. step_min (по умолчанию 15), tz | сегодня по 15-минутным бакетам |
hour_histogram | { weekdays:[...], start, end, tz } | конверсии по часам |
funnel | steps:[{key,label,value}] | pricing до checkout |
breakdown | items:[{label,value,unit?,sub?/money?,country?,parts?}], опционально ranges:{today,week,month,all} | подписки по плану, by engine, by surface; визиты по источникам или странам |
list | items:[{text,count?,severity?}] на верхнем уровне метрики, не внутри value | последние ошибки |
table | items:[{…}], один объект на строку; колонки из dashboard.options.columns, без них ключи первой строки | очереди, последние отправки |
text | value (string) | произвольный текст |
Метрика 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. Render (как показать на дашборде)
dashboard.render выбирает визуал (обычно совпадает с kind,
но может отличаться, напр. показать percent как badge).
Тоже открытое множество.
| render | Вид | Опции (dashboard.options) |
|---|---|---|
kpi | Карточка: имя + крупное значение + дельта; со stack – части одним рядом под значением | tone, stack |
badge | Статус-пилюля / пиктограмма по severity | – |
chart | Линейный график (Month/Lifetime, rolling-7 avg, hover); при stack: true – стековые столбцы по дням со строкой Total | series, defaultMode, stack |
intraday | Сегодня по бакетам (96 за UTC-сутки): стек-бары или сглаженные линии, ось времени HH:MM, hover-ридаут по всем сериям | mode (bars | lines, по умолчанию lines) |
hour_histogram | Почасовые бары, группировка Weekdays / Weekend / By day of week | series |
funnel | Ступени с конверсией между ними | – |
breakdown | Горизонтальные категорийные бары со значением + долей от суммы. С ranges – переключатель сегодня / 7 дней / 30 дней / всё время; с map: countries – карта мира над списком; со stack – бары из переключаемых частей | items, defaultRange, map, stack |
table | Таблица (напр. фидбек) | columns |
text | Абзац / inline | – |
status | Состояние-светофор по severity | – |
list | Список элементов с опц. count / severity | – |
intraday vs chart. Ось chart –
дневная (points[].date = YYYY-MM-DD), а режим Month
режет её до последних 30 точек: 96 внутридневных бакетов туда не помещаются.
intraday – это наш первопартийный Today-график, вынесенный в контракт: та
же договорённость, где minute – минуты от начала UTC-суток
(0, 15, …, 1425), но серии и данные объявляет источник. Присылайте только непустые
бакеты – остальное уплотняет панель.
Пустой бакет – это null, а не 0. Для среднего
(например, среднее время конвертации) ноль был бы неправдой, поэтому линия
рвётся, а не падает в пол. Пропущенный бакет и value: null
эквивалентны: линия разрывается, бара нет, в ридауте «–».
Стек: итог и части на одном графике и в одном списке. Если метрика складывается из частей (например, просмотры двух сайтов), присылайте итог и части ОДНОЙ метрикой: панель рисует части друг над другом, а легенда включает и выключает каждую, без отдельных вкладок и выпадающих фильтров. Оба расширения аддитивны: рендерер, который их не знает, рисует ту же метрику как раньше.
График: kind: "series" и dashboard.options.stack: true.
Серии складываются в порядке массива, первая – нижний слой. Панель рисует столбец на каждый
день (в lifetime – на каждое 7-дневное окно) с сегментом на каждую видимую серию, поэтому верх
столбца – точная сумма видимых серий; день, где все они нулевые, остаётся без столбца. Клик по
легенде скрывает слой (последний видимый остаётся); остальные перерисовываются от нуля, ось
значений следует за видимой суммой. При наведении на столбец или переходе к нему стрелками
ридаут показывает каждый видимый слой и строку Total. Lifetime складывает rolling-7 среднее каждой серии, с маркером ~.
Любое другое значение stack оставляет наложенные линии.
Список: kind: "breakdown", в dashboard.options.stack –
упорядоченный список частей [{key, label}], у каждого элемента в items
и в каждом окне ranges – parts: {"key": число}. value
обязан быть равен сумме всех частей. Панель показывает по чипу на часть, чип переключает её для
всей карточки во всех окнах. Строка показывает сумму видимых частей, строки с нулём скрываются,
список пересортировывается по этой сумме, а бар делится на сегменты по видимым частям. С
map: "countries" круги на карте следуют видимым суммам. Элемент без
parts считается целиком при любых тогглах.
{ "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" } ] } } }
Плитка: kpi-плитка вида number или delta
принимает тот же список dashboard.options.stack и parts: {"key": число}
у самой метрики. Крупное значение остаётся value, оно обязано быть равно сумме всех
частей. Под ним панель печатает части одним рядом в порядке стека (на узкой плитке ряд переносится), цветом, который то же имя
получает на стековом графике вкладки; часть с нулём приглушена. Переключателей у плитки нет.
Заголовки в списке проектов и Telegram по-прежнему читают 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. Список проектов: пользователи и цели с базовой вкладки
Список проектов показывает несколько чисел веб-сервиса, не открывая его. Панель берёт их только
с вкладки base и только у метрики с числовым value.
Выигрывает первое совпадение.
| Метрика | Как панель её находит | Где показывается |
|---|---|---|
| DAU, WAU, MAU | id, label или имя на дашборде ровно dau, wau или mau (регистр не важен). | WAU и MAU в колонке «Пользователи», DAU в «Сегодня». |
| Цели 1–3 | id ровно goal_1, goal_2 или goal_3. | «Сегодня», в одной строке с DAU: пиктограмма и число. |
Цель это целевое действие ваших пользователей: регистрации, покупки, отправленные формы. Окно
числа выбираете вы, панель показывает его как есть. Рекомендуем считать за сегодня и называть
окно в подписи (например, «Регистрации сегодня»). Подпись идёт в тултип, до 80 символов. Поле
icon принимает имя из каталога раздела 4; эмодзи у целей не принимаются. Без
пиктограммы или с неизвестным именем панель рисует target. Ноль показывается
приглушённым прочерком.
{ "id": "goal_1", "kind": "number", "label": "Sign-ups today", "value": 12, "icon": "user-plus",
"dashboard": { "show": true, "tab": "base", "section": "goals", "render": "kpi" } }Панель хранит только последние значения, как у DAU. Каждый успешный опрос их перезаписывает, а цель, пропавшая из ответа, исчезает из списка.
Несколько поверхностей
Источник кастомных метрик принадлежит одной поверхности вашего сервиса, и панель хранит числа по поверхностям. Список проектов показывает их сумму, страница проекта показывает каждую поверхность отдельно.
- Несколько источников одной поверхности считаются как последний опрошенный. Источники без поверхности образуют ещё одну группу.
- Считаются только производственные поверхности. Поверхность в среде staging, test или development не входит.
- Поверхность, чьи последние числа отстают от самых свежих больше чем на 48 часов, не входит.
- Каждое окно складывается по тем поверхностям, которые его шлют, а цели складываются по id, поэтому цель должна значить одно и то же на всех поверхностях.
- Сумма это верхняя граница: человек, который пользуется двумя поверхностями, посчитан дважды.
Расширения Chrome
Для расширения действует то же соглашение. Если его фид шлёт dau и цели на вкладке base, список проектов показывает их отдельной строкой под счётчиками установок, удалений, фидбека и возвратов, в том же формате, что строка «Сегодня» веб-сервиса.
- WAU и MAU расширения в списке не показываются: его колонка «Пользователи» показывает пользователей из Chrome Web Store.
- Считайте DAU и цели за текущие UTC-сутки: счётчики в той же строке считаются по UTC.
- Несколько источников одного расширения суммируются по правилам выше. Дельты к вчера нет.
- Строку видит только автор. Кнопка обновления в строке опрашивает и кастомные метрики.
5.4. Пороги (warn и crit)
Числовая метрика может нести thresholds. Тогда severity метрики считает
сама панель, при каждом опросе и на каждом приёме отчёта heartbeat, и рисует порог
рядом со значением: 41 (порог 20/100), жёлтым между warn и crit,
красным за crit. Severity, которую сервис прислал для такой метрики, заменяется
посчитанной.
| Поле | Смысл |
|---|---|
direction | above: хуже, когда выше (очередь, CPU). below: хуже, когда ниже (свободный диск, дни до конца). |
warn, crit | Числа. Для above значение от warn жёлтое, от crit красное; below зеркально. Для above warn не больше crit, для below не меньше. |
unit | Необязательная подпись до 16 байт: порог 80/95%. |
recover | Необязательный гистерезис: метрика, которая была warn или crit, возвращается в ok, только когда значение ушло за recover; между ними она остаётся 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" }
}- Порог сравнивается с
value; уdurationсseconds(иначеminutes), уbytesсbytes(иначеkb). У нечисловых kinds, напримерstatus,listилиtable, порогов не бывает. - Нет числа в этот раз:
idle, серый, в свет не входит. - Кривой блок в отчёте heartbeat отклоняется (
422 invalid_report). В ответе опроса он отбрасывается, метрика рисуется как прислана, а причина видна в последней ошибке источника. - В отчёте heartbeat свет heartbeat не бывает ниже пробитого порога (см. контракт мониторинга).
Плановый опрос. У веб-сервиса в настройках источника можно включить плановый опрос. Тогда панель опрашивает эндпоинт с интервалом от пола вашего тарифа (5 минут на триале и Indie, 1 минута на Studio и Fleet) до 24 часов тем же запросом, что и любой опрос: честный User-Agent и не больше одного запроса в минуту. Опрос занимает один unit мониторинга, поэтому на Free его нет. Пробитый порог открывает инцидент по этой метрике и присылает уведомление; повторный пробой уведомления не повторяет. Инцидент закрывается после одного опроса без пробоя, если интервал не меньше часа, и после двух опросов подряд без пробоя, если интервал короче. Неудачный опрос ничего не меняет и показывает причину в последней ошибке. Инциденты дают до 20 метрик с порогами на источник.
6. Палитрой владеет панель
Цвета графиков и серий назначает панель из бренд-токенов, а не
источник. options.series[] несёт только идентичность
({key,label,order}); поле color в payload – advisory и
игнорируется. Семантические ключи маппятся на фикс-токены
(paid в accent, free в приглушённый нейтрал,
failed/error в danger,
ready/success в success); прочие ключи берут
категориальную бренд-палитру позиционно. Заливки и линии серий держат контраст не ниже
3:1 в обеих темах (WCAG 1.4.11), текст и подписи не ниже 4.5:1. Это защищает бренд
от произвольного внешнего hex.
7. Шаблоны Telegram
telegram.header – заголовок сообщения;
telegram.lines[] – логические строки
({ id, order, prefix, when }). Композер собирает все метрики с
данной telegram.line, фильтрует по when, сортирует по
pos, рендерит каждую по шаблону format и склеивает
через sep метрики. Сначала печатается prefix строки.
Всё – plain text.
| Плейсхолдер | Выход |
|---|---|
{value} | По kind (number 1,200; percent 99%) |
{value:k} | 1.2k / 3.0M (компактно) |
{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} | Эмодзи по severity: 🟢 ok, 🟡 warn, 🔴 crit, ⚪ idle |
{threshold} | Порог метрики: threshold 20/100 (строка Telegram по-английски). Без него в format у метрики с порогами в конце дописывается (threshold 20/100). |
{trend} | ↗ / ↘ (±5pp к 7d) |
8. Условия показа (when)
when управляет тем, показывается ли поле метрики (или целая строка
Telegram). Значения:
| when | Показывает поле |
|---|---|
always | Всегда (по умолчанию). |
nonzero | Только когда значение не 0 / не пусто. |
severity>=warn | Только когда severity warn или crit. |
changed | Только когда изменилось с прошлого снапшота. |
Неизвестные значения when ведут себя как always (forward-compat).
9. Пример 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. Машиночитаемый контракт
Формальное, импортируемое описание этого эндпоинта – файл OpenAPI 3.1. Он несёт ту же версию контракта (2.1.0) и является единственным каноническим машинным артефактом, общим для всех языков.