@growth-labs/ops-digest
v0.2.6
Published
Worker-runtime digest engine for D1/WAE operational metrics, anomaly callouts, notify delivery, and KV idempotency.
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 or 'iso' for ISO timestamp text columns. |
| d1.row-count-by-event | Counts D1 rows whose event column matches configured names. Defaults to event_name and processed_at. |
| d1.row-count-by-field | Counts D1 rows grouped by a configured field and returns a record keyed by group. Supports optional filterColumn / filterValues and timeColumnUnit: 'iso'. |
| d1.unique-count-by-event | Counts distinct non-empty values in a D1 column for rows whose event column matches configured names. Defaults to event_name and processed_at; set uniqueColumn and timeColumnUnit explicitly for identity/event tables. |
| 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. |
| http.json-snapshot | GETs a JSON URL and projects a numeric dotted-path value or entry count into the metric pipeline. |
All WAE handlers validate dataset identifiers before query execution and read
the API token from the configured secret name on env.
For D1 tables that store ISO timestamp text, set timeColumnUnit: 'iso' so
the handler binds ISO strings instead of numeric seconds:
{
handler: 'd1.row-count-by-field',
name: 'critical-findings-by-site',
table: 'gl_findings',
groupBy: 'site',
filterColumn: 'severity',
filterValues: ['critical'],
timeColumn: 'detected_at',
timeColumnUnit: 'iso',
}Standard Render
The built-in standard renderer starts every digest with the report type and
the exact UTC window. Exact 24-hour windows render as Daily Ops Digest; rolling
sub-day windows render as Ops Digest.
*Fronts - Daily Ops Digest*
Window: 2026-05-24 00:00-2026-05-25 00:00 UTC*Fronts - Ops Digest*
Window: 2026-05-25 06:00-09:00 UTCAnomaly 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.
