@uptimizr/metrics 0.1.0 → 0.2.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 +129 -0
- package/README.md +43 -21
- package/dist/envelopes.d.ts +280 -0
- package/dist/envelopes.d.ts.map +1 -0
- package/dist/envelopes.js +289 -0
- package/dist/envelopes.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/narrative.d.ts +164 -0
- package/dist/narrative.d.ts.map +1 -0
- package/dist/narrative.js +150 -0
- package/dist/narrative.js.map +1 -0
- package/dist/panelSpec.d.ts +145 -0
- package/dist/panelSpec.d.ts.map +1 -0
- package/dist/panelSpec.js +286 -0
- package/dist/panelSpec.js.map +1 -0
- package/dist/query.d.ts +192 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +461 -0
- package/dist/query.js.map +1 -0
- package/dist/registry.d.ts +948 -98
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +954 -13
- package/dist/registry.js.map +1 -1
- package/llms.txt +12 -3
- package/package.json +4 -4
package/dist/registry.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* the Zod querystring that actually serves it.
|
|
36
36
|
*/
|
|
37
37
|
import { z } from "zod";
|
|
38
|
+
import { NARRATIVE_LIMITS, sessionNarrativeEntrySchema } from "./narrative.js";
|
|
38
39
|
/**
|
|
39
40
|
* Every exported `build*` aggregation name in `@uptimizr/db`'s
|
|
40
41
|
* `query/aggregations.ts`. The registry must cover them all.
|
|
@@ -64,6 +65,7 @@ export const AGGREGATION_BUILDER_NAMES = [
|
|
|
64
65
|
"buildCapabilityChanges",
|
|
65
66
|
"buildClickGazeRay",
|
|
66
67
|
"buildCompileStalls",
|
|
68
|
+
"buildCustomEventVocabulary",
|
|
67
69
|
"buildDeadClicks",
|
|
68
70
|
"buildDistinctScenes",
|
|
69
71
|
"buildErrorHeatmap",
|
|
@@ -134,6 +136,65 @@ export const DIMENSION_COLUMNS = {
|
|
|
134
136
|
"device.browser": "session_start.payload.device.browser (derived from the UA, ADR 0042)",
|
|
135
137
|
"device.os": "session_start.payload.device.os (derived from the UA, ADR 0042)",
|
|
136
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* The column a {@link DimensionId} appears as in a metric's `row` when that
|
|
141
|
+
* metric is keyed by it.
|
|
142
|
+
*
|
|
143
|
+
* Several dimensions are *filterable* on a metric without being part of its
|
|
144
|
+
* grain (`top_meshes` can be scoped to a `session` but returns one row per
|
|
145
|
+
* mesh), which is what {@link MetricDefinition.grainDimensions} exists to
|
|
146
|
+
* express — this map is how that declaration is **checked**: every grain
|
|
147
|
+
* dimension must project one of the names listed here, and
|
|
148
|
+
* `src/__tests__/registry.test.ts` fails the build when one does not.
|
|
149
|
+
*
|
|
150
|
+
* Alternatives are listed where the projection name has varied: a scene rollup
|
|
151
|
+
* spells `scene` as `scene_id` and a transition as `from_scene`, and the `name`
|
|
152
|
+
* column surfaces as `kind` (an interaction or gesture kind), `action` (an input
|
|
153
|
+
* action) or `phase` (a compile stall). The **first** entry is the canonical
|
|
154
|
+
* spelling the generic group-by tier projects when the metric's own row does not
|
|
155
|
+
* already name one. `cameraMode` has none: no aggregation projects the camera
|
|
156
|
+
* type as a column, so it is a filter on the delegated tier and a generic-only
|
|
157
|
+
* group-by column (`camera_mode`) on the other.
|
|
158
|
+
*/
|
|
159
|
+
export const DIMENSION_ROW_COLUMNS = {
|
|
160
|
+
scene: ["scene_id", "scene"],
|
|
161
|
+
session: ["session_id"],
|
|
162
|
+
mesh: ["mesh"],
|
|
163
|
+
name: ["name", "kind", "action", "phase"],
|
|
164
|
+
source: ["source"],
|
|
165
|
+
event_type: ["event_type"],
|
|
166
|
+
cameraMode: [],
|
|
167
|
+
"device.engine": ["engine"],
|
|
168
|
+
"device.renderer": ["renderer"],
|
|
169
|
+
"device.isMobile": ["is_mobile"],
|
|
170
|
+
"device.browser": ["browser"],
|
|
171
|
+
"device.os": ["os"],
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* The dimensions the **generic group-by tier** can render (design sketch §C.2).
|
|
175
|
+
*
|
|
176
|
+
* Restricted to what the store promotes to a column (`scene_id`, `session_id`,
|
|
177
|
+
* `mesh`, `name`, `source`, `event_type`) plus the session attributes every
|
|
178
|
+
* engine can read out of one `session_start` sub-select with the same SQL
|
|
179
|
+
* (`cameraMode`, the four `device.*` strings). `device.isMobile` is deliberately
|
|
180
|
+
* absent: it is the one session attribute that is a boolean rather than a label,
|
|
181
|
+
* so grouping by it would produce `"true"` / `"false"` text on some engines and
|
|
182
|
+
* `1` / `0` on others — the exact cross-engine disagreement this tier exists to
|
|
183
|
+
* avoid. Filter by it instead, or group by `device.os`.
|
|
184
|
+
*/
|
|
185
|
+
export const GENERIC_DIMENSIONS = [
|
|
186
|
+
"scene",
|
|
187
|
+
"session",
|
|
188
|
+
"mesh",
|
|
189
|
+
"name",
|
|
190
|
+
"source",
|
|
191
|
+
"event_type",
|
|
192
|
+
"cameraMode",
|
|
193
|
+
"device.engine",
|
|
194
|
+
"device.renderer",
|
|
195
|
+
"device.browser",
|
|
196
|
+
"device.os",
|
|
197
|
+
];
|
|
137
198
|
/**
|
|
138
199
|
* The single mapping from a request parameter to the option field it drives.
|
|
139
200
|
* Consumers (docs generator, OpenAPI, the DSL) read filter semantics here rather
|
|
@@ -208,7 +269,8 @@ export const FILTER_TARGETS = {
|
|
|
208
269
|
bucket: {
|
|
209
270
|
option: "BuilderOptions",
|
|
210
271
|
field: "bucket",
|
|
211
|
-
description: "
|
|
272
|
+
description: "Bucket width. On `fps_histogram` a histogram bin width in FPS; on the insight " +
|
|
273
|
+
"primitives the time grain of the series, `day` or `hour`.",
|
|
212
274
|
},
|
|
213
275
|
bucketMs: {
|
|
214
276
|
option: "BuilderOptions",
|
|
@@ -310,6 +372,62 @@ export const FILTER_TARGETS = {
|
|
|
310
372
|
field: "conversion",
|
|
311
373
|
description: "JSON funnel-step predicate for the success event. Omit to report views only.",
|
|
312
374
|
},
|
|
375
|
+
minDwellMs: {
|
|
376
|
+
option: "SessionNarrativeOptions",
|
|
377
|
+
field: "minDwellMs",
|
|
378
|
+
description: "Session narrative only: a mesh must hold attention for at least this many milliseconds " +
|
|
379
|
+
"before it earns a `dwell` entry. Raise it to keep only the meshes that were really looked at.",
|
|
380
|
+
},
|
|
381
|
+
maxEntries: {
|
|
382
|
+
option: "SessionNarrativeOptions",
|
|
383
|
+
field: "maxEntries",
|
|
384
|
+
description: "Session narrative only: the maximum number of entries, oldest first. The closing `summary` " +
|
|
385
|
+
"entry always survives and reports whether anything was dropped.",
|
|
386
|
+
},
|
|
387
|
+
metric: {
|
|
388
|
+
option: "InsightOptions",
|
|
389
|
+
field: "metric",
|
|
390
|
+
description: "The registry metric the insight is computed over. Must be a `comparable` metric that has a " +
|
|
391
|
+
"portable bucket series; a metric without one is rejected with the list of ids that do.",
|
|
392
|
+
},
|
|
393
|
+
metrics: {
|
|
394
|
+
option: "InsightOptions",
|
|
395
|
+
field: "metrics",
|
|
396
|
+
description: "Comma-separated allowlist of registry metric ids to scan instead of the curated default " +
|
|
397
|
+
"set. Bounded by the per-request metric cap the registry `limits` declare.",
|
|
398
|
+
},
|
|
399
|
+
window: {
|
|
400
|
+
option: "InsightOptions",
|
|
401
|
+
field: "windowDays",
|
|
402
|
+
description: "Length of the baseline window in days, counted back from `until`. Ignored when `since` is " +
|
|
403
|
+
"given explicitly.",
|
|
404
|
+
},
|
|
405
|
+
refSince: {
|
|
406
|
+
option: "InsightOptions",
|
|
407
|
+
field: "refSince",
|
|
408
|
+
description: "Inclusive lower bound of the reference window a change is measured against, epoch " +
|
|
409
|
+
"milliseconds. Defaults to the equal-length window immediately before the current range.",
|
|
410
|
+
},
|
|
411
|
+
refUntil: {
|
|
412
|
+
option: "InsightOptions",
|
|
413
|
+
field: "refUntil",
|
|
414
|
+
description: "Exclusive upper bound of the reference window, epoch milliseconds. Defaults to `since`.",
|
|
415
|
+
},
|
|
416
|
+
// --- anomalies (#306) ---
|
|
417
|
+
sensitivity: {
|
|
418
|
+
option: "InsightOptions",
|
|
419
|
+
field: "sensitivity",
|
|
420
|
+
description: "How far out of line a bucket must be before it is reported, in median-absolute-deviations " +
|
|
421
|
+
"of the trailing window. Higher means fewer, more extreme findings. 1-10, default 3.",
|
|
422
|
+
},
|
|
423
|
+
// --- significance / scene health (#307) ---
|
|
424
|
+
weights: {
|
|
425
|
+
option: "InsightOptions",
|
|
426
|
+
field: "weights",
|
|
427
|
+
description: "JSON object overriding the declared per-factor weights of a composite score, e.g. " +
|
|
428
|
+
'`{"error_rate":0.5}`. Factors the object does not name keep their declared weight; an ' +
|
|
429
|
+
"unknown factor id is rejected rather than ignored.",
|
|
430
|
+
},
|
|
313
431
|
format: {
|
|
314
432
|
option: "ResultEnvelope",
|
|
315
433
|
field: "(response layer)",
|
|
@@ -318,6 +436,15 @@ export const FILTER_TARGETS = {
|
|
|
318
436
|
"or a trend, with shares, a sample size, a templated reading and the caveats).",
|
|
319
437
|
},
|
|
320
438
|
};
|
|
439
|
+
/**
|
|
440
|
+
* The capability `metric`'s endpoint requires, with the `query` default applied.
|
|
441
|
+
* Consumers partition the catalog with this rather than reading the optional
|
|
442
|
+
* field directly, so "no capability declared" cannot be mistaken for "no
|
|
443
|
+
* capability required".
|
|
444
|
+
*/
|
|
445
|
+
export function metricCapability(metric) {
|
|
446
|
+
return metric.endpoint?.capability ?? "query";
|
|
447
|
+
}
|
|
321
448
|
// --- Row-schema building blocks ------------------------------------------
|
|
322
449
|
//
|
|
323
450
|
// Numeric columns are **strict** `z.number()`: they describe what the collector
|
|
@@ -421,6 +548,7 @@ export const METRIC_REGISTRY = {
|
|
|
421
548
|
endpoint: { method: "GET", path: "/api/v1/sessions" },
|
|
422
549
|
grain: "session",
|
|
423
550
|
dimensions: ["session"],
|
|
551
|
+
grainDimensions: ["session"],
|
|
424
552
|
filters: ["since", "until", "bins", "limit", "cameraMode", "format"],
|
|
425
553
|
row: z.object({
|
|
426
554
|
session_id: text,
|
|
@@ -467,6 +595,7 @@ export const METRIC_REGISTRY = {
|
|
|
467
595
|
endpoint: { method: "GET", path: "/api/v1/sessions/:id/meta", pathParams: ["session"] },
|
|
468
596
|
grain: "session",
|
|
469
597
|
dimensions: ["session"],
|
|
598
|
+
grainDimensions: [],
|
|
470
599
|
filters: [],
|
|
471
600
|
row: z.object({
|
|
472
601
|
sessionId: text,
|
|
@@ -500,6 +629,91 @@ export const METRIC_REGISTRY = {
|
|
|
500
629
|
related: ["list_sessions", "perf_by_device"],
|
|
501
630
|
category: "sessions",
|
|
502
631
|
},
|
|
632
|
+
session_narrative: {
|
|
633
|
+
id: "session_narrative",
|
|
634
|
+
title: "Session narrative",
|
|
635
|
+
description: "An ordered, compacted account of what one session did — scene changes, the meshes it " +
|
|
636
|
+
"dwelled on, its interactions, performance dips, errors and how it ended — timestamps " +
|
|
637
|
+
"relative to its first event, plus a closing totals entry. A compaction of the raw " +
|
|
638
|
+
"per-session stream, gated on `query:raw` and raw-session retention (ADR 0003).",
|
|
639
|
+
endpoint: {
|
|
640
|
+
method: "GET",
|
|
641
|
+
path: "/api/v1/sessions/:id/narrative",
|
|
642
|
+
pathParams: ["session"],
|
|
643
|
+
capability: "query:raw",
|
|
644
|
+
},
|
|
645
|
+
grain: "row",
|
|
646
|
+
dimensions: ["session"],
|
|
647
|
+
// A narrative entry is not keyed by the session: the session is the *scope*,
|
|
648
|
+
// named in the path, and no row column projects it (cf. `session_meta`).
|
|
649
|
+
grainDimensions: [],
|
|
650
|
+
filters: ["minDwellMs", "fpsThreshold", "maxEntries", "format"],
|
|
651
|
+
row: sessionNarrativeEntrySchema,
|
|
652
|
+
columns: {
|
|
653
|
+
tMs: {
|
|
654
|
+
description: "Milliseconds since the session's first event. Relative by design — a narrative never carries a wall-clock time.",
|
|
655
|
+
unit: "ms",
|
|
656
|
+
},
|
|
657
|
+
kind: {
|
|
658
|
+
description: "What the entry is about: scene / dwell / interaction / perf_dip / error / diagnostic / capability / xr / end / summary.",
|
|
659
|
+
unit: "label",
|
|
660
|
+
label: true,
|
|
661
|
+
},
|
|
662
|
+
summary: {
|
|
663
|
+
description: "One templated line of prose, composed from the entry's own fields — never free text copied out of an event payload.",
|
|
664
|
+
},
|
|
665
|
+
refs: {
|
|
666
|
+
description: "The named things the entry points at: `mesh`, `scene` and `name` (a custom-event or input-action name). Nothing else is ever referenced.",
|
|
667
|
+
},
|
|
668
|
+
durationMs: {
|
|
669
|
+
description: "How long the entry spans, for the kinds that cover a stretch of time.",
|
|
670
|
+
unit: "ms",
|
|
671
|
+
},
|
|
672
|
+
count: {
|
|
673
|
+
description: "How many source events the entry collapses (dwell samples, dip frames, …).",
|
|
674
|
+
unit: "count",
|
|
675
|
+
},
|
|
676
|
+
totals: {
|
|
677
|
+
description: "Session totals (events, duration, scenes, meshes, interactions, dips, errors). Present on the closing `summary` entry only.",
|
|
678
|
+
},
|
|
679
|
+
truncated: {
|
|
680
|
+
description: "Whether entries were dropped to honour `maxEntries`. Present on the closing `summary` entry only.",
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
limits: {
|
|
684
|
+
maxRows: NARRATIVE_LIMITS.maxMaxEntries,
|
|
685
|
+
maxSummaryRows: NARRATIVE_LIMITS.defaultMaxEntries,
|
|
686
|
+
},
|
|
687
|
+
interpretation: "Read it top to bottom as a story: where the session went, what held its attention, what it " +
|
|
688
|
+
"touched, and what went wrong. The closing `summary` entry gives the totals and says whether " +
|
|
689
|
+
"anything was dropped — if `truncated` is true, raise `minDwellMs` or `maxEntries` rather " +
|
|
690
|
+
"than trusting the tail.",
|
|
691
|
+
caveats: [
|
|
692
|
+
"Refused with 403 unless the collector has `ENABLE_RAW_SESSION_RETENTION` enabled AND the key holds `query:raw` (ADR 0003).",
|
|
693
|
+
"Returns 404 when the session id is unknown to the project, or when retention was enabled only after it was recorded.",
|
|
694
|
+
"A projection, not the raw stream: no visitor hash, no URL or page metadata, no positions or rays, and no `device` detail. Custom-event property **keys** are listed; their values never are.",
|
|
695
|
+
"Dwell comes from the sampled `mesh_visibility` / `hover_dwell` channels (ADR 0012), so it ranks attention rather than measuring it exactly.",
|
|
696
|
+
"Not an aggregation — there is no `build*` builder, and it takes no time range.",
|
|
697
|
+
],
|
|
698
|
+
sourceChannels: [
|
|
699
|
+
"session_start",
|
|
700
|
+
"session_end",
|
|
701
|
+
"scene_change",
|
|
702
|
+
"mesh_visibility",
|
|
703
|
+
"hover_dwell",
|
|
704
|
+
"mesh_interaction",
|
|
705
|
+
"pointer_click",
|
|
706
|
+
"input_action",
|
|
707
|
+
"custom",
|
|
708
|
+
"frame_perf",
|
|
709
|
+
"runtime_error",
|
|
710
|
+
"graphics_diagnostic",
|
|
711
|
+
"capability_change",
|
|
712
|
+
"xr_boundary_proximity",
|
|
713
|
+
],
|
|
714
|
+
related: ["session_meta", "list_sessions", "session_trajectory"],
|
|
715
|
+
category: "sessions",
|
|
716
|
+
},
|
|
503
717
|
scene_representation: {
|
|
504
718
|
id: "scene_representation",
|
|
505
719
|
title: "Scene representation",
|
|
@@ -513,6 +727,7 @@ export const METRIC_REGISTRY = {
|
|
|
513
727
|
},
|
|
514
728
|
grain: "scene",
|
|
515
729
|
dimensions: ["scene"],
|
|
730
|
+
grainDimensions: [],
|
|
516
731
|
filters: [],
|
|
517
732
|
row: z.object({
|
|
518
733
|
projectId: text,
|
|
@@ -578,6 +793,7 @@ export const METRIC_REGISTRY = {
|
|
|
578
793
|
endpoint: { method: "GET", path: "/api/v1/scenes" },
|
|
579
794
|
grain: "scene",
|
|
580
795
|
dimensions: ["scene"],
|
|
796
|
+
grainDimensions: ["scene"],
|
|
581
797
|
filters: ["since", "until", "limit", "format"],
|
|
582
798
|
row: z.object({ scene_id: text, events: int, last_seen: ts }),
|
|
583
799
|
columns: {
|
|
@@ -611,8 +827,9 @@ export const METRIC_REGISTRY = {
|
|
|
611
827
|
endpoint: { method: "GET", path: "/api/v1/timeseries" },
|
|
612
828
|
grain: "bucket",
|
|
613
829
|
dimensions: ["scene", "event_type"],
|
|
830
|
+
grainDimensions: [],
|
|
614
831
|
filters: ["since", "until", "interval", "scene", "type", "format"],
|
|
615
|
-
row: z.object({ bucket: int, events: int, avg_fps:
|
|
832
|
+
row: z.object({ bucket: int, events: int, avg_fps: numOrNull }),
|
|
616
833
|
columns: {
|
|
617
834
|
bucket: {
|
|
618
835
|
description: "Bucket start as epoch milliseconds.",
|
|
@@ -622,13 +839,15 @@ export const METRIC_REGISTRY = {
|
|
|
622
839
|
},
|
|
623
840
|
events: { description: "Events in the bucket.", unit: "count", measure: true },
|
|
624
841
|
avg_fps: {
|
|
625
|
-
description: "Mean FPS of the `frame_perf` samples in the bucket; `
|
|
842
|
+
description: "Mean FPS of the `frame_perf` samples in the bucket; `null` when the bucket had " +
|
|
843
|
+
"none — an average over no samples is absent, never zero.",
|
|
626
844
|
unit: "fps",
|
|
627
845
|
},
|
|
628
846
|
},
|
|
629
847
|
limits: { maxRows: 1000, maxSummaryRows: 8 },
|
|
630
|
-
interpretation: "Read it as a trend, not a level. A bucket with no `frame_perf` samples reports `avg_fps`
|
|
631
|
-
"
|
|
848
|
+
interpretation: "Read it as a trend, not a level. A bucket with no `frame_perf` samples reports `avg_fps` " +
|
|
849
|
+
"`null` — traffic the engine never sampled perf for, not a stall. A `0` means the samples " +
|
|
850
|
+
"really did average zero.",
|
|
632
851
|
caveats: [
|
|
633
852
|
"Buckets are fixed width; a range shorter than one `interval` collapses to a single bucket.",
|
|
634
853
|
"Volume scales with the capture fidelity dial (ADR 0012) — a rise can be a sampling change rather than traffic.",
|
|
@@ -648,7 +867,26 @@ export const METRIC_REGISTRY = {
|
|
|
648
867
|
builder: "buildEventTypeCounts",
|
|
649
868
|
endpoint: { method: "GET", path: "/api/v1/event-counts" },
|
|
650
869
|
grain: "row",
|
|
651
|
-
dimensions: [
|
|
870
|
+
dimensions: [
|
|
871
|
+
"event_type",
|
|
872
|
+
"scene",
|
|
873
|
+
"session",
|
|
874
|
+
"source",
|
|
875
|
+
"mesh",
|
|
876
|
+
"name",
|
|
877
|
+
"cameraMode",
|
|
878
|
+
"device.os",
|
|
879
|
+
"device.browser",
|
|
880
|
+
"device.engine",
|
|
881
|
+
"device.renderer",
|
|
882
|
+
],
|
|
883
|
+
grainDimensions: ["event_type"],
|
|
884
|
+
genericGroupBy: {
|
|
885
|
+
// The whole stream: this metric's subject *is* every event, so no
|
|
886
|
+
// `eventTypes` narrowing and no scope predicate.
|
|
887
|
+
eventTypes: [],
|
|
888
|
+
measures: [{ column: "count", kind: "count" }],
|
|
889
|
+
},
|
|
652
890
|
filters: ["since", "until", "scene", "format"],
|
|
653
891
|
row: z.object({ event_type: text, count: int }),
|
|
654
892
|
columns: {
|
|
@@ -681,6 +919,7 @@ export const METRIC_REGISTRY = {
|
|
|
681
919
|
builder: "buildEventsDaily",
|
|
682
920
|
grain: "bucket",
|
|
683
921
|
dimensions: ["event_type"],
|
|
922
|
+
grainDimensions: ["event_type"],
|
|
684
923
|
filters: [],
|
|
685
924
|
row: z.object({ day: day, event_type: text, events: int }),
|
|
686
925
|
columns: {
|
|
@@ -719,6 +958,7 @@ export const METRIC_REGISTRY = {
|
|
|
719
958
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/pointer" },
|
|
720
959
|
grain: "bin",
|
|
721
960
|
dimensions: ["scene", "session", "source", "cameraMode"],
|
|
961
|
+
grainDimensions: [],
|
|
722
962
|
filters: [
|
|
723
963
|
"since",
|
|
724
964
|
"until",
|
|
@@ -760,6 +1000,7 @@ export const METRIC_REGISTRY = {
|
|
|
760
1000
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/mesh-uv" },
|
|
761
1001
|
grain: "bin",
|
|
762
1002
|
dimensions: ["scene", "session", "source", "mesh"],
|
|
1003
|
+
grainDimensions: [],
|
|
763
1004
|
filters: ["since", "until", "bins", "limit", "scene", "session", "source", "mesh", "format"],
|
|
764
1005
|
row: heatmapBinRow,
|
|
765
1006
|
columns: {
|
|
@@ -790,6 +1031,7 @@ export const METRIC_REGISTRY = {
|
|
|
790
1031
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/world" },
|
|
791
1032
|
grain: "voxel",
|
|
792
1033
|
dimensions: ["scene", "source", "cameraMode"],
|
|
1034
|
+
grainDimensions: [],
|
|
793
1035
|
filters: [
|
|
794
1036
|
"since",
|
|
795
1037
|
"until",
|
|
@@ -827,6 +1069,7 @@ export const METRIC_REGISTRY = {
|
|
|
827
1069
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/world/stats" },
|
|
828
1070
|
grain: "project",
|
|
829
1071
|
dimensions: ["scene", "source", "cameraMode"],
|
|
1072
|
+
grainDimensions: [],
|
|
830
1073
|
filters: ["since", "until", "cellSize", "scene", "source", "cameraMode", "region", "format"],
|
|
831
1074
|
row: spatialStatsRow,
|
|
832
1075
|
columns: {
|
|
@@ -858,6 +1101,7 @@ export const METRIC_REGISTRY = {
|
|
|
858
1101
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/gaze" },
|
|
859
1102
|
grain: "voxel",
|
|
860
1103
|
dimensions: ["scene", "session", "cameraMode"],
|
|
1104
|
+
grainDimensions: [],
|
|
861
1105
|
filters: [
|
|
862
1106
|
"since",
|
|
863
1107
|
"until",
|
|
@@ -900,6 +1144,7 @@ export const METRIC_REGISTRY = {
|
|
|
900
1144
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/gaze/stats" },
|
|
901
1145
|
grain: "project",
|
|
902
1146
|
dimensions: ["scene", "session", "cameraMode"],
|
|
1147
|
+
grainDimensions: [],
|
|
903
1148
|
filters: ["since", "until", "cellSize", "scene", "session", "cameraMode", "region", "format"],
|
|
904
1149
|
row: spatialStatsRow,
|
|
905
1150
|
columns: {
|
|
@@ -931,6 +1176,7 @@ export const METRIC_REGISTRY = {
|
|
|
931
1176
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/camera" },
|
|
932
1177
|
grain: "bin",
|
|
933
1178
|
dimensions: ["scene", "session", "cameraMode"],
|
|
1179
|
+
grainDimensions: [],
|
|
934
1180
|
filters: ["since", "until", "bins", "limit", "scene", "session", "cameraMode", "format"],
|
|
935
1181
|
row: z.object({ azimuth_bin: int, elevation_bin: int, count: int }),
|
|
936
1182
|
columns: {
|
|
@@ -968,6 +1214,7 @@ export const METRIC_REGISTRY = {
|
|
|
968
1214
|
endpoint: { method: "GET", path: "/api/v1/coverage/view-histogram" },
|
|
969
1215
|
grain: "bucket",
|
|
970
1216
|
dimensions: ["scene", "session", "cameraMode"],
|
|
1217
|
+
grainDimensions: [],
|
|
971
1218
|
filters: ["since", "until", "bins", "limit", "scene", "session", "cameraMode", "format"],
|
|
972
1219
|
row: z.object({ bucket: int, sessions: int }),
|
|
973
1220
|
columns: {
|
|
@@ -1007,6 +1254,7 @@ export const METRIC_REGISTRY = {
|
|
|
1007
1254
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/position" },
|
|
1008
1255
|
grain: "bin",
|
|
1009
1256
|
dimensions: ["scene", "session", "cameraMode"],
|
|
1257
|
+
grainDimensions: [],
|
|
1010
1258
|
filters: [
|
|
1011
1259
|
"since",
|
|
1012
1260
|
"until",
|
|
@@ -1052,6 +1300,7 @@ export const METRIC_REGISTRY = {
|
|
|
1052
1300
|
},
|
|
1053
1301
|
grain: "row",
|
|
1054
1302
|
dimensions: ["session", "scene"],
|
|
1303
|
+
grainDimensions: [],
|
|
1055
1304
|
filters: ["since", "until", "limit", "scene", "format"],
|
|
1056
1305
|
row: z.object({ ts: int, x: num, y: num, z: num }),
|
|
1057
1306
|
columns: {
|
|
@@ -1086,6 +1335,7 @@ export const METRIC_REGISTRY = {
|
|
|
1086
1335
|
endpoint: { method: "GET", path: "/api/v1/paths" },
|
|
1087
1336
|
grain: "row",
|
|
1088
1337
|
dimensions: ["session", "scene", "cameraMode"],
|
|
1338
|
+
grainDimensions: ["session"],
|
|
1089
1339
|
filters: ["since", "until", "cellSize", "limit", "scene", "cameraMode", "format"],
|
|
1090
1340
|
row: z.object({ session_id: text, ts: int, gx: int, gz: int }),
|
|
1091
1341
|
columns: {
|
|
@@ -1120,6 +1370,7 @@ export const METRIC_REGISTRY = {
|
|
|
1120
1370
|
endpoint: { method: "GET", path: "/api/v1/coverage" },
|
|
1121
1371
|
grain: "voxel",
|
|
1122
1372
|
dimensions: ["scene", "session"],
|
|
1373
|
+
grainDimensions: [],
|
|
1123
1374
|
filters: ["since", "until", "cellSize", "limit", "scene", "session", "format"],
|
|
1124
1375
|
row: voxelCountRow,
|
|
1125
1376
|
columns: {
|
|
@@ -1152,6 +1403,7 @@ export const METRIC_REGISTRY = {
|
|
|
1152
1403
|
endpoint: { method: "GET", path: "/api/v1/camera/distance" },
|
|
1153
1404
|
grain: "bucket",
|
|
1154
1405
|
dimensions: ["scene", "session"],
|
|
1406
|
+
grainDimensions: [],
|
|
1155
1407
|
filters: [
|
|
1156
1408
|
"since",
|
|
1157
1409
|
"until",
|
|
@@ -1196,6 +1448,7 @@ export const METRIC_REGISTRY = {
|
|
|
1196
1448
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/click-rays" },
|
|
1197
1449
|
grain: "voxel",
|
|
1198
1450
|
dimensions: ["scene", "session", "source", "mesh"],
|
|
1451
|
+
grainDimensions: ["mesh"],
|
|
1199
1452
|
filters: ["since", "until", "cellSize", "limit", "scene", "source", "session", "format"],
|
|
1200
1453
|
row: z.object({
|
|
1201
1454
|
cam_vx: int,
|
|
@@ -1254,6 +1507,7 @@ export const METRIC_REGISTRY = {
|
|
|
1254
1507
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/flow" },
|
|
1255
1508
|
grain: "bin",
|
|
1256
1509
|
dimensions: ["scene", "session", "mesh", "cameraMode"],
|
|
1510
|
+
grainDimensions: ["mesh"],
|
|
1257
1511
|
filters: [
|
|
1258
1512
|
"since",
|
|
1259
1513
|
"until",
|
|
@@ -1334,7 +1588,26 @@ export const METRIC_REGISTRY = {
|
|
|
1334
1588
|
builder: "buildTopMeshes",
|
|
1335
1589
|
endpoint: { method: "GET", path: "/api/v1/meshes/top" },
|
|
1336
1590
|
grain: "mesh",
|
|
1337
|
-
dimensions: [
|
|
1591
|
+
dimensions: [
|
|
1592
|
+
"mesh",
|
|
1593
|
+
"session",
|
|
1594
|
+
"scene",
|
|
1595
|
+
"source",
|
|
1596
|
+
"event_type",
|
|
1597
|
+
"cameraMode",
|
|
1598
|
+
"device.os",
|
|
1599
|
+
"device.browser",
|
|
1600
|
+
"device.engine",
|
|
1601
|
+
"device.renderer",
|
|
1602
|
+
],
|
|
1603
|
+
grainDimensions: ["mesh"],
|
|
1604
|
+
genericGroupBy: {
|
|
1605
|
+
// Every mesh-referencing event, gaze included — the same population the
|
|
1606
|
+
// canned builder counts, which is why `hasMesh` is not optional here.
|
|
1607
|
+
eventTypes: [],
|
|
1608
|
+
scope: ["hasMesh"],
|
|
1609
|
+
measures: [{ column: "count", kind: "count" }],
|
|
1610
|
+
},
|
|
1338
1611
|
filters: ["since", "until", "bins", "limit", "session", "format"],
|
|
1339
1612
|
row: z.object({ mesh: text, count: int }),
|
|
1340
1613
|
columns: {
|
|
@@ -1365,7 +1638,25 @@ export const METRIC_REGISTRY = {
|
|
|
1365
1638
|
builder: "buildTopMeshesBySource",
|
|
1366
1639
|
endpoint: { method: "GET", path: "/api/v1/meshes/sources" },
|
|
1367
1640
|
grain: "mesh",
|
|
1368
|
-
dimensions: [
|
|
1641
|
+
dimensions: [
|
|
1642
|
+
"mesh",
|
|
1643
|
+
"source",
|
|
1644
|
+
"scene",
|
|
1645
|
+
"session",
|
|
1646
|
+
"cameraMode",
|
|
1647
|
+
"name",
|
|
1648
|
+
"event_type",
|
|
1649
|
+
"device.os",
|
|
1650
|
+
"device.browser",
|
|
1651
|
+
"device.engine",
|
|
1652
|
+
"device.renderer",
|
|
1653
|
+
],
|
|
1654
|
+
grainDimensions: ["mesh", "source"],
|
|
1655
|
+
genericGroupBy: {
|
|
1656
|
+
eventTypes: ["mesh_interaction", "pointer_click"],
|
|
1657
|
+
scope: ["hasMesh"],
|
|
1658
|
+
measures: [{ column: "count", kind: "count" }],
|
|
1659
|
+
},
|
|
1369
1660
|
filters: [
|
|
1370
1661
|
"since",
|
|
1371
1662
|
"until",
|
|
@@ -1407,6 +1698,7 @@ export const METRIC_REGISTRY = {
|
|
|
1407
1698
|
endpoint: { method: "GET", path: "/api/v1/meshes/trend" },
|
|
1408
1699
|
grain: "bucket",
|
|
1409
1700
|
dimensions: ["mesh", "scene", "session", "source", "cameraMode"],
|
|
1701
|
+
grainDimensions: ["mesh"],
|
|
1410
1702
|
filters: [
|
|
1411
1703
|
"since",
|
|
1412
1704
|
"until",
|
|
@@ -1449,6 +1741,7 @@ export const METRIC_REGISTRY = {
|
|
|
1449
1741
|
endpoint: { method: "GET", path: "/api/v1/meshes/dwell" },
|
|
1450
1742
|
grain: "mesh",
|
|
1451
1743
|
dimensions: ["mesh", "scene", "session"],
|
|
1744
|
+
grainDimensions: ["mesh"],
|
|
1452
1745
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
1453
1746
|
row: z.object({
|
|
1454
1747
|
mesh: text,
|
|
@@ -1498,6 +1791,7 @@ export const METRIC_REGISTRY = {
|
|
|
1498
1791
|
endpoint: { method: "GET", path: "/api/v1/meshes/blind-spots" },
|
|
1499
1792
|
grain: "mesh",
|
|
1500
1793
|
dimensions: ["mesh", "scene", "session"],
|
|
1794
|
+
grainDimensions: ["mesh"],
|
|
1501
1795
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
1502
1796
|
row: z.object({
|
|
1503
1797
|
mesh: text,
|
|
@@ -1542,7 +1836,24 @@ export const METRIC_REGISTRY = {
|
|
|
1542
1836
|
builder: "buildMeshInteractionKinds",
|
|
1543
1837
|
endpoint: { method: "GET", path: "/api/v1/meshes/kinds" },
|
|
1544
1838
|
grain: "mesh",
|
|
1545
|
-
dimensions: [
|
|
1839
|
+
dimensions: [
|
|
1840
|
+
"mesh",
|
|
1841
|
+
"name",
|
|
1842
|
+
"scene",
|
|
1843
|
+
"session",
|
|
1844
|
+
"source",
|
|
1845
|
+
"cameraMode",
|
|
1846
|
+
"device.os",
|
|
1847
|
+
"device.browser",
|
|
1848
|
+
"device.engine",
|
|
1849
|
+
"device.renderer",
|
|
1850
|
+
],
|
|
1851
|
+
grainDimensions: ["mesh", "name"],
|
|
1852
|
+
genericGroupBy: {
|
|
1853
|
+
eventTypes: ["mesh_interaction"],
|
|
1854
|
+
scope: ["hasMesh"],
|
|
1855
|
+
measures: [{ column: "count", kind: "count" }],
|
|
1856
|
+
},
|
|
1546
1857
|
filters: [
|
|
1547
1858
|
"since",
|
|
1548
1859
|
"until",
|
|
@@ -1588,6 +1899,7 @@ export const METRIC_REGISTRY = {
|
|
|
1588
1899
|
endpoint: { method: "GET", path: "/api/v1/meshes/reachability" },
|
|
1589
1900
|
grain: "mesh",
|
|
1590
1901
|
dimensions: ["mesh", "scene", "session", "source", "cameraMode"],
|
|
1902
|
+
grainDimensions: ["mesh"],
|
|
1591
1903
|
filters: [
|
|
1592
1904
|
"since",
|
|
1593
1905
|
"until",
|
|
@@ -1637,6 +1949,7 @@ export const METRIC_REGISTRY = {
|
|
|
1637
1949
|
endpoint: { method: "GET", path: "/api/v1/clicks/dead" },
|
|
1638
1950
|
grain: "project",
|
|
1639
1951
|
dimensions: ["scene", "session", "source", "cameraMode"],
|
|
1952
|
+
grainDimensions: [],
|
|
1640
1953
|
filters: [
|
|
1641
1954
|
"since",
|
|
1642
1955
|
"until",
|
|
@@ -1683,6 +1996,7 @@ export const METRIC_REGISTRY = {
|
|
|
1683
1996
|
endpoint: { method: "GET", path: "/api/v1/clicks/rage" },
|
|
1684
1997
|
grain: "row",
|
|
1685
1998
|
dimensions: ["session", "mesh", "scene", "source", "cameraMode"],
|
|
1999
|
+
grainDimensions: ["session", "mesh"],
|
|
1686
2000
|
filters: [
|
|
1687
2001
|
"since",
|
|
1688
2002
|
"until",
|
|
@@ -1730,6 +2044,7 @@ export const METRIC_REGISTRY = {
|
|
|
1730
2044
|
endpoint: { method: "GET", path: "/api/v1/hover/dwell" },
|
|
1731
2045
|
grain: "mesh",
|
|
1732
2046
|
dimensions: ["mesh", "scene", "session", "source", "cameraMode"],
|
|
2047
|
+
grainDimensions: ["mesh"],
|
|
1733
2048
|
filters: [
|
|
1734
2049
|
"since",
|
|
1735
2050
|
"until",
|
|
@@ -1770,7 +2085,38 @@ export const METRIC_REGISTRY = {
|
|
|
1770
2085
|
builder: "buildInteractionsBySource",
|
|
1771
2086
|
endpoint: { method: "GET", path: "/api/v1/interactions/sources" },
|
|
1772
2087
|
grain: "row",
|
|
1773
|
-
dimensions: [
|
|
2088
|
+
dimensions: [
|
|
2089
|
+
"event_type",
|
|
2090
|
+
"source",
|
|
2091
|
+
"scene",
|
|
2092
|
+
"session",
|
|
2093
|
+
"cameraMode",
|
|
2094
|
+
"mesh",
|
|
2095
|
+
"name",
|
|
2096
|
+
"device.os",
|
|
2097
|
+
"device.browser",
|
|
2098
|
+
"device.engine",
|
|
2099
|
+
"device.renderer",
|
|
2100
|
+
],
|
|
2101
|
+
grainDimensions: ["event_type", "source"],
|
|
2102
|
+
genericGroupBy: {
|
|
2103
|
+
// Exactly the event types the canned builder lists — the events that
|
|
2104
|
+
// carry an input source.
|
|
2105
|
+
eventTypes: [
|
|
2106
|
+
"pointer_move",
|
|
2107
|
+
"pointer_click",
|
|
2108
|
+
"pointer_down",
|
|
2109
|
+
"pointer_up",
|
|
2110
|
+
"mesh_interaction",
|
|
2111
|
+
"hover_dwell",
|
|
2112
|
+
"camera_gesture",
|
|
2113
|
+
"input_action",
|
|
2114
|
+
],
|
|
2115
|
+
measures: [
|
|
2116
|
+
{ column: "count", kind: "count" },
|
|
2117
|
+
{ column: "sessions", kind: "sessions" },
|
|
2118
|
+
],
|
|
2119
|
+
},
|
|
1774
2120
|
filters: [
|
|
1775
2121
|
"since",
|
|
1776
2122
|
"until",
|
|
@@ -1810,7 +2156,23 @@ export const METRIC_REGISTRY = {
|
|
|
1810
2156
|
builder: "buildTopInputActions",
|
|
1811
2157
|
endpoint: { method: "GET", path: "/api/v1/input-actions/top" },
|
|
1812
2158
|
grain: "row",
|
|
1813
|
-
dimensions: [
|
|
2159
|
+
dimensions: [
|
|
2160
|
+
"name",
|
|
2161
|
+
"source",
|
|
2162
|
+
"scene",
|
|
2163
|
+
"session",
|
|
2164
|
+
"cameraMode",
|
|
2165
|
+
"device.os",
|
|
2166
|
+
"device.browser",
|
|
2167
|
+
"device.engine",
|
|
2168
|
+
"device.renderer",
|
|
2169
|
+
],
|
|
2170
|
+
grainDimensions: ["name", "source"],
|
|
2171
|
+
genericGroupBy: {
|
|
2172
|
+
eventTypes: ["input_action"],
|
|
2173
|
+
scope: ["hasName"],
|
|
2174
|
+
measures: [{ column: "count", kind: "count" }],
|
|
2175
|
+
},
|
|
1814
2176
|
filters: [
|
|
1815
2177
|
"since",
|
|
1816
2178
|
"until",
|
|
@@ -1845,6 +2207,60 @@ export const METRIC_REGISTRY = {
|
|
|
1845
2207
|
comparable: { primary: "count", direction: "neutral", minSample: 30 },
|
|
1846
2208
|
category: "interaction",
|
|
1847
2209
|
},
|
|
2210
|
+
custom_event_vocabulary: {
|
|
2211
|
+
id: "custom_event_vocabulary",
|
|
2212
|
+
title: "Discovered custom-event vocabulary",
|
|
2213
|
+
description: "Which developer-defined `custom` event names the project actually emits, how often, over " +
|
|
2214
|
+
"how many distinct sessions, and the union of `props` keys observed on each name with a " +
|
|
2215
|
+
"coarse type per key (ADR 0051 §5). One row per custom-event name. This is how an agent " +
|
|
2216
|
+
"learns that `add_to_cart` exists and carries `sku` and `qty` — nothing else in the read " +
|
|
2217
|
+
"surface enumerates an application's own event vocabulary.",
|
|
2218
|
+
builder: "buildCustomEventVocabulary",
|
|
2219
|
+
endpoint: { method: "GET", path: "/api/v1/vocabulary/custom-events" },
|
|
2220
|
+
grain: "row",
|
|
2221
|
+
dimensions: ["name", "scene"],
|
|
2222
|
+
// One row per custom-event name; `scene` is a filter, never a key.
|
|
2223
|
+
grainDimensions: ["name"],
|
|
2224
|
+
filters: ["since", "until", "scene", "limit", "format"],
|
|
2225
|
+
row: z.object({
|
|
2226
|
+
name: text,
|
|
2227
|
+
count: int,
|
|
2228
|
+
sessions: int,
|
|
2229
|
+
props: z.record(z.string(), z.enum(["string", "number", "boolean", "null", "mixed"])),
|
|
2230
|
+
}),
|
|
2231
|
+
columns: {
|
|
2232
|
+
name: {
|
|
2233
|
+
description: "Developer-chosen custom-event name, e.g. `add_to_cart`.",
|
|
2234
|
+
unit: "label",
|
|
2235
|
+
label: true,
|
|
2236
|
+
},
|
|
2237
|
+
count: { description: "Times the event fired over the range.", unit: "count", measure: true },
|
|
2238
|
+
sessions: { description: "Distinct sessions that emitted it.", unit: "sessions" },
|
|
2239
|
+
props: {
|
|
2240
|
+
description: "Observed `props` keys mapped to a coarse JSON type (`string` / `number` / `boolean` / " +
|
|
2241
|
+
"`null` / `mixed`). `mixed` means the key arrived with more than one kind; `null` means " +
|
|
2242
|
+
"every sampled value was null.",
|
|
2243
|
+
unit: "label",
|
|
2244
|
+
},
|
|
2245
|
+
},
|
|
2246
|
+
limits: { maxRows: 200, maxSummaryRows: 20 },
|
|
2247
|
+
interpretation: "A discovery read, not a KPI: use it to find the real event names and prop keys before " +
|
|
2248
|
+
"filtering or funnelling on them. `count` and `sessions` are exact over the range; `props` " +
|
|
2249
|
+
"is a sample.",
|
|
2250
|
+
caveats: [
|
|
2251
|
+
"`props` is discovered from the 20 most recent events per name, not from the whole range: a " +
|
|
2252
|
+
"key that stopped being emitted long ago will be absent, and a rarely-sent optional key " +
|
|
2253
|
+
"may be missed. Treat it as a vocabulary hint, never as a schema.",
|
|
2254
|
+
"Only prop key names and value kinds are reported — never a prop value (ADR 0003).",
|
|
2255
|
+
"Names are ranked by `count` and capped by `limit`, so a long tail of rare custom events may " +
|
|
2256
|
+
"be truncated.",
|
|
2257
|
+
"An empty result means the project emits no `custom` events over the range, not that custom " +
|
|
2258
|
+
"events are unsupported.",
|
|
2259
|
+
],
|
|
2260
|
+
sourceChannels: ["custom"],
|
|
2261
|
+
related: ["variant_leaderboard", "funnel", "event_counts"],
|
|
2262
|
+
category: "interaction",
|
|
2263
|
+
},
|
|
1848
2264
|
camera_gestures: {
|
|
1849
2265
|
id: "camera_gestures",
|
|
1850
2266
|
title: "Camera navigation gestures",
|
|
@@ -1854,7 +2270,30 @@ export const METRIC_REGISTRY = {
|
|
|
1854
2270
|
builder: "buildCameraGestures",
|
|
1855
2271
|
endpoint: { method: "GET", path: "/api/v1/camera-gestures" },
|
|
1856
2272
|
grain: "row",
|
|
1857
|
-
dimensions: [
|
|
2273
|
+
dimensions: [
|
|
2274
|
+
"name",
|
|
2275
|
+
"scene",
|
|
2276
|
+
"session",
|
|
2277
|
+
"source",
|
|
2278
|
+
"cameraMode",
|
|
2279
|
+
"device.os",
|
|
2280
|
+
"device.browser",
|
|
2281
|
+
"device.engine",
|
|
2282
|
+
"device.renderer",
|
|
2283
|
+
],
|
|
2284
|
+
grainDimensions: ["name"],
|
|
2285
|
+
genericGroupBy: {
|
|
2286
|
+
// The one generic metric whose measures are not all counts: gesture
|
|
2287
|
+
// duration rides in the promoted `visible_ms` column, and sum / avg / max
|
|
2288
|
+
// over a promoted column render the same on every engine.
|
|
2289
|
+
eventTypes: ["camera_gesture"],
|
|
2290
|
+
measures: [
|
|
2291
|
+
{ column: "gestures", kind: "count" },
|
|
2292
|
+
{ column: "total_ms", kind: "sum", of: "visible_ms" },
|
|
2293
|
+
{ column: "avg_ms", kind: "avg", of: "visible_ms" },
|
|
2294
|
+
{ column: "max_ms", kind: "max", of: "visible_ms" },
|
|
2295
|
+
],
|
|
2296
|
+
},
|
|
1858
2297
|
filters: [
|
|
1859
2298
|
"since",
|
|
1860
2299
|
"until",
|
|
@@ -1897,6 +2336,7 @@ export const METRIC_REGISTRY = {
|
|
|
1897
2336
|
endpoint: { method: "GET", path: "/api/v1/navigation" },
|
|
1898
2337
|
grain: "session",
|
|
1899
2338
|
dimensions: ["session", "scene"],
|
|
2339
|
+
grainDimensions: ["session"],
|
|
1900
2340
|
filters: ["since", "until", "moveThreshold", "limit", "scene", "session", "format"],
|
|
1901
2341
|
row: z.object({
|
|
1902
2342
|
session_id: text,
|
|
@@ -1942,6 +2382,7 @@ export const METRIC_REGISTRY = {
|
|
|
1942
2382
|
endpoint: { method: "GET", path: "/api/v1/backtrack" },
|
|
1943
2383
|
grain: "scene",
|
|
1944
2384
|
dimensions: ["scene", "session"],
|
|
2385
|
+
grainDimensions: ["scene"],
|
|
1945
2386
|
filters: ["since", "until", "cellSize", "limit", "scene", "session", "format"],
|
|
1946
2387
|
row: z.object({
|
|
1947
2388
|
scene: text,
|
|
@@ -1999,6 +2440,7 @@ export const METRIC_REGISTRY = {
|
|
|
1999
2440
|
endpoint: { method: "GET", path: "/api/v1/perf" },
|
|
2000
2441
|
grain: "project",
|
|
2001
2442
|
dimensions: ["session"],
|
|
2443
|
+
grainDimensions: [],
|
|
2002
2444
|
filters: ["since", "until", "bins", "limit", "session", "format"],
|
|
2003
2445
|
row: z.object({ samples: int, avg_fps: numOrNull, min_fps: numOrNull, p50_fps: numOrNull }),
|
|
2004
2446
|
columns: {
|
|
@@ -2033,6 +2475,7 @@ export const METRIC_REGISTRY = {
|
|
|
2033
2475
|
endpoint: { method: "GET", path: "/api/v1/perf/render-scale" },
|
|
2034
2476
|
grain: "project",
|
|
2035
2477
|
dimensions: ["session"],
|
|
2478
|
+
grainDimensions: [],
|
|
2036
2479
|
filters: ["since", "until", "bins", "limit", "session", "format"],
|
|
2037
2480
|
row: z.object({
|
|
2038
2481
|
samples: int,
|
|
@@ -2087,6 +2530,7 @@ export const METRIC_REGISTRY = {
|
|
|
2087
2530
|
endpoint: { method: "GET", path: "/api/v1/perf/distribution" },
|
|
2088
2531
|
grain: "project",
|
|
2089
2532
|
dimensions: ["scene", "session"],
|
|
2533
|
+
grainDimensions: [],
|
|
2090
2534
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2091
2535
|
row: z.object({
|
|
2092
2536
|
sessions: int,
|
|
@@ -2132,6 +2576,7 @@ export const METRIC_REGISTRY = {
|
|
|
2132
2576
|
endpoint: { method: "GET", path: "/api/v1/perf/fps-histogram" },
|
|
2133
2577
|
grain: "bucket",
|
|
2134
2578
|
dimensions: ["scene", "session"],
|
|
2579
|
+
grainDimensions: [],
|
|
2135
2580
|
filters: ["since", "until", "bins", "limit", "scene", "session", "bucket", "format"],
|
|
2136
2581
|
row: z.object({ bucket: int, sessions: int }),
|
|
2137
2582
|
columns: {
|
|
@@ -2170,6 +2615,7 @@ export const METRIC_REGISTRY = {
|
|
|
2170
2615
|
endpoint: { method: "GET", path: "/api/v1/perf/frame-time" },
|
|
2171
2616
|
grain: "project",
|
|
2172
2617
|
dimensions: ["scene", "session"],
|
|
2618
|
+
grainDimensions: [],
|
|
2173
2619
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2174
2620
|
row: z.object({ sessions: int, samples: intOrNull, p50_ms: numOrNull, p95_ms: numOrNull }),
|
|
2175
2621
|
columns: {
|
|
@@ -2209,6 +2655,7 @@ export const METRIC_REGISTRY = {
|
|
|
2209
2655
|
endpoint: { method: "GET", path: "/api/v1/perf/jank" },
|
|
2210
2656
|
grain: "project",
|
|
2211
2657
|
dimensions: ["scene", "session"],
|
|
2658
|
+
grainDimensions: [],
|
|
2212
2659
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2213
2660
|
row: z.object({
|
|
2214
2661
|
sessions: int,
|
|
@@ -2256,6 +2703,7 @@ export const METRIC_REGISTRY = {
|
|
|
2256
2703
|
endpoint: { method: "GET", path: "/api/v1/perf/churn" },
|
|
2257
2704
|
grain: "project",
|
|
2258
2705
|
dimensions: ["scene", "session"],
|
|
2706
|
+
grainDimensions: [],
|
|
2259
2707
|
filters: [
|
|
2260
2708
|
"since",
|
|
2261
2709
|
"until",
|
|
@@ -2327,6 +2775,13 @@ export const METRIC_REGISTRY = {
|
|
|
2327
2775
|
"scene",
|
|
2328
2776
|
"session",
|
|
2329
2777
|
],
|
|
2778
|
+
grainDimensions: [
|
|
2779
|
+
"device.engine",
|
|
2780
|
+
"device.isMobile",
|
|
2781
|
+
"device.renderer",
|
|
2782
|
+
"device.browser",
|
|
2783
|
+
"device.os",
|
|
2784
|
+
],
|
|
2330
2785
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2331
2786
|
row: z.object({
|
|
2332
2787
|
engine: text,
|
|
@@ -2382,6 +2837,7 @@ export const METRIC_REGISTRY = {
|
|
|
2382
2837
|
endpoint: { method: "GET", path: "/api/v1/perf/by-scene" },
|
|
2383
2838
|
grain: "scene",
|
|
2384
2839
|
dimensions: ["scene", "session"],
|
|
2840
|
+
grainDimensions: ["scene"],
|
|
2385
2841
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2386
2842
|
row: z.object({ scene_id: text, sessions: int, samples: int, p50_fps: num }),
|
|
2387
2843
|
columns: {
|
|
@@ -2417,6 +2873,7 @@ export const METRIC_REGISTRY = {
|
|
|
2417
2873
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/perf" },
|
|
2418
2874
|
grain: "voxel",
|
|
2419
2875
|
dimensions: ["scene", "session"],
|
|
2876
|
+
grainDimensions: [],
|
|
2420
2877
|
filters: ["since", "until", "cellSize", "limit", "scene", "session", "format"],
|
|
2421
2878
|
row: z.object({ vx: int, vy: int, vz: int, samples: int, avg_fps: num, min_fps: num }),
|
|
2422
2879
|
columns: {
|
|
@@ -2449,6 +2906,7 @@ export const METRIC_REGISTRY = {
|
|
|
2449
2906
|
builder: "buildPerfDaily",
|
|
2450
2907
|
grain: "bucket",
|
|
2451
2908
|
dimensions: [],
|
|
2909
|
+
grainDimensions: [],
|
|
2452
2910
|
filters: [],
|
|
2453
2911
|
row: z.object({ day: day, samples: int, avg_fps: num, min_fps: num, p50_fps: num }),
|
|
2454
2912
|
columns: {
|
|
@@ -2486,6 +2944,7 @@ export const METRIC_REGISTRY = {
|
|
|
2486
2944
|
endpoint: { method: "GET", path: "/api/v1/perf/compile-stalls" },
|
|
2487
2945
|
grain: "row",
|
|
2488
2946
|
dimensions: ["name", "scene", "session"],
|
|
2947
|
+
grainDimensions: ["name"],
|
|
2489
2948
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2490
2949
|
row: z.object({ phase: text, stalls: int, total_ms: num, avg_ms: num, max_ms: num }),
|
|
2491
2950
|
columns: {
|
|
@@ -2526,6 +2985,7 @@ export const METRIC_REGISTRY = {
|
|
|
2526
2985
|
endpoint: { method: "GET", path: "/api/v1/perf/resources" },
|
|
2527
2986
|
grain: "project",
|
|
2528
2987
|
dimensions: ["session"],
|
|
2988
|
+
grainDimensions: [],
|
|
2529
2989
|
filters: ["since", "until", "bins", "limit", "session", "format"],
|
|
2530
2990
|
row: z.object({
|
|
2531
2991
|
samples: int,
|
|
@@ -2578,6 +3038,7 @@ export const METRIC_REGISTRY = {
|
|
|
2578
3038
|
endpoint: { method: "GET", path: "/api/v1/perf/resource-percentiles" },
|
|
2579
3039
|
grain: "project",
|
|
2580
3040
|
dimensions: ["scene", "session"],
|
|
3041
|
+
grainDimensions: [],
|
|
2581
3042
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2582
3043
|
row: z.object({
|
|
2583
3044
|
sessions: int,
|
|
@@ -2634,6 +3095,7 @@ export const METRIC_REGISTRY = {
|
|
|
2634
3095
|
endpoint: { method: "GET", path: "/api/v1/perf/stability" },
|
|
2635
3096
|
grain: "project",
|
|
2636
3097
|
dimensions: ["scene", "session"],
|
|
3098
|
+
grainDimensions: [],
|
|
2637
3099
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2638
3100
|
row: z.object({ context_losses: int, compile_stalls: int, incidents: int }),
|
|
2639
3101
|
columns: {
|
|
@@ -2664,6 +3126,7 @@ export const METRIC_REGISTRY = {
|
|
|
2664
3126
|
endpoint: { method: "GET", path: "/api/v1/graphics-diagnostics" },
|
|
2665
3127
|
grain: "row",
|
|
2666
3128
|
dimensions: ["scene", "session"],
|
|
3129
|
+
grainDimensions: [],
|
|
2667
3130
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2668
3131
|
row: z.object({ severity: text, category: text, backend: text, incidents: int }),
|
|
2669
3132
|
columns: {
|
|
@@ -2710,6 +3173,7 @@ export const METRIC_REGISTRY = {
|
|
|
2710
3173
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/errors" },
|
|
2711
3174
|
grain: "voxel",
|
|
2712
3175
|
dimensions: ["scene", "session"],
|
|
3176
|
+
grainDimensions: [],
|
|
2713
3177
|
filters: [
|
|
2714
3178
|
"since",
|
|
2715
3179
|
"until",
|
|
@@ -2754,6 +3218,7 @@ export const METRIC_REGISTRY = {
|
|
|
2754
3218
|
endpoint: { method: "GET", path: "/api/v1/rendering-technology" },
|
|
2755
3219
|
grain: "row",
|
|
2756
3220
|
dimensions: ["scene", "session"],
|
|
3221
|
+
grainDimensions: [],
|
|
2757
3222
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2758
3223
|
row: z.object({
|
|
2759
3224
|
api: text,
|
|
@@ -2797,6 +3262,7 @@ export const METRIC_REGISTRY = {
|
|
|
2797
3262
|
endpoint: { method: "GET", path: "/api/v1/capabilities" },
|
|
2798
3263
|
grain: "row",
|
|
2799
3264
|
dimensions: ["name", "scene", "session"],
|
|
3265
|
+
grainDimensions: ["name"],
|
|
2800
3266
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2801
3267
|
row: z.object({ kind: text, from: text, to: text, changes: int }),
|
|
2802
3268
|
columns: {
|
|
@@ -2841,6 +3307,7 @@ export const METRIC_REGISTRY = {
|
|
|
2841
3307
|
endpoint: { method: "GET", path: "/api/v1/xr/rotation" },
|
|
2842
3308
|
grain: "session",
|
|
2843
3309
|
dimensions: ["session", "scene"],
|
|
3310
|
+
grainDimensions: ["session"],
|
|
2844
3311
|
filters: ["since", "until", "rapidTurn", "limit", "scene", "session", "format"],
|
|
2845
3312
|
row: z.object({
|
|
2846
3313
|
session_id: text,
|
|
@@ -2893,6 +3360,7 @@ export const METRIC_REGISTRY = {
|
|
|
2893
3360
|
endpoint: { method: "GET", path: "/api/v1/xr/sources" },
|
|
2894
3361
|
grain: "row",
|
|
2895
3362
|
dimensions: ["source", "scene", "session"],
|
|
3363
|
+
grainDimensions: ["source"],
|
|
2896
3364
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2897
3365
|
row: z.object({ source: text, interactions: int, sessions: int }),
|
|
2898
3366
|
columns: {
|
|
@@ -2923,6 +3391,7 @@ export const METRIC_REGISTRY = {
|
|
|
2923
3391
|
endpoint: { method: "GET", path: "/api/v1/xr/abandonment" },
|
|
2924
3392
|
grain: "session",
|
|
2925
3393
|
dimensions: ["session", "scene"],
|
|
3394
|
+
grainDimensions: ["session"],
|
|
2926
3395
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2927
3396
|
row: z.object({
|
|
2928
3397
|
session_id: text,
|
|
@@ -2966,6 +3435,7 @@ export const METRIC_REGISTRY = {
|
|
|
2966
3435
|
endpoint: { method: "GET", path: "/api/v1/xr/locomotion" },
|
|
2967
3436
|
grain: "session",
|
|
2968
3437
|
dimensions: ["session", "scene"],
|
|
3438
|
+
grainDimensions: ["session"],
|
|
2969
3439
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
2970
3440
|
row: z.object({
|
|
2971
3441
|
session_id: text,
|
|
@@ -3021,6 +3491,7 @@ export const METRIC_REGISTRY = {
|
|
|
3021
3491
|
endpoint: { method: "GET", path: "/api/v1/xr/tracking" },
|
|
3022
3492
|
grain: "session",
|
|
3023
3493
|
dimensions: ["session", "scene", "source"],
|
|
3494
|
+
grainDimensions: ["session"],
|
|
3024
3495
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
3025
3496
|
row: z.object({
|
|
3026
3497
|
session_id: text,
|
|
@@ -3071,6 +3542,7 @@ export const METRIC_REGISTRY = {
|
|
|
3071
3542
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/boundary" },
|
|
3072
3543
|
grain: "voxel",
|
|
3073
3544
|
dimensions: ["scene", "session"],
|
|
3545
|
+
grainDimensions: [],
|
|
3074
3546
|
filters: ["since", "until", "cellSize", "limit", "scene", "session", "region", "format"],
|
|
3075
3547
|
row: voxelCountRow,
|
|
3076
3548
|
columns: {
|
|
@@ -3108,6 +3580,7 @@ export const METRIC_REGISTRY = {
|
|
|
3108
3580
|
endpoint: { method: "GET", path: "/api/v1/heatmaps/boundary/stats" },
|
|
3109
3581
|
grain: "project",
|
|
3110
3582
|
dimensions: ["scene", "session"],
|
|
3583
|
+
grainDimensions: [],
|
|
3111
3584
|
filters: ["since", "until", "cellSize", "scene", "session", "region", "format"],
|
|
3112
3585
|
row: spatialStatsRow,
|
|
3113
3586
|
columns: {
|
|
@@ -3136,6 +3609,7 @@ export const METRIC_REGISTRY = {
|
|
|
3136
3609
|
endpoint: { method: "GET", path: "/api/v1/xr/boundary-contacts" },
|
|
3137
3610
|
grain: "session",
|
|
3138
3611
|
dimensions: ["session", "scene"],
|
|
3612
|
+
grainDimensions: ["session"],
|
|
3139
3613
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
3140
3614
|
row: z.object({ session_id: text, contacts: int, near_ms: num }),
|
|
3141
3615
|
columns: {
|
|
@@ -3170,6 +3644,7 @@ export const METRIC_REGISTRY = {
|
|
|
3170
3644
|
endpoint: { method: "GET", path: "/api/v1/ar/placement/time-to-place" },
|
|
3171
3645
|
grain: "bucket",
|
|
3172
3646
|
dimensions: ["scene", "session"],
|
|
3647
|
+
grainDimensions: [],
|
|
3173
3648
|
filters: ["since", "until", "bins", "limit", "scene", "session", "bucketMs", "format"],
|
|
3174
3649
|
row: z.object({ bucket: int, placements: int }),
|
|
3175
3650
|
columns: {
|
|
@@ -3204,6 +3679,7 @@ export const METRIC_REGISTRY = {
|
|
|
3204
3679
|
endpoint: { method: "GET", path: "/api/v1/ar/placement/attempts" },
|
|
3205
3680
|
grain: "bucket",
|
|
3206
3681
|
dimensions: ["scene", "session"],
|
|
3682
|
+
grainDimensions: [],
|
|
3207
3683
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
3208
3684
|
row: z.object({ attempts: int, placements: int }),
|
|
3209
3685
|
columns: {
|
|
@@ -3242,6 +3718,7 @@ export const METRIC_REGISTRY = {
|
|
|
3242
3718
|
endpoint: { method: "GET", path: "/api/v1/ar/placement/surfaces" },
|
|
3243
3719
|
grain: "row",
|
|
3244
3720
|
dimensions: ["scene", "session"],
|
|
3721
|
+
grainDimensions: [],
|
|
3245
3722
|
filters: ["since", "until", "bins", "limit", "scene", "session", "format"],
|
|
3246
3723
|
row: z.object({ surface: text, placements: int, avg_scale: num }),
|
|
3247
3724
|
columns: {
|
|
@@ -3282,6 +3759,7 @@ export const METRIC_REGISTRY = {
|
|
|
3282
3759
|
endpoint: { method: "GET", path: "/api/v1/funnel" },
|
|
3283
3760
|
grain: "bucket",
|
|
3284
3761
|
dimensions: ["scene", "cameraMode"],
|
|
3762
|
+
grainDimensions: [],
|
|
3285
3763
|
filters: ["since", "until", "scene", "cameraMode", "steps", "format"],
|
|
3286
3764
|
row: z.object({ step: int, sessions: int }),
|
|
3287
3765
|
columns: {
|
|
@@ -3321,6 +3799,7 @@ export const METRIC_REGISTRY = {
|
|
|
3321
3799
|
endpoint: { method: "GET", path: "/api/v1/scene-retention" },
|
|
3322
3800
|
grain: "row",
|
|
3323
3801
|
dimensions: ["scene"],
|
|
3802
|
+
grainDimensions: [],
|
|
3324
3803
|
filters: ["since", "until", "limit", "format"],
|
|
3325
3804
|
row: z.object({ from_scene: text, to_scene: text, sessions: int }),
|
|
3326
3805
|
columns: {
|
|
@@ -3356,6 +3835,7 @@ export const METRIC_REGISTRY = {
|
|
|
3356
3835
|
endpoint: { method: "GET", path: "/api/v1/load-bounce" },
|
|
3357
3836
|
grain: "bucket",
|
|
3358
3837
|
dimensions: ["scene"],
|
|
3838
|
+
grainDimensions: [],
|
|
3359
3839
|
filters: ["since", "until", "scene", "bands", "format"],
|
|
3360
3840
|
row: z.object({ band: int, sessions: int, bounced: int }),
|
|
3361
3841
|
columns: {
|
|
@@ -3397,6 +3877,7 @@ export const METRIC_REGISTRY = {
|
|
|
3397
3877
|
endpoint: { method: "GET", path: "/api/v1/variant-leaderboard" },
|
|
3398
3878
|
grain: "row",
|
|
3399
3879
|
dimensions: ["name", "scene", "cameraMode"],
|
|
3880
|
+
grainDimensions: [],
|
|
3400
3881
|
filters: ["since", "until", "scene", "cameraMode", "variant", "conversion", "limit", "format"],
|
|
3401
3882
|
row: z.object({
|
|
3402
3883
|
variant: text,
|
|
@@ -3437,6 +3918,451 @@ export const METRIC_REGISTRY = {
|
|
|
3437
3918
|
comparable: { primary: "conversions", direction: "up", minSample: 50 },
|
|
3438
3919
|
category: "conversion",
|
|
3439
3920
|
},
|
|
3921
|
+
// =========================================================================
|
|
3922
|
+
// Insights (ADR 0051 §4, design sketch §D)
|
|
3923
|
+
//
|
|
3924
|
+
// Derived metrics: computed in pure TypeScript over *another* metric's
|
|
3925
|
+
// portable bucket series (`@uptimizr/db`'s `src/insights/`) rather than by a
|
|
3926
|
+
// `build*` aggregation of their own. They are the answer to the two questions
|
|
3927
|
+
// an agent otherwise re-derives from raw rows on every turn — "what is normal
|
|
3928
|
+
// here" and "what changed" — computed once, the same way, on every engine.
|
|
3929
|
+
// =========================================================================
|
|
3930
|
+
insight_baseline: {
|
|
3931
|
+
id: "insight_baseline",
|
|
3932
|
+
title: "Metric baseline",
|
|
3933
|
+
description: "What is normal for one metric in one scene. Buckets a comparable metric's headline column " +
|
|
3934
|
+
"into days or hours over a trailing window and reduces the series to its centre (mean, " +
|
|
3935
|
+
"median), its ordinary spread (MAD, p10, p90) and its drift (least-squares slope per " +
|
|
3936
|
+
"bucket). One row per request: the reference distribution a single later observation should " +
|
|
3937
|
+
"be judged against, so 'is 42 FPS bad here?' has an answer that does not depend on the " +
|
|
3938
|
+
"reader's memory.",
|
|
3939
|
+
derived: "insight",
|
|
3940
|
+
endpoint: { method: "GET", path: "/api/v1/insights/baseline" },
|
|
3941
|
+
grain: "project",
|
|
3942
|
+
dimensions: ["scene"],
|
|
3943
|
+
// One row per request; `scene` is both the filter and the key the row
|
|
3944
|
+
// carries, so it is the grain (#304).
|
|
3945
|
+
grainDimensions: ["scene"],
|
|
3946
|
+
filters: ["metric", "scene", "window", "bucket", "since", "until", "format"],
|
|
3947
|
+
row: z.object({
|
|
3948
|
+
metric: text,
|
|
3949
|
+
scene: text,
|
|
3950
|
+
sampleSize: int,
|
|
3951
|
+
buckets: int,
|
|
3952
|
+
mean: numOrNull,
|
|
3953
|
+
median: numOrNull,
|
|
3954
|
+
mad: numOrNull,
|
|
3955
|
+
p10: numOrNull,
|
|
3956
|
+
p90: numOrNull,
|
|
3957
|
+
slope: numOrNull,
|
|
3958
|
+
}),
|
|
3959
|
+
columns: {
|
|
3960
|
+
metric: { description: "The registry metric the baseline is of.", unit: "id" },
|
|
3961
|
+
scene: {
|
|
3962
|
+
description: "The scene it was scoped to; empty when the baseline spans every scene.",
|
|
3963
|
+
unit: "id",
|
|
3964
|
+
},
|
|
3965
|
+
sampleSize: {
|
|
3966
|
+
description: "Events (or distinct sessions, for a session-valued metric) behind the whole series.",
|
|
3967
|
+
unit: "count",
|
|
3968
|
+
},
|
|
3969
|
+
buckets: { description: "How many buckets carried a value.", unit: "count" },
|
|
3970
|
+
mean: { description: "Arithmetic mean of the bucket values." },
|
|
3971
|
+
median: { description: "Median bucket value — the headline 'normal'.", measure: true },
|
|
3972
|
+
mad: {
|
|
3973
|
+
description: "Median absolute deviation: the typical bucket-to-bucket swing, and the unit a robust " +
|
|
3974
|
+
"z-score is measured in.",
|
|
3975
|
+
},
|
|
3976
|
+
p10: { description: "10th percentile of the bucket values — the ordinary floor." },
|
|
3977
|
+
p90: { description: "90th percentile of the bucket values — the ordinary ceiling." },
|
|
3978
|
+
slope: {
|
|
3979
|
+
description: "Least-squares slope against the bucket index: change per bucket over the window.",
|
|
3980
|
+
},
|
|
3981
|
+
},
|
|
3982
|
+
limits: { maxRows: 1, maxSummaryRows: 1 },
|
|
3983
|
+
interpretation: "Read `median` as the centre and `mad` as the tolerance: a later observation more than a few " +
|
|
3984
|
+
"MADs from the median is unusual for this scene, one inside p10..p90 is ordinary. `slope` is " +
|
|
3985
|
+
"the drift *within* the window — a baseline with a steep slope is not a stable reference and " +
|
|
3986
|
+
"should be re-read over a shorter window before it is used to judge anything.",
|
|
3987
|
+
caveats: [
|
|
3988
|
+
"Only `comparable` metrics with a portable bucket series can be baselined; a metric whose value is defined by a join, a window function or a caller-supplied funnel predicate is rejected rather than approximated, and the error names every id that is available.",
|
|
3989
|
+
"Window bounds are snapped down to whole buckets, so the current (incomplete) day or hour is excluded — otherwise every morning would look like a collapse. The snapped window is echoed in the `table` / `summary` envelopes.",
|
|
3990
|
+
"Buckets with no matching events are absent from the series rather than zero: a metric that was silent for a week has a narrower baseline, not a lower one.",
|
|
3991
|
+
"Every statistic is `null` when the window carried no values, and `slope` is `null` under two buckets. Read `null` as 'no data', never as 0.",
|
|
3992
|
+
"The series reproduces the metric's headline column per bucket; where the metric's own endpoint aggregates per session first (ADR 0028 §1), a bucket value and that endpoint's headline can differ slightly.",
|
|
3993
|
+
],
|
|
3994
|
+
sourceChannels: [],
|
|
3995
|
+
related: ["insight_movers", "timeseries", "perf_summary", "events_daily"],
|
|
3996
|
+
category: "insights",
|
|
3997
|
+
},
|
|
3998
|
+
insight_movers: {
|
|
3999
|
+
id: "insight_movers",
|
|
4000
|
+
title: "What changed",
|
|
4001
|
+
description: "What moved, ranked. For every comparable metric in scope, compares the current range with a " +
|
|
4002
|
+
"reference range (the previous equal window by default) and ranks the differences by a " +
|
|
4003
|
+
"robust z-score — the change divided by how much that metric normally swings, so a metric " +
|
|
4004
|
+
"that is always volatile has to move much further than a steady one before it is called a " +
|
|
4005
|
+
"mover. One row per metric: the top risers, the top fallers, and the ones that did not move.",
|
|
4006
|
+
derived: "insight",
|
|
4007
|
+
endpoint: { method: "GET", path: "/api/v1/insights/movers" },
|
|
4008
|
+
grain: "row",
|
|
4009
|
+
dimensions: ["scene"],
|
|
4010
|
+
// One row per metric. Its rows carry no registry dimension column at all —
|
|
4011
|
+
// the key is the metric id, which is not one — so there is nothing for the
|
|
4012
|
+
// generic group-by tier to regroup by (#304).
|
|
4013
|
+
grainDimensions: [],
|
|
4014
|
+
filters: [
|
|
4015
|
+
"scene",
|
|
4016
|
+
"metrics",
|
|
4017
|
+
"bucket",
|
|
4018
|
+
"limit",
|
|
4019
|
+
"since",
|
|
4020
|
+
"until",
|
|
4021
|
+
"refSince",
|
|
4022
|
+
"refUntil",
|
|
4023
|
+
"format",
|
|
4024
|
+
],
|
|
4025
|
+
row: z.object({
|
|
4026
|
+
metric: text,
|
|
4027
|
+
dimensionValue: z.string().nullable(),
|
|
4028
|
+
current: numOrNull,
|
|
4029
|
+
previous: numOrNull,
|
|
4030
|
+
delta: numOrNull,
|
|
4031
|
+
deltaPct: numOrNull,
|
|
4032
|
+
z: numOrNull,
|
|
4033
|
+
direction: text,
|
|
4034
|
+
aboveMinSample: z.boolean(),
|
|
4035
|
+
sampleSize: num,
|
|
4036
|
+
}),
|
|
4037
|
+
columns: {
|
|
4038
|
+
metric: { description: "The registry metric that moved.", unit: "id", label: true },
|
|
4039
|
+
dimensionValue: {
|
|
4040
|
+
description: "The dimension value the move is attributed to. Always null in v1 — movers are computed " +
|
|
4041
|
+
"at the scene (or project) level; per-dimension attribution arrives with `anomalies`.",
|
|
4042
|
+
unit: "id",
|
|
4043
|
+
},
|
|
4044
|
+
current: { description: "The metric's headline column over the current range." },
|
|
4045
|
+
previous: { description: "The same over the reference range." },
|
|
4046
|
+
delta: { description: "`current - previous`, in the metric's own unit." },
|
|
4047
|
+
deltaPct: {
|
|
4048
|
+
description: "Relative change; null when the reference value was zero or missing.",
|
|
4049
|
+
unit: "ratio",
|
|
4050
|
+
},
|
|
4051
|
+
z: {
|
|
4052
|
+
description: "Robust z-score: `delta` divided by the median absolute deviation of the reference " +
|
|
4053
|
+
"bucket series. The ranking key.",
|
|
4054
|
+
unit: "ratio",
|
|
4055
|
+
measure: true,
|
|
4056
|
+
},
|
|
4057
|
+
direction: {
|
|
4058
|
+
description: "Whether a rise is good (`up`), bad (`down`) or merely a fact (`neutral`), from the " +
|
|
4059
|
+
"moved metric's own registry entry. It qualifies the sign of `delta`; it is not the " +
|
|
4060
|
+
"direction this metric actually moved in.",
|
|
4061
|
+
unit: "label",
|
|
4062
|
+
},
|
|
4063
|
+
aboveMinSample: {
|
|
4064
|
+
description: "Whether the current range cleared the metric's declared minimum denominator. False " +
|
|
4065
|
+
"means the delta is real arithmetic but not evidence.",
|
|
4066
|
+
},
|
|
4067
|
+
sampleSize: {
|
|
4068
|
+
description: "The current range's denominator — events, or distinct sessions for a session-valued " +
|
|
4069
|
+
"metric — that `aboveMinSample` was decided on.",
|
|
4070
|
+
},
|
|
4071
|
+
},
|
|
4072
|
+
limits: { maxRows: 150, maxSummaryRows: 10 },
|
|
4073
|
+
interpretation: "Read the rows in order: risers first, then fallers, then the ones that did not move. |z| is " +
|
|
4074
|
+
"'how many typical swings': under ~2 the move is inside this metric's ordinary noise, over " +
|
|
4075
|
+
"~3 it is worth explaining. Combine `direction` with the sign of `delta` to know whether a " +
|
|
4076
|
+
"move is good or bad — a rise in a `down` metric (errors, dead clicks, jank) is a " +
|
|
4077
|
+
"regression. Rows with `aboveMinSample: false` are sorted last and must not be reported as " +
|
|
4078
|
+
"findings.",
|
|
4079
|
+
caveats: [
|
|
4080
|
+
"Cost is bounded by a per-request cap on how many metrics are scanned (one grouped scan each); without a `metrics` allowlist a curated default set is used, so a move in a metric outside that set is not reported. Name it explicitly to include it.",
|
|
4081
|
+
"Only metrics with a portable bucket series participate. Funnels, cohort metrics and anything defined by the relationship between consecutive events are outside the scan by construction.",
|
|
4082
|
+
"`aboveMinSample: false` means the denominator is too small for the delta to be evidence — the row is kept rather than dropped so 'we cannot tell' stays distinguishable from 'nothing changed', but it must not be read as a finding.",
|
|
4083
|
+
"The spread is taken over the *reference* window's buckets, floored at 1% of that window's level — so a one-bucket or perfectly flat reference cannot make every metric score in the billions and turn the ranking into a comparison of raw deltas across incompatible units. A reference that is flat *at zero* has no level to floor against, so the first data of its kind scores a very large |z|: correct (it is unprecedented) but only meaningful once `aboveMinSample` is true.",
|
|
4084
|
+
"Window bounds are snapped down to whole buckets, so the current incomplete day or hour is excluded from both ranges and the two stay exactly comparable.",
|
|
4085
|
+
"`deltaPct` is null when the reference value was zero: a percentage change from nothing is undefined. Read `delta` in that case.",
|
|
4086
|
+
"A metric is compared with itself over time, never across projects or scenes: sampling rates (ADR 0012) make absolute counts incomparable between them.",
|
|
4087
|
+
],
|
|
4088
|
+
sourceChannels: [],
|
|
4089
|
+
related: ["insight_baseline", "events_daily", "perf_daily", "event_counts"],
|
|
4090
|
+
category: "insights",
|
|
4091
|
+
},
|
|
4092
|
+
// =========================================================================
|
|
4093
|
+
// --- anomalies (#306) ---
|
|
4094
|
+
// =========================================================================
|
|
4095
|
+
insight_anomalies: {
|
|
4096
|
+
id: "insight_anomalies",
|
|
4097
|
+
title: "Anomalous buckets",
|
|
4098
|
+
description: "When one metric stopped behaving, and what inside it accounts for that. Walks a comparable " +
|
|
4099
|
+
"metric's day or hour bucket series and returns only the buckets that do not belong in it: " +
|
|
4100
|
+
"a `spike` or a `drop` when a single bucket sits more than `sensitivity` median absolute " +
|
|
4101
|
+
"deviations from the buckets just before it, and a `shift` at the bucket where a CUSUM " +
|
|
4102
|
+
"change-point says the level moved and stayed moved. Where the metric declares a dimension " +
|
|
4103
|
+
"it can be split by, the row also names the dimension value holding the largest share of " +
|
|
4104
|
+
"the excess — the difference between 'errors tripled on the 14th' and 'graphics " +
|
|
4105
|
+
"diagnostics tripled on the 14th'.",
|
|
4106
|
+
derived: "insight",
|
|
4107
|
+
endpoint: { method: "GET", path: "/api/v1/insights/anomalies" },
|
|
4108
|
+
grain: "row",
|
|
4109
|
+
dimensions: ["scene"],
|
|
4110
|
+
// One row per anomalous bucket; `scene` is the only registry dimension the
|
|
4111
|
+
// row carries (the metric id and the bucket start are not) (#304).
|
|
4112
|
+
grainDimensions: ["scene"],
|
|
4113
|
+
filters: ["metric", "scene", "window", "bucket", "sensitivity", "since", "until", "format"],
|
|
4114
|
+
row: z.object({
|
|
4115
|
+
metric: text,
|
|
4116
|
+
scene: text,
|
|
4117
|
+
bucketStart: num,
|
|
4118
|
+
value: numOrNull,
|
|
4119
|
+
expected: numOrNull,
|
|
4120
|
+
z: numOrNull,
|
|
4121
|
+
kind: text,
|
|
4122
|
+
contributor: z.object({ dimension: text, value: text, share: numOrNull }).nullable(),
|
|
4123
|
+
sampleSize: num,
|
|
4124
|
+
}),
|
|
4125
|
+
columns: {
|
|
4126
|
+
metric: { description: "The registry metric the series is of.", unit: "id", label: true },
|
|
4127
|
+
scene: {
|
|
4128
|
+
description: "The scene it was scoped to; empty when the series spans every scene.",
|
|
4129
|
+
unit: "id",
|
|
4130
|
+
},
|
|
4131
|
+
bucketStart: {
|
|
4132
|
+
description: "Start of the anomalous bucket, epoch milliseconds (UTC, aligned to the grain). For a " +
|
|
4133
|
+
"`shift` this is the bucket the level changed at, not the bucket the change was detected in.",
|
|
4134
|
+
unit: "timestamp",
|
|
4135
|
+
},
|
|
4136
|
+
value: {
|
|
4137
|
+
description: "The observed value. For a `shift`, the level that held after the change-point rather " +
|
|
4138
|
+
"than one bucket's reading.",
|
|
4139
|
+
},
|
|
4140
|
+
expected: {
|
|
4141
|
+
description: "What the bucket should have read: the median of the trailing window, or for a " +
|
|
4142
|
+
"`shift` the level that held before the change-point.",
|
|
4143
|
+
},
|
|
4144
|
+
z: {
|
|
4145
|
+
description: "Signed robust z-score: `(value - expected)` divided by the trailing window's median " +
|
|
4146
|
+
"absolute deviation rescaled to a standard deviation (x1.4826) and floored at 1% of the " +
|
|
4147
|
+
"level. It is measured in standard deviations, so `sensitivity: 3` means 'beyond three " +
|
|
4148
|
+
"sigma'. `insight_movers` reports the same ratio *unscaled* because it ranks rather " +
|
|
4149
|
+
"than thresholds, so the two columns differ by that constant factor.",
|
|
4150
|
+
unit: "ratio",
|
|
4151
|
+
measure: true,
|
|
4152
|
+
},
|
|
4153
|
+
kind: {
|
|
4154
|
+
description: "`spike` (one bucket far above the trailing window), `drop` (far below) or `shift` (the " +
|
|
4155
|
+
"level moved and stayed moved).",
|
|
4156
|
+
unit: "label",
|
|
4157
|
+
},
|
|
4158
|
+
contributor: {
|
|
4159
|
+
description: "The dimension value accounting for the excess: `{ dimension, value, share }`, where " +
|
|
4160
|
+
"`share` is its fraction of the total same-signed excess. Null when the metric declares " +
|
|
4161
|
+
"no split dimension, when the split would be degenerate, or when no value moved with the " +
|
|
4162
|
+
"finding.",
|
|
4163
|
+
},
|
|
4164
|
+
sampleSize: {
|
|
4165
|
+
description: "The anomalous bucket's own denominator — events, or distinct sessions for a " +
|
|
4166
|
+
"session-valued metric.",
|
|
4167
|
+
unit: "count",
|
|
4168
|
+
},
|
|
4169
|
+
},
|
|
4170
|
+
limits: { maxRows: 200, maxSummaryRows: 8 },
|
|
4171
|
+
interpretation: "Rows are ordered by bucket, oldest first, so the result reads as a timeline. |z| is 'how " +
|
|
4172
|
+
"many typical swings out': at the default `sensitivity` of 3 nothing under 3 appears at all, " +
|
|
4173
|
+
"and a `shift` is usually the more important finding than a `spike` because it is still " +
|
|
4174
|
+
"true today. Read `contributor.share`: above ~0.8 the anomaly *is* that dimension value and " +
|
|
4175
|
+
"the investigation has one place to start; near the reciprocal of the number of values it is " +
|
|
4176
|
+
"spread evenly and the cause is more likely to be global. An empty result means the series " +
|
|
4177
|
+
"held together over the window, not that there was no data - check `insight_baseline`'s " +
|
|
4178
|
+
"`buckets` for that.",
|
|
4179
|
+
caveats: [
|
|
4180
|
+
"Only `comparable` metrics with a portable bucket series can be scored; a metric whose value is defined by a join, a window function or a caller-supplied funnel predicate is rejected with the list of ids that are available, rather than approximated.",
|
|
4181
|
+
"A bucket is judged against the buckets before it (14 at day grain, 168 at hour grain), never against itself, and a bucket with fewer than 5 buckets of history behind it is not judged at all. The first days of a project are therefore silent by construction rather than a wall of findings.",
|
|
4182
|
+
"The spread is floored at 1% of the trailing level, so a perfectly flat metric does not report every subsequent bucket as an infinite z. A series that is flat at *zero* has no level to floor against, so the first data of its kind can read as a very large spike.",
|
|
4183
|
+
"Buckets with no matching events are absent from the series rather than zero, so a gap in capture is not reported as a drop. A metric that genuinely fell to zero *did* record events, so it is.",
|
|
4184
|
+
"Change-points are detected sequentially, so a shift is reported at the bucket the level moved at but can only be found once enough buckets after it have accumulated. The most recent few buckets of a window are therefore under-covered for `shift` - re-read after more data lands.",
|
|
4185
|
+
"Contributor attribution costs at most 3 extra grouped scans per request whatever the data looks like: adjacent findings share one window, and only the three most extreme windows are re-read. Quieter findings carry `contributor: null` - narrow `since` / `until` around one to attribute it.",
|
|
4186
|
+
"The contributor is one dimension, chosen per metric from the promoted columns (mesh, source, name, event type, scene). It is not a cause: it is where the excess sits. A metric whose real explanation is the device or the release has no column for it and reports null.",
|
|
4187
|
+
"Window bounds are snapped down to whole buckets, so the incomplete day or hour in progress is never scored - otherwise every morning would report a drop.",
|
|
4188
|
+
"A sustained level change is reported twice over: once as the `shift` at its change-point, and again as `drop`/`spike` rows for the buckets right after it, until the trailing window has caught up with the new level. Those are two true statements about one event ('it moved on the 14th' and 'the 15th was far below what the fortnight before it predicted'), not a duplicate row - read the `shift` as the finding and the points as its first days.",
|
|
4189
|
+
"`z` is measured in standard deviations (the MAD rescaled by 1.4826), so it is directly comparable with a normal-distribution intuition but **not** byte-comparable with `insight_movers`' `z`, which is the same ratio unscaled.",
|
|
4190
|
+
],
|
|
4191
|
+
sourceChannels: [],
|
|
4192
|
+
related: ["insight_baseline", "insight_movers", "timeseries", "error_heatmap"],
|
|
4193
|
+
category: "insights",
|
|
4194
|
+
},
|
|
4195
|
+
// --- significance / scene health (#307) ---------------------------------
|
|
4196
|
+
//
|
|
4197
|
+
// The other two §4 primitives. `insight_significance` answers the question a
|
|
4198
|
+
// robust z deliberately does not — *could this difference be chance?* — and
|
|
4199
|
+
// `insight_scene_health` collapses six of those readings into one comparable
|
|
4200
|
+
// score per scene, with every factor traceable back to the metric behind it.
|
|
4201
|
+
insight_significance: {
|
|
4202
|
+
id: "insight_significance",
|
|
4203
|
+
title: "Statistical significance",
|
|
4204
|
+
description: "Is that difference real? Compares one comparable metric across two windows and reports the " +
|
|
4205
|
+
"effect, its 95% confidence interval and a two-sided p-value, with the test chosen from what " +
|
|
4206
|
+
"the measure *is*: a two-proportion z with Wilson intervals for a declared rate, Welch's t " +
|
|
4207
|
+
"over the per-bucket values for a level, an exact Poisson rate test for a bare count. One " +
|
|
4208
|
+
"row per request, and a `powerNote` saying what these sample sizes could and could not have " +
|
|
4209
|
+
"detected.",
|
|
4210
|
+
derived: "insight",
|
|
4211
|
+
endpoint: { method: "GET", path: "/api/v1/insights/significance" },
|
|
4212
|
+
grain: "project",
|
|
4213
|
+
dimensions: ["scene"],
|
|
4214
|
+
// One row per request; `scene` is the only registry dimension the row
|
|
4215
|
+
// carries, so it is the grain (#304).
|
|
4216
|
+
grainDimensions: ["scene"],
|
|
4217
|
+
filters: ["metric", "scene", "bucket", "since", "until", "refSince", "refUntil", "format"],
|
|
4218
|
+
row: z.object({
|
|
4219
|
+
metric: text,
|
|
4220
|
+
scene: text,
|
|
4221
|
+
a: z.object({ value: numOrNull, n: num }),
|
|
4222
|
+
b: z.object({ value: numOrNull, n: num }),
|
|
4223
|
+
effect: numOrNull,
|
|
4224
|
+
ci95: z.tuple([numOrNull, numOrNull]),
|
|
4225
|
+
p: numOrNull,
|
|
4226
|
+
test: text,
|
|
4227
|
+
effectUnit: text,
|
|
4228
|
+
significant: z.boolean(),
|
|
4229
|
+
powerNote: text,
|
|
4230
|
+
}),
|
|
4231
|
+
columns: {
|
|
4232
|
+
metric: { description: "The registry metric being compared.", unit: "id", label: true },
|
|
4233
|
+
scene: {
|
|
4234
|
+
description: "The scene it was scoped to; empty when the comparison spans every scene.",
|
|
4235
|
+
unit: "id",
|
|
4236
|
+
},
|
|
4237
|
+
a: {
|
|
4238
|
+
description: "The current window: `value` in the metric's own unit (a proportion for a rate, a " +
|
|
4239
|
+
"per-bucket rate for a count, a mean of bucket values for a level) and `n`, the " +
|
|
4240
|
+
"denominator it rests on — trials for a proportion, buckets otherwise. `value * n` " +
|
|
4241
|
+
"recovers the total the arm was computed from, whichever test ran.",
|
|
4242
|
+
},
|
|
4243
|
+
b: { description: "The reference window, in the same shape as `a`." },
|
|
4244
|
+
effect: {
|
|
4245
|
+
description: "`a.value - b.value`, in the unit named by `effectUnit`.",
|
|
4246
|
+
measure: true,
|
|
4247
|
+
},
|
|
4248
|
+
ci95: {
|
|
4249
|
+
description: "The 95% confidence interval for `effect`, as `[lo, hi]`. An interval that straddles 0 " +
|
|
4250
|
+
"is the finding, whatever `p` says.",
|
|
4251
|
+
},
|
|
4252
|
+
p: {
|
|
4253
|
+
description: "Two-sided p-value for the null hypothesis of no difference.",
|
|
4254
|
+
unit: "ratio",
|
|
4255
|
+
},
|
|
4256
|
+
test: {
|
|
4257
|
+
description: "Which test produced the row: `two_proportion_z`, `welch_t` or `poisson_rate`.",
|
|
4258
|
+
unit: "label",
|
|
4259
|
+
},
|
|
4260
|
+
effectUnit: { description: "What one unit of `effect` means.", unit: "label" },
|
|
4261
|
+
significant: {
|
|
4262
|
+
description: "Whether `p` cleared alpha = 0.05. A convenience, never a substitute for `ci95`.",
|
|
4263
|
+
},
|
|
4264
|
+
powerNote: {
|
|
4265
|
+
description: "The smallest difference these sample sizes could have detected at 80% power, plus any " +
|
|
4266
|
+
"assumption the data strained (overdispersed counts, too few buckets).",
|
|
4267
|
+
unit: "label",
|
|
4268
|
+
},
|
|
4269
|
+
},
|
|
4270
|
+
limits: { maxRows: 1, maxSummaryRows: 1 },
|
|
4271
|
+
interpretation: "Read `ci95` before `p`. A narrow interval around a small effect is evidence that nothing " +
|
|
4272
|
+
"much changed; a wide interval containing 0 is evidence of nothing at all, and `powerNote` " +
|
|
4273
|
+
"says which of the two you are looking at. `p` is two-sided throughout: it answers 'is there " +
|
|
4274
|
+
"a difference', not 'is it an improvement' — combine it with the sign of `effect` and the " +
|
|
4275
|
+
"compared metric's own `direction` for that.",
|
|
4276
|
+
caveats: [
|
|
4277
|
+
"v1 compares two **time windows**, not two segments. A variant-versus-variant or device-versus-device contrast needs the bucket series split by a promoted dimension, which is not available yet; asking for one is rejected rather than answered with the wrong contrast.",
|
|
4278
|
+
"The test is chosen from the measure, not from the caller: a metric whose comparable primary declares a `rateOf` denominator gets the two-proportion z, a bare count gets the Poisson rate test, and everything else gets Welch's t. A caller cannot ask for a different one.",
|
|
4279
|
+
"Welch's t treats each **bucket** as one observation, so `n` is the number of days or hours compared, never the number of events. Consecutive frame samples inside a day are not independent, and counting them would manufacture a p-value of 1e-40 for ordinary day-to-day drift.",
|
|
4280
|
+
"For a Welch comparison `a.value` is the **mean** of the bucket values even where `movers` would report the window sum, so that `effect` and `value` can be read together; `n` is the bucket count, so the sum is one multiplication away.",
|
|
4281
|
+
"The Poisson test assumes counts that do not vary more than a Poisson process would. When the buckets say otherwise, the p-value is optimistic and `powerNote` says so — treat it as an upper bound on the evidence.",
|
|
4282
|
+
"The Poisson interval is the normal approximation on the rate difference (there is no closed-form exact one); only the p-value is exact.",
|
|
4283
|
+
"The two-proportion p-value uses the pooled variance under the null while the interval is unpooled and Newcombe-hybrid. In a borderline case the two can disagree about whether 0 is excluded; that is the textbook procedure, and the interval is the one to believe.",
|
|
4284
|
+
"Window bounds are snapped down to whole buckets, so the current incomplete day or hour is excluded from both windows and the two stay exactly comparable.",
|
|
4285
|
+
"Statistical significance is not importance. A large enough sample makes a difference of no consequence significant; `effect` and `effectUnit` are what say whether it matters.",
|
|
4286
|
+
],
|
|
4287
|
+
sourceChannels: [],
|
|
4288
|
+
related: ["insight_movers", "insight_baseline", "insight_scene_health"],
|
|
4289
|
+
category: "insights",
|
|
4290
|
+
},
|
|
4291
|
+
insight_scene_health: {
|
|
4292
|
+
id: "insight_scene_health",
|
|
4293
|
+
title: "Scene health score",
|
|
4294
|
+
description: "Which scene is in trouble, and why. Scores each scene 0-100 over six weighted factors — " +
|
|
4295
|
+
"perf stability, jank, errors, dead clicks, exploration coverage and XR abandonment — each " +
|
|
4296
|
+
"normalised against the project's own baseline over the preceding equal window. One row per " +
|
|
4297
|
+
"scene, least healthy first, and every factor carries the metric id, the raw value, the " +
|
|
4298
|
+
"baseline it was compared with and the weight it contributed, so the score can always be " +
|
|
4299
|
+
"taken apart.",
|
|
4300
|
+
derived: "insight",
|
|
4301
|
+
endpoint: { method: "GET", path: "/api/v1/insights/scene-health" },
|
|
4302
|
+
grain: "scene",
|
|
4303
|
+
dimensions: ["scene"],
|
|
4304
|
+
// One row per scene — the grain is the scene itself (#304).
|
|
4305
|
+
grainDimensions: ["scene"],
|
|
4306
|
+
filters: ["scene", "window", "since", "until", "bucket", "limit", "weights", "format"],
|
|
4307
|
+
row: z.object({
|
|
4308
|
+
scene: text,
|
|
4309
|
+
score: numOrNull,
|
|
4310
|
+
factors: z.array(z.object({
|
|
4311
|
+
id: text,
|
|
4312
|
+
metric: text,
|
|
4313
|
+
raw: numOrNull,
|
|
4314
|
+
baseline: numOrNull,
|
|
4315
|
+
score: numOrNull,
|
|
4316
|
+
weight: num,
|
|
4317
|
+
unit: text,
|
|
4318
|
+
note: text,
|
|
4319
|
+
})),
|
|
4320
|
+
sampleSize: int,
|
|
4321
|
+
since: int,
|
|
4322
|
+
until: int,
|
|
4323
|
+
}),
|
|
4324
|
+
columns: {
|
|
4325
|
+
scene: { description: "The scene scored.", unit: "id", label: true },
|
|
4326
|
+
score: {
|
|
4327
|
+
description: "Weighted mean of the available factors, 0-100. 50 is exactly the project norm for the " +
|
|
4328
|
+
"preceding window; higher is healthier. Null when no factor could be scored.",
|
|
4329
|
+
measure: true,
|
|
4330
|
+
},
|
|
4331
|
+
factors: {
|
|
4332
|
+
description: "One entry per factor: `id`, the `metric` behind it, its `raw` value in `unit`, the " +
|
|
4333
|
+
"project `baseline` it was compared with, its normalised `score`, the `weight` it " +
|
|
4334
|
+
"carried, and a `note` saying what the raw number measures (or why it was not scored).",
|
|
4335
|
+
},
|
|
4336
|
+
sampleSize: {
|
|
4337
|
+
description: "Sessions started in the scene over the window — what the score rests on.",
|
|
4338
|
+
unit: "sessions",
|
|
4339
|
+
},
|
|
4340
|
+
since: { description: "Start of the scored window, epoch ms.", unit: "epoch-ms" },
|
|
4341
|
+
until: { description: "End of the scored window, epoch ms.", unit: "epoch-ms" },
|
|
4342
|
+
},
|
|
4343
|
+
limits: { maxRows: 10, maxSummaryRows: 10 },
|
|
4344
|
+
interpretation: "The score is a **comparison, not a grade**: it says how this scene is doing against the " +
|
|
4345
|
+
"rest of this project's recent past, so a project where everything is equally bad reads 50 " +
|
|
4346
|
+
"everywhere. Read the lowest-scoring scene first, then the factor whose own score is " +
|
|
4347
|
+
"furthest below 50 — that factor's `metric` is the endpoint to open next, and its `raw` " +
|
|
4348
|
+
"versus `baseline` is the sentence to write. A factor with `score: null` was not counted; " +
|
|
4349
|
+
"its `note` says why.",
|
|
4350
|
+
caveats: [
|
|
4351
|
+
"The default weights are a judgement, declared in this entry so they can be argued with: perf stability 0.25, error rate 0.25, jank rate 0.2, dead-click rate 0.15, coverage 0.1, XR abandonment 0.05. Pass `weights` as a JSON object to override any of them; unnamed factors keep their default.",
|
|
4352
|
+
"Normalisation is against the **project as a whole** over the preceding equal window, not against an absolute target. A new project with one week of data has no baseline and scores null.",
|
|
4353
|
+
"Unlike `insight_baseline` and `insight_movers`, the window **includes the bucket in progress** rather than snapping down to the last complete one: every factor is a rate or a percentile, neither of which a partial bucket distorts, and excluding today would make the score answer about yesterday. The window actually measured is echoed as `since` / `until` on every row.",
|
|
4354
|
+
"A factor with no data in the window, or no project baseline to compare against, is reported with `score: null` and excluded from the weighted mean — the remaining weights are renormalised. The row still lists it, so a missing factor is visible rather than silently folded in.",
|
|
4355
|
+
"XR abandonment is absent in a project with no XR traffic: a scene nobody visited in VR is not an unhealthy VR scene.",
|
|
4356
|
+
"Coverage is positioned camera samples per session, not a voxel-coverage percentage: the true percentage needs the registered scene bounds and has no portable per-bucket form. It is only ever read against the project's own baseline.",
|
|
4357
|
+
"The jank factor is the *pooled* long-frames-per-sampled-window rate, while the `jank_rate` metric's own endpoint reports a per-session median (ADR 0028 Section 1). They agree in direction, not in value.",
|
|
4358
|
+
"Rage clicks are not folded into the dead-click factor: they are defined by the gap between consecutive clicks and have no portable per-bucket form.",
|
|
4359
|
+
"Fewer than about 20 sessions makes a scene's score noise. The row is returned anyway, with its `sampleSize`, so 'we cannot tell' stays distinguishable from 'this scene is fine'.",
|
|
4360
|
+
"Without a `scene` the scan is bounded to the busiest scenes by event volume, so a quiet scene can be missing from the list entirely. Name it explicitly to score it.",
|
|
4361
|
+
],
|
|
4362
|
+
sourceChannels: [],
|
|
4363
|
+
related: ["insight_movers", "insight_baseline", "insight_significance"],
|
|
4364
|
+
category: "insights",
|
|
4365
|
+
},
|
|
3440
4366
|
};
|
|
3441
4367
|
/** Every registry id, in declaration order. */
|
|
3442
4368
|
export const METRIC_IDS = Object.keys(METRIC_REGISTRY);
|
|
@@ -3479,6 +4405,21 @@ export function metricForBuilder(builder) {
|
|
|
3479
4405
|
* catalog stays a superset of the hand-written one (design sketch §A.4).
|
|
3480
4406
|
*/
|
|
3481
4407
|
export function isResourceMetric(metric) {
|
|
3482
|
-
return metric.builder === undefined;
|
|
4408
|
+
return metric.builder === undefined && metric.derived === undefined;
|
|
4409
|
+
}
|
|
4410
|
+
/**
|
|
4411
|
+
* A **derived** entry is computed in TypeScript over another metric's data
|
|
4412
|
+
* rather than by a `build*` aggregation (ADR 0051 §4): the insight primitives.
|
|
4413
|
+
* It has no builder, but — unlike a {@link isResourceMetric resource} — it is a
|
|
4414
|
+
* real aggregate with a querystring, a time range and the `format` envelope, so
|
|
4415
|
+
* every consumer that treats `builder != null` as "is an aggregate" should ask
|
|
4416
|
+
* this instead.
|
|
4417
|
+
*/
|
|
4418
|
+
export function isDerivedMetric(metric) {
|
|
4419
|
+
return metric.derived !== undefined;
|
|
4420
|
+
}
|
|
4421
|
+
/** Whether a metric is served as an aggregate over a querystring (not a resource read). */
|
|
4422
|
+
export function isAggregateMetric(metric) {
|
|
4423
|
+
return metric.builder !== undefined || metric.derived !== undefined;
|
|
3483
4424
|
}
|
|
3484
4425
|
//# sourceMappingURL=registry.js.map
|