Ext Ops Panel
Connect

Home/Docs/Custom metrics

Custom metrics contract

Contract version 2.0.0 · payload schema_version 2.0

Custom metrics let your extension's own service push whatever numbers matter for it (conversions, revenue, backend health, funnels) into the same panel that already tracks installs and Chrome Web Store health. The contract is metadata-driven and dual-surface: every metric declares where and how it appears both on the dashboard and in the Telegram status, so the panel stays a generic renderer with no product-specific code.

↓ Download OpenAPI 3.1 (YAML) /custom-metrics-openapi.yaml · info.version 2.0.0

1. How it works (dual surface)

Your service exposes one metrics endpoint. Ext Ops Panel is the client: it pulls that endpoint (never the reverse) and renders the response. The response drives two surfaces at once:

The panel merges your metrics with the first-party base metrics (installs / uninstalls / reinstalls / feedback / Chrome Web Store / health), which live in the reserved base tab and Telegram line.

2. The endpoint and auth

The panel calls your endpoint with GET and expects the v2 payload as the 200 response. Two pull modes share the same shape:

Authentication is one of none, bearer or api_key_header. Secrets are stored encrypted at rest on the panel backend and are never handed to the dashboard extension. If a pull fails, the panel degrades gracefully (last snapshot or hidden) without breaking the base metrics.

3. Payload structure

The top-level payload carries the layout (tabs, sections, Telegram lines) and the metrics array.

FieldTypeNotes
schema_versionstringRequired. Must be "2.0".
generated_atstring (date-time)When the payload was composed (freshness / idle detection).
tabsTab[]Dashboard tabs. See section 4.
sectionsSection[]Groups of metrics inside a tab (tab, id, title, order).
telegramTelegramMetaheader template + lines[]. See section 7.
metricsMetric[]The metrics themselves. See sections 5 and 6.

4. Tabs and tab icons

A Tab is { id, title, order, icon? }. The reserved base tab is the panel's own (icon="layout-dashboard"); your service adds the rest. When many tabs are open, the active tab shows icon + title and the others collapse to the icon only (title stays in the tooltip and aria-label).

The icon value is one of two things:

The panel ships a curated subset of 37 Lucide names (kept lean for the MV3 bundle):

5. Metric: value + two placement blocks

A metric carries a typed value plus two independent blocks: dashboard (where and how in the extension) and telegram (where and how in the message). Either can be omitted or hidden via show:false, so a metric can live on one surface only.

FieldNotes
idStable metric id, unique in the payload.
kindData type (section 5.1). Selects which value fields apply.
labelCanonical human label.
value and kind fieldsPayload fields depend on kind (section 5.1).
severityOne of ok, info, warn, crit (maps to color / emoji).
shareableOptional boolean, default true. When false the metric is owner-only: on a project shared read-only it is stripped server-side and never reaches an observer / public view (use for financial metrics). Governs access, not placement.
dashboard{ show, tab, section, order, name, render, options } (section 5.2).
telegram{ show, line, pos, format, sep, when } (sections 6 and 8).

5.1. Kinds (data type + payload)

Kinds are an open set: an unknown kind is ignored by the renderer (forward compatible).

kindPayload fieldsExample use
numbervalue (+ optional delta, unit)install / feedback counts
deltavalue, delta (renders 1200 (+72))total (+today)
percentvalue (0 to 100)engine success rate
durationseconds or minutesp50/p95/avg, freshness
bytesbytes or kboutput size (~522KB)
ratingvalue, count5.0★ (1)
moneyvalue, currency, optional deltaMRR, spend
statusstate + severityhealth traffic light
seriespoints:[{date,count}], optional multi series:[{name,points}]daily installs, active users
intradayseries:[{name,intraday:[{minute,value}]}], optional step_min (default 15), tztoday by 15-minute buckets
hour_histogram{ weekdays:[...], start, end, tz }conversions by hour
funnelsteps:[{key,label,value}]pricing to checkout
breakdownitems:[{label,value,unit?,sub?/money?}]subs by plan, by engine, by surface
listitems:[{text,count?,severity?}]recent errors
textvalue (string)free text

5.2. Render (how to show on the dashboard)

dashboard.render picks the visual (usually matches kind, but may differ, e.g. show a percent as a badge). Also an open set.

renderLookOptions (dashboard.options)
kpiCard: name + large value + deltatone
badgeStatus pill / pictogram by severity
chartLine chart (Month/Lifetime, rolling-7 avg, hover)series, defaultMode
intradayToday by buckets (96 per UTC day): stacked bars or smoothed lines, time axis HH:MM, hover readout across seriesmode (bars | lines, default lines)
hour_histogramHourly bars, grouped Weekdays / Weekend / By day of weekseries
funnelSteps with conversion between them
breakdownHorizontal categorical bars with value + share of totalitems
tableTable (e.g. feedback)columns
textParagraph / inline
statusTraffic-light state by severity
listList of items with optional count / severity

intraday vs chart. The chart axis is daily (points[].date = YYYY-MM-DD) and its Month view slices to the last 30 points, so today's 96 buckets do not fit on it. intraday is the first-party Today chart exposed as a contract kind: the same convention where minute counts minutes from the start of the UTC day (0, 15, …, 1425), but the series and data are declared by the source. Send only non-empty buckets, the panel densifies the rest.

An empty bucket is null, not 0. For an average (e.g. mean conversion time) a zero would be a lie, so the line breaks instead of dropping to the floor. A missing bucket and value: null are equivalent: the line breaks, no bar is drawn, and the hover readout shows a dash.

6. The panel owns the palette

Chart and series colors are assigned by the panel from brand tokens, not by the source. options.series[] carries identity only ({key,label,order}); a color field in the payload is advisory and ignored. Semantic keys map to fixed tokens (paid to accent, free to muted neutral, failed/error to danger, ready/success to success); other keys take the categorical brand palette positionally. Contrast stays at least 4.5:1 in both themes. This protects the brand from arbitrary external hex.

7. Telegram templates

telegram.header is the message header; telegram.lines[] are logical lines ({ id, order, prefix, when }). The composer gathers every metric with a given telegram.line, filters by when, sorts by pos, renders each via its format template, and joins them with each metric's sep. The line prefix is printed first. Everything is plain text.

PlaceholderOutput
{value}By kind (number 1,200; percent 99%)
{value:k}1.2k / 3.0M (compact)
{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 emoji: 🟢 ok, 🟡 warn, 🔴 crit, ⚪ idle
{trend} / (±5pp vs 7d)

8. Visibility conditions (when)

when controls whether a metric field (or a whole Telegram line) shows. Values:

whenShows the field
alwaysAlways (default).
nonzeroOnly when the value is not 0 / empty.
severity>=warnOnly when severity is warn or crit.
changedOnly when it changed since the previous snapshot.

Unknown when values behave as always (forward compatible).

9. Example 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"
      }
    }
  ]
}
Only native v2 is supported. Your service returns metrics plus a ready layout per this contract; there are no legacy-format connectors. Kinds and render types come from the declared sets above; unknown ones are ignored so a newer source never breaks an older panel build.

10. The machine-readable contract

The formal, importable description of this endpoint is the OpenAPI 3.1 file. It carries the same contract version (2.0.0) and is the single canonical machine artifact, shared across languages.

↓ Download OpenAPI 3.1 (YAML) /custom-metrics-openapi.yaml