@uptimizr/db 2.0.1 → 2.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.
- package/AGENTS.md +228 -3
- package/README.md +13 -0
- package/dist/duckdb/migrations.d.ts.map +1 -1
- package/dist/duckdb/migrations.js +169 -0
- package/dist/duckdb/migrations.js.map +1 -1
- package/dist/duckdb/panelSpecs.d.ts +29 -0
- package/dist/duckdb/panelSpecs.d.ts.map +1 -0
- package/dist/duckdb/panelSpecs.js +104 -0
- package/dist/duckdb/panelSpecs.js.map +1 -0
- package/dist/duckdb/projectMetadata.d.ts +43 -0
- package/dist/duckdb/projectMetadata.d.ts.map +1 -0
- package/dist/duckdb/projectMetadata.js +222 -0
- package/dist/duckdb/projectMetadata.js.map +1 -0
- package/dist/duckdb/subscriptions.d.ts +45 -0
- package/dist/duckdb/subscriptions.d.ts.map +1 -0
- package/dist/duckdb/subscriptions.js +202 -0
- package/dist/duckdb/subscriptions.js.map +1 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -3
- package/dist/index.js.map +1 -1
- package/dist/insights/anomalies.d.ts +213 -0
- package/dist/insights/anomalies.d.ts.map +1 -0
- package/dist/insights/anomalies.js +340 -0
- package/dist/insights/anomalies.js.map +1 -0
- package/dist/insights/baseline.d.ts +51 -0
- package/dist/insights/baseline.d.ts.map +1 -0
- package/dist/insights/baseline.js +48 -0
- package/dist/insights/baseline.js.map +1 -0
- package/dist/insights/buckets.d.ts +114 -0
- package/dist/insights/buckets.d.ts.map +1 -0
- package/dist/insights/buckets.js +220 -0
- package/dist/insights/buckets.js.map +1 -0
- package/dist/insights/changepoint.d.ts +130 -0
- package/dist/insights/changepoint.d.ts.map +1 -0
- package/dist/insights/changepoint.js +209 -0
- package/dist/insights/changepoint.js.map +1 -0
- package/dist/insights/evaluate.d.ts +63 -0
- package/dist/insights/evaluate.d.ts.map +1 -0
- package/dist/insights/evaluate.js +158 -0
- package/dist/insights/evaluate.js.map +1 -0
- package/dist/insights/health.d.ts +219 -0
- package/dist/insights/health.d.ts.map +1 -0
- package/dist/insights/health.js +349 -0
- package/dist/insights/health.js.map +1 -0
- package/dist/insights/index.d.ts +45 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +40 -0
- package/dist/insights/index.js.map +1 -0
- package/dist/insights/measures.d.ts +222 -0
- package/dist/insights/measures.d.ts.map +1 -0
- package/dist/insights/measures.js +431 -0
- package/dist/insights/measures.js.map +1 -0
- package/dist/insights/movers.d.ts +123 -0
- package/dist/insights/movers.d.ts.map +1 -0
- package/dist/insights/movers.js +166 -0
- package/dist/insights/movers.js.map +1 -0
- package/dist/insights/significance.d.ts +221 -0
- package/dist/insights/significance.d.ts.map +1 -0
- package/dist/insights/significance.js +531 -0
- package/dist/insights/significance.js.map +1 -0
- package/dist/insights/stats.d.ts +104 -0
- package/dist/insights/stats.d.ts.map +1 -0
- package/dist/insights/stats.js +181 -0
- package/dist/insights/stats.js.map +1 -0
- package/dist/insights/windows.d.ts +95 -0
- package/dist/insights/windows.d.ts.map +1 -0
- package/dist/insights/windows.js +97 -0
- package/dist/insights/windows.js.map +1 -0
- package/dist/metadata.d.ts +178 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +80 -0
- package/dist/metadata.js.map +1 -1
- package/dist/narrative/build.d.ts +78 -0
- package/dist/narrative/build.d.ts.map +1 -0
- package/dist/narrative/build.js +437 -0
- package/dist/narrative/build.js.map +1 -0
- package/dist/narrative/index.d.ts +14 -0
- package/dist/narrative/index.d.ts.map +1 -0
- package/dist/narrative/index.js +13 -0
- package/dist/narrative/index.js.map +1 -0
- package/dist/narrative/text.d.ts +25 -0
- package/dist/narrative/text.d.ts.map +1 -0
- package/dist/narrative/text.js +41 -0
- package/dist/narrative/text.js.map +1 -0
- package/dist/parity/cases.d.ts +3 -0
- package/dist/parity/cases.d.ts.map +1 -1
- package/dist/parity/cases.js +387 -7
- package/dist/parity/cases.js.map +1 -1
- package/dist/parity/compare.d.ts +19 -0
- package/dist/parity/compare.d.ts.map +1 -1
- package/dist/parity/compare.js +19 -0
- package/dist/parity/compare.js.map +1 -1
- package/dist/parity/fixtures.d.ts.map +1 -1
- package/dist/parity/fixtures.js +25 -0
- package/dist/parity/fixtures.js.map +1 -1
- package/dist/query/aggregations.d.ts +46 -1
- package/dist/query/aggregations.d.ts.map +1 -1
- package/dist/query/aggregations.js +86 -0
- package/dist/query/aggregations.js.map +1 -1
- package/dist/query/clickhouseDialect.d.ts.map +1 -1
- package/dist/query/clickhouseDialect.js +5 -1
- package/dist/query/clickhouseDialect.js.map +1 -1
- package/dist/query/customEventVocabulary.d.ts +46 -0
- package/dist/query/customEventVocabulary.d.ts.map +1 -0
- package/dist/query/customEventVocabulary.js +127 -0
- package/dist/query/customEventVocabulary.js.map +1 -0
- package/dist/query/dsl/builders.d.ts +38 -0
- package/dist/query/dsl/builders.d.ts.map +1 -0
- package/dist/query/dsl/builders.js +35 -0
- package/dist/query/dsl/builders.js.map +1 -0
- package/dist/query/dsl/compare.d.ts +167 -0
- package/dist/query/dsl/compare.d.ts.map +1 -0
- package/dist/query/dsl/compare.js +358 -0
- package/dist/query/dsl/compare.js.map +1 -0
- package/dist/query/dsl/compile.d.ts +89 -0
- package/dist/query/dsl/compile.d.ts.map +1 -0
- package/dist/query/dsl/compile.js +174 -0
- package/dist/query/dsl/compile.js.map +1 -0
- package/dist/query/dsl/explain.d.ts +115 -0
- package/dist/query/dsl/explain.d.ts.map +1 -0
- package/dist/query/dsl/explain.js +143 -0
- package/dist/query/dsl/explain.js.map +1 -0
- package/dist/query/dsl/generic.d.ts +127 -0
- package/dist/query/dsl/generic.d.ts.map +1 -0
- package/dist/query/dsl/generic.js +323 -0
- package/dist/query/dsl/generic.js.map +1 -0
- package/dist/query/dsl/index.d.ts +30 -0
- package/dist/query/dsl/index.d.ts.map +1 -0
- package/dist/query/dsl/index.js +23 -0
- package/dist/query/dsl/index.js.map +1 -0
- package/dist/query/dsl/order.d.ts +41 -0
- package/dist/query/dsl/order.d.ts.map +1 -0
- package/dist/query/dsl/order.js +55 -0
- package/dist/query/dsl/order.js.map +1 -0
- package/dist/query/dsl/significance.d.ts +130 -0
- package/dist/query/dsl/significance.d.ts.map +1 -0
- package/dist/query/dsl/significance.js +224 -0
- package/dist/query/dsl/significance.js.map +1 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +8 -0
- package/dist/query/index.js.map +1 -1
- package/dist/query/summary/index.d.ts +4 -2
- package/dist/query/summary/index.d.ts.map +1 -1
- package/dist/query/summary/index.js +2 -1
- package/dist/query/summary/index.js.map +1 -1
- package/dist/query/summary/labels.d.ts +101 -0
- package/dist/query/summary/labels.d.ts.map +1 -0
- package/dist/query/summary/labels.js +261 -0
- package/dist/query/summary/labels.js.map +1 -0
- package/dist/query/summary/reading.d.ts.map +1 -1
- package/dist/query/summary/reading.js +21 -2
- package/dist/query/summary/reading.js.map +1 -1
- package/dist/query/summary/schema.d.ts +250 -20
- package/dist/query/summary/schema.d.ts.map +1 -1
- package/dist/query/summary/schema.js +135 -123
- package/dist/query/summary/schema.js.map +1 -1
- package/dist/query/summary/summarize.d.ts.map +1 -1
- package/dist/query/summary/summarize.js +91 -5
- package/dist/query/summary/summarize.js.map +1 -1
- package/dist/query/summary/types.d.ts +77 -2
- package/dist/query/summary/types.d.ts.map +1 -1
- package/dist/query/types.d.ts +47 -1
- package/dist/query/types.d.ts.map +1 -1
- package/dist/subscriptions.d.ts +208 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +153 -0
- package/dist/subscriptions.js.map +1 -0
- package/llms.txt +9 -0
- package/package.json +8 -8
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The one generic bucket query** behind both insight primitives
|
|
3
|
+
* (ADR 0051 §4, design sketch §D).
|
|
4
|
+
*
|
|
5
|
+
* `buildMetricBuckets` renders a {@link BucketMeasure} — the declarative
|
|
6
|
+
* description of how a comparable metric's primary column is reproduced per
|
|
7
|
+
* time bucket, from `measures.ts` — into a single grouped scan of `events`:
|
|
8
|
+
*
|
|
9
|
+
* ```sql
|
|
10
|
+
* SELECT <time bucket> AS bucket, <aggregate> AS value, <denominator> AS sample_size
|
|
11
|
+
* FROM events
|
|
12
|
+
* WHERE project_id = ? AND event_type IN (…) AND …
|
|
13
|
+
* GROUP BY bucket ORDER BY bucket ASC
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* It is authored against the {@link Dialect} interface like every aggregation in
|
|
17
|
+
* `query/aggregations.ts`, and parity-tested on all four engines through
|
|
18
|
+
* `src/parity/cases.ts`. It deliberately does **not** live in
|
|
19
|
+
* `query/aggregations.ts`: the `build*` exports of that module are the registry's
|
|
20
|
+
* closed list of *metrics*, each of which must have its own registry entry and
|
|
21
|
+
* endpoint. This is not a metric — it is the shared input of two, and has no
|
|
22
|
+
* endpoint, no row schema and no tool of its own. Keeping it here keeps the
|
|
23
|
+
* registry's coverage invariant meaningful.
|
|
24
|
+
*
|
|
25
|
+
* No statistic is computed in SQL. The query returns raw per-bucket values and
|
|
26
|
+
* everything else — mean, median, MAD, quantiles, slope, robust z — is pure
|
|
27
|
+
* TypeScript in `stats.ts`, so no two engines can disagree about an insight.
|
|
28
|
+
*/
|
|
29
|
+
import { ParamBag, rangeClause, sceneClause } from "../query/dialect.js";
|
|
30
|
+
import { BUCKET_SECONDS, BUCKET_SPLIT_COLUMNS, resolveBucketMeasure, } from "./measures.js";
|
|
31
|
+
/** The SQL expression for a promoted value column, per aggregate. */
|
|
32
|
+
function valueColumnExpr(column, aggregate, d) {
|
|
33
|
+
switch (column) {
|
|
34
|
+
case "fps":
|
|
35
|
+
return "fps";
|
|
36
|
+
case "visible_ms":
|
|
37
|
+
return "visible_ms";
|
|
38
|
+
case "long_frames":
|
|
39
|
+
// Promoted, `NOT NULL DEFAULT 0`, and a 0 here is a real observation — a
|
|
40
|
+
// sampled window in which no frame ran long. Unlike `js_heap_bytes` it is
|
|
41
|
+
// never nulled out: excluding the good windows would report every project
|
|
42
|
+
// as permanently janky.
|
|
43
|
+
return "long_frames";
|
|
44
|
+
case "js_heap_bytes":
|
|
45
|
+
// A heap reading of 0 means "the browser did not report one" (the column
|
|
46
|
+
// is `NOT NULL DEFAULT 0`), so averages and percentiles must exclude it —
|
|
47
|
+
// exactly what `buildResourceSummary` / `buildResourcePercentiles` do. A
|
|
48
|
+
// `max` keeps the raw column: the largest of a set that includes 0 is
|
|
49
|
+
// unaffected by it, and nulling it out would only cost a branch.
|
|
50
|
+
return aggregate === "max" ? "js_heap_bytes" : "nullIf(js_heap_bytes, 0)";
|
|
51
|
+
case "ar_placement_scale":
|
|
52
|
+
// Not promoted; read from the payload exactly as `buildArPlacementSurfaces`
|
|
53
|
+
// reads it, so the bucket series and the metric agree.
|
|
54
|
+
return d.jsonFloat("payload", "scale");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The `SELECT` expression that produces a bucket's value. */
|
|
58
|
+
function aggregateExpr(aggregate, d) {
|
|
59
|
+
switch (aggregate.kind) {
|
|
60
|
+
case "count":
|
|
61
|
+
return "count(*)";
|
|
62
|
+
case "sessions":
|
|
63
|
+
return "count(DISTINCT session_id)";
|
|
64
|
+
case "sum":
|
|
65
|
+
return `sum(${valueColumnExpr(aggregate.column, "sum", d)})`;
|
|
66
|
+
case "avg":
|
|
67
|
+
return `avg(${valueColumnExpr(aggregate.column, "avg", d)})`;
|
|
68
|
+
case "max":
|
|
69
|
+
return `max(${valueColumnExpr(aggregate.column, "max", d)})`;
|
|
70
|
+
case "quantile":
|
|
71
|
+
return d.quantile(valueColumnExpr(aggregate.column, "quantile", d), aggregate.q);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The `SELECT` expression for the bucket's denominator.
|
|
76
|
+
*
|
|
77
|
+
* For a session-valued measure the denominator *is* the distinct-session count
|
|
78
|
+
* (`minSample: 30` on `view_coverage_histogram` means thirty sessions, not
|
|
79
|
+
* thirty camera samples); for every other measure it is the number of events
|
|
80
|
+
* that contributed.
|
|
81
|
+
*/
|
|
82
|
+
function sampleSizeExpr(aggregate) {
|
|
83
|
+
return aggregate.kind === "sessions" ? "count(DISTINCT session_id)" : "count(*)";
|
|
84
|
+
}
|
|
85
|
+
/** `AND event_type IN (…)`, or `""` when the measure counts every channel. */
|
|
86
|
+
function eventTypeClause(bag, measure) {
|
|
87
|
+
const types = measure.eventTypes;
|
|
88
|
+
if (types.length === 0)
|
|
89
|
+
return "";
|
|
90
|
+
if (types.length === 1) {
|
|
91
|
+
return ` AND event_type = ${bag.add("bmType0", "string", types[0])}`;
|
|
92
|
+
}
|
|
93
|
+
const placeholders = types.map((type, index) => bag.add(`bmType${index}`, "string", type));
|
|
94
|
+
return ` AND event_type IN (${placeholders.join(", ")})`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* One extra predicate, rendered from the closed vocabulary in `measures.ts`.
|
|
98
|
+
*
|
|
99
|
+
* Column names come from a compile-time union and values are always bound as
|
|
100
|
+
* parameters, so nothing a caller can influence reaches the SQL text.
|
|
101
|
+
*/
|
|
102
|
+
function predicateClause(bag, d, predicate, index) {
|
|
103
|
+
switch (predicate.kind) {
|
|
104
|
+
case "eq":
|
|
105
|
+
return ` AND ${predicate.column} = ${bag.add(`bmP${index}`, "string", predicate.value)}`;
|
|
106
|
+
case "ne":
|
|
107
|
+
return ` AND ${predicate.column} <> ${bag.add(`bmP${index}`, "string", predicate.value)}`;
|
|
108
|
+
case "in": {
|
|
109
|
+
const placeholders = predicate.values.map((value, i) => bag.add(`bmP${index}_${i}`, "string", value));
|
|
110
|
+
return ` AND ${predicate.column} IN (${placeholders.join(", ")})`;
|
|
111
|
+
}
|
|
112
|
+
case "geometry":
|
|
113
|
+
return ` AND ${d.arrayLength(predicate.column)} = ${predicate.arity}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the per-bucket series of a metric's comparable primary column.
|
|
118
|
+
*
|
|
119
|
+
* Throws when the metric has no portable bucket form — the route validates
|
|
120
|
+
* against `BUCKETABLE_METRIC_IDS` at the edge and answers `400`, so reaching
|
|
121
|
+
* this is a programming error rather than bad input.
|
|
122
|
+
*/
|
|
123
|
+
export function buildMetricBuckets(projectId, opts, d) {
|
|
124
|
+
const measure = resolveBucketMeasure(opts.metric, opts.series);
|
|
125
|
+
if (measure == null) {
|
|
126
|
+
throw new Error(opts.series == null
|
|
127
|
+
? `metric '${opts.metric}' has no portable bucket series`
|
|
128
|
+
: `metric '${opts.metric}' declares no '${opts.series}' series`);
|
|
129
|
+
}
|
|
130
|
+
const bag = new ParamBag(d);
|
|
131
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
132
|
+
const seconds = BUCKET_SECONDS[opts.bucket ?? "day"];
|
|
133
|
+
const interval = bag.add("bmInterval", "u32", seconds);
|
|
134
|
+
const types = eventTypeClause(bag, measure);
|
|
135
|
+
const range = rangeClause(bag, opts);
|
|
136
|
+
const scene = sceneClause(bag, opts);
|
|
137
|
+
const predicates = (measure.where ?? [])
|
|
138
|
+
.map((predicate, index) => predicateClause(bag, d, predicate, index))
|
|
139
|
+
.join("");
|
|
140
|
+
// --- anomalies (#306): the optional grouped split ------------------------
|
|
141
|
+
// The column comes from the measure's own declaration via a compile-time
|
|
142
|
+
// union, so the only way a name reaches the SQL text is by being in
|
|
143
|
+
// `BUCKET_SPLIT_COLUMNS`. The grouping key is projected, grouped and ordered
|
|
144
|
+
// by so the rows arrive in a stable order on every engine.
|
|
145
|
+
const splitColumn = opts.groupBy == null ? null : BUCKET_SPLIT_COLUMNS[opts.groupBy];
|
|
146
|
+
const splitSelect = splitColumn == null ? "" : `,\n ${splitColumn} AS dimension_value`;
|
|
147
|
+
const splitGroup = splitColumn == null ? "" : `, ${splitColumn}`;
|
|
148
|
+
const splitOrder = splitColumn == null ? "" : `, ${splitColumn} ASC`;
|
|
149
|
+
return {
|
|
150
|
+
query: `
|
|
151
|
+
SELECT
|
|
152
|
+
${d.timeBucketMs("ts", interval)} AS bucket,
|
|
153
|
+
${aggregateExpr(measure.aggregate, d)} AS value,
|
|
154
|
+
${sampleSizeExpr(measure.aggregate)} AS sample_size${splitSelect}
|
|
155
|
+
FROM events
|
|
156
|
+
WHERE project_id = ${pid}${types}${range}${scene}${predicates}
|
|
157
|
+
GROUP BY bucket${splitGroup}
|
|
158
|
+
ORDER BY bucket ASC${splitOrder}
|
|
159
|
+
`,
|
|
160
|
+
query_params: bag.values,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Total order over bucket rows: by bucket, then by the split value on a grouped
|
|
165
|
+
* read (#306).
|
|
166
|
+
*
|
|
167
|
+
* Compared by code point rather than `localeCompare`, so the order is a property
|
|
168
|
+
* of the data instead of the ICU build the collector happens to run on — the same
|
|
169
|
+
* reason every statistic in this directory is computed in TypeScript.
|
|
170
|
+
*/
|
|
171
|
+
export function byBucketThenDimension(a, b) {
|
|
172
|
+
if (a.bucket !== b.bucket)
|
|
173
|
+
return a.bucket - b.bucket;
|
|
174
|
+
const left = a.dimension_value ?? "";
|
|
175
|
+
const right = b.dimension_value ?? "";
|
|
176
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Normalise driver output into a {@link MetricBucketRow}.
|
|
180
|
+
*
|
|
181
|
+
* `buildMetricBuckets` produces no registry metric, so it carries no
|
|
182
|
+
* `QuerySpec.metric` tag and the store-edge coercion (ADR 0051 §2) has no row
|
|
183
|
+
* schema to coerce it against. ClickHouse string-encodes 64-bit counts over
|
|
184
|
+
* HTTP, so the numbers are parsed here instead — the one place every store's
|
|
185
|
+
* bucket read passes through.
|
|
186
|
+
*/
|
|
187
|
+
export function toMetricBucketRows(rows) {
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
const bucket = toNumber(row.bucket);
|
|
191
|
+
if (bucket == null)
|
|
192
|
+
continue;
|
|
193
|
+
out.push({
|
|
194
|
+
bucket,
|
|
195
|
+
value: toNumber(row.value),
|
|
196
|
+
sample_size: toNumber(row.sample_size) ?? 0,
|
|
197
|
+
// Present only on a grouped read (#306); a null group key is the store's
|
|
198
|
+
// "unknown", which every other aggregation also renders as `''`.
|
|
199
|
+
...(Object.prototype.hasOwnProperty.call(row, "dimension_value")
|
|
200
|
+
? { dimension_value: row.dimension_value == null ? "" : String(row.dimension_value) }
|
|
201
|
+
: {}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// Stable on both shapes: bucket first, then the split value, so a grouped
|
|
205
|
+
// series is grouped in TypeScript in the order the engines returned it.
|
|
206
|
+
return out.sort(byBucketThenDimension);
|
|
207
|
+
}
|
|
208
|
+
/** A finite number from a driver cell, or `null`. */
|
|
209
|
+
function toNumber(value) {
|
|
210
|
+
if (typeof value === "number")
|
|
211
|
+
return Number.isFinite(value) ? value : null;
|
|
212
|
+
if (typeof value === "bigint")
|
|
213
|
+
return Number(value);
|
|
214
|
+
if (typeof value === "string") {
|
|
215
|
+
const parsed = Number(value.trim());
|
|
216
|
+
return value.trim() !== "" && Number.isFinite(parsed) ? parsed : null;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=buckets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buckets.js","sourceRoot":"","sources":["../../src/insights/buckets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Rolling robust statistics and change-point detection** (ADR 0051 §4,
|
|
3
|
+
* design sketch §D row `anomalies`).
|
|
4
|
+
*
|
|
5
|
+
* Two generic, series-shaped algorithms that `anomalies.ts` composes, kept in
|
|
6
|
+
* their own module for the same reason `stats.ts` exists: they are pure
|
|
7
|
+
* functions of `readonly number[]` with no notion of a metric, a bucket, a store
|
|
8
|
+
* or a dialect, and they are unit-tested against hand-built series rather than
|
|
9
|
+
* against a query result.
|
|
10
|
+
*
|
|
11
|
+
* ## Why these two, and not a model
|
|
12
|
+
*
|
|
13
|
+
* A self-hosted collector has one project's history and no training budget, so
|
|
14
|
+
* an anomaly detector here has to work on a few dozen buckets, explain itself in
|
|
15
|
+
* one number, and never need tuning per metric. Two classical methods do exactly
|
|
16
|
+
* that, and they answer *different* questions — which is why both are here:
|
|
17
|
+
*
|
|
18
|
+
* - {@link rollingRobustStats} answers **"is this bucket unlike the ones just
|
|
19
|
+
* before it?"** — a single surprising day. It is a *point* detector.
|
|
20
|
+
* - {@link cusumChangePoints} answers **"did the level move and stay moved?"** —
|
|
21
|
+
* a regression that is only 1.5 MADs deep and so never trips a point detector,
|
|
22
|
+
* but that persists for a fortnight. It is a *sequential* detector, and it is
|
|
23
|
+
* the one that catches the shape a release regression actually has.
|
|
24
|
+
*
|
|
25
|
+
* Both are robust by construction: the centre is a median and the scale is a
|
|
26
|
+
* median absolute deviation, so one catastrophic bucket in the trailing window
|
|
27
|
+
* cannot widen the tolerance until nothing is ever abnormal again.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The trailing centre and spread for one index of a series.
|
|
31
|
+
*
|
|
32
|
+
* `null` in both fields means the index had too little history to be judged —
|
|
33
|
+
* never `0`, which would claim a perfectly flat past.
|
|
34
|
+
*/
|
|
35
|
+
export interface RollingRobustPoint {
|
|
36
|
+
/** Median of the trailing window; `null` when it held too few values. */
|
|
37
|
+
median: number | null;
|
|
38
|
+
/** Median absolute deviation of the same window; `null` alongside a null median. */
|
|
39
|
+
mad: number | null;
|
|
40
|
+
/** How many values the trailing window actually carried. */
|
|
41
|
+
count: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The trailing median and MAD for every index of `values`.
|
|
45
|
+
*
|
|
46
|
+
* The window for index `i` is `values[i - window .. i - 1]` — **the point itself
|
|
47
|
+
* is excluded**, which is the whole point. Including it drags the median toward
|
|
48
|
+
* the very value being judged and inflates the MAD by the deviation being
|
|
49
|
+
* measured, so a big enough spike partly hides itself. Excluding it makes the
|
|
50
|
+
* comparison "against what came before", which is what a reader means by
|
|
51
|
+
* "unusual".
|
|
52
|
+
*
|
|
53
|
+
* Indices with fewer than `minCount` trailing values report `null`: the first
|
|
54
|
+
* days of a project are not evidence about the first days of a project, and a
|
|
55
|
+
* detector that flags them produces a page of findings on every new install.
|
|
56
|
+
*
|
|
57
|
+
* Non-finite entries are skipped (they never enter a window and are never
|
|
58
|
+
* scored), so a sparse series behaves like the shorter series it really is.
|
|
59
|
+
*/
|
|
60
|
+
export declare function rollingRobustStats(values: readonly number[], window: number, minCount: number): RollingRobustPoint[];
|
|
61
|
+
/**
|
|
62
|
+
* Slack ("allowance") of the tabular CUSUM, in MAD units.
|
|
63
|
+
*
|
|
64
|
+
* The accumulator only grows once a deviation exceeds this, so ordinary
|
|
65
|
+
* bucket-to-bucket noise cancels instead of drifting upward forever. Half a MAD
|
|
66
|
+
* is the textbook choice — it is the shift size the chart is tuned to detect
|
|
67
|
+
* fastest — and keeping it a constant rather than a parameter is deliberate:
|
|
68
|
+
* `sensitivity` already moves the decision threshold, and two dials that both
|
|
69
|
+
* mean "how eager" would be impossible for a caller to reason about.
|
|
70
|
+
*/
|
|
71
|
+
export declare const CUSUM_SLACK = 0.5;
|
|
72
|
+
/**
|
|
73
|
+
* The decision threshold, as a multiple of `sensitivity`.
|
|
74
|
+
*
|
|
75
|
+
* The alarm fires when the accumulated excess passes
|
|
76
|
+
* `sensitivity × CUSUM_DECISION_FACTOR` MADs. At the default `sensitivity` of 3
|
|
77
|
+
* that is 6 MADs of *accumulated* excess — reachable by four consecutive buckets
|
|
78
|
+
* two MADs off, or by a dozen buckets barely off, but not by noise. It keeps the
|
|
79
|
+
* one dial monotone: a higher `sensitivity` means fewer findings for the point
|
|
80
|
+
* detector and for the change-point detector alike.
|
|
81
|
+
*/
|
|
82
|
+
export declare const CUSUM_DECISION_FACTOR = 2;
|
|
83
|
+
/** A detected level shift in a series. */
|
|
84
|
+
export interface ChangePoint {
|
|
85
|
+
/** Index at which the shift began — the last point before the drift started. */
|
|
86
|
+
index: number;
|
|
87
|
+
/** Direction of the shift. */
|
|
88
|
+
direction: "up" | "down";
|
|
89
|
+
/** Median of the segment before the change-point. */
|
|
90
|
+
before: number | null;
|
|
91
|
+
/** Median of the segment from the change-point to the next one (or the end). */
|
|
92
|
+
after: number | null;
|
|
93
|
+
/** Accumulated excess, in MADs, at the bucket that raised the alarm. */
|
|
94
|
+
score: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Two-sided tabular CUSUM over `values`, segmented so several level changes in
|
|
98
|
+
* one series are found rather than one.
|
|
99
|
+
*
|
|
100
|
+
* ```
|
|
101
|
+
* eᵢ = clip((xᵢ − reference) / spread, ±sensitivity)
|
|
102
|
+
* S⁺ᵢ = max(0, S⁺ᵢ₋₁ + eᵢ − k) S⁻ᵢ = max(0, S⁻ᵢ₋₁ − eᵢ − k)
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* Three decisions make this a *change-point* detector rather than a chart:
|
|
106
|
+
*
|
|
107
|
+
* 1. **The reference is the level the segment started at** — the median of its
|
|
108
|
+
* first `referenceLength` values — not the median of the whole series. With a
|
|
109
|
+
* whole-series centre, a series that steps up halfway sits *above* its own
|
|
110
|
+
* median for the entire second half and *below* it for the entire first, so
|
|
111
|
+
* the accumulator starts drifting at index 0 and the alarm names the wrong
|
|
112
|
+
* bucket. Measuring against where the series began is what makes "it moved"
|
|
113
|
+
* a statement about a moment.
|
|
114
|
+
* 2. **An alarm is reported where the accumulator last left zero**, not where it
|
|
115
|
+
* crossed the threshold. A CUSUM crosses several buckets after the level
|
|
116
|
+
* actually moved — that lag is how it earns its sensitivity — and reporting
|
|
117
|
+
* the crossing would put the finding on the wrong day.
|
|
118
|
+
* 3. **After an alarm the series is re-segmented from the change-point**, with
|
|
119
|
+
* the reference re-estimated over the new regime. Without that, a shifted
|
|
120
|
+
* series keeps deviating from the old reference forever and alarms again
|
|
121
|
+
* every few buckets; with it, a level that moves and moves back reports
|
|
122
|
+
* exactly two change-points.
|
|
123
|
+
*
|
|
124
|
+
* `spread` is supplied by the caller (`anomalies.ts` passes the same floored MAD
|
|
125
|
+
* the point detector uses, so both halves of the primitive are measured in one
|
|
126
|
+
* unit). A non-positive or missing `spread` means there is nothing to detect and
|
|
127
|
+
* the result is empty.
|
|
128
|
+
*/
|
|
129
|
+
export declare function cusumChangePoints(values: readonly number[], spread: number | null, sensitivity: number, referenceLength: number): ChangePoint[];
|
|
130
|
+
//# sourceMappingURL=changepoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changepoint.d.ts","sourceRoot":"","sources":["../../src/insights/changepoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,oFAAoF;IACpF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,kBAAkB,EAAE,CAyBtB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,MAAM,CAAC;AAE/B;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAoBvC,0CAA0C;AAC1C,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,qDAAqD;IACrD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,GACtB,WAAW,EAAE,CAyCf"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Rolling robust statistics and change-point detection** (ADR 0051 §4,
|
|
3
|
+
* design sketch §D row `anomalies`).
|
|
4
|
+
*
|
|
5
|
+
* Two generic, series-shaped algorithms that `anomalies.ts` composes, kept in
|
|
6
|
+
* their own module for the same reason `stats.ts` exists: they are pure
|
|
7
|
+
* functions of `readonly number[]` with no notion of a metric, a bucket, a store
|
|
8
|
+
* or a dialect, and they are unit-tested against hand-built series rather than
|
|
9
|
+
* against a query result.
|
|
10
|
+
*
|
|
11
|
+
* ## Why these two, and not a model
|
|
12
|
+
*
|
|
13
|
+
* A self-hosted collector has one project's history and no training budget, so
|
|
14
|
+
* an anomaly detector here has to work on a few dozen buckets, explain itself in
|
|
15
|
+
* one number, and never need tuning per metric. Two classical methods do exactly
|
|
16
|
+
* that, and they answer *different* questions — which is why both are here:
|
|
17
|
+
*
|
|
18
|
+
* - {@link rollingRobustStats} answers **"is this bucket unlike the ones just
|
|
19
|
+
* before it?"** — a single surprising day. It is a *point* detector.
|
|
20
|
+
* - {@link cusumChangePoints} answers **"did the level move and stay moved?"** —
|
|
21
|
+
* a regression that is only 1.5 MADs deep and so never trips a point detector,
|
|
22
|
+
* but that persists for a fortnight. It is a *sequential* detector, and it is
|
|
23
|
+
* the one that catches the shape a release regression actually has.
|
|
24
|
+
*
|
|
25
|
+
* Both are robust by construction: the centre is a median and the scale is a
|
|
26
|
+
* median absolute deviation, so one catastrophic bucket in the trailing window
|
|
27
|
+
* cannot widen the tolerance until nothing is ever abnormal again.
|
|
28
|
+
*/
|
|
29
|
+
import { median, medianAbsoluteDeviation } from "./stats.js";
|
|
30
|
+
/**
|
|
31
|
+
* The trailing median and MAD for every index of `values`.
|
|
32
|
+
*
|
|
33
|
+
* The window for index `i` is `values[i - window .. i - 1]` — **the point itself
|
|
34
|
+
* is excluded**, which is the whole point. Including it drags the median toward
|
|
35
|
+
* the very value being judged and inflates the MAD by the deviation being
|
|
36
|
+
* measured, so a big enough spike partly hides itself. Excluding it makes the
|
|
37
|
+
* comparison "against what came before", which is what a reader means by
|
|
38
|
+
* "unusual".
|
|
39
|
+
*
|
|
40
|
+
* Indices with fewer than `minCount` trailing values report `null`: the first
|
|
41
|
+
* days of a project are not evidence about the first days of a project, and a
|
|
42
|
+
* detector that flags them produces a page of findings on every new install.
|
|
43
|
+
*
|
|
44
|
+
* Non-finite entries are skipped (they never enter a window and are never
|
|
45
|
+
* scored), so a sparse series behaves like the shorter series it really is.
|
|
46
|
+
*/
|
|
47
|
+
export function rollingRobustStats(values, window, minCount) {
|
|
48
|
+
const out = [];
|
|
49
|
+
const width = Math.max(1, Math.floor(window));
|
|
50
|
+
const floor = Math.max(1, Math.floor(minCount));
|
|
51
|
+
// The trailing values in a plain array: the windows are tens to a few hundred
|
|
52
|
+
// entries and a median needs them sorted anyway, so an incremental structure
|
|
53
|
+
// would add ordering bugs to buy nothing measurable.
|
|
54
|
+
const trailing = [];
|
|
55
|
+
for (const value of values) {
|
|
56
|
+
if (trailing.length < floor) {
|
|
57
|
+
out.push({ median: null, mad: null, count: trailing.length });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const centre = median(trailing);
|
|
61
|
+
out.push({
|
|
62
|
+
median: centre,
|
|
63
|
+
mad: medianAbsoluteDeviation(trailing),
|
|
64
|
+
count: trailing.length,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (Number.isFinite(value)) {
|
|
68
|
+
trailing.push(value);
|
|
69
|
+
if (trailing.length > width)
|
|
70
|
+
trailing.shift();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Slack ("allowance") of the tabular CUSUM, in MAD units.
|
|
77
|
+
*
|
|
78
|
+
* The accumulator only grows once a deviation exceeds this, so ordinary
|
|
79
|
+
* bucket-to-bucket noise cancels instead of drifting upward forever. Half a MAD
|
|
80
|
+
* is the textbook choice — it is the shift size the chart is tuned to detect
|
|
81
|
+
* fastest — and keeping it a constant rather than a parameter is deliberate:
|
|
82
|
+
* `sensitivity` already moves the decision threshold, and two dials that both
|
|
83
|
+
* mean "how eager" would be impossible for a caller to reason about.
|
|
84
|
+
*/
|
|
85
|
+
export const CUSUM_SLACK = 0.5;
|
|
86
|
+
/**
|
|
87
|
+
* The decision threshold, as a multiple of `sensitivity`.
|
|
88
|
+
*
|
|
89
|
+
* The alarm fires when the accumulated excess passes
|
|
90
|
+
* `sensitivity × CUSUM_DECISION_FACTOR` MADs. At the default `sensitivity` of 3
|
|
91
|
+
* that is 6 MADs of *accumulated* excess — reachable by four consecutive buckets
|
|
92
|
+
* two MADs off, or by a dozen buckets barely off, but not by noise. It keeps the
|
|
93
|
+
* one dial monotone: a higher `sensitivity` means fewer findings for the point
|
|
94
|
+
* detector and for the change-point detector alike.
|
|
95
|
+
*/
|
|
96
|
+
export const CUSUM_DECISION_FACTOR = 2;
|
|
97
|
+
/**
|
|
98
|
+
* Why each deviation is clamped to ±`sensitivity` before it is accumulated.
|
|
99
|
+
*
|
|
100
|
+
* A textbook CUSUM alarms on a single large outlier: one bucket ten MADs out
|
|
101
|
+
* contributes 9.5 on its own and trips any reasonable threshold, so every spike
|
|
102
|
+
* would also be reported as a level shift and `kind` would stop meaning
|
|
103
|
+
* anything. Winsorising at exactly the point detector's own threshold fixes
|
|
104
|
+
* that by construction: a bucket the point detector already calls a `spike`
|
|
105
|
+
* contributes at most `sensitivity − k`, so **no single bucket can raise a
|
|
106
|
+
* change-point alarm** at the default settings — it takes three saturated
|
|
107
|
+
* buckets in a row, or a longer run of smaller ones. That is precisely the
|
|
108
|
+
* difference between "a bad Tuesday" and "it has been worse since Tuesday",
|
|
109
|
+
* which is the distinction `kind` exists to make.
|
|
110
|
+
*/
|
|
111
|
+
function winsorise(deviation, limit) {
|
|
112
|
+
return Math.max(-limit, Math.min(limit, deviation));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Two-sided tabular CUSUM over `values`, segmented so several level changes in
|
|
116
|
+
* one series are found rather than one.
|
|
117
|
+
*
|
|
118
|
+
* ```
|
|
119
|
+
* eᵢ = clip((xᵢ − reference) / spread, ±sensitivity)
|
|
120
|
+
* S⁺ᵢ = max(0, S⁺ᵢ₋₁ + eᵢ − k) S⁻ᵢ = max(0, S⁻ᵢ₋₁ − eᵢ − k)
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* Three decisions make this a *change-point* detector rather than a chart:
|
|
124
|
+
*
|
|
125
|
+
* 1. **The reference is the level the segment started at** — the median of its
|
|
126
|
+
* first `referenceLength` values — not the median of the whole series. With a
|
|
127
|
+
* whole-series centre, a series that steps up halfway sits *above* its own
|
|
128
|
+
* median for the entire second half and *below* it for the entire first, so
|
|
129
|
+
* the accumulator starts drifting at index 0 and the alarm names the wrong
|
|
130
|
+
* bucket. Measuring against where the series began is what makes "it moved"
|
|
131
|
+
* a statement about a moment.
|
|
132
|
+
* 2. **An alarm is reported where the accumulator last left zero**, not where it
|
|
133
|
+
* crossed the threshold. A CUSUM crosses several buckets after the level
|
|
134
|
+
* actually moved — that lag is how it earns its sensitivity — and reporting
|
|
135
|
+
* the crossing would put the finding on the wrong day.
|
|
136
|
+
* 3. **After an alarm the series is re-segmented from the change-point**, with
|
|
137
|
+
* the reference re-estimated over the new regime. Without that, a shifted
|
|
138
|
+
* series keeps deviating from the old reference forever and alarms again
|
|
139
|
+
* every few buckets; with it, a level that moves and moves back reports
|
|
140
|
+
* exactly two change-points.
|
|
141
|
+
*
|
|
142
|
+
* `spread` is supplied by the caller (`anomalies.ts` passes the same floored MAD
|
|
143
|
+
* the point detector uses, so both halves of the primitive are measured in one
|
|
144
|
+
* unit). A non-positive or missing `spread` means there is nothing to detect and
|
|
145
|
+
* the result is empty.
|
|
146
|
+
*/
|
|
147
|
+
export function cusumChangePoints(values, spread, sensitivity, referenceLength) {
|
|
148
|
+
const out = [];
|
|
149
|
+
if (spread == null || !(spread > 0) || values.length < 2)
|
|
150
|
+
return out;
|
|
151
|
+
const threshold = Math.max(0, sensitivity) * CUSUM_DECISION_FACTOR;
|
|
152
|
+
if (!(threshold > 0))
|
|
153
|
+
return out;
|
|
154
|
+
const reference = Math.max(2, Math.floor(referenceLength));
|
|
155
|
+
let from = 0;
|
|
156
|
+
while (from < values.length) {
|
|
157
|
+
const segment = values.slice(from);
|
|
158
|
+
const alarm = firstAlarm(segment, median(segment.slice(0, Math.min(reference, segment.length))), spread, threshold, sensitivity);
|
|
159
|
+
if (alarm == null)
|
|
160
|
+
break;
|
|
161
|
+
const at = from + alarm.index;
|
|
162
|
+
out.push({
|
|
163
|
+
index: at,
|
|
164
|
+
direction: alarm.direction,
|
|
165
|
+
before: null,
|
|
166
|
+
after: null,
|
|
167
|
+
score: alarm.score,
|
|
168
|
+
});
|
|
169
|
+
// Strictly forward, so a pathological series cannot loop here.
|
|
170
|
+
from = at + 1;
|
|
171
|
+
}
|
|
172
|
+
// Levels are filled in once the segment boundaries are known: `before` is the
|
|
173
|
+
// regime that ended at the change-point and `after` the one that began there,
|
|
174
|
+
// each closed by its neighbour rather than running to the end of the series.
|
|
175
|
+
for (const [position, change] of out.entries()) {
|
|
176
|
+
const previous = position === 0 ? 0 : out[position - 1].index;
|
|
177
|
+
const next = position === out.length - 1 ? values.length : out[position + 1].index;
|
|
178
|
+
change.before = median(values.slice(previous, change.index));
|
|
179
|
+
change.after = median(values.slice(change.index, next));
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/** Where one segment first departs from its own opening level, if it does. */
|
|
184
|
+
function firstAlarm(values, reference, spread, threshold, sensitivity) {
|
|
185
|
+
if (reference == null)
|
|
186
|
+
return null;
|
|
187
|
+
let up = 0;
|
|
188
|
+
let down = 0;
|
|
189
|
+
let upStart = 0;
|
|
190
|
+
let downStart = 0;
|
|
191
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
192
|
+
const value = values[index];
|
|
193
|
+
if (!Number.isFinite(value))
|
|
194
|
+
continue;
|
|
195
|
+
const deviation = winsorise((value - reference) / spread, sensitivity);
|
|
196
|
+
if (up === 0)
|
|
197
|
+
upStart = index;
|
|
198
|
+
if (down === 0)
|
|
199
|
+
downStart = index;
|
|
200
|
+
up = Math.max(0, up + deviation - CUSUM_SLACK);
|
|
201
|
+
down = Math.max(0, down - deviation - CUSUM_SLACK);
|
|
202
|
+
if (up > threshold)
|
|
203
|
+
return { index: upStart, direction: "up", score: up };
|
|
204
|
+
if (down > threshold)
|
|
205
|
+
return { index: downStart, direction: "down", score: down };
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=changepoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changepoint.js","sourceRoot":"","sources":["../../src/insights/changepoint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-memory evaluator for a {@link BucketMeasure} (ADR 0051 §4).
|
|
3
|
+
*
|
|
4
|
+
* `buildMetricBuckets` renders a measure to SQL for the four persistent stores.
|
|
5
|
+
* The collector also ships an in-memory store — the one the playground and the
|
|
6
|
+
* end-to-end harness boot without a database — which has no SQL to run. Rather
|
|
7
|
+
* than let the insight endpoints go dark there (an empty series reads as "no
|
|
8
|
+
* data", which is a different and misleading claim), the same declarative
|
|
9
|
+
* measure is evaluated directly over events here.
|
|
10
|
+
*
|
|
11
|
+
* Pure, and deliberately the *same shape* as the SQL: one pass that groups by
|
|
12
|
+
* bucket, then one aggregate per bucket, using the same closed predicate
|
|
13
|
+
* vocabulary. `src/__tests__/insights.test.ts` runs both paths over the same
|
|
14
|
+
* rows so the two cannot drift.
|
|
15
|
+
*/
|
|
16
|
+
import { type BucketGrain, type BucketSplitDimension, type BucketVariant } from "./measures.js";
|
|
17
|
+
import { type MetricBucketRow } from "./buckets.js";
|
|
18
|
+
/**
|
|
19
|
+
* The promoted event columns a measure can read — the in-memory mirror of the
|
|
20
|
+
* `events` table's column set. A store maps its own event representation into
|
|
21
|
+
* this once, and the evaluator never sees the store's shape.
|
|
22
|
+
*/
|
|
23
|
+
export interface BucketEventLike {
|
|
24
|
+
/** Event time, epoch milliseconds. */
|
|
25
|
+
ts: number;
|
|
26
|
+
event_type: string;
|
|
27
|
+
scene_id: string;
|
|
28
|
+
session_id: string;
|
|
29
|
+
mesh?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
source?: string;
|
|
32
|
+
fps?: number;
|
|
33
|
+
visible_ms?: number;
|
|
34
|
+
js_heap_bytes?: number;
|
|
35
|
+
long_frames?: number;
|
|
36
|
+
position?: readonly number[];
|
|
37
|
+
direction?: readonly number[];
|
|
38
|
+
hit_point?: readonly number[];
|
|
39
|
+
screen?: readonly number[];
|
|
40
|
+
/** `ar_placement.scale`, the one measure value that lives in the payload. */
|
|
41
|
+
ar_placement_scale?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Scope an evaluation the way the SQL's `WHERE` clause does. */
|
|
44
|
+
export interface EvaluateBucketOptions {
|
|
45
|
+
metric: string;
|
|
46
|
+
/** A named auxiliary series of the metric rather than its headline one (#307). */
|
|
47
|
+
series?: BucketVariant;
|
|
48
|
+
bucket?: BucketGrain;
|
|
49
|
+
since?: number;
|
|
50
|
+
until?: number;
|
|
51
|
+
scene?: string;
|
|
52
|
+
/** Split the series by one promoted dimension, as `buildMetricBuckets` does. */
|
|
53
|
+
groupBy?: BucketSplitDimension;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Evaluate a metric's bucket series over events held in memory.
|
|
57
|
+
*
|
|
58
|
+
* Returns the same rows `buildMetricBuckets` would: ascending by bucket, with
|
|
59
|
+
* buckets that matched nothing simply absent. Throws for a metric with no
|
|
60
|
+
* portable bucket form, exactly as the builder does.
|
|
61
|
+
*/
|
|
62
|
+
export declare function evaluateBucketMeasure(events: Iterable<BucketEventLike>, opts: EvaluateBucketOptions): MetricBucketRow[];
|
|
63
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../src/insights/evaluate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAGL,KAAK,WAAW,EAGhB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAG3E;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,iEAAiE;AACjE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,gFAAgF;IAChF,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AA2DD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,QAAQ,CAAC,eAAe,CAAC,EACjC,IAAI,EAAE,qBAAqB,GAC1B,eAAe,EAAE,CAwDnB"}
|