Custom apps
The code
Metrics are objects that carry their own fetch. One file, one export, no dispatch to write.
connector.mjs has one default export. Each metric carries its own fetch,
so there is no dispatch to write.
import { createApp, createMetric } from "@notchmetrics/core";
const downloads = createMetric({
id: "downloads",
label: "Downloads",
unit: "count",
series: { granularities: ["day"] },
methodology: "Downloads reported by the registry for the window.",
params: [{ key: "package", label: "Package", type: "string" }],
async fetch(ctx, request) {
const name = request.params.package ?? "react";
const response = await ctx.http(
`https://api.npmjs.org/downloads/range/${request.from}:${request.to}/${name}`,
);
if (!response.ok) throw new Error(`The registry returned ${response.status}.`);
const body = await response.json();
const points = body.downloads.map((day) => ({ t: day.day, v: day.downloads }));
return { value: points.reduce((total, point) => total + point.v, 0), points };
},
});
export default createApp({
id: "npm-downloads",
name: "npm downloads",
description: "Download counts for a package on the npm registry.",
allowedHosts: ["api.npmjs.org"],
metrics: [downloads],
});
Everything about a metric sits in one object: what it is called, what it means, what the widget asks for, and the code that answers it. Adding one is adding an object to that array.
The import is types only. Nothing is loaded at run time, so a connector with
no node_modules beside it runs exactly the same.
createMetric
| Field | What it does |
|---|---|
id |
Names the metric. Used to route a request, so it is stable. |
label |
What the widget gallery shows. |
shortLabel |
What fits in a tile, twelve characters. Defaults to label. |
unit |
currency, count, ratio, duration or bytes. |
methodology |
How this number is derived, in one sentence. Required. |
params |
Fields the widget supplies, like a repository name. |
fetch |
Answers the metric for one window. |
The kind
Set exactly one. It decides which shapes the widget offers and whether the window means anything.
scalar: true; // one number for now
series: {
granularities: ["day"];
} // a number per bucket
total: {
granularities: ["day"];
} // one number for the window
list: {
limit: 6;
} // rows of categories
A series and a total compare against the previous window by default. Pass
compare: false where the API cannot answer that cheaply.
Money
unit: "currency" takes an integer in minor units and an ISO 4217 code.
Never a float.
return { value: 125_00, currency: "GBP" };
The context
fetch and verify both receive ctx.
| Field | What it is |
|---|---|
ctx.credentials |
Only the keys your manifest declares, frozen |
ctx.http |
The only network access |
ctx.timeRange |
The window, as from, to, granularity and timeZone |
ctx.signal |
Aborted when the run exceeds timeoutMs |
ctx.log |
Lands in the run log, never on the tile |
credentials is filtered before your code runs, so a key the app never asked
for is not readable by it.
Use ctx.http
const response = await ctx.http("https://api.example.com/v1/users", {
headers: { authorization: `Bearer ${ctx.credentials.api_key}` },
});
A bare fetch bypasses the host allowlist, the https requirement, the
redirect re-check and the 4 MB cap. Use ctx.http.
It returns ok, status, headers, json() and text().
The request
| Field | What it is |
|---|---|
request.params |
Whatever the widget supplied for your declared params |
request.from, request.to |
The window, as ISO timestamps |
request.range, request.granularity |
The window as the user chose it |
request.comparePrevious |
Whether the previous window is wanted |
request.id |
Which widget asked. Opaque, and echoed back |
request.id is what keeps two tiles on one metric apart. A tile for react
and a tile for vue are identical in every other field.
What fetch returns
value is required. Everything else is optional.
| Field | For |
|---|---|
previousValue |
The delta against the previous equal window |
points |
A series, as { t, v } |
items |
Rows, for a list metric |
currency |
An ISO 4217 code, for a currency metric |
note |
One line the tile shows under the figure |
verify
Optional. An app against a public API has nothing to prove.
Where there is a credential, verify runs before the app is usable.
export default createApp({
// the app's own fields, then:
async verify(ctx) {
const response = await ctx.http("https://api.example.com/v1/account", {
headers: { authorization: `Bearer ${ctx.credentials.api_key}` },
});
if (!response.ok) throw new Error(`The API key was rejected (${response.status}).`);
const account = await response.json();
return { accountLabel: account.name, currency: account.currency };
},
metrics: [downloads],
});
Throw to reject. A key that does not work must not become an app that quietly shows nothing.
Return unavailableMetrics to name the numbers this credential cannot reach.
A read only token that sees usage but not billing says so once, and those
widgets explain themselves.
Errors
Throw with a sentence somebody can act on. The message reaches the tile, so
The API returned 429. is useful and undefined is not an object is not.
One metric that throws does not affect the others. A run that passes
timeoutMs is killed, and every metric in that run shares the budget.
The older shape
A connector that exports verify and fetchMetric as named functions still
runs. That shape declared its metrics in a hand-written manifest and dispatched on
request.metric inside one function.
Nothing forces a rewrite. New connectors should use createApp,
because an id spelled two ways is a class of bug the newer shape cannot have.