Metrics and connectors

A worked example across every layer. Three vendors, one metric pipeline, three views, and the rule that decides what we store.

1 min read Updated Aug 12, 2026

Metrics and connectors

A worked example, not a new layer.

Three asks arrive together: a revenue dashboard from Stripe, a product dashboard from PostHog, and a system errors dashboard from Sentry. Each one must become rows in tables that already exist. If any of them needs new machinery, the architecture has failed its own test.

METRICS AND CONNECTORS — three vendors, one pipeline, one table, three views
METRICS AND CONNECTORS — three vendors, one pipeline, one table, three views
VENDORS — we run none of this
VENDORS — we run none of this
Stripe
finance
Stripefinance
PostHog
product
PostHogproduct
Sentry
engineering
Sentryengineering
INGEST — one shape, three rows · no model in this path
INGEST — one shape, three rows · no model in thispath
Trigger
schedule · webhook · both
Triggerschedule · webhook · both
Vendor tool and adapter
auth · paging · rate limit · retry
Vendor tool and adapterauth · paging · rate limit · retry
Normalize
keep one number per period
Normalizekeep one number per period
metrics.upsert
idempotent on key · grain · at
metrics.upsertidempotent on key · grain · at
COMPANY STATE
COMPANY STATE
metrics
one number, one key, one period
every source, one shape
metricsone number, one key, one periodevery source, one shape
external_refs
our record knows its vendor id
we map, we never mirror
external_refsour record knows its vendor idwe map, we never mirror
CONSUMERS
CONSUMERS
Company views
Finance · Product · Engineering
one chart component
Company viewsFinance · Product · Engineeringone chart component
Agents
read the metric in a brief
Agentsread the metric in a brief
Threshold triggers
a number starts a run
Threshold triggersa number starts a run
Vendor detail · live read
one invoice, one event
Vendor detail · live readone invoice, one event
fetch
fetch
upsert
upsert
one query
one query
drill-down
drill-down
WHAT WE KEEP — a number over time becomes a metrics row · an entity we already own becomes an external_refs mapping
a record detail is never stored, and it is read live — we store what the company is operated on, and we link out for the rest
WHAT WE KEEP — a number over time becomes a metrics row · an entity we already own becomes an external_refs mappinga record detail is never stored, and it is read live — we store what the company is operated on, and we link out for the rest
LEGEND
LEGEND
costs an LLM call
costs an LLM call
deterministic plumbing
deterministic plumbing
what we store
what we store
ordinary component · dashed is not ours
ordinary component · dashed is not ours
a zone or a band
a zone or a band
Text is not SVG - cannot display
METRICS AND CONNECTORS — three vendors, one pipeline, one table, three views
METRICS AND CONNECTORS — three vendors, one pipeline, one table, three views
VENDORS — we run none of this
VENDORS — we run none of this
Stripe
finance
Stripefinance
PostHog
product
PostHogproduct
Sentry
engineering
Sentryengineering
INGEST — one shape, three rows · no model in this path
INGEST — one shape, three rows · no model in thispath
Trigger
schedule · webhook · both
Triggerschedule · webhook · both
Vendor tool and adapter
auth · paging · rate limit · retry
Vendor tool and adapterauth · paging · rate limit · retry
Normalize
keep one number per period
Normalizekeep one number per period
metrics.upsert
idempotent on key · grain · at
metrics.upsertidempotent on key · grain · at
COMPANY STATE
COMPANY STATE
metrics
one number, one key, one period
every source, one shape
metricsone number, one key, one periodevery source, one shape
external_refs
our record knows its vendor id
we map, we never mirror
external_refsour record knows its vendor idwe map, we never mirror
CONSUMERS
CONSUMERS
Company views
Finance · Product · Engineering
one chart component
Company viewsFinance · Product · Engineeringone chart component
Agents
read the metric in a brief
Agentsread the metric in a brief
Threshold triggers
a number starts a run
Threshold triggersa number starts a run
Vendor detail · live read
one invoice, one event
Vendor detail · live readone invoice, one event
fetch
fetch
upsert
upsert
one query
one query
drill-down
drill-down
WHAT WE KEEP — a number over time becomes a metrics row · an entity we already own becomes an external_refs mapping
a record detail is never stored, and it is read live — we store what the company is operated on, and we link out for the rest
WHAT WE KEEP — a number over time becomes a metrics row · an entity we already own becomes an external_refs mappinga record detail is never stored, and it is read live — we store what the company is operated on, and we link out for the rest
LEGEND
LEGEND
costs an LLM call
costs an LLM call
deterministic plumbing
deterministic plumbing
what we store
what we store
ordinary component · dashed is not ours
ordinary component · dashed is not ours
a zone or a band
a zone or a band
Text is not SVG - cannot display
Three vendors on the left, one ingest shape, one metrics table at the core, and three kinds of consumer on the right. The footer states what we keep and what we never store.

Do not build three dashboards

Build one metric pipeline, three source adapters, and three filtered views.

Three dashboards means three ingest shapes, three storage decisions and three page types, and the fourth source costs the same again. One pipeline means the fourth source is a connections row plus a workflows row.

What we keep

This is the rule that decides everything else. There are three answers, and no fourth.

The dataWhere it landsWhy
A number over time — MRR, DAU, error rateA metrics row in company stateAn agent reasons over it, and a trigger fires on it
An entity we already own — a Stripe customerAn external_refs mapping to the CRM rowWe map a vendor id. We never mirror a vendor's object model
A record detail — one invoice, one error eventNothing is stored. A read tool fetches it at renderIt is unbounded, the vendor owns it, and it is a drill-down

So each dashboard is stored numbers plus a live drill-down.

We are not rebuilding three analytics products. We store the few numbers the company is operated on, so that they sit in one view and an agent can act on them. Deep analysis stays in the vendor product, behind a link. That boundary is the main scope risk in this feature, and it is easy to cross by accident.

The pipeline

One shape. Every source uses it.

TEXT
trigger  →  vendor tool  →  normalize  →  metrics.upsert  →  metrics  →  view
  L4           L2              L4              L2              L3         L6
StageLayerWhat it is
TriggerL4A triggers row: a schedule, a webhook, or both.
Vendor toolL2A tools row over an adapter, and a connections row for the credential.
NormalizeL4A workflow step. It discards the payload and keeps one number per period.
metrics.upsertL2An internal tool. A run writes to company state only through a tool.
metricsL3One table, one shape, every source.
ViewL6The Company view, filtered by domain. Not a new surface.

No model runs anywhere in this pipeline. Ingest is deterministic. An agent reads the result later; it does not do the ingest.

The three sources

SourceDomainPushStored as metricsRead live
StripeFinanceWebhook, plus a daily reconcileMRR, new customers, churn rate, failed payment countOne invoice, one subscription
PostHogProductNone. Schedule onlyDAU and WAU, activation rate, retention, feature adoptionOne funnel, a session replay
SentryEngineeringWebhook, plus a reconcile every 15 minutesError rate, new issues, regressions, p95 latencyOne issue, one event

A webhook always has a schedule beside it. Delivery is at-least-once and a message can be missed. The schedule re-reads the same window and the upsert makes the repeat harmless. A webhook on its own is a dashboard that is quietly wrong.

Why we store the number

A live proxy renders the same chart, and it costs less today. This is what it cannot do.

TEXT
a new metrics row lands:  error_rate = 4.1%   (yesterday it was 0.4%)
        │
        ▼
a trigger is watching that metric
        │
        ▼
an agent starts. Its brief holds the metric, the runbook and the last incident
        │
        ▼
it proposes: open a P1 task, and page the on-call
        │
        ▼
policy: allow, or require approval
        │
        ├── allow ──────────────────────────────► the tool acts
        └── require approval ──► a person decides ──► the tool acts

A trigger cannot watch a chart that is fetched at render time. Storing the number is what turns a dashboard into something that watches the company. The dashboard is the side effect; the trigger is the product.

Rules

  • A metric is company state. It is not a cache of a vendor, and it is not a report.
  • Ingest is an upsert, never an append. The key is (key, grain, at, dimensions). A vendor restates a closed period, so the window must be re-runnable.
  • One metric key means one thing, in every source. churn_rate has one definition, one grain and one unit. Two definitions is worse than no metric.
  • The query definition is versioned with the metric. A PostHog insight can be edited, which silently changes what the number means. Store the definition version on the row.
  • Store the native currency, and a normalized value beside it. The rate is itself a metric, with its own source.
  • Everything is UTC, and at is the period start. A boundary bug is invisible and it never gets found.
  • A drill-down is a read tool, and it is still policy-checked. Read-through is not a bypass.
  • Never mirror a vendor's object model. If the answer needs one, the answer is a link to the vendor.

Data model

Two tables, and one worked shape.

TEXT
metric
  key           mrr · new_customers · churn_rate · dau · activation_rate · error_rate
  source        stripe · posthog · sentry
  domain        finance · product · engineering
  grain         hour · day · month
  at            the period start, UTC
  value         numeric
  unit          currency · count · ratio · millisecond
  dimensions    plan, country, release
  definition    the version of the query that produced it
  observed_at   when we read it
  source_ref    the vendor id behind it
TableHolds
metricsOne number, for one key, at one period. Every source writes the same shape.
external_refsOne mapping: our entity, our id, the connection, the vendor id. This is what makes revenue per account possible without mirroring Stripe.

connections and tools are already in the tools layer. Three connections and about a dozen tool rows cover all three sources.

Risks

RiskMitigation
It grows into an analytics product, badlyStore only numbers the company is operated on. Deep analysis is a link to the vendor
A vendor restates a closed period and the chart is wrongUpsert on the key, and re-read a trailing window on every schedule
A webhook is missed, and the number is quietly staleEvery webhook trigger has a reconcile schedule beside it
A metric definition changes and history becomes incomparableThe definition version is on the row. A change is a new version, never an edit
Ingest hits a vendor rate limit and retries make it worseThe adapter owns the limit and backs off. The run waits at a step boundary
A metric key means two things in two viewsOne key, one definition, one owner. A second meaning is a second key
Vendor credentials sit in a rowThe vault holds the value. connections holds a reference

Open questions

  1. What is the trailing reconcile window per source? Stripe refunds can land weeks late, and Sentry merges issues.
  2. Do we store a metric per account, or only company-wide? Per account is what makes revenue actionable, and it multiplies the row count.
  3. Who owns a metric definition? A metric with no owner drifts.
  4. Does a threshold live on the metric, on a trigger row, or on a policy limit? All three are defensible, and only one should be true.
  5. What is the retention for hourly grain? A daily rollup is cheap, and it loses the incident detail.

Diagram source. ac-docs/diagrams/mission-control-layers.drawio, page mission-control-metrics. Edit that page, then run ./scripts/export-diagrams.sh mission-control-layers. Update this page and the diagram together.