@growth-labs/ops-digest
v0.2.1
Published
Worker-runtime digest engine for D1/WAE operational metrics, anomaly callouts, notify delivery, and KV idempotency.
Downloads
650
Readme
@growth-labs/ops-digest
Worker-runtime digest engine for operational metrics. It reads D1 and Workers
Analytics Engine data, computes baselines and anomaly callouts, renders a
plain-text digest, sends it through @growth-labs/notify, and stores a KV
idempotency marker so reruns do not double-post.
It does not own schemas, write analytics, deliver notifications directly, or
schedule Workers. Consumers own D1 tables, WAE datasets, notify bindings, and
[triggers] cron configuration.
Install
pnpm add @growth-labs/ops-digest @growth-labs/analytics @growth-labs/notifyBasic Usage
import { opsDigest } from '@growth-labs/ops-digest'
const digest = opsDigest({
realmId: 'fulcrum-labs',
d1Binding: 'SITE_DB',
authWae: {
accountId: 'b8fd8daf73edd8fe6b6bd18eeaacf2bb',
dataset: 'auth_events',
apiTokenSecret: 'WAE_QUERY_TOKEN',
realmFilter: 'fulcrum-labs',
},
idempotencyKv: 'DIGEST_KV',
notifyConfig: { channels: ['slack'], severity: 'info' },
digests: [
{
name: 'fronts-daily',
brandLabel: 'Fronts',
schedule: '0 9 * * *',
window: 'previous-day-utc',
metrics: [
{
handler: 'wae.event-count',
name: 'signins',
source: 'authWae',
eventName: 'auth.signin.success',
},
{
handler: 'd1.row-count',
name: 'new-subscribers',
table: 'subscriptions',
timeColumn: 'created_at',
},
],
anomaly: { method: 'iqr', medianFloor: 5 },
render: 'standard',
channels: ['slack'],
},
],
})
export default {
scheduled: digest.scheduledHandler,
}For manual ops debugging:
await digest.runDigest('fronts-daily', env, ctx)Handlers
| Handler | Description |
| --- | --- |
| d1.row-count | Counts rows in a D1 table bounded by a timestamp column. Defaults to created_at seconds; set timeColumnUnit: 'ms' for millisecond columns. |
| d1.row-count-by-event | Counts D1 rows whose event column matches configured names. Defaults to event_name and processed_at. |
| wae.event-count | Counts WAE rows for one blob1 event name and optional source realm/site filter. |
| wae.event-count-by-blob | Counts one WAE event grouped by a blob field and returns a record keyed by group. |
| wae.unique-count | Counts distinct values in a WAE blob field, optionally filtered by event name. |
| wae.avg-double | Averages a WAE double field, optionally filtered by event name. |
| wae.top-n-by-blob | Returns ordered { group, count } rows for the top N values of a blob field. |
| composite.ratio | Computes nested numerator and denominator metrics and returns numerator / denominator. |
All WAE handlers validate dataset identifiers before query execution and read
the API token from the configured secret name on env.
Anomaly Tuning
method: 'iqr' compares today against thirty trailing daily windows. It skips
callouts when history has fewer than fourteen values or when the historical
median is below medianFloor.
anomaly: {
method: 'iqr',
medianFloor: 5,
iqrMultiplier: 1.5,
overrides: {
'write-for-us': { medianFloor: 1 },
},
}Use method: 'none' to disable anomaly callouts for a digest.
Multiple Digests
Mount one handler and list every cron in the Worker trigger config. Dispatch is
an exact string match on event.cron.
digests: [
{ name: 'fronts-daily', schedule: '0 9 * * *', /* ... */ },
{ name: 'fronts-weekly', schedule: '0 9 * * 1', /* ... */ },
]Windows
The window field on each digest accepts:
| Form | Description |
| --- | --- |
| 'previous-day-utc' / 'previous-week-utc' / 'previous-month-utc' | Closed UTC reporting windows that exclude partial today. |
| { days: N, anchorTo: 'utc-midnight' } | Trailing N-day window ending at the current UTC midnight. |
| { minutes: N } | Rolling window covering the last N minutes ending at now(). SQL: INTERVAL 'N' MINUTE. |
| { hours: N } | Rolling window covering the last N hours ending at now(). SQL: INTERVAL 'N' HOUR. |
Rolling sub-day windows are anchored to now() (not UTC midnight) and are the
right fit for high-frequency digests — for example, an every-15-minutes "what
just shipped" pulse, or a 3-hour Slack progress digest:
{
name: 'fronts-progress',
brandLabel: 'Fronts',
schedule: '0 */3 * * *',
window: { hours: 3 },
/* metrics, anomaly, render, channels */
}minutes and hours must be positive integers; non-integer or non-positive
values fail schema validation. Note that rolling-window digest names still
collide on the KV idempotency key when the window-start date stays the same
within a calendar day — pick distinct names per cadence.
Idempotency
Markers are written to the configured KV binding with this shape:
digest:<digest-name>:<YYYY-MM-DD window start>The marker value is sha256(renderedDigest). Duplicate reruns skip delivery.
Changed reruns also skip delivery and log a warning; the first digest for a
window remains canonical.
