@growth-labs/ops-digest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +129 -0
  3. package/SPEC.md +25 -0
  4. package/dist/anomaly.d.ts +3 -0
  5. package/dist/anomaly.d.ts.map +1 -0
  6. package/dist/anomaly.js +50 -0
  7. package/dist/anomaly.js.map +1 -0
  8. package/dist/engine.d.ts +4 -0
  9. package/dist/engine.d.ts.map +1 -0
  10. package/dist/engine.js +149 -0
  11. package/dist/engine.js.map +1 -0
  12. package/dist/handlers/common.d.ts +12 -0
  13. package/dist/handlers/common.d.ts.map +1 -0
  14. package/dist/handlers/common.js +68 -0
  15. package/dist/handlers/common.js.map +1 -0
  16. package/dist/handlers/composite-ratio.d.ts +7 -0
  17. package/dist/handlers/composite-ratio.d.ts.map +1 -0
  18. package/dist/handlers/composite-ratio.js +9 -0
  19. package/dist/handlers/composite-ratio.js.map +1 -0
  20. package/dist/handlers/d1-row-count-by-event.d.ts +7 -0
  21. package/dist/handlers/d1-row-count-by-event.d.ts.map +1 -0
  22. package/dist/handlers/d1-row-count-by-event.js +21 -0
  23. package/dist/handlers/d1-row-count-by-event.js.map +1 -0
  24. package/dist/handlers/d1-row-count.d.ts +7 -0
  25. package/dist/handlers/d1-row-count.d.ts.map +1 -0
  26. package/dist/handlers/d1-row-count.js +12 -0
  27. package/dist/handlers/d1-row-count.js.map +1 -0
  28. package/dist/handlers/index.d.ts +29 -0
  29. package/dist/handlers/index.d.ts.map +1 -0
  30. package/dist/handlers/index.js +60 -0
  31. package/dist/handlers/index.js.map +1 -0
  32. package/dist/handlers/wae-avg-double.d.ts +7 -0
  33. package/dist/handlers/wae-avg-double.d.ts.map +1 -0
  34. package/dist/handlers/wae-avg-double.js +12 -0
  35. package/dist/handlers/wae-avg-double.js.map +1 -0
  36. package/dist/handlers/wae-event-count-by-blob.d.ts +7 -0
  37. package/dist/handlers/wae-event-count-by-blob.d.ts.map +1 -0
  38. package/dist/handlers/wae-event-count-by-blob.js +14 -0
  39. package/dist/handlers/wae-event-count-by-blob.js.map +1 -0
  40. package/dist/handlers/wae-event-count.d.ts +7 -0
  41. package/dist/handlers/wae-event-count.d.ts.map +1 -0
  42. package/dist/handlers/wae-event-count.js +11 -0
  43. package/dist/handlers/wae-event-count.js.map +1 -0
  44. package/dist/handlers/wae-top-n-by-blob.d.ts +7 -0
  45. package/dist/handlers/wae-top-n-by-blob.d.ts.map +1 -0
  46. package/dist/handlers/wae-top-n-by-blob.js +18 -0
  47. package/dist/handlers/wae-top-n-by-blob.js.map +1 -0
  48. package/dist/handlers/wae-unique-count.d.ts +7 -0
  49. package/dist/handlers/wae-unique-count.d.ts.map +1 -0
  50. package/dist/handlers/wae-unique-count.js +12 -0
  51. package/dist/handlers/wae-unique-count.js.map +1 -0
  52. package/dist/idempotency.d.ts +6 -0
  53. package/dist/idempotency.d.ts.map +1 -0
  54. package/dist/idempotency.js +25 -0
  55. package/dist/idempotency.js.map +1 -0
  56. package/dist/index.d.ts +5 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +29 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/render.d.ts +4 -0
  61. package/dist/render.d.ts.map +1 -0
  62. package/dist/render.js +73 -0
  63. package/dist/render.js.map +1 -0
  64. package/dist/schema.d.ts +264 -0
  65. package/dist/schema.d.ts.map +1 -0
  66. package/dist/schema.js +102 -0
  67. package/dist/schema.js.map +1 -0
  68. package/dist/types.d.ts +146 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/dist/window.d.ts +7 -0
  73. package/dist/window.d.ts.map +1 -0
  74. package/dist/window.js +28 -0
  75. package/dist/window.js.map +1 -0
  76. package/package.json +40 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @growth-labs/ops-digest Changelog
2
+
3
+ ## 0.1.0 - 2026-05-18
4
+
5
+ - Initial release.
6
+ - Adds the `opsDigest()` Worker-runtime digest engine with exact-cron scheduled dispatch and manual `runDigest()` execution.
7
+ - Adds D1 metric handlers, WAE metric handlers, `composite.ratio`, IQR anomaly detection, Slack-friendly standard rendering, and KV idempotency markers.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @growth-labs/ops-digest
2
+
3
+ Worker-runtime digest engine for operational metrics. It reads D1 and Workers
4
+ Analytics Engine data, computes baselines and anomaly callouts, renders a
5
+ plain-text digest, sends it through `@growth-labs/notify`, and stores a KV
6
+ idempotency marker so reruns do not double-post.
7
+
8
+ It does not own schemas, write analytics, deliver notifications directly, or
9
+ schedule Workers. Consumers own D1 tables, WAE datasets, notify bindings, and
10
+ `[triggers]` cron configuration.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pnpm add @growth-labs/ops-digest @growth-labs/analytics @growth-labs/notify
16
+ ```
17
+
18
+ ## Basic Usage
19
+
20
+ ```ts
21
+ import { opsDigest } from '@growth-labs/ops-digest'
22
+
23
+ const digest = opsDigest({
24
+ realmId: 'fulcrum-labs',
25
+ d1Binding: 'SITE_DB',
26
+ authWae: {
27
+ accountId: 'b8fd8daf73edd8fe6b6bd18eeaacf2bb',
28
+ dataset: 'auth_events',
29
+ apiTokenSecret: 'WAE_QUERY_TOKEN',
30
+ realmFilter: 'fulcrum-labs',
31
+ },
32
+ idempotencyKv: 'DIGEST_KV',
33
+ notifyConfig: { channels: ['slack'], severity: 'info' },
34
+ digests: [
35
+ {
36
+ name: 'fronts-daily',
37
+ brandLabel: 'Fronts',
38
+ schedule: '0 9 * * *',
39
+ window: 'previous-day-utc',
40
+ metrics: [
41
+ {
42
+ handler: 'wae.event-count',
43
+ name: 'signins',
44
+ source: 'authWae',
45
+ eventName: 'auth.signin.success',
46
+ },
47
+ {
48
+ handler: 'd1.row-count',
49
+ name: 'new-subscribers',
50
+ table: 'subscriptions',
51
+ timeColumn: 'created_at',
52
+ },
53
+ ],
54
+ anomaly: { method: 'iqr', medianFloor: 5 },
55
+ render: 'standard',
56
+ channels: ['slack'],
57
+ },
58
+ ],
59
+ })
60
+
61
+ export default {
62
+ scheduled: digest.scheduledHandler,
63
+ }
64
+ ```
65
+
66
+ For manual ops debugging:
67
+
68
+ ```ts
69
+ await digest.runDigest('fronts-daily', env, ctx)
70
+ ```
71
+
72
+ ## Handlers
73
+
74
+ | Handler | Description |
75
+ | --- | --- |
76
+ | `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. |
77
+ | `d1.row-count-by-event` | Counts D1 rows whose event column matches configured names. Defaults to `event_name` and `processed_at`. |
78
+ | `wae.event-count` | Counts WAE rows for one `blob1` event name and optional source realm/site filter. |
79
+ | `wae.event-count-by-blob` | Counts one WAE event grouped by a blob field and returns a record keyed by group. |
80
+ | `wae.unique-count` | Counts distinct values in a WAE blob field, optionally filtered by event name. |
81
+ | `wae.avg-double` | Averages a WAE double field, optionally filtered by event name. |
82
+ | `wae.top-n-by-blob` | Returns ordered `{ group, count }` rows for the top N values of a blob field. |
83
+ | `composite.ratio` | Computes nested numerator and denominator metrics and returns `numerator / denominator`. |
84
+
85
+ All WAE handlers validate dataset identifiers before query execution and read
86
+ the API token from the configured secret name on `env`.
87
+
88
+ ## Anomaly Tuning
89
+
90
+ `method: 'iqr'` compares today against thirty trailing daily windows. It skips
91
+ callouts when history has fewer than fourteen values or when the historical
92
+ median is below `medianFloor`.
93
+
94
+ ```ts
95
+ anomaly: {
96
+ method: 'iqr',
97
+ medianFloor: 5,
98
+ iqrMultiplier: 1.5,
99
+ overrides: {
100
+ 'write-for-us': { medianFloor: 1 },
101
+ },
102
+ }
103
+ ```
104
+
105
+ Use `method: 'none'` to disable anomaly callouts for a digest.
106
+
107
+ ## Multiple Digests
108
+
109
+ Mount one handler and list every cron in the Worker trigger config. Dispatch is
110
+ an exact string match on `event.cron`.
111
+
112
+ ```ts
113
+ digests: [
114
+ { name: 'fronts-daily', schedule: '0 9 * * *', /* ... */ },
115
+ { name: 'fronts-weekly', schedule: '0 9 * * 1', /* ... */ },
116
+ ]
117
+ ```
118
+
119
+ ## Idempotency
120
+
121
+ Markers are written to the configured KV binding with this shape:
122
+
123
+ ```txt
124
+ digest:<digest-name>:<YYYY-MM-DD window start>
125
+ ```
126
+
127
+ The marker value is `sha256(renderedDigest)`. Duplicate reruns skip delivery.
128
+ Changed reruns also skip delivery and log a warning; the first digest for a
129
+ window remains canonical.
package/SPEC.md ADDED
@@ -0,0 +1,25 @@
1
+ # @growth-labs/ops-digest v0.1.0 Spec
2
+
3
+ `@growth-labs/ops-digest` is an operational digest engine for Cloudflare Workers.
4
+
5
+ It accepts one or more digest definitions. Each definition declares a cron schedule, closed UTC window, metric handlers, anomaly detection settings, a render template, and notify channels. The engine computes current metrics, computes seven trailing baseline windows, computes a thirty-day anomaly history, renders a Slack-friendly digest, posts through `@growth-labs/notify`, and writes a KV idempotency marker keyed by digest name and window start.
6
+
7
+ ## Scope
8
+
9
+ In scope for v0.1.0:
10
+
11
+ - D1 read-only aggregate handlers.
12
+ - Workers Analytics Engine read-only aggregate handlers through `@growth-labs/analytics` query utilities.
13
+ - IQR anomaly detection with a median volume floor.
14
+ - Built-in `standard` text renderer.
15
+ - KV idempotency with seven-day TTL.
16
+ - Exact cron dispatch through `scheduledHandler`.
17
+
18
+ Out of scope for v0.1.0:
19
+
20
+ - Package-owned D1 schema or writes.
21
+ - Notification delivery internals.
22
+ - Worker scheduling beyond exporting `scheduledHandler`.
23
+ - Custom render templates.
24
+ - Historical digest backfills.
25
+ - WAE query batching optimizations.
@@ -0,0 +1,3 @@
1
+ import type { AnomalyConfig, AnomalyHit } from './types.js';
2
+ export declare function detectAnomaly(current: number, history: number[], config: AnomalyConfig, metricName?: string): AnomalyHit | null;
3
+ //# sourceMappingURL=anomaly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anomaly.d.ts","sourceRoot":"","sources":["../src/anomaly.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE3D,wBAAgB,aAAa,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EAAE,EACjB,MAAM,EAAE,aAAa,EACrB,UAAU,SAAK,GACb,UAAU,GAAG,IAAI,CA2CnB"}
@@ -0,0 +1,50 @@
1
+ export function detectAnomaly(current, history, config, metricName = '') {
2
+ const override = metricName ? config.overrides?.[metricName] : undefined;
3
+ const effective = { ...config, ...override };
4
+ if (effective.method === 'none')
5
+ return null;
6
+ if (history.length < 14)
7
+ return null;
8
+ const sorted = [...history].sort((a, b) => a - b);
9
+ const medianValue = median(sorted);
10
+ if (medianValue < effective.medianFloor)
11
+ return null;
12
+ const q1 = median(sorted.slice(0, Math.floor(sorted.length / 2)));
13
+ const q3 = median(sorted.slice(Math.ceil(sorted.length / 2)));
14
+ const iqr = q3 - q1;
15
+ const multiplier = effective.iqrMultiplier ?? 1.5;
16
+ const lowerBand = medianValue - multiplier * iqr;
17
+ const upperBand = medianValue + multiplier * iqr;
18
+ if (current < lowerBand) {
19
+ return {
20
+ metric: metricName,
21
+ current,
22
+ median: medianValue,
23
+ iqr,
24
+ lowerBand,
25
+ upperBand,
26
+ direction: 'below',
27
+ };
28
+ }
29
+ if (current > upperBand) {
30
+ return {
31
+ metric: metricName,
32
+ current,
33
+ median: medianValue,
34
+ iqr,
35
+ lowerBand,
36
+ upperBand,
37
+ direction: 'above',
38
+ };
39
+ }
40
+ return null;
41
+ }
42
+ function median(sorted) {
43
+ if (sorted.length === 0)
44
+ return 0;
45
+ const middle = Math.floor(sorted.length / 2);
46
+ if (sorted.length % 2 === 1)
47
+ return sorted[middle];
48
+ return (sorted[middle - 1] + sorted[middle]) / 2;
49
+ }
50
+ //# sourceMappingURL=anomaly.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anomaly.js","sourceRoot":"","sources":["../src/anomaly.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,aAAa,CAC5B,OAAe,EACf,OAAiB,EACjB,MAAqB,EACrB,UAAU,GAAG,EAAE;IAEf,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACxE,MAAM,SAAS,GAAG,EAAE,GAAG,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAA;IAE5C,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAC5C,IAAI,OAAO,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,IAAI,CAAA;IAEpC,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACjD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAClC,IAAI,WAAW,GAAG,SAAS,CAAC,WAAW;QAAE,OAAO,IAAI,CAAA;IAEpD,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IACjE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7D,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,CAAA;IACnB,MAAM,UAAU,GAAG,SAAS,CAAC,aAAa,IAAI,GAAG,CAAA;IACjD,MAAM,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,GAAG,CAAA;IAChD,MAAM,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,GAAG,CAAA;IAEhD,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC;QACzB,OAAO;YACN,MAAM,EAAE,UAAU;YAClB,OAAO;YACP,MAAM,EAAE,WAAW;YACnB,GAAG;YACH,SAAS;YACT,SAAS;YACT,SAAS,EAAE,OAAO;SAClB,CAAA;IACF,CAAC;IAED,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC;QACzB,OAAO;YACN,MAAM,EAAE,UAAU;YAClB,OAAO;YACP,MAAM,EAAE,WAAW;YACnB,GAAG;YACH,SAAS;YACT,SAAS;YACT,SAAS,EAAE,OAAO;SAClB,CAAA;IACF,CAAC;IAED,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,SAAS,MAAM,CAAC,MAAgB;IAC/B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IACjC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC5C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAA;IAClD,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAA;AACjD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { DigestDefinition, DigestRunResult, MetricValue, OpsDigestConfig, OpsDigestEnv } from './types.js';
2
+ export declare function runDigest(definition: DigestDefinition, config: OpsDigestConfig, env: OpsDigestEnv, _ctx: ExecutionContext): Promise<DigestRunResult>;
3
+ export declare function metricValueForEngine(value: MetricValue): number;
4
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACX,gBAAgB,EAChB,eAAe,EAIf,WAAW,EACX,eAAe,EACf,YAAY,EAIZ,MAAM,YAAY,CAAA;AAGnB,wBAAsB,SAAS,CAC9B,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,CAAC,CAiD1B;AAyHD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAE/D"}
package/dist/engine.js ADDED
@@ -0,0 +1,149 @@
1
+ import { notify } from '@growth-labs/notify';
2
+ import { detectAnomaly } from './anomaly.js';
3
+ import { metricValueToNumber } from './handlers/common.js';
4
+ import { computeMetric } from './handlers/index.js';
5
+ import { checkIdempotency, formatIdempotencyKey, hashRenderedDigest, writeIdempotencyMarker, } from './idempotency.js';
6
+ import { renderDigest } from './render.js';
7
+ import { resolveDigestWindow, shiftWindowDays, splitTrailingDays } from './window.js';
8
+ export async function runDigest(definition, config, env, _ctx) {
9
+ const window = resolveDigestWindow(definition.window);
10
+ const idempotencyKey = formatIdempotencyKey(definition.name, window.startMs);
11
+ const errors = [];
12
+ const context = {
13
+ d1: resolveBinding(env, config.d1Binding),
14
+ sources: buildSources(config),
15
+ env,
16
+ };
17
+ const metrics = [];
18
+ for (const metric of definition.metrics) {
19
+ const result = await computeMetricResult(metric, definition, window, context, errors);
20
+ if (result)
21
+ metrics.push(result);
22
+ }
23
+ const rendered = renderDigest(definition.render, metrics, definition.brandLabel, window);
24
+ const renderedHash = await hashRenderedDigest(rendered);
25
+ const kv = resolveBinding(env, config.idempotencyKv);
26
+ const state = await checkIdempotency(kv, definition.name, window.startMs, renderedHash);
27
+ if (state === 'duplicate') {
28
+ return buildResult(definition.name, window, rendered, idempotencyKey, false, metrics, errors);
29
+ }
30
+ if (state === 'changed') {
31
+ console.warn('ops-digest: idempotency marker changed; skipping rerun', {
32
+ key: idempotencyKey,
33
+ });
34
+ return buildResult(definition.name, window, rendered, idempotencyKey, false, metrics, errors);
35
+ }
36
+ const severity = metrics.some((metric) => metric.anomaly) ? 'warning' : 'info';
37
+ const notifyResult = await notify(env, {
38
+ ...config.notifyConfig,
39
+ channels: definition.channels,
40
+ severity,
41
+ title: `${definition.brandLabel} Ops Digest`,
42
+ body: rendered,
43
+ dedupKey: idempotencyKey,
44
+ });
45
+ const posted = notifyResult.sent.length > 0;
46
+ if (posted) {
47
+ await writeIdempotencyMarker(kv, definition.name, window.startMs, renderedHash);
48
+ }
49
+ else {
50
+ errors.push(`notify: no channels sent (${notifyResult.failed.map((f) => f.error).join('; ')})`);
51
+ }
52
+ return buildResult(definition.name, window, rendered, idempotencyKey, posted, metrics, errors);
53
+ }
54
+ async function computeMetricResult(metric, definition, window, context, errors) {
55
+ try {
56
+ const current = await computeMetric(metric, window, context);
57
+ const currentNumber = metricValueToNumber(current);
58
+ const baseline = await computeBaseline(metric, window, context, errors);
59
+ const history = await computeHistory(metric, window, context, errors);
60
+ const delta = formatPercentChange(currentNumber, baseline);
61
+ const anomaly = detectAnomaly(currentNumber, history, definition.anomaly, metric.name) ?? undefined;
62
+ if (anomaly)
63
+ anomaly.metric = metric.name;
64
+ return {
65
+ name: metric.name,
66
+ current,
67
+ baseline,
68
+ delta,
69
+ anomaly,
70
+ };
71
+ }
72
+ catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ errors.push(`${metric.name}: ${message}`);
75
+ console.error('ops-digest: metric failed', { metric: metric.name, err });
76
+ return null;
77
+ }
78
+ }
79
+ async function computeBaseline(metric, window, context, errors) {
80
+ const values = [];
81
+ for (let offset = 1; offset <= 7; offset += 1) {
82
+ try {
83
+ values.push(metricValueToNumber(await computeMetric(metric, shiftWindowDays(window, offset), context)));
84
+ }
85
+ catch (err) {
86
+ const message = err instanceof Error ? err.message : String(err);
87
+ errors.push(`${metric.name} baseline-${offset}: ${message}`);
88
+ }
89
+ }
90
+ if (values.length === 0)
91
+ return 0;
92
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
93
+ }
94
+ async function computeHistory(metric, window, context, errors) {
95
+ const values = [];
96
+ for (const [index, historyWindow] of splitTrailingDays(window, 30).entries()) {
97
+ try {
98
+ values.push(metricValueToNumber(await computeMetric(metric, historyWindow, context)));
99
+ }
100
+ catch (err) {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ errors.push(`${metric.name} history-${index + 1}: ${message}`);
103
+ }
104
+ }
105
+ return values;
106
+ }
107
+ function buildResult(digestName, window, rendered, idempotencyKey, posted, metrics, errors) {
108
+ return {
109
+ digestName,
110
+ windowStart: window.startMs,
111
+ windowEnd: window.endMs,
112
+ rendered,
113
+ idempotencyKey,
114
+ posted,
115
+ metrics,
116
+ errors,
117
+ };
118
+ }
119
+ function buildSources(config) {
120
+ return {
121
+ ...(config.authWae ? { authWae: config.authWae } : {}),
122
+ ...(config.contentWae ? { contentWae: config.contentWae } : {}),
123
+ ...(config.extraWae ?? {}),
124
+ };
125
+ }
126
+ function resolveBinding(env, name) {
127
+ const binding = env[name];
128
+ if (!binding)
129
+ throw new Error(`ops-digest: env binding "${name}" is missing`);
130
+ return binding;
131
+ }
132
+ function formatPercentChange(current, previous) {
133
+ if (previous === 0) {
134
+ if (current === 0)
135
+ return { value: '0%', direction: 'flat' };
136
+ return { value: '—', direction: current > 0 ? 'up' : 'down' };
137
+ }
138
+ const pct = ((current - previous) / previous) * 100;
139
+ if (pct === 0)
140
+ return { value: '0%', direction: 'flat' };
141
+ return {
142
+ value: `${Math.abs(pct).toFixed(1)}%`,
143
+ direction: pct > 0 ? 'up' : 'down',
144
+ };
145
+ }
146
+ export function metricValueForEngine(value) {
147
+ return metricValueToNumber(value);
148
+ }
149
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { HandlerContext, MetricValue, ResolvedDigestWindow, WaeSourceConfig } from '../types.js';
2
+ export declare function quoteIdentifier(identifier: string): string;
3
+ export declare function assertSqlIdentifier(identifier: string): void;
4
+ export declare function numericResult(row: Record<string, unknown> | null | undefined): number;
5
+ export declare function metricValueToNumber(value: MetricValue): number;
6
+ export declare function resolveD1(context: HandlerContext): D1Database;
7
+ export declare function d1TimeBounds(window: ResolvedDigestWindow, unit: 's' | 'ms' | undefined): [number, number];
8
+ export declare function resolveWaeSource(context: HandlerContext, sourceName: string): WaeSourceConfig;
9
+ export declare function queryWaeRows<T extends Record<string, unknown>>(source: WaeSourceConfig, context: HandlerContext, sql: string): Promise<T[]>;
10
+ export declare function waeConditions(source: WaeSourceConfig, window: ResolvedDigestWindow, eventName?: string): string[];
11
+ export declare function sqlString(value: string): string;
12
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/handlers/common.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,cAAc,EACd,WAAW,EACX,oBAAoB,EACpB,eAAe,EACf,MAAM,aAAa,CAAA;AAKpB,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAI5D;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKrF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAI9D;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,CAG7D;AAED,wBAAgB,YAAY,CAC3B,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,GAAG,GAAG,IAAI,GAAG,SAAS,GAC1B,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,eAAe,CAI7F;AAED,wBAAsB,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,CAAC,EAAE,CAAC,CASd;AAED,wBAAgB,aAAa,CAC5B,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,oBAAoB,EAC5B,SAAS,CAAC,EAAE,MAAM,GAChB,MAAM,EAAE,CAOV;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C"}
@@ -0,0 +1,68 @@
1
+ import { queryWAE } from '@growth-labs/analytics/utils/wae-query';
2
+ import { assertValidWaeDataset } from '@growth-labs/analytics/utils/wae-validate';
3
+ import { waeWindowSql } from '../window.js';
4
+ const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
+ export function quoteIdentifier(identifier) {
6
+ assertSqlIdentifier(identifier);
7
+ return `"${identifier}"`;
8
+ }
9
+ export function assertSqlIdentifier(identifier) {
10
+ if (!IDENTIFIER_PATTERN.test(identifier)) {
11
+ throw new Error(`ops-digest: invalid SQL identifier "${identifier}"`);
12
+ }
13
+ }
14
+ export function numericResult(row) {
15
+ const value = row?.c;
16
+ if (typeof value === 'number')
17
+ return value;
18
+ if (typeof value === 'string')
19
+ return Number(value);
20
+ return 0;
21
+ }
22
+ export function metricValueToNumber(value) {
23
+ if (typeof value === 'number')
24
+ return value;
25
+ if (Array.isArray(value))
26
+ return value.reduce((sum, item) => sum + item.count, 0);
27
+ return Object.values(value).reduce((sum, count) => sum + count, 0);
28
+ }
29
+ export function resolveD1(context) {
30
+ if (!context.d1)
31
+ throw new Error('ops-digest: D1 binding is missing');
32
+ return context.d1;
33
+ }
34
+ export function d1TimeBounds(window, unit) {
35
+ if (unit === 'ms')
36
+ return [window.startMs, window.endMs];
37
+ return [Math.floor(window.startMs / 1000), Math.floor(window.endMs / 1000)];
38
+ }
39
+ export function resolveWaeSource(context, sourceName) {
40
+ const source = context.sources?.[sourceName];
41
+ if (!source)
42
+ throw new Error(`ops-digest: WAE source "${sourceName}" is not configured`);
43
+ return source;
44
+ }
45
+ export async function queryWaeRows(source, context, sql) {
46
+ assertValidWaeDataset(source.dataset, source.realmFilter ?? source.siteFilter ?? '');
47
+ const token = context.env?.[source.apiTokenSecret];
48
+ if (typeof token !== 'string' || token.trim() === '') {
49
+ throw new Error(`ops-digest: WAE token secret "${source.apiTokenSecret}" is missing`);
50
+ }
51
+ const fetchFn = context.env?.fetch ?? fetch;
52
+ const result = await queryWAE(source.accountId, source.dataset, sql, token, fetchFn);
53
+ return result.data;
54
+ }
55
+ export function waeConditions(source, window, eventName) {
56
+ const conditions = [];
57
+ if (eventName)
58
+ conditions.push(`blob1 = ${sqlString(eventName)}`);
59
+ const filter = source.realmFilter ?? source.siteFilter;
60
+ if (filter)
61
+ conditions.push(`blob2 = ${sqlString(filter)}`);
62
+ conditions.push(waeWindowSql(window));
63
+ return conditions;
64
+ }
65
+ export function sqlString(value) {
66
+ return `'${value.replaceAll("'", "''")}'`;
67
+ }
68
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/handlers/common.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { HandlerContext, MetricDefinition, ResolvedDigestWindow } from '../types.js';
2
+ type CompositeRatioMetric = Extract<MetricDefinition, {
3
+ handler: 'composite.ratio';
4
+ }>;
5
+ export declare function computeCompositeRatio(metric: CompositeRatioMetric, window: ResolvedDigestWindow, context: HandlerContext, compute: (metric: MetricDefinition, window: ResolvedDigestWindow, context: HandlerContext) => Promise<unknown>): Promise<number>;
6
+ export {};
7
+ //# sourceMappingURL=composite-ratio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composite-ratio.d.ts","sourceRoot":"","sources":["../../src/handlers/composite-ratio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGzF,KAAK,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,EAAE;IAAE,OAAO,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAAA;AAErF,wBAAsB,qBAAqB,CAC1C,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,CACR,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,KACnB,OAAO,CAAC,OAAO,CAAC,GACnB,OAAO,CAAC,MAAM,CAAC,CAWjB"}
@@ -0,0 +1,9 @@
1
+ import { metricValueToNumber } from './common.js';
2
+ export async function computeCompositeRatio(metric, window, context, compute) {
3
+ const numerator = metricValueToNumber((await compute(metric.numerator, window, context)));
4
+ const denominator = metricValueToNumber((await compute(metric.denominator, window, context)));
5
+ if (denominator === 0)
6
+ return 0;
7
+ return numerator / denominator;
8
+ }
9
+ //# sourceMappingURL=composite-ratio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composite-ratio.js","sourceRoot":"","sources":["../../src/handlers/composite-ratio.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAIjD,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,MAA4B,EAC5B,MAA4B,EAC5B,OAAuB,EACvB,OAIqB;IAErB,MAAM,SAAS,GAAG,mBAAmB,CACpC,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAA8C,CAC/F,CAAA;IACD,MAAM,WAAW,GAAG,mBAAmB,CACtC,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,CAE/C,CACJ,CAAA;IACD,IAAI,WAAW,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAC/B,OAAO,SAAS,GAAG,WAAW,CAAA;AAC/B,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { HandlerContext, MetricDefinition, ResolvedDigestWindow } from '../types.js';
2
+ type D1RowCountByEventMetric = Extract<MetricDefinition, {
3
+ handler: 'd1.row-count-by-event';
4
+ }>;
5
+ export declare function computeD1RowCountByEvent(metric: D1RowCountByEventMetric, window: ResolvedDigestWindow, context: HandlerContext): Promise<number>;
6
+ export {};
7
+ //# sourceMappingURL=d1-row-count-by-event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-row-count-by-event.d.ts","sourceRoot":"","sources":["../../src/handlers/d1-row-count-by-event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGzF,KAAK,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,EAAE;IAAE,OAAO,EAAE,uBAAuB,CAAA;CAAE,CAAC,CAAA;AAE9F,wBAAsB,wBAAwB,CAC7C,MAAM,EAAE,uBAAuB,EAC/B,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
@@ -0,0 +1,21 @@
1
+ import { d1TimeBounds, numericResult, quoteIdentifier, resolveD1 } from './common.js';
2
+ export async function computeD1RowCountByEvent(metric, window, context) {
3
+ if (metric.eventNames.length === 0)
4
+ return 0;
5
+ const d1 = resolveD1(context);
6
+ const eventColumn = metric.eventColumn ?? 'event_name';
7
+ const timeColumn = metric.timeColumn ?? 'processed_at';
8
+ const placeholders = metric.eventNames.map(() => '?').join(', ');
9
+ const [start, end] = d1TimeBounds(window, metric.timeColumnUnit);
10
+ const row = await d1
11
+ .prepare([
12
+ `SELECT COUNT(*) AS c FROM ${quoteIdentifier(metric.table)}`,
13
+ `WHERE ${quoteIdentifier(eventColumn)} IN (${placeholders})`,
14
+ `AND ${quoteIdentifier(timeColumn)} >= ?`,
15
+ `AND ${quoteIdentifier(timeColumn)} < ?`,
16
+ ].join(' '))
17
+ .bind(...metric.eventNames, start, end)
18
+ .first();
19
+ return numericResult(row);
20
+ }
21
+ //# sourceMappingURL=d1-row-count-by-event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-row-count-by-event.js","sourceRoot":"","sources":["../../src/handlers/d1-row-count-by-event.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAIrF,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,MAA+B,EAC/B,MAA4B,EAC5B,OAAuB;IAEvB,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAC5C,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,CAAA;IAC7B,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,YAAY,CAAA;IACtD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,cAAc,CAAA;IACtD,MAAM,YAAY,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAChE,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC,CAAA;IAChE,MAAM,GAAG,GAAG,MAAM,EAAE;SAClB,OAAO,CACP;QACC,6BAA6B,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;QAC5D,SAAS,eAAe,CAAC,WAAW,CAAC,QAAQ,YAAY,GAAG;QAC5D,OAAO,eAAe,CAAC,UAAU,CAAC,OAAO;QACzC,OAAO,eAAe,CAAC,UAAU,CAAC,MAAM;KACxC,CAAC,IAAI,CAAC,GAAG,CAAC,CACX;SACA,IAAI,CAAC,GAAG,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,GAAG,CAAC;SACtC,KAAK,EAA2B,CAAA;IAClC,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;AAC1B,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { HandlerContext, MetricDefinition, ResolvedDigestWindow } from '../types.js';
2
+ type D1RowCountMetric = Extract<MetricDefinition, {
3
+ handler: 'd1.row-count';
4
+ }>;
5
+ export declare function computeD1RowCount(metric: D1RowCountMetric, window: ResolvedDigestWindow, context: HandlerContext): Promise<number>;
6
+ export {};
7
+ //# sourceMappingURL=d1-row-count.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-row-count.d.ts","sourceRoot":"","sources":["../../src/handlers/d1-row-count.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAGzF,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE;IAAE,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC,CAAA;AAE9E,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,CAWjB"}
@@ -0,0 +1,12 @@
1
+ import { d1TimeBounds, numericResult, quoteIdentifier, resolveD1 } from './common.js';
2
+ export async function computeD1RowCount(metric, window, context) {
3
+ const d1 = resolveD1(context);
4
+ const timeColumn = metric.timeColumn ?? 'created_at';
5
+ const [start, end] = d1TimeBounds(window, metric.timeColumnUnit);
6
+ const row = await d1
7
+ .prepare(`SELECT COUNT(*) AS c FROM ${quoteIdentifier(metric.table)} WHERE ${quoteIdentifier(timeColumn)} >= ? AND ${quoteIdentifier(timeColumn)} < ?`)
8
+ .bind(start, end)
9
+ .first();
10
+ return numericResult(row);
11
+ }
12
+ //# sourceMappingURL=d1-row-count.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-row-count.js","sourceRoot":"","sources":["../../src/handlers/d1-row-count.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAIrF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,MAAwB,EACxB,MAA4B,EAC5B,OAAuB;IAEvB,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,CAAA;IAC7B,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,YAAY,CAAA;IACpD,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC,CAAA;IAChE,MAAM,GAAG,GAAG,MAAM,EAAE;SAClB,OAAO,CACP,6BAA6B,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,eAAe,CAAC,UAAU,CAAC,aAAa,eAAe,CAAC,UAAU,CAAC,MAAM,CAC7I;SACA,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC;SAChB,KAAK,EAA2B,CAAA;IAClC,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;AAC1B,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { HandlerContext, MetricDefinition, MetricValue, ResolvedDigestWindow } from '../types.js';
2
+ export declare const handlerRegistry: {
3
+ readonly 'd1.row-count': {
4
+ readonly description: "Counts D1 rows in the digest window using a created/processed timestamp column.";
5
+ };
6
+ readonly 'd1.row-count-by-event': {
7
+ readonly description: "Counts D1 rows whose event column matches one of the configured event names.";
8
+ };
9
+ readonly 'wae.event-count': {
10
+ readonly description: "Counts WAE rows for one event name and optional realm/site filter.";
11
+ };
12
+ readonly 'wae.event-count-by-blob': {
13
+ readonly description: "Counts one WAE event grouped by a blob field.";
14
+ };
15
+ readonly 'wae.unique-count': {
16
+ readonly description: "Counts distinct values in a WAE blob field.";
17
+ };
18
+ readonly 'wae.avg-double': {
19
+ readonly description: "Averages a WAE double field.";
20
+ };
21
+ readonly 'wae.top-n-by-blob': {
22
+ readonly description: "Returns the top N WAE blob values by event count.";
23
+ };
24
+ readonly 'composite.ratio': {
25
+ readonly description: "Computes numerator / denominator using nested metric definitions.";
26
+ };
27
+ };
28
+ export declare function computeMetric(metric: MetricDefinition, window: ResolvedDigestWindow, context: HandlerContext): Promise<MetricValue>;
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,oBAAoB,EACpB,MAAM,aAAa,CAAA;AAUpB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;CAyBlB,CAAA;AAEV,wBAAsB,aAAa,CAClC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,cAAc,GACrB,OAAO,CAAC,WAAW,CAAC,CAqBtB"}