@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,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **`anomalies`** — "when did this go wrong, and what inside it went wrong?"
|
|
3
|
+
* (ADR 0051 §4, design sketch §D row `anomalies`).
|
|
4
|
+
*
|
|
5
|
+
* `baseline` says what normal looks like and `movers` says that something
|
|
6
|
+
* changed between two windows a caller had to choose. Neither names **a
|
|
7
|
+
* bucket**. This primitive walks one metric's bucket series and returns the
|
|
8
|
+
* buckets that do not belong in it, each labelled with what kind of departure it
|
|
9
|
+
* is and — where the metric declares a dimension it can be split by — which
|
|
10
|
+
* value inside the metric accounts for the excess.
|
|
11
|
+
*
|
|
12
|
+
* Pure, like the rest of `src/insights/`: it takes the rows `buildMetricBuckets`
|
|
13
|
+
* produced and returns rows. No store, no request, no dialect, so DuckDB,
|
|
14
|
+
* ClickHouse, Postgres and SQL Server cannot disagree about whether a Tuesday
|
|
15
|
+
* was abnormal.
|
|
16
|
+
*
|
|
17
|
+
* ## Three kinds, two detectors
|
|
18
|
+
*
|
|
19
|
+
* | `kind` | Detector | The shape it catches |
|
|
20
|
+
* | ------- | -------- | -------------------- |
|
|
21
|
+
* | `spike` | robust z vs the trailing window | one bucket far above what came before |
|
|
22
|
+
* | `drop` | the same, below | one bucket far below it |
|
|
23
|
+
* | `shift` | CUSUM over the whole series | a level that moved and *stayed* moved |
|
|
24
|
+
*
|
|
25
|
+
* The third is not a refinement of the first two, it is the case they
|
|
26
|
+
* structurally cannot see: a release that costs 6 FPS every day from Tuesday
|
|
27
|
+
* onward is never more than a MAD or two off on any single day, so no point
|
|
28
|
+
* detector will ever flag it, and yet it is the finding that matters most. Both
|
|
29
|
+
* detectors are driven by the *same* `sensitivity` dial and measured in the same
|
|
30
|
+
* unit (MADs of the series' own spread), so the two halves of a result are
|
|
31
|
+
* comparable rather than two unrelated alarms sharing an endpoint.
|
|
32
|
+
*
|
|
33
|
+
* ## The denominator: a scaled MAD, with a floor
|
|
34
|
+
*
|
|
35
|
+
* `sensitivity` is a **threshold**, not a ranking key, so the unit it counts has
|
|
36
|
+
* to be calibrated or the default is meaningless. The MAD of normally
|
|
37
|
+
* distributed data is about 0.674σ, so a raw `|z| > 3` on MADs is really "beyond
|
|
38
|
+
* 2σ" — a two-sided tail of roughly 4% *per bucket*, which over a 28-day window
|
|
39
|
+
* reports one or two perfectly ordinary days every time it is called. That is
|
|
40
|
+
* exactly the experience that makes people switch anomaly detection off.
|
|
41
|
+
*
|
|
42
|
+
* The spread is therefore `max(MAD × {@link MAD_TO_SIGMA}, 1% × |median|)`: the
|
|
43
|
+
* MAD rescaled into standard deviations, so `sensitivity: 3` means what a reader
|
|
44
|
+
* assumes it means, and then floored the way `movers` floors it. The floor is
|
|
45
|
+
* what stops a metric that sat at exactly 60 FPS for a fortnight — trailing MAD
|
|
46
|
+
* of 0 — from reporting a 59.9 FPS day as an infinite departure.
|
|
47
|
+
*
|
|
48
|
+
* `movers` reports the **unscaled** ratio, because there it ranks rather than
|
|
49
|
+
* thresholds and a constant factor changes no ordering. The two `z` columns
|
|
50
|
+
* therefore differ by that constant, which the registry entries both say.
|
|
51
|
+
*
|
|
52
|
+
* ## Cost is bounded, and declared
|
|
53
|
+
*
|
|
54
|
+
* The detection itself is one bucket read — the same grouped scan every insight
|
|
55
|
+
* makes. Attribution costs **at most {@link ANOMALY_MAX_CONTRIBUTOR_SCANS} extra
|
|
56
|
+
* grouped scans per request**, no matter how many buckets were anomalous:
|
|
57
|
+
* adjacent findings are merged into windows, the windows are ranked by how
|
|
58
|
+
* extreme they are, and only the top few are re-read. A metric with no declared
|
|
59
|
+
* split dimension pays for none.
|
|
60
|
+
*/
|
|
61
|
+
import { MIN_SPREAD_FRACTION } from "./movers.js";
|
|
62
|
+
import { MAD_TO_SIGMA, median, medianAbsoluteDeviation, round } from "./stats.js";
|
|
63
|
+
import { cusumChangePoints, rollingRobustStats } from "./changepoint.js";
|
|
64
|
+
import { BUCKET_SECONDS, bucketMeasureFor, } from "./measures.js";
|
|
65
|
+
/** Default `sensitivity`: a bucket must be more than three MADs out. */
|
|
66
|
+
export const DEFAULT_ANOMALY_SENSITIVITY = 3;
|
|
67
|
+
/** Smallest `sensitivity` a caller may ask for. */
|
|
68
|
+
export const MIN_ANOMALY_SENSITIVITY = 1;
|
|
69
|
+
/**
|
|
70
|
+
* Largest `sensitivity` a caller may ask for.
|
|
71
|
+
*
|
|
72
|
+
* Bounded at both ends on purpose. Below 1 every bucket is an anomaly and the
|
|
73
|
+
* answer is noise; above 10 nothing short of a total outage is, and a caller who
|
|
74
|
+
* wants that has asked the wrong question — they want a threshold alert, which
|
|
75
|
+
* is stage 3's `subscriptions`, not a detector.
|
|
76
|
+
*/
|
|
77
|
+
export const MAX_ANOMALY_SENSITIVITY = 10;
|
|
78
|
+
/**
|
|
79
|
+
* How many buckets of history each bucket is judged against, per grain.
|
|
80
|
+
*
|
|
81
|
+
* A fortnight at day grain: long enough for the median to be stable, short
|
|
82
|
+
* enough that a month-old regime does not keep flagging today. A week at hour
|
|
83
|
+
* grain, which is 168 buckets — the shortest window that contains every hour of
|
|
84
|
+
* every weekday, so a quiet Sunday 03:00 is compared against a distribution that
|
|
85
|
+
* has seen quiet nights rather than against Tuesday lunchtime.
|
|
86
|
+
*/
|
|
87
|
+
export const ANOMALY_TRAILING_BUCKETS = {
|
|
88
|
+
day: 14,
|
|
89
|
+
hour: 168,
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Trailing values a bucket needs before it can be judged at all.
|
|
93
|
+
*
|
|
94
|
+
* Under five, the median and the MAD are describing the accident of which four
|
|
95
|
+
* buckets happened to exist. Buckets with less history than this are reported as
|
|
96
|
+
* nothing rather than as findings — which is why a brand-new project reads
|
|
97
|
+
* "no anomalies" instead of "every day is an anomaly".
|
|
98
|
+
*/
|
|
99
|
+
export const ANOMALY_MIN_TRAILING = 5;
|
|
100
|
+
/**
|
|
101
|
+
* The hard cap on extra grouped scans spent on attribution, per request.
|
|
102
|
+
*
|
|
103
|
+
* Contributor attribution re-reads the series grouped by one dimension over an
|
|
104
|
+
* anomalous window. Adjacent anomalous buckets are one window, and only the
|
|
105
|
+
* three most extreme windows are re-read, so the endpoint's cost is
|
|
106
|
+
* `1 + min(windows, 3)` grouped scans whatever the data looks like. A caller who
|
|
107
|
+
* wants attribution on a quieter finding narrows `since`/`until` around it; that
|
|
108
|
+
* is cheaper for the store than lifting the cap would be.
|
|
109
|
+
*/
|
|
110
|
+
export const ANOMALY_MAX_CONTRIBUTOR_SCANS = 3;
|
|
111
|
+
/** Decimals an anomaly's numbers are rounded to before they leave the API. */
|
|
112
|
+
export const ANOMALY_PRECISION = 6;
|
|
113
|
+
/** Clamp a caller's `sensitivity` into the supported range. */
|
|
114
|
+
export function clampSensitivity(value) {
|
|
115
|
+
if (value == null || !Number.isFinite(value))
|
|
116
|
+
return DEFAULT_ANOMALY_SENSITIVITY;
|
|
117
|
+
return Math.min(MAX_ANOMALY_SENSITIVITY, Math.max(MIN_ANOMALY_SENSITIVITY, value));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The dimension `anomalies` would attribute this metric's excess to, or `null`.
|
|
121
|
+
*
|
|
122
|
+
* `null` for a metric that declares none, and also for a `scene`-split metric on
|
|
123
|
+
* a request that is already scoped to one scene: the split would return a single
|
|
124
|
+
* value with a share of 1, which is not an explanation, and the scan that
|
|
125
|
+
* produced it would be wasted.
|
|
126
|
+
*/
|
|
127
|
+
export function contributorDimensionFor(metric, opts = {}) {
|
|
128
|
+
const splitBy = bucketMeasureFor(metric)?.splitBy;
|
|
129
|
+
if (splitBy == null)
|
|
130
|
+
return null;
|
|
131
|
+
if (splitBy === "scene" && opts.scene != null && opts.scene.length > 0)
|
|
132
|
+
return null;
|
|
133
|
+
return splitBy;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The spread a departure is measured in: the MAD rescaled to a standard
|
|
137
|
+
* deviation ({@link MAD_TO_SIGMA}), floored at {@link MIN_SPREAD_FRACTION} of the
|
|
138
|
+
* level. `null` when there is no spread to speak of.
|
|
139
|
+
*
|
|
140
|
+
* The rescaling is what makes `sensitivity` a calibrated dial (see the module
|
|
141
|
+
* doc); the floor is `movers`' own, so a flat window behaves identically in both
|
|
142
|
+
* primitives.
|
|
143
|
+
*/
|
|
144
|
+
function flooredSpread(centre, mad) {
|
|
145
|
+
if (centre == null || mad == null || !Number.isFinite(mad))
|
|
146
|
+
return null;
|
|
147
|
+
return Math.max(mad * MAD_TO_SIGMA, Math.abs(centre) * MIN_SPREAD_FRACTION);
|
|
148
|
+
}
|
|
149
|
+
/** A finite, bucket-ordered series from the store's rows. */
|
|
150
|
+
function seriesOf(rows) {
|
|
151
|
+
return [...rows]
|
|
152
|
+
.filter((row) => row.value != null && Number.isFinite(row.value))
|
|
153
|
+
.sort((a, b) => a.bucket - b.bucket)
|
|
154
|
+
.map((row) => ({
|
|
155
|
+
bucket: row.bucket,
|
|
156
|
+
value: row.value,
|
|
157
|
+
sample: Number.isFinite(row.sample_size) ? row.sample_size : 0,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Score one metric's bucket series and return its anomalous buckets, ordered by
|
|
162
|
+
* bucket, `contributor` still `null`.
|
|
163
|
+
*
|
|
164
|
+
* Point findings come first in time order, then the change-points, and a bucket
|
|
165
|
+
* that is both a spike and the start of a shift is reported twice — deliberately:
|
|
166
|
+
* "Tuesday was the worst day" and "Tuesday is when it stopped recovering" are two
|
|
167
|
+
* different facts, and collapsing them loses the second one.
|
|
168
|
+
*/
|
|
169
|
+
export function detectAnomalies(metric, scene, rows, opts) {
|
|
170
|
+
const series = seriesOf(rows);
|
|
171
|
+
const sensitivity = clampSensitivity(opts.sensitivity);
|
|
172
|
+
const out = [];
|
|
173
|
+
if (series.length === 0)
|
|
174
|
+
return out;
|
|
175
|
+
const values = series.map((point) => point.value);
|
|
176
|
+
// --- point detection: each bucket against the ones just before it ---------
|
|
177
|
+
const trailing = rollingRobustStats(values, ANOMALY_TRAILING_BUCKETS[opts.bucket], ANOMALY_MIN_TRAILING);
|
|
178
|
+
for (const [index, point] of series.entries()) {
|
|
179
|
+
const stats = trailing[index];
|
|
180
|
+
if (stats == null || stats.median == null)
|
|
181
|
+
continue;
|
|
182
|
+
// The same floored spread `movers` divides by, so a `z` of 4 means the same
|
|
183
|
+
// thing in both primitives — and taken from the trailing statistics already
|
|
184
|
+
// computed, so scoring a bucket stays O(1) on top of the rolling pass.
|
|
185
|
+
const spread = flooredSpread(stats.median, stats.mad);
|
|
186
|
+
if (spread == null || !(spread > 0))
|
|
187
|
+
continue;
|
|
188
|
+
const z = (point.value - stats.median) / spread;
|
|
189
|
+
if (Math.abs(z) <= sensitivity)
|
|
190
|
+
continue;
|
|
191
|
+
out.push({
|
|
192
|
+
metric,
|
|
193
|
+
scene: scene ?? "",
|
|
194
|
+
bucketStart: point.bucket,
|
|
195
|
+
value: round(point.value, ANOMALY_PRECISION),
|
|
196
|
+
expected: round(stats.median, ANOMALY_PRECISION),
|
|
197
|
+
z: round(z, ANOMALY_PRECISION),
|
|
198
|
+
kind: z > 0 ? "spike" : "drop",
|
|
199
|
+
contributor: null,
|
|
200
|
+
sampleSize: point.sample,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
// --- change-points: a level that moved and stayed moved ------------------
|
|
204
|
+
// The spread is the whole series', so a shift's z is in the same MAD unit as a
|
|
205
|
+
// spike's; the *reference level* each segment is judged against is the level
|
|
206
|
+
// it started at, which the segmenting in `cusumChangePoints` re-estimates.
|
|
207
|
+
const spread = flooredSpread(median(values), medianAbsoluteDeviation(values));
|
|
208
|
+
for (const change of cusumChangePoints(values, spread, sensitivity, ANOMALY_TRAILING_BUCKETS[opts.bucket])) {
|
|
209
|
+
const point = series[change.index];
|
|
210
|
+
if (point == null)
|
|
211
|
+
continue;
|
|
212
|
+
out.push(shiftRow(metric, scene, point, change, spread));
|
|
213
|
+
}
|
|
214
|
+
return out.sort((a, b) => a.bucketStart - b.bucketStart || kindOrder(a.kind) - kindOrder(b.kind));
|
|
215
|
+
}
|
|
216
|
+
/** Point findings before the change-point that starts at the same bucket. */
|
|
217
|
+
function kindOrder(kind) {
|
|
218
|
+
return kind === "shift" ? 1 : 0;
|
|
219
|
+
}
|
|
220
|
+
/** One `shift` row: the change-point bucket, carrying the pre and post levels. */
|
|
221
|
+
function shiftRow(metric, scene, point, change, spread) {
|
|
222
|
+
const delta = change.after != null && change.before != null ? change.after - change.before : null;
|
|
223
|
+
return {
|
|
224
|
+
metric,
|
|
225
|
+
scene: scene ?? "",
|
|
226
|
+
bucketStart: point.bucket,
|
|
227
|
+
value: round(change.after, ANOMALY_PRECISION),
|
|
228
|
+
expected: round(change.before, ANOMALY_PRECISION),
|
|
229
|
+
// The size of the *level change*, in the same MAD unit as a spike's z — not
|
|
230
|
+
// the CUSUM statistic, which is an accumulation and would not be comparable.
|
|
231
|
+
z: delta == null || spread == null || !(spread > 0)
|
|
232
|
+
? null
|
|
233
|
+
: round(delta / spread, ANOMALY_PRECISION),
|
|
234
|
+
kind: "shift",
|
|
235
|
+
contributor: null,
|
|
236
|
+
sampleSize: point.sample,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Merge anomalous buckets into the windows a contributor scan has to cover, most
|
|
241
|
+
* extreme first, capped at {@link ANOMALY_MAX_CONTRIBUTOR_SCANS}.
|
|
242
|
+
*
|
|
243
|
+
* Adjacent findings — a three-day outage is three rows — share one window and
|
|
244
|
+
* therefore one scan. Each window is widened backwards by the trailing length so
|
|
245
|
+
* the same scan also carries the history each split value's own "expected" is
|
|
246
|
+
* computed from; without that the attribution would compare a value against
|
|
247
|
+
* nothing and name whichever dimension happens to be biggest.
|
|
248
|
+
*/
|
|
249
|
+
export function contributorWindows(rows, opts) {
|
|
250
|
+
const width = BUCKET_SECONDS[opts.bucket] * 1000;
|
|
251
|
+
const trailing = ANOMALY_TRAILING_BUCKETS[opts.bucket] * width;
|
|
252
|
+
const ordered = [...rows].sort((a, b) => a.bucketStart - b.bucketStart);
|
|
253
|
+
const groups = [];
|
|
254
|
+
for (const row of ordered) {
|
|
255
|
+
const score = row.z == null ? 0 : Math.abs(row.z);
|
|
256
|
+
const last = groups[groups.length - 1];
|
|
257
|
+
// A `shift` is a statement about everything from its bucket onward, so its
|
|
258
|
+
// window runs to the end of the series; a point finding is one bucket.
|
|
259
|
+
const until = row.kind === "shift" ? opts.seriesUntil : row.bucketStart + width;
|
|
260
|
+
if (last != null && row.bucketStart <= last.until) {
|
|
261
|
+
last.until = Math.max(last.until, until);
|
|
262
|
+
last.score = Math.max(last.score, score);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
groups.push({ since: row.bucketStart, until, score });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return groups
|
|
269
|
+
.sort((a, b) => b.score - a.score || a.since - b.since)
|
|
270
|
+
.slice(0, ANOMALY_MAX_CONTRIBUTOR_SCANS)
|
|
271
|
+
.map((group) => ({ since: group.since - trailing, until: group.until }))
|
|
272
|
+
.sort((a, b) => a.since - b.since);
|
|
273
|
+
}
|
|
274
|
+
/** Whether an anomalous bucket falls inside a contributor window. */
|
|
275
|
+
export function inContributorWindow(row, window) {
|
|
276
|
+
return row.bucketStart >= window.since && row.bucketStart < window.until;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Name the dimension value that accounts for one row's excess.
|
|
280
|
+
*
|
|
281
|
+
* `splitRows` are the same measure re-read grouped by `dimension` over the row's
|
|
282
|
+
* contributor window. For each value the excess is `observed − prior`, where
|
|
283
|
+
* `prior` is that value's median over the buckets *before* the finding and
|
|
284
|
+
* `observed` is its value in the anomalous bucket (or, for a `shift`, its median
|
|
285
|
+
* from the change-point onward — the same before/after comparison the row
|
|
286
|
+
* itself reports, done one dimension value at a time).
|
|
287
|
+
*
|
|
288
|
+
* The winner is the largest excess **with the same sign as the row's own**: a
|
|
289
|
+
* spike is explained by what went up, and a value that happened to fall during
|
|
290
|
+
* an error spike is not a contributor to it. `share` is that value's fraction of
|
|
291
|
+
* the summed same-signed excess, so the shares of all contributing values add to
|
|
292
|
+
* 1 and a reported share of 0.9 means what a reader thinks it means.
|
|
293
|
+
*
|
|
294
|
+
* Returns `null` when nothing can be named: no split rows, or an excess of zero,
|
|
295
|
+
* or every split value moving against the finding.
|
|
296
|
+
*/
|
|
297
|
+
export function attributeContributor(row, dimension, splitRows) {
|
|
298
|
+
if (row.value == null || row.expected == null)
|
|
299
|
+
return null;
|
|
300
|
+
const direction = Math.sign(row.value - row.expected);
|
|
301
|
+
if (direction === 0)
|
|
302
|
+
return null;
|
|
303
|
+
const byValue = new Map();
|
|
304
|
+
for (const split of splitRows) {
|
|
305
|
+
if (split.value == null || !Number.isFinite(split.value))
|
|
306
|
+
continue;
|
|
307
|
+
const key = split.dimension_value ?? "";
|
|
308
|
+
const list = byValue.get(key);
|
|
309
|
+
if (list == null)
|
|
310
|
+
byValue.set(key, [{ bucket: split.bucket, value: split.value }]);
|
|
311
|
+
else
|
|
312
|
+
list.push({ bucket: split.bucket, value: split.value });
|
|
313
|
+
}
|
|
314
|
+
if (byValue.size === 0)
|
|
315
|
+
return null;
|
|
316
|
+
const excesses = [];
|
|
317
|
+
for (const [key, points] of byValue) {
|
|
318
|
+
const prior = median(points.filter((p) => p.bucket < row.bucketStart).map((p) => p.value)) ?? 0;
|
|
319
|
+
const observed = row.kind === "shift"
|
|
320
|
+
? median(points.filter((p) => p.bucket >= row.bucketStart).map((p) => p.value))
|
|
321
|
+
: (points.find((p) => p.bucket === row.bucketStart)?.value ?? null);
|
|
322
|
+
// A split value that vanished in the anomalous bucket contributes its own
|
|
323
|
+
// absence: for a drop that is the whole explanation.
|
|
324
|
+
const excess = (observed ?? 0) - prior;
|
|
325
|
+
if (Math.sign(excess) === direction)
|
|
326
|
+
excesses.push({ value: key, excess });
|
|
327
|
+
}
|
|
328
|
+
if (excesses.length === 0)
|
|
329
|
+
return null;
|
|
330
|
+
let total = 0;
|
|
331
|
+
for (const entry of excesses)
|
|
332
|
+
total += entry.excess;
|
|
333
|
+
const winner = excesses.reduce((best, entry) => Math.abs(entry.excess) > Math.abs(best.excess) ? entry : best);
|
|
334
|
+
return {
|
|
335
|
+
dimension,
|
|
336
|
+
value: winner.value,
|
|
337
|
+
share: total === 0 ? null : round(winner.excess / total, ANOMALY_PRECISION),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=anomalies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anomalies.js","sourceRoot":"","sources":["../../src/insights/anomalies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **`baseline`** — "what is normal for this scene?" (ADR 0051 §4, sketch §D).
|
|
3
|
+
*
|
|
4
|
+
* Reduces a metric's per-bucket series to the handful of numbers that let a
|
|
5
|
+
* reader (or an agent) judge a single later observation without re-deriving the
|
|
6
|
+
* distribution: where the series sits (`mean`, `median`), how much it normally
|
|
7
|
+
* moves (`mad`), what its ordinary range looks like (`p10`, `p90`), and whether
|
|
8
|
+
* it is drifting (`slope`).
|
|
9
|
+
*
|
|
10
|
+
* Pure: it takes the rows `buildMetricBuckets` produced and returns one row. It
|
|
11
|
+
* never sees a store, a request or a dialect, so the same series produces the
|
|
12
|
+
* same baseline on every engine — which is the point of computing statistics in
|
|
13
|
+
* TypeScript rather than in five dialects' SQL.
|
|
14
|
+
*/
|
|
15
|
+
import type { MetricBucketRow } from "./buckets.js";
|
|
16
|
+
/**
|
|
17
|
+
* What `baseline` reports. One row per request.
|
|
18
|
+
*
|
|
19
|
+
* Every statistic is nullable and `null` means *"not defined for this series"* —
|
|
20
|
+
* an empty window, or fewer than two buckets for a slope. A brand-new project
|
|
21
|
+
* reads `null`, never `0`.
|
|
22
|
+
*/
|
|
23
|
+
export interface BaselineRow {
|
|
24
|
+
/** The registry metric the series is of. */
|
|
25
|
+
metric: string;
|
|
26
|
+
/** The scene it was scoped to; `''` when the baseline spans every scene. */
|
|
27
|
+
scene: string;
|
|
28
|
+
/** How many buckets carried a value. */
|
|
29
|
+
buckets: number;
|
|
30
|
+
mean: number | null;
|
|
31
|
+
median: number | null;
|
|
32
|
+
/** Median absolute deviation — the typical bucket-to-bucket swing. */
|
|
33
|
+
mad: number | null;
|
|
34
|
+
p10: number | null;
|
|
35
|
+
p90: number | null;
|
|
36
|
+
/** Least-squares slope against the bucket index: change per bucket. */
|
|
37
|
+
slope: number | null;
|
|
38
|
+
/** Events (or sessions, for a session-valued metric) behind the whole series. */
|
|
39
|
+
sampleSize: number;
|
|
40
|
+
}
|
|
41
|
+
/** How many decimals a baseline statistic is rounded to before it leaves the API. */
|
|
42
|
+
export declare const BASELINE_PRECISION = 6;
|
|
43
|
+
/**
|
|
44
|
+
* Reduce a bucket series to its baseline.
|
|
45
|
+
*
|
|
46
|
+
* Buckets whose `value` is `null` (an aggregate over no rows) are excluded from
|
|
47
|
+
* every statistic but still counted in `sampleSize`, because their events did
|
|
48
|
+
* happen — they simply did not produce a value for this measure.
|
|
49
|
+
*/
|
|
50
|
+
export declare function computeBaseline(metric: string, scene: string | undefined, rows: readonly MetricBucketRow[]): BaselineRow;
|
|
51
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/insights/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,sEAAsE;IACtE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qFAAqF;AACrF,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,SAAS,eAAe,EAAE,GAC/B,WAAW,CAsBb"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **`baseline`** — "what is normal for this scene?" (ADR 0051 §4, sketch §D).
|
|
3
|
+
*
|
|
4
|
+
* Reduces a metric's per-bucket series to the handful of numbers that let a
|
|
5
|
+
* reader (or an agent) judge a single later observation without re-deriving the
|
|
6
|
+
* distribution: where the series sits (`mean`, `median`), how much it normally
|
|
7
|
+
* moves (`mad`), what its ordinary range looks like (`p10`, `p90`), and whether
|
|
8
|
+
* it is drifting (`slope`).
|
|
9
|
+
*
|
|
10
|
+
* Pure: it takes the rows `buildMetricBuckets` produced and returns one row. It
|
|
11
|
+
* never sees a store, a request or a dialect, so the same series produces the
|
|
12
|
+
* same baseline on every engine — which is the point of computing statistics in
|
|
13
|
+
* TypeScript rather than in five dialects' SQL.
|
|
14
|
+
*/
|
|
15
|
+
import { leastSquaresSlope, mean, median, medianAbsoluteDeviation, quantile, round, } from "./stats.js";
|
|
16
|
+
/** How many decimals a baseline statistic is rounded to before it leaves the API. */
|
|
17
|
+
export const BASELINE_PRECISION = 6;
|
|
18
|
+
/**
|
|
19
|
+
* Reduce a bucket series to its baseline.
|
|
20
|
+
*
|
|
21
|
+
* Buckets whose `value` is `null` (an aggregate over no rows) are excluded from
|
|
22
|
+
* every statistic but still counted in `sampleSize`, because their events did
|
|
23
|
+
* happen — they simply did not produce a value for this measure.
|
|
24
|
+
*/
|
|
25
|
+
export function computeBaseline(metric, scene, rows) {
|
|
26
|
+
const values = [];
|
|
27
|
+
let sampleSize = 0;
|
|
28
|
+
// Ordered by bucket, so the slope is a slope against *time* rather than
|
|
29
|
+
// against whatever order the driver happened to return rows in.
|
|
30
|
+
for (const row of [...rows].sort((a, b) => a.bucket - b.bucket)) {
|
|
31
|
+
sampleSize += Number.isFinite(row.sample_size) ? row.sample_size : 0;
|
|
32
|
+
if (row.value != null && Number.isFinite(row.value))
|
|
33
|
+
values.push(row.value);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
metric,
|
|
37
|
+
scene: scene ?? "",
|
|
38
|
+
buckets: values.length,
|
|
39
|
+
mean: round(mean(values), BASELINE_PRECISION),
|
|
40
|
+
median: round(median(values), BASELINE_PRECISION),
|
|
41
|
+
mad: round(medianAbsoluteDeviation(values), BASELINE_PRECISION),
|
|
42
|
+
p10: round(quantile(values, 0.1), BASELINE_PRECISION),
|
|
43
|
+
p90: round(quantile(values, 0.9), BASELINE_PRECISION),
|
|
44
|
+
slope: round(leastSquaresSlope(values), BASELINE_PRECISION),
|
|
45
|
+
sampleSize,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/insights/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,iBAAiB,EACjB,IAAI,EACJ,MAAM,EACN,uBAAuB,EACvB,QAAQ,EACR,KAAK,GACN,MAAM,YAAY,CAAC;AA6BpB,qFAAqF;AACrF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAc,EACd,KAAyB,EACzB,IAAgC;IAEhC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,wEAAwE;IACxE,gEAAgE;IAChE,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,UAAU,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACrE,IAAI,GAAG,CAAC,KAAK,IAAI,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO;QACL,MAAM;QACN,KAAK,EAAE,KAAK,IAAI,EAAE;QAClB,OAAO,EAAE,MAAM,CAAC,MAAM;QACtB,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;QAC7C,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;QACjD,GAAG,EAAE,KAAK,CAAC,uBAAuB,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;QAC/D,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,kBAAkB,CAAC;QACrD,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,kBAAkB,CAAC;QACrD,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;QAC3D,UAAU;KACX,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 type { Dialect } from "../query/dialect.js";
|
|
30
|
+
import type { QuerySpec, RangeOptions, SceneOptions } from "../query/types.js";
|
|
31
|
+
import { type BucketGrain, type BucketSplitDimension, type BucketVariant } from "./measures.js";
|
|
32
|
+
/** What to bucket, over what window, for whom. */
|
|
33
|
+
export interface MetricBucketOptions extends RangeOptions, SceneOptions {
|
|
34
|
+
/** The registry metric whose primary column the series reproduces. */
|
|
35
|
+
metric: string;
|
|
36
|
+
/** Time grain; defaults to `day`. */
|
|
37
|
+
bucket?: BucketGrain;
|
|
38
|
+
/**
|
|
39
|
+
* Split the series by one promoted dimension — the measure's own
|
|
40
|
+
* {@link BucketMeasure.splitBy}, never a caller-supplied column.
|
|
41
|
+
*
|
|
42
|
+
* With it, each row additionally carries `dimension_value` and the grouped
|
|
43
|
+
* scan answers "which mesh / channel / source accounts for this bucket?" in
|
|
44
|
+
* one extra read rather than one read per candidate value. Without it the
|
|
45
|
+
* query, its rows and its cost are exactly what they were before (#305).
|
|
46
|
+
*/
|
|
47
|
+
groupBy?: BucketSplitDimension;
|
|
48
|
+
/**
|
|
49
|
+
* Read one of the metric’s **named auxiliary series** instead of its headline
|
|
50
|
+
* one (the rate denominator, an FPS tail, … — see `measures.ts`, #307).
|
|
51
|
+
*
|
|
52
|
+
* Never caller input: `significance` and `scene_health` set it from the
|
|
53
|
+
* registry and from their own fixed factor catalog respectively, so the value
|
|
54
|
+
* is always one of a compile-time union.
|
|
55
|
+
*
|
|
56
|
+
* Named `series` rather than `variant` because `variant` is already an
|
|
57
|
+
* aggregation option elsewhere (the variant-leaderboard's custom-event
|
|
58
|
+
* predicate), and the store-parity harness builds every aggregation from one
|
|
59
|
+
* shared option bag: two unrelated meanings under one key would silently
|
|
60
|
+
* cross over.
|
|
61
|
+
*/
|
|
62
|
+
series?: BucketVariant;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One bucket of a metric's series, as the store returns it.
|
|
66
|
+
*
|
|
67
|
+
* `value` is nullable because an aggregate over no matching rows is SQL-`NULL`
|
|
68
|
+
* (a bucket in which a `quantile` had nothing to rank), and `null` must reach
|
|
69
|
+
* the statistics as "no observation" rather than as `0`. Buckets with no
|
|
70
|
+
* matching events at all are simply absent — the series is sparse, and
|
|
71
|
+
* `baseline`/`movers` treat absence as absence.
|
|
72
|
+
*/
|
|
73
|
+
export interface MetricBucketRow {
|
|
74
|
+
/** Bucket start, epoch milliseconds (UTC, aligned to the grain). */
|
|
75
|
+
bucket: number;
|
|
76
|
+
/** The metric's primary column for this bucket, or `null` when undefined. */
|
|
77
|
+
value: number | null;
|
|
78
|
+
/** The denominator behind `value` — what `minSample` is compared against. */
|
|
79
|
+
sample_size: number;
|
|
80
|
+
/**
|
|
81
|
+
* The split dimension's value for this row, present only on a grouped read
|
|
82
|
+
* (`groupBy`). `''` is the store's "unknown"/unattributed, exactly as it is
|
|
83
|
+
* in every other aggregation.
|
|
84
|
+
*/
|
|
85
|
+
dimension_value?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Build the per-bucket series of a metric's comparable primary column.
|
|
89
|
+
*
|
|
90
|
+
* Throws when the metric has no portable bucket form — the route validates
|
|
91
|
+
* against `BUCKETABLE_METRIC_IDS` at the edge and answers `400`, so reaching
|
|
92
|
+
* this is a programming error rather than bad input.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildMetricBuckets(projectId: string, opts: MetricBucketOptions, d: Dialect): QuerySpec;
|
|
95
|
+
/**
|
|
96
|
+
* Total order over bucket rows: by bucket, then by the split value on a grouped
|
|
97
|
+
* read (#306).
|
|
98
|
+
*
|
|
99
|
+
* Compared by code point rather than `localeCompare`, so the order is a property
|
|
100
|
+
* of the data instead of the ICU build the collector happens to run on — the same
|
|
101
|
+
* reason every statistic in this directory is computed in TypeScript.
|
|
102
|
+
*/
|
|
103
|
+
export declare function byBucketThenDimension(a: MetricBucketRow, b: MetricBucketRow): number;
|
|
104
|
+
/**
|
|
105
|
+
* Normalise driver output into a {@link MetricBucketRow}.
|
|
106
|
+
*
|
|
107
|
+
* `buildMetricBuckets` produces no registry metric, so it carries no
|
|
108
|
+
* `QuerySpec.metric` tag and the store-edge coercion (ADR 0051 §2) has no row
|
|
109
|
+
* schema to coerce it against. ClickHouse string-encodes 64-bit counts over
|
|
110
|
+
* HTTP, so the numbers are parsed here instead — the one place every store's
|
|
111
|
+
* bucket read passes through.
|
|
112
|
+
*/
|
|
113
|
+
export declare function toMetricBucketRows(rows: readonly Record<string, unknown>[]): MetricBucketRow[];
|
|
114
|
+
//# sourceMappingURL=buckets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buckets.d.ts","sourceRoot":"","sources":["../../src/insights/buckets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAKL,KAAK,WAAW,EAGhB,KAAK,oBAAoB,EAEzB,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AAEvB,kDAAkD;AAClD,MAAM,WAAW,mBAAoB,SAAQ,YAAY,EAAE,YAAY;IACrE,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAE/B;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAkGD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,mBAAmB,EACzB,CAAC,EAAE,OAAO,GACT,SAAS,CA0CX;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,MAAM,CAKpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,eAAe,EAAE,CAmB9F"}
|