@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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Portable bucket measures** — the input side of the insight primitives
|
|
3
|
+
* (ADR 0051 §4, design sketch §D).
|
|
4
|
+
*
|
|
5
|
+
* `baseline` and `movers` both answer questions about *one number moving over
|
|
6
|
+
* time*: "what is normal for this scene", "what changed since last week". Both
|
|
7
|
+
* therefore need the same thing from the store — a per-bucket series of a
|
|
8
|
+
* comparable metric's `comparable.primary` column — and neither may compute
|
|
9
|
+
* statistics in SQL, because five dialects' `quantile`, `median` and
|
|
10
|
+
* `stddev` do not agree and the answer must not depend on which engine a
|
|
11
|
+
* self-hoster chose.
|
|
12
|
+
*
|
|
13
|
+
* This module is the declaration that makes that series portable. For each
|
|
14
|
+
* comparable registry metric it records **how to reproduce that metric's
|
|
15
|
+
* primary column per time bucket** as a single grouped scan of `events`:
|
|
16
|
+
*
|
|
17
|
+
* - which capture channels the metric reads (`eventTypes`);
|
|
18
|
+
* - a closed vocabulary of extra predicates (`where`) — a promoted-column
|
|
19
|
+
* equality or set membership, or a geometry-arity guard. Never free SQL, so
|
|
20
|
+
* nothing here can become an injection surface;
|
|
21
|
+
* - the aggregate that produces the value (`count`, distinct `sessions`, `sum`,
|
|
22
|
+
* `avg`, `max`, `quantile`);
|
|
23
|
+
* - how bucket values combine into a window total (`rollup`).
|
|
24
|
+
*
|
|
25
|
+
* `buckets.ts` renders exactly one generic query from this data, per dialect.
|
|
26
|
+
*
|
|
27
|
+
* ## What is *not* here, and why
|
|
28
|
+
*
|
|
29
|
+
* 42 of the registry's 63 `comparable` metrics have a faithful portable bucket
|
|
30
|
+
* form. The rest do not, and are deliberately absent rather than approximated:
|
|
31
|
+
*
|
|
32
|
+
* - **funnel-shaped** metrics (`funnel`, `load_bounce_funnel`,
|
|
33
|
+
* `variant_leaderboard`, `scene_retention`) are defined by request-supplied
|
|
34
|
+
* step predicates or by a cohort that spans buckets — bucketing them changes
|
|
35
|
+
* what they mean;
|
|
36
|
+
* - **join- or window-function-shaped** metrics (`click_rays`, `flow_links`,
|
|
37
|
+
* `mesh_reachability`, `navigation_stats`, `backtrack_ratio`,
|
|
38
|
+
* `xr_rotation`, `xr_abandonment`, `perf_churn`, `rage_clicks`) derive a row
|
|
39
|
+
* from the *relationship between consecutive events*, which a per-bucket
|
|
40
|
+
* `GROUP BY` cannot express without changing the answer at every bucket
|
|
41
|
+
* boundary;
|
|
42
|
+
* - **per-session-then-aggregate** ratios (`jank_rate`) and payload-derived
|
|
43
|
+
* percentiles that are not promoted columns (`frame_time_percentiles`,
|
|
44
|
+
* `render_scale_truth`, `mesh_uv_heatmap`, `mesh_blind_spots`,
|
|
45
|
+
* `xr_boundary_contacts`) would need a second aggregation level or a JSON
|
|
46
|
+
* extraction whose null-handling differs from the metric's own.
|
|
47
|
+
*
|
|
48
|
+
* An approximate series is worse than no series: `baseline` would report a
|
|
49
|
+
* "normal" that the metric's own endpoint never produces. Asking for one of
|
|
50
|
+
* those metrics is a `400` naming the ids that *do* have a series
|
|
51
|
+
* ({@link BUCKETABLE_METRIC_IDS}), which is a better answer than a plausible
|
|
52
|
+
* wrong number. Widening the catalog is additive — add an entry here, a parity
|
|
53
|
+
* case in `src/parity/cases.ts`, and the metric becomes available to both
|
|
54
|
+
* primitives with no route or registry change.
|
|
55
|
+
*/
|
|
56
|
+
import type { MetricId } from "@uptimizr/metrics";
|
|
57
|
+
/** The two time grains an insight series can be bucketed at. */
|
|
58
|
+
export type BucketGrain = "day" | "hour";
|
|
59
|
+
/** Bucket width in seconds, the unit `Dialect.timeBucketMs` takes. */
|
|
60
|
+
export declare const BUCKET_SECONDS: Readonly<Record<BucketGrain, number>>;
|
|
61
|
+
/** Whether an arbitrary string names a supported bucket grain. */
|
|
62
|
+
export declare function isBucketGrain(value: string): value is BucketGrain;
|
|
63
|
+
/**
|
|
64
|
+
* Promoted numeric columns a measure may aggregate.
|
|
65
|
+
*
|
|
66
|
+
* Restricted to columns the events table actually promotes, so a measure never
|
|
67
|
+
* needs a JSON extraction whose null semantics differ per engine. The one
|
|
68
|
+
* exception is `ar_placement_scale`, which `buildArPlacementSurfaces` itself
|
|
69
|
+
* reads with `jsonFloat` — it is rendered the same way here so the two agree.
|
|
70
|
+
*/
|
|
71
|
+
export type BucketValueColumn = "fps" | "visible_ms" | "js_heap_bytes" | "long_frames" | "ar_placement_scale";
|
|
72
|
+
/**
|
|
73
|
+
* A predicate a measure may add beyond its event-type filter.
|
|
74
|
+
*
|
|
75
|
+
* A closed vocabulary of three shapes over promoted columns only. Values are
|
|
76
|
+
* compile-time constants declared in {@link BUCKET_MEASURES}, never request
|
|
77
|
+
* input, and are bound as parameters regardless.
|
|
78
|
+
*/
|
|
79
|
+
export type BucketPredicate =
|
|
80
|
+
/** `mesh`/`name`/`source` equals (or does not equal) a constant. `''` is the store's "unknown". */
|
|
81
|
+
{
|
|
82
|
+
readonly kind: "eq" | "ne";
|
|
83
|
+
readonly column: "mesh" | "name" | "source";
|
|
84
|
+
readonly value: string;
|
|
85
|
+
}
|
|
86
|
+
/** `name`/`source` is one of a constant set. */
|
|
87
|
+
| {
|
|
88
|
+
readonly kind: "in";
|
|
89
|
+
readonly column: "name" | "source";
|
|
90
|
+
readonly values: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A vector column carries a full coordinate (`arrayLength(col) = arity`) — the
|
|
94
|
+
* same guard every spatial aggregation applies before it bins a point, so the
|
|
95
|
+
* bucket count matches the heatmap's own row total.
|
|
96
|
+
*/
|
|
97
|
+
| {
|
|
98
|
+
readonly kind: "geometry";
|
|
99
|
+
readonly column: string;
|
|
100
|
+
readonly arity: 2 | 3;
|
|
101
|
+
};
|
|
102
|
+
/** The aggregate that turns a bucket's matching events into one value. */
|
|
103
|
+
export type BucketAggregate = {
|
|
104
|
+
readonly kind: "count";
|
|
105
|
+
} | {
|
|
106
|
+
readonly kind: "sessions";
|
|
107
|
+
} | {
|
|
108
|
+
readonly kind: "sum";
|
|
109
|
+
readonly column: BucketValueColumn;
|
|
110
|
+
} | {
|
|
111
|
+
readonly kind: "avg";
|
|
112
|
+
readonly column: BucketValueColumn;
|
|
113
|
+
} | {
|
|
114
|
+
readonly kind: "max";
|
|
115
|
+
readonly column: BucketValueColumn;
|
|
116
|
+
} | {
|
|
117
|
+
readonly kind: "quantile";
|
|
118
|
+
readonly column: BucketValueColumn;
|
|
119
|
+
readonly q: number;
|
|
120
|
+
};
|
|
121
|
+
/** How a metric's per-bucket values combine into one number for a whole window. */
|
|
122
|
+
export type BucketRollup =
|
|
123
|
+
/** Additive quantities (counts, durations, sessions): the window value is the sum. */
|
|
124
|
+
"sum"
|
|
125
|
+
/** Levels (an FPS median, a heap percentile, a scale): the window value is the mean of the buckets. */
|
|
126
|
+
| "mean";
|
|
127
|
+
/** The promoted dimensions a bucket series may be split by. A `DimensionId` subset. */
|
|
128
|
+
export type BucketSplitDimension = "scene" | "mesh" | "name" | "source" | "event_type";
|
|
129
|
+
/** The `events` column behind each split dimension. */
|
|
130
|
+
export declare const BUCKET_SPLIT_COLUMNS: Readonly<Record<BucketSplitDimension, string>>;
|
|
131
|
+
/** Whether an arbitrary string names a supported split dimension. */
|
|
132
|
+
export declare function isBucketSplitDimension(value: string): value is BucketSplitDimension;
|
|
133
|
+
/** How to reproduce one metric's `comparable.primary` as a per-bucket series. */
|
|
134
|
+
export interface BucketMeasure {
|
|
135
|
+
/**
|
|
136
|
+
* The registry column this series stands for — always the metric's
|
|
137
|
+
* `comparable.primary`, asserted in `src/__tests__/insights.test.ts`.
|
|
138
|
+
*/
|
|
139
|
+
readonly column: string;
|
|
140
|
+
/** Capture channels the series counts over. Empty means *every* event type. */
|
|
141
|
+
readonly eventTypes: readonly string[];
|
|
142
|
+
/** Extra predicates, ANDed with the event-type filter. */
|
|
143
|
+
readonly where?: readonly BucketPredicate[];
|
|
144
|
+
readonly aggregate: BucketAggregate;
|
|
145
|
+
readonly rollup: BucketRollup;
|
|
146
|
+
/**
|
|
147
|
+
* The one dimension this series can be split by, for `anomalies`' contributor
|
|
148
|
+
* attribution (#306). Absent means the metric's excess has no single promoted
|
|
149
|
+
* column that explains it, and the contributor is reported as `null`.
|
|
150
|
+
*/
|
|
151
|
+
readonly splitBy?: BucketSplitDimension;
|
|
152
|
+
/** Where the bucket form differs in emphasis from the metric's own endpoint. */
|
|
153
|
+
readonly note?: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The portable bucket form of every comparable metric that has one, keyed by
|
|
157
|
+
* metric id. See the module doc for what is deliberately absent.
|
|
158
|
+
*/
|
|
159
|
+
export declare const BUCKET_MEASURES: Readonly<Partial<Record<MetricId, BucketMeasure>>>;
|
|
160
|
+
/**
|
|
161
|
+
* Every metric that has a portable bucket series, sorted — the list a `400`
|
|
162
|
+
* quotes back when a caller asks for one that does not.
|
|
163
|
+
*/
|
|
164
|
+
export declare const BUCKETABLE_METRIC_IDS: readonly MetricId[];
|
|
165
|
+
/** The bucket measure for a metric, or `undefined` when it has no portable series. */
|
|
166
|
+
export declare function bucketMeasureFor(metric: string): BucketMeasure | undefined;
|
|
167
|
+
/** Whether a metric id can be asked for a bucket series. */
|
|
168
|
+
export declare function isBucketableMetric(metric: string): boolean;
|
|
169
|
+
/**
|
|
170
|
+
* The metrics `movers` scans when the caller names none, in the order it scans
|
|
171
|
+
* them, capped by {@link MOVERS_MAX_METRICS}.
|
|
172
|
+
*
|
|
173
|
+
* Curated rather than "every bucketable metric": each request costs one grouped
|
|
174
|
+
* scan per metric, so an uncapped default would make a single `movers` call the
|
|
175
|
+
* most expensive endpoint the collector serves. The list leads with the signals
|
|
176
|
+
* a person actually asks "what changed?" about — traffic, performance, errors,
|
|
177
|
+
* interaction — and covers each of them exactly once, so two entries never
|
|
178
|
+
* report the same move twice under different names. Any other bucketable metric
|
|
179
|
+
* is still reachable through the `metrics=` allowlist.
|
|
180
|
+
*/
|
|
181
|
+
export declare const MOVERS_DEFAULT_METRICS: readonly MetricId[];
|
|
182
|
+
/**
|
|
183
|
+
* The hard cap on how many metrics one `movers` request may scan.
|
|
184
|
+
*
|
|
185
|
+
* Each scanned metric is one grouped scan of `events` over the combined
|
|
186
|
+
* reference+current window, so the cost of the endpoint is linear in this
|
|
187
|
+
* number and bounded by it whatever the caller asks for. Enforced in two
|
|
188
|
+
* places, both of which are visible to the caller: the route's `metrics=`
|
|
189
|
+
* schema rejects a longer allowlist with a `400`, and
|
|
190
|
+
* {@link MOVERS_DEFAULT_METRICS} is asserted to be no longer than this.
|
|
191
|
+
*/
|
|
192
|
+
export declare const MOVERS_MAX_METRICS = 24;
|
|
193
|
+
/**
|
|
194
|
+
* The named auxiliary series a metric may declare.
|
|
195
|
+
*
|
|
196
|
+
* - `denominator` — the `rateOf` column beside a rate numerator;
|
|
197
|
+
* - `numerator` — the counted part of a ratio whose metric has no main measure;
|
|
198
|
+
* - `p05` — the 5th percentile of a distribution whose main measure is its median.
|
|
199
|
+
*/
|
|
200
|
+
export type BucketVariant = "numerator" | "denominator" | "p05";
|
|
201
|
+
/**
|
|
202
|
+
* Named auxiliary series, by metric. See the section header for why these are
|
|
203
|
+
* separate from {@link BUCKET_MEASURES} rather than entries in it.
|
|
204
|
+
*/
|
|
205
|
+
export declare const BUCKET_MEASURE_VARIANTS: Readonly<Partial<Record<MetricId, Readonly<Partial<Record<BucketVariant, BucketMeasure>>>>>>;
|
|
206
|
+
/**
|
|
207
|
+
* The auxiliary series a metric declares under `variant`, or `undefined`.
|
|
208
|
+
*
|
|
209
|
+
* Kept separate from {@link bucketMeasureFor} so that "has a portable bucket
|
|
210
|
+
* series" — the question `baseline` and `movers` validate a caller's `metric`
|
|
211
|
+
* against — keeps meaning exactly what it meant before: a faithful per-bucket
|
|
212
|
+
* form of the metric's *own* headline column.
|
|
213
|
+
*/
|
|
214
|
+
export declare function bucketVariantFor(metric: string, variant: BucketVariant): BucketMeasure | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* The measure a bucket read resolves to: a metric's main series, or the named
|
|
217
|
+
* auxiliary one. The single lookup `buildMetricBuckets` and
|
|
218
|
+
* `evaluateBucketMeasure` share, so the SQL and the in-memory path cannot
|
|
219
|
+
* resolve the same request differently.
|
|
220
|
+
*/
|
|
221
|
+
export declare function resolveBucketMeasure(metric: string, variant?: BucketVariant): BucketMeasure | undefined;
|
|
222
|
+
//# sourceMappingURL=measures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measures.d.ts","sourceRoot":"","sources":["../../src/insights/measures.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Portable bucket measures** — the input side of the insight primitives
|
|
3
|
+
* (ADR 0051 §4, design sketch §D).
|
|
4
|
+
*
|
|
5
|
+
* `baseline` and `movers` both answer questions about *one number moving over
|
|
6
|
+
* time*: "what is normal for this scene", "what changed since last week". Both
|
|
7
|
+
* therefore need the same thing from the store — a per-bucket series of a
|
|
8
|
+
* comparable metric's `comparable.primary` column — and neither may compute
|
|
9
|
+
* statistics in SQL, because five dialects' `quantile`, `median` and
|
|
10
|
+
* `stddev` do not agree and the answer must not depend on which engine a
|
|
11
|
+
* self-hoster chose.
|
|
12
|
+
*
|
|
13
|
+
* This module is the declaration that makes that series portable. For each
|
|
14
|
+
* comparable registry metric it records **how to reproduce that metric's
|
|
15
|
+
* primary column per time bucket** as a single grouped scan of `events`:
|
|
16
|
+
*
|
|
17
|
+
* - which capture channels the metric reads (`eventTypes`);
|
|
18
|
+
* - a closed vocabulary of extra predicates (`where`) — a promoted-column
|
|
19
|
+
* equality or set membership, or a geometry-arity guard. Never free SQL, so
|
|
20
|
+
* nothing here can become an injection surface;
|
|
21
|
+
* - the aggregate that produces the value (`count`, distinct `sessions`, `sum`,
|
|
22
|
+
* `avg`, `max`, `quantile`);
|
|
23
|
+
* - how bucket values combine into a window total (`rollup`).
|
|
24
|
+
*
|
|
25
|
+
* `buckets.ts` renders exactly one generic query from this data, per dialect.
|
|
26
|
+
*
|
|
27
|
+
* ## What is *not* here, and why
|
|
28
|
+
*
|
|
29
|
+
* 42 of the registry's 63 `comparable` metrics have a faithful portable bucket
|
|
30
|
+
* form. The rest do not, and are deliberately absent rather than approximated:
|
|
31
|
+
*
|
|
32
|
+
* - **funnel-shaped** metrics (`funnel`, `load_bounce_funnel`,
|
|
33
|
+
* `variant_leaderboard`, `scene_retention`) are defined by request-supplied
|
|
34
|
+
* step predicates or by a cohort that spans buckets — bucketing them changes
|
|
35
|
+
* what they mean;
|
|
36
|
+
* - **join- or window-function-shaped** metrics (`click_rays`, `flow_links`,
|
|
37
|
+
* `mesh_reachability`, `navigation_stats`, `backtrack_ratio`,
|
|
38
|
+
* `xr_rotation`, `xr_abandonment`, `perf_churn`, `rage_clicks`) derive a row
|
|
39
|
+
* from the *relationship between consecutive events*, which a per-bucket
|
|
40
|
+
* `GROUP BY` cannot express without changing the answer at every bucket
|
|
41
|
+
* boundary;
|
|
42
|
+
* - **per-session-then-aggregate** ratios (`jank_rate`) and payload-derived
|
|
43
|
+
* percentiles that are not promoted columns (`frame_time_percentiles`,
|
|
44
|
+
* `render_scale_truth`, `mesh_uv_heatmap`, `mesh_blind_spots`,
|
|
45
|
+
* `xr_boundary_contacts`) would need a second aggregation level or a JSON
|
|
46
|
+
* extraction whose null-handling differs from the metric's own.
|
|
47
|
+
*
|
|
48
|
+
* An approximate series is worse than no series: `baseline` would report a
|
|
49
|
+
* "normal" that the metric's own endpoint never produces. Asking for one of
|
|
50
|
+
* those metrics is a `400` naming the ids that *do* have a series
|
|
51
|
+
* ({@link BUCKETABLE_METRIC_IDS}), which is a better answer than a plausible
|
|
52
|
+
* wrong number. Widening the catalog is additive — add an entry here, a parity
|
|
53
|
+
* case in `src/parity/cases.ts`, and the metric becomes available to both
|
|
54
|
+
* primitives with no route or registry change.
|
|
55
|
+
*/
|
|
56
|
+
/** Bucket width in seconds, the unit `Dialect.timeBucketMs` takes. */
|
|
57
|
+
export const BUCKET_SECONDS = {
|
|
58
|
+
day: 86_400,
|
|
59
|
+
hour: 3_600,
|
|
60
|
+
};
|
|
61
|
+
/** Whether an arbitrary string names a supported bucket grain. */
|
|
62
|
+
export function isBucketGrain(value) {
|
|
63
|
+
return value === "day" || value === "hour";
|
|
64
|
+
}
|
|
65
|
+
/** The `events` column behind each split dimension. */
|
|
66
|
+
export const BUCKET_SPLIT_COLUMNS = {
|
|
67
|
+
scene: "scene_id",
|
|
68
|
+
mesh: "mesh",
|
|
69
|
+
name: "name",
|
|
70
|
+
source: "source",
|
|
71
|
+
event_type: "event_type",
|
|
72
|
+
};
|
|
73
|
+
/** Whether an arbitrary string names a supported split dimension. */
|
|
74
|
+
export function isBucketSplitDimension(value) {
|
|
75
|
+
return Object.prototype.hasOwnProperty.call(BUCKET_SPLIT_COLUMNS, value);
|
|
76
|
+
}
|
|
77
|
+
/** Shorthand for the commonest measure: "count these channels' events per bucket". */
|
|
78
|
+
function counted(column, eventTypes, where, note) {
|
|
79
|
+
return {
|
|
80
|
+
column,
|
|
81
|
+
eventTypes,
|
|
82
|
+
...(where ? { where } : {}),
|
|
83
|
+
aggregate: { kind: "count" },
|
|
84
|
+
rollup: "sum",
|
|
85
|
+
...(note ? { note } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** A vector-column arity guard, matching the spatial aggregations' own. */
|
|
89
|
+
function geometry(column, arity) {
|
|
90
|
+
return { kind: "geometry", column, arity };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Declare the dimension a measure's excess can be attributed to (#306).
|
|
94
|
+
*
|
|
95
|
+
* A wrapper rather than an extra parameter, so the catalog below stays one
|
|
96
|
+
* expression per metric and giving a metric attribution is a purely additive
|
|
97
|
+
* edit to its own line.
|
|
98
|
+
*/
|
|
99
|
+
function split(measure, splitBy) {
|
|
100
|
+
return { ...measure, splitBy };
|
|
101
|
+
}
|
|
102
|
+
/** The median-FPS series shared by every metric whose primary is `p50_fps`. */
|
|
103
|
+
const P50_FPS = {
|
|
104
|
+
column: "p50_fps",
|
|
105
|
+
eventTypes: ["frame_perf"],
|
|
106
|
+
aggregate: { kind: "quantile", column: "fps", q: 0.5 },
|
|
107
|
+
rollup: "mean",
|
|
108
|
+
// The only promoted dimension a frame rate can be attributed to: an FPS drop
|
|
109
|
+
// confined to one scene is a scene problem, one spread across them is a build
|
|
110
|
+
// problem. Device is the split a reader would ask for next, but it lives in
|
|
111
|
+
// the `session_start` payload rather than on the frame, so it is not reachable
|
|
112
|
+
// from a single grouped scan of `events` (#306).
|
|
113
|
+
splitBy: "scene",
|
|
114
|
+
note: "The bucket series is the median over the bucket's raw `frame_perf` samples. The metric's own " +
|
|
115
|
+
"endpoint computes per session and then aggregates (ADR 0028 §1), so a bucket median and the " +
|
|
116
|
+
"endpoint's headline can differ slightly when one session dominates a bucket.",
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The portable bucket form of every comparable metric that has one, keyed by
|
|
120
|
+
* metric id. See the module doc for what is deliberately absent.
|
|
121
|
+
*/
|
|
122
|
+
export const BUCKET_MEASURES = {
|
|
123
|
+
// --- volume / orientation ------------------------------------------------
|
|
124
|
+
list_sessions: split(counted("events", []), "event_type"),
|
|
125
|
+
list_scenes: split(counted("events", []), "scene"),
|
|
126
|
+
timeseries: split(counted("events", []), "event_type"),
|
|
127
|
+
event_counts: split(counted("count", []), "event_type"),
|
|
128
|
+
events_daily: split(counted("events", []), "event_type"),
|
|
129
|
+
// --- attention / spatial -------------------------------------------------
|
|
130
|
+
// Each counts exactly the events its heatmap bins, geometry guard included, so
|
|
131
|
+
// the series total equals the sum of the heatmap's own bin counts.
|
|
132
|
+
pointer_heatmap: split(counted("count", ["pointer_move", "pointer_click"], [geometry("screen", 2)]), "event_type"),
|
|
133
|
+
world_heatmap: split(counted("count", ["pointer_move", "pointer_click"], [geometry("hit_point", 3)]), "mesh"),
|
|
134
|
+
gaze_heatmap: split(counted("count", ["camera_sample"], [geometry("hit_point", 3)]), "mesh"),
|
|
135
|
+
camera_heatmap: split(counted("count", ["camera_sample"], [geometry("direction", 3)]), "scene"),
|
|
136
|
+
position_heatmap: split(counted("count", ["camera_sample"], [geometry("position", 3)]), "scene"),
|
|
137
|
+
scene_coverage: split(counted("count", ["camera_sample"], [geometry("position", 3)]), "scene"),
|
|
138
|
+
view_coverage_histogram: {
|
|
139
|
+
column: "sessions",
|
|
140
|
+
eventTypes: ["camera_sample"],
|
|
141
|
+
aggregate: { kind: "sessions" },
|
|
142
|
+
rollup: "sum",
|
|
143
|
+
note: "Distinct sessions are counted *within* each bucket, so a session that spans two buckets " +
|
|
144
|
+
"contributes to both and the window total is an upper bound on distinct sessions.",
|
|
145
|
+
},
|
|
146
|
+
// --- meshes / interaction -----------------------------------------------
|
|
147
|
+
top_meshes: split(counted("count", ["mesh_interaction", "pointer_click", "camera_sample"], [{ kind: "ne", column: "mesh", value: "" }]), "mesh"),
|
|
148
|
+
mesh_sources: split(counted("count", ["mesh_interaction", "pointer_click"], [{ kind: "ne", column: "mesh", value: "" }]), "source"),
|
|
149
|
+
mesh_trend: split(counted("count", ["mesh_interaction", "pointer_click"], [{ kind: "ne", column: "mesh", value: "" }]), "mesh"),
|
|
150
|
+
mesh_interaction_kinds: split(counted("count", ["mesh_interaction"], [{ kind: "ne", column: "mesh", value: "" }]), "name"),
|
|
151
|
+
mesh_dwell: {
|
|
152
|
+
column: "visible_ms",
|
|
153
|
+
eventTypes: ["mesh_visibility"],
|
|
154
|
+
where: [{ kind: "ne", column: "mesh", value: "" }],
|
|
155
|
+
aggregate: { kind: "sum", column: "visible_ms" },
|
|
156
|
+
rollup: "sum",
|
|
157
|
+
splitBy: "mesh",
|
|
158
|
+
},
|
|
159
|
+
dead_clicks: split(counted("dead_clicks", ["pointer_click"], [{ kind: "eq", column: "mesh", value: "" }], "Counts the clicks that hit nothing. The metric's own `total_clicks` denominator is not " +
|
|
160
|
+
"part of the series — read `pointer_heatmap` alongside it for click volume."), "source"),
|
|
161
|
+
hover_dwell: {
|
|
162
|
+
column: "dwell_ms",
|
|
163
|
+
eventTypes: ["hover_dwell"],
|
|
164
|
+
aggregate: { kind: "sum", column: "visible_ms" },
|
|
165
|
+
rollup: "sum",
|
|
166
|
+
splitBy: "mesh",
|
|
167
|
+
},
|
|
168
|
+
interaction_sources: split(counted("count", ["pointer_click", "pointer_move", "mesh_interaction", "input_action"]), "source"),
|
|
169
|
+
top_input_actions: split(counted("count", ["input_action"]), "name"),
|
|
170
|
+
camera_gestures: split(counted("gestures", ["camera_gesture"]), "name"),
|
|
171
|
+
// --- performance ---------------------------------------------------------
|
|
172
|
+
perf_summary: P50_FPS,
|
|
173
|
+
perf_distribution: P50_FPS,
|
|
174
|
+
perf_by_scene: P50_FPS,
|
|
175
|
+
perf_by_device: P50_FPS,
|
|
176
|
+
perf_daily: P50_FPS,
|
|
177
|
+
perf_heatmap: {
|
|
178
|
+
column: "avg_fps",
|
|
179
|
+
eventTypes: ["frame_perf"],
|
|
180
|
+
aggregate: { kind: "avg", column: "fps" },
|
|
181
|
+
rollup: "mean",
|
|
182
|
+
splitBy: "scene",
|
|
183
|
+
},
|
|
184
|
+
fps_histogram: {
|
|
185
|
+
column: "sessions",
|
|
186
|
+
eventTypes: ["frame_perf"],
|
|
187
|
+
aggregate: { kind: "sessions" },
|
|
188
|
+
rollup: "sum",
|
|
189
|
+
splitBy: "scene",
|
|
190
|
+
note: "Distinct sessions with perf samples, counted within each bucket; a session spanning two " +
|
|
191
|
+
"buckets contributes to both.",
|
|
192
|
+
},
|
|
193
|
+
compile_stalls: {
|
|
194
|
+
column: "total_ms",
|
|
195
|
+
eventTypes: ["compile_stall"],
|
|
196
|
+
aggregate: { kind: "sum", column: "visible_ms" },
|
|
197
|
+
rollup: "sum",
|
|
198
|
+
splitBy: "name",
|
|
199
|
+
},
|
|
200
|
+
resource_summary: {
|
|
201
|
+
column: "max_js_heap_bytes",
|
|
202
|
+
eventTypes: ["resource_sample"],
|
|
203
|
+
aggregate: { kind: "max", column: "js_heap_bytes" },
|
|
204
|
+
rollup: "mean",
|
|
205
|
+
note: "Each bucket reports its own peak heap; the window value is the mean of those peaks.",
|
|
206
|
+
},
|
|
207
|
+
resource_percentiles: {
|
|
208
|
+
column: "p95_js_heap_bytes",
|
|
209
|
+
eventTypes: ["resource_sample"],
|
|
210
|
+
aggregate: { kind: "quantile", column: "js_heap_bytes", q: 0.95 },
|
|
211
|
+
rollup: "mean",
|
|
212
|
+
},
|
|
213
|
+
// --- errors / stability --------------------------------------------------
|
|
214
|
+
stability_counts: split(counted("incidents", ["context_lost", "compile_stall"]), "event_type"),
|
|
215
|
+
graphics_diagnostics: split(counted("incidents", ["graphics_diagnostic"]), "scene"),
|
|
216
|
+
error_heatmap: split(counted("count", ["runtime_error", "graphics_diagnostic"]), "event_type"),
|
|
217
|
+
capability_changes: split(counted("changes", ["capability_change"]), "name"),
|
|
218
|
+
rendering_technology: {
|
|
219
|
+
column: "sessions",
|
|
220
|
+
eventTypes: ["session_start"],
|
|
221
|
+
aggregate: { kind: "sessions" },
|
|
222
|
+
rollup: "sum",
|
|
223
|
+
splitBy: "scene",
|
|
224
|
+
},
|
|
225
|
+
// --- XR / AR -------------------------------------------------------------
|
|
226
|
+
xr_locomotion: {
|
|
227
|
+
column: "locomotion_ms",
|
|
228
|
+
eventTypes: ["camera_gesture"],
|
|
229
|
+
where: [{ kind: "in", column: "name", values: ["fly", "navigate"] }],
|
|
230
|
+
aggregate: { kind: "sum", column: "visible_ms" },
|
|
231
|
+
rollup: "sum",
|
|
232
|
+
splitBy: "name",
|
|
233
|
+
},
|
|
234
|
+
xr_tracking_quality: {
|
|
235
|
+
column: "degraded_ms",
|
|
236
|
+
eventTypes: ["capability_change"],
|
|
237
|
+
where: [{ kind: "eq", column: "name", value: "tracking" }],
|
|
238
|
+
aggregate: { kind: "sum", column: "visible_ms" },
|
|
239
|
+
rollup: "sum",
|
|
240
|
+
},
|
|
241
|
+
boundary_heatmap: split(counted("count", ["xr_boundary_proximity"]), "scene"),
|
|
242
|
+
ar_placement_time_to_place: counted("placements", ["ar_placement"]),
|
|
243
|
+
ar_placement_attempts: counted("placements", ["ar_placement"]),
|
|
244
|
+
ar_placement_surfaces: {
|
|
245
|
+
column: "avg_scale",
|
|
246
|
+
eventTypes: ["ar_placement"],
|
|
247
|
+
aggregate: { kind: "avg", column: "ar_placement_scale" },
|
|
248
|
+
rollup: "mean",
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Every metric that has a portable bucket series, sorted — the list a `400`
|
|
253
|
+
* quotes back when a caller asks for one that does not.
|
|
254
|
+
*/
|
|
255
|
+
export const BUCKETABLE_METRIC_IDS = Object.keys(BUCKET_MEASURES).sort();
|
|
256
|
+
/** The bucket measure for a metric, or `undefined` when it has no portable series. */
|
|
257
|
+
export function bucketMeasureFor(metric) {
|
|
258
|
+
return Object.prototype.hasOwnProperty.call(BUCKET_MEASURES, metric)
|
|
259
|
+
? BUCKET_MEASURES[metric]
|
|
260
|
+
: undefined;
|
|
261
|
+
}
|
|
262
|
+
/** Whether a metric id can be asked for a bucket series. */
|
|
263
|
+
export function isBucketableMetric(metric) {
|
|
264
|
+
return bucketMeasureFor(metric) != null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The metrics `movers` scans when the caller names none, in the order it scans
|
|
268
|
+
* them, capped by {@link MOVERS_MAX_METRICS}.
|
|
269
|
+
*
|
|
270
|
+
* Curated rather than "every bucketable metric": each request costs one grouped
|
|
271
|
+
* scan per metric, so an uncapped default would make a single `movers` call the
|
|
272
|
+
* most expensive endpoint the collector serves. The list leads with the signals
|
|
273
|
+
* a person actually asks "what changed?" about — traffic, performance, errors,
|
|
274
|
+
* interaction — and covers each of them exactly once, so two entries never
|
|
275
|
+
* report the same move twice under different names. Any other bucketable metric
|
|
276
|
+
* is still reachable through the `metrics=` allowlist.
|
|
277
|
+
*/
|
|
278
|
+
export const MOVERS_DEFAULT_METRICS = [
|
|
279
|
+
// traffic and reach
|
|
280
|
+
"list_sessions",
|
|
281
|
+
"fps_histogram",
|
|
282
|
+
"rendering_technology",
|
|
283
|
+
// performance
|
|
284
|
+
"perf_summary",
|
|
285
|
+
"perf_heatmap",
|
|
286
|
+
"compile_stalls",
|
|
287
|
+
"resource_percentiles",
|
|
288
|
+
// errors and stability
|
|
289
|
+
"error_heatmap",
|
|
290
|
+
"stability_counts",
|
|
291
|
+
"graphics_diagnostics",
|
|
292
|
+
"capability_changes",
|
|
293
|
+
// interaction
|
|
294
|
+
"pointer_heatmap",
|
|
295
|
+
"dead_clicks",
|
|
296
|
+
"top_meshes",
|
|
297
|
+
"mesh_interaction_kinds",
|
|
298
|
+
"mesh_dwell",
|
|
299
|
+
"hover_dwell",
|
|
300
|
+
"top_input_actions",
|
|
301
|
+
"camera_gestures",
|
|
302
|
+
// attention and navigation
|
|
303
|
+
"camera_heatmap",
|
|
304
|
+
"gaze_heatmap",
|
|
305
|
+
"scene_coverage",
|
|
306
|
+
// XR / AR
|
|
307
|
+
"xr_locomotion",
|
|
308
|
+
"boundary_heatmap",
|
|
309
|
+
];
|
|
310
|
+
/**
|
|
311
|
+
* The hard cap on how many metrics one `movers` request may scan.
|
|
312
|
+
*
|
|
313
|
+
* Each scanned metric is one grouped scan of `events` over the combined
|
|
314
|
+
* reference+current window, so the cost of the endpoint is linear in this
|
|
315
|
+
* number and bounded by it whatever the caller asks for. Enforced in two
|
|
316
|
+
* places, both of which are visible to the caller: the route's `metrics=`
|
|
317
|
+
* schema rejects a longer allowlist with a `400`, and
|
|
318
|
+
* {@link MOVERS_DEFAULT_METRICS} is asserted to be no longer than this.
|
|
319
|
+
*/
|
|
320
|
+
export const MOVERS_MAX_METRICS = 24;
|
|
321
|
+
/** XR input sources (ADR 0011) — the same set `buildXrAbandonment` restricts to. */
|
|
322
|
+
const XR_SOURCES = ["xr-controller", "hand", "gaze", "transient"];
|
|
323
|
+
/** Event channels that carry the input-source vocabulary, as `xr_abandonment` reads them. */
|
|
324
|
+
const XR_INTERACTION_CHANNELS = ["pointer_click", "pointer_move", "mesh_interaction"];
|
|
325
|
+
/** Distinct sessions per bucket — the denominator of every "per session" factor. */
|
|
326
|
+
function perSessionDenominator(eventTypes, note) {
|
|
327
|
+
return {
|
|
328
|
+
column: "sessions",
|
|
329
|
+
eventTypes,
|
|
330
|
+
aggregate: { kind: "sessions" },
|
|
331
|
+
rollup: "sum",
|
|
332
|
+
note,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Named auxiliary series, by metric. See the section header for why these are
|
|
337
|
+
* separate from {@link BUCKET_MEASURES} rather than entries in it.
|
|
338
|
+
*/
|
|
339
|
+
export const BUCKET_MEASURE_VARIANTS = {
|
|
340
|
+
// The `rateOf` denominator of the dead-click rate: every click, not only the
|
|
341
|
+
// ones that hit nothing. `dead_clicks / total_clicks` is then a genuine
|
|
342
|
+
// proportion, which is what makes a two-proportion test legitimate.
|
|
343
|
+
dead_clicks: {
|
|
344
|
+
denominator: counted("total_clicks", ["pointer_click"]),
|
|
345
|
+
},
|
|
346
|
+
// The tail of the FPS distribution rather than its middle. Health asks "how
|
|
347
|
+
// bad does it get here", and a scene whose median is 60 while its 5th
|
|
348
|
+
// percentile is 12 is not a smooth scene — the median alone cannot say that.
|
|
349
|
+
perf_summary: {
|
|
350
|
+
p05: {
|
|
351
|
+
column: "p50_fps",
|
|
352
|
+
eventTypes: ["frame_perf"],
|
|
353
|
+
aggregate: { kind: "quantile", column: "fps", q: 0.05 },
|
|
354
|
+
rollup: "mean",
|
|
355
|
+
note: "The 5th percentile of the bucket's raw `frame_perf` samples — the frame rate in the " +
|
|
356
|
+
"worst twentieth of sampled windows, not the median the metric's own headline reports.",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
// `jank_rate`'s own endpoint computes a rate per session and then takes the
|
|
360
|
+
// median of those (ADR 0028 §1), which no `GROUP BY` reproduces. This pair is
|
|
361
|
+
// the *pooled* rate over the same raw material: long frames per sampled
|
|
362
|
+
// window, across every session in the bucket. A different statistic, named as
|
|
363
|
+
// one, and the only portable one.
|
|
364
|
+
jank_rate: {
|
|
365
|
+
numerator: {
|
|
366
|
+
column: "total_long_frames",
|
|
367
|
+
eventTypes: ["frame_perf"],
|
|
368
|
+
aggregate: { kind: "sum", column: "long_frames" },
|
|
369
|
+
rollup: "sum",
|
|
370
|
+
note: "Long frames summed across every session in the bucket. The metric's own `median_rate` " +
|
|
371
|
+
"is a per-session median, so the two agree in direction but not in value.",
|
|
372
|
+
},
|
|
373
|
+
denominator: counted("sessions", ["frame_perf"], undefined, "Sampled perf windows in scope — the denominator the SDK's own rate is per."),
|
|
374
|
+
},
|
|
375
|
+
// Errors per session. The numerator is the metric's own count; the
|
|
376
|
+
// denominator is sessions, because an error count with no traffic behind it
|
|
377
|
+
// says nothing — twice the errors on three times the visitors is an
|
|
378
|
+
// improvement.
|
|
379
|
+
error_heatmap: {
|
|
380
|
+
denominator: perSessionDenominator(["session_start"], "Sessions started in the bucket — the denominator that turns an error count into a rate."),
|
|
381
|
+
},
|
|
382
|
+
// Exploration per session. A true voxel-coverage *percentage* needs the
|
|
383
|
+
// scene's registered bounds (`scene_representation`) and has no portable
|
|
384
|
+
// per-bucket form; positioned camera samples per session is the portable
|
|
385
|
+
// proxy, and it is normalised against the project's own baseline rather than
|
|
386
|
+
// read as an absolute.
|
|
387
|
+
scene_coverage: {
|
|
388
|
+
denominator: perSessionDenominator(["camera_sample"], "Sessions that produced a camera sample in the bucket."),
|
|
389
|
+
},
|
|
390
|
+
// XR interactions per XR session. `xr_abandonment`'s own endpoint needs a
|
|
391
|
+
// session-level anti-join; this pair is the portable inverse signal — a
|
|
392
|
+
// headset session that interacts with nothing is the abandonment the metric
|
|
393
|
+
// is looking for. Both sides are restricted to XR input sources, so the
|
|
394
|
+
// factor is absent (rather than zero) in a project with no XR traffic.
|
|
395
|
+
xr_abandonment: {
|
|
396
|
+
numerator: counted("xr_interactions", XR_INTERACTION_CHANNELS, [
|
|
397
|
+
{ kind: "in", column: "source", values: XR_SOURCES },
|
|
398
|
+
]),
|
|
399
|
+
denominator: {
|
|
400
|
+
column: "session_id",
|
|
401
|
+
eventTypes: XR_INTERACTION_CHANNELS,
|
|
402
|
+
where: [{ kind: "in", column: "source", values: XR_SOURCES }],
|
|
403
|
+
aggregate: { kind: "sessions" },
|
|
404
|
+
rollup: "sum",
|
|
405
|
+
note: "Distinct sessions that produced at least one XR-sourced interaction in the bucket.",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* The auxiliary series a metric declares under `variant`, or `undefined`.
|
|
411
|
+
*
|
|
412
|
+
* Kept separate from {@link bucketMeasureFor} so that "has a portable bucket
|
|
413
|
+
* series" — the question `baseline` and `movers` validate a caller's `metric`
|
|
414
|
+
* against — keeps meaning exactly what it meant before: a faithful per-bucket
|
|
415
|
+
* form of the metric's *own* headline column.
|
|
416
|
+
*/
|
|
417
|
+
export function bucketVariantFor(metric, variant) {
|
|
418
|
+
if (!Object.prototype.hasOwnProperty.call(BUCKET_MEASURE_VARIANTS, metric))
|
|
419
|
+
return undefined;
|
|
420
|
+
return BUCKET_MEASURE_VARIANTS[metric]?.[variant];
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The measure a bucket read resolves to: a metric's main series, or the named
|
|
424
|
+
* auxiliary one. The single lookup `buildMetricBuckets` and
|
|
425
|
+
* `evaluateBucketMeasure` share, so the SQL and the in-memory path cannot
|
|
426
|
+
* resolve the same request differently.
|
|
427
|
+
*/
|
|
428
|
+
export function resolveBucketMeasure(metric, variant) {
|
|
429
|
+
return variant == null ? bucketMeasureFor(metric) : bucketVariantFor(metric, variant);
|
|
430
|
+
}
|
|
431
|
+
//# sourceMappingURL=measures.js.map
|