@uptimizr/db 0.2.1 → 0.4.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/dist/events.d.ts.map +1 -1
- package/dist/events.js +6 -3
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/parity/cases.d.ts.map +1 -1
- package/dist/parity/cases.js +124 -1
- package/dist/parity/cases.js.map +1 -1
- package/dist/query/aggregations.d.ts +133 -4
- package/dist/query/aggregations.d.ts.map +1 -1
- package/dist/query/aggregations.js +381 -4
- package/dist/query/aggregations.js.map +1 -1
- package/dist/query/dialect.d.ts +14 -1
- package/dist/query/dialect.d.ts.map +1 -1
- package/dist/query/dialect.js +23 -0
- package/dist/query/dialect.js.map +1 -1
- package/dist/query/types.d.ts +126 -0
- package/dist/query/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregations.d.ts","sourceRoot":"","sources":["../../src/query/aggregations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"aggregations.d.ts","sourceRoot":"","sources":["../../src/query/aggregations.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,28 @@
|
|
|
9
9
|
* Invariant: no multi-tenant concepts here. Filtering is by `project_id` and the
|
|
10
10
|
* optional scene/source/session dimensions only.
|
|
11
11
|
*/
|
|
12
|
-
import { ParamBag, cameraModeClause, dayRangeClause, rangeClause, sceneClause, sessionClause, sourceClause, } from "./dialect.js";
|
|
12
|
+
import { ParamBag, cameraModeClause, dayRangeClause, rangeClause, regionClause, sceneClause, sessionClause, sourceClause, } from "./dialect.js";
|
|
13
|
+
/** World/gaze voxel coordinates derive from the raycast `hit_point` vector. */
|
|
14
|
+
const HIT_POINT_COLS = { x: "hit_point[1]", y: "hit_point[2]", z: "hit_point[3]" };
|
|
15
|
+
/** Floor-plan cells derive from the camera `position` vector (Y is height). */
|
|
16
|
+
const POSITION_COLS = { x: "position[1]", y: "position[2]", z: "position[3]" };
|
|
17
|
+
/**
|
|
18
|
+
* Bounds-driven default voxel size (ADR 0040 §1). Picks a `cellSize` so the
|
|
19
|
+
* longest axis of `bounds` spans roughly `targetCells` cells, keeping spatial
|
|
20
|
+
* resolution proportional to scene extent instead of a fixed world-unit default
|
|
21
|
+
* that dissolves large scenes into a few coarse blocks. Returns `null` for a
|
|
22
|
+
* missing or degenerate (zero/negative longest-axis) box so the caller can fall
|
|
23
|
+
* back to its fixed default.
|
|
24
|
+
*/
|
|
25
|
+
export function defaultCellSizeForBounds(bounds, targetCells = 64) {
|
|
26
|
+
if (bounds == null)
|
|
27
|
+
return null;
|
|
28
|
+
const [minX, minY, minZ, maxX, maxY, maxZ] = bounds;
|
|
29
|
+
const longest = Math.max(maxX - minX, maxY - minY, maxZ - minZ);
|
|
30
|
+
if (!Number.isFinite(longest) || longest <= 0 || targetCells <= 0)
|
|
31
|
+
return null;
|
|
32
|
+
return longest / targetCells;
|
|
33
|
+
}
|
|
13
34
|
/** List sessions for a project with event counts and time bounds. */
|
|
14
35
|
export function buildListSessions(projectId, opts, d) {
|
|
15
36
|
const bag = new ParamBag(d);
|
|
@@ -76,6 +97,7 @@ export function buildWorldHeatmap(projectId, opts, d) {
|
|
|
76
97
|
const scene = sceneClause(bag, opts);
|
|
77
98
|
const source = sourceClause(bag, opts);
|
|
78
99
|
const cameraMode = cameraModeClause(bag, d, projectId, opts);
|
|
100
|
+
const region = regionClause(bag, opts, HIT_POINT_COLS);
|
|
79
101
|
const limit = bag.add("limit", "u32", opts.limit ?? 1000);
|
|
80
102
|
return {
|
|
81
103
|
query: `
|
|
@@ -87,7 +109,7 @@ export function buildWorldHeatmap(projectId, opts, d) {
|
|
|
87
109
|
FROM events
|
|
88
110
|
WHERE project_id = ${pid}
|
|
89
111
|
AND event_type IN ('pointer_move', 'pointer_click')
|
|
90
|
-
AND length(hit_point) = 3${range}${scene}${source}${cameraMode}
|
|
112
|
+
AND length(hit_point) = 3${range}${scene}${source}${cameraMode}${region}
|
|
91
113
|
GROUP BY vx, vy, vz
|
|
92
114
|
ORDER BY count DESC
|
|
93
115
|
LIMIT ${limit}
|
|
@@ -95,6 +117,41 @@ export function buildWorldHeatmap(projectId, opts, d) {
|
|
|
95
117
|
query_params: bag.values,
|
|
96
118
|
};
|
|
97
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Scene-wide totals for the world (pointer) heatmap (ADR 0040 §3): the true count
|
|
122
|
+
* of occupied voxels and total hits, computed with no `LIMIT` so the viewer can
|
|
123
|
+
* report "showing top N of M cells" and reason about cold spots/coverage. Shares
|
|
124
|
+
* every filter (including {@link RegionOptions.region}) with {@link buildWorldHeatmap}.
|
|
125
|
+
* Uses a grouped sub-select so the dialect needs no `COUNT(DISTINCT tuple)`.
|
|
126
|
+
*/
|
|
127
|
+
export function buildWorldHeatmapStats(projectId, opts, d) {
|
|
128
|
+
const bag = new ParamBag(d);
|
|
129
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
130
|
+
const cellSize = bag.add("cellSize", "f64", opts.cellSize ?? 0.5);
|
|
131
|
+
const range = rangeClause(bag, opts);
|
|
132
|
+
const scene = sceneClause(bag, opts);
|
|
133
|
+
const source = sourceClause(bag, opts);
|
|
134
|
+
const cameraMode = cameraModeClause(bag, d, projectId, opts);
|
|
135
|
+
const region = regionClause(bag, opts, HIT_POINT_COLS);
|
|
136
|
+
return {
|
|
137
|
+
query: `
|
|
138
|
+
SELECT count() AS cells, coalesce(sum(c), 0) AS hits
|
|
139
|
+
FROM (
|
|
140
|
+
SELECT
|
|
141
|
+
floor(hit_point[1] / ${cellSize}) AS vx,
|
|
142
|
+
floor(hit_point[2] / ${cellSize}) AS vy,
|
|
143
|
+
floor(hit_point[3] / ${cellSize}) AS vz,
|
|
144
|
+
count() AS c
|
|
145
|
+
FROM events
|
|
146
|
+
WHERE project_id = ${pid}
|
|
147
|
+
AND event_type IN ('pointer_move', 'pointer_click')
|
|
148
|
+
AND length(hit_point) = 3${range}${scene}${source}${cameraMode}${region}
|
|
149
|
+
GROUP BY vx, vy, vz
|
|
150
|
+
) t
|
|
151
|
+
`,
|
|
152
|
+
query_params: bag.values,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
98
155
|
/**
|
|
99
156
|
* World-space (3D) gaze heatmap (ADR 0030): voxel-bin the camera-pose gaze
|
|
100
157
|
* surface hits (`camera_sample.hitPoint`) into the same uniform grid as the
|
|
@@ -113,6 +170,7 @@ export function buildGazeHeatmap(projectId, opts, d) {
|
|
|
113
170
|
const scene = sceneClause(bag, opts);
|
|
114
171
|
const session = sessionClause(bag, opts);
|
|
115
172
|
const cameraMode = cameraModeClause(bag, d, projectId, opts);
|
|
173
|
+
const region = regionClause(bag, opts, HIT_POINT_COLS);
|
|
116
174
|
const limit = bag.add("limit", "u32", opts.limit ?? 1000);
|
|
117
175
|
return {
|
|
118
176
|
query: `
|
|
@@ -124,7 +182,7 @@ export function buildGazeHeatmap(projectId, opts, d) {
|
|
|
124
182
|
FROM events
|
|
125
183
|
WHERE project_id = ${pid}
|
|
126
184
|
AND event_type = 'camera_sample'
|
|
127
|
-
AND length(hit_point) = 3${range}${scene}${session}${cameraMode}
|
|
185
|
+
AND length(hit_point) = 3${range}${scene}${session}${cameraMode}${region}
|
|
128
186
|
GROUP BY vx, vy, vz
|
|
129
187
|
ORDER BY count DESC
|
|
130
188
|
LIMIT ${limit}
|
|
@@ -132,6 +190,39 @@ export function buildGazeHeatmap(projectId, opts, d) {
|
|
|
132
190
|
query_params: bag.values,
|
|
133
191
|
};
|
|
134
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Scene-wide totals for the gaze heatmap (ADR 0040 §3): the true occupied-voxel
|
|
195
|
+
* and hit counts behind the truncated top-N gaze voxels, with no `LIMIT`. Shares
|
|
196
|
+
* every filter (including {@link RegionOptions.region}) with {@link buildGazeHeatmap}.
|
|
197
|
+
*/
|
|
198
|
+
export function buildGazeHeatmapStats(projectId, opts, d) {
|
|
199
|
+
const bag = new ParamBag(d);
|
|
200
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
201
|
+
const cellSize = bag.add("cellSize", "f64", opts.cellSize ?? 0.5);
|
|
202
|
+
const range = rangeClause(bag, opts);
|
|
203
|
+
const scene = sceneClause(bag, opts);
|
|
204
|
+
const session = sessionClause(bag, opts);
|
|
205
|
+
const cameraMode = cameraModeClause(bag, d, projectId, opts);
|
|
206
|
+
const region = regionClause(bag, opts, HIT_POINT_COLS);
|
|
207
|
+
return {
|
|
208
|
+
query: `
|
|
209
|
+
SELECT count() AS cells, coalesce(sum(c), 0) AS hits
|
|
210
|
+
FROM (
|
|
211
|
+
SELECT
|
|
212
|
+
floor(hit_point[1] / ${cellSize}) AS vx,
|
|
213
|
+
floor(hit_point[2] / ${cellSize}) AS vy,
|
|
214
|
+
floor(hit_point[3] / ${cellSize}) AS vz,
|
|
215
|
+
count() AS c
|
|
216
|
+
FROM events
|
|
217
|
+
WHERE project_id = ${pid}
|
|
218
|
+
AND event_type = 'camera_sample'
|
|
219
|
+
AND length(hit_point) = 3${range}${scene}${session}${cameraMode}${region}
|
|
220
|
+
GROUP BY vx, vy, vz
|
|
221
|
+
) t
|
|
222
|
+
`,
|
|
223
|
+
query_params: bag.values,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
135
226
|
/**
|
|
136
227
|
* View-direction heatmap: bin camera forward vectors by spherical angles.
|
|
137
228
|
* `azimuth = atan2(z, x)`, `elevation = asin(y / |v|)`, each bucketed into `bins`.
|
|
@@ -174,6 +265,7 @@ export function buildCameraPositionHeatmap(projectId, opts, d) {
|
|
|
174
265
|
const scene = sceneClause(bag, opts);
|
|
175
266
|
const session = sessionClause(bag, opts);
|
|
176
267
|
const cameraMode = cameraModeClause(bag, d, projectId, opts);
|
|
268
|
+
const region = regionClause(bag, opts, POSITION_COLS);
|
|
177
269
|
const limit = bag.add("limit", "u32", opts.limit ?? 2000);
|
|
178
270
|
return {
|
|
179
271
|
query: `
|
|
@@ -185,7 +277,7 @@ export function buildCameraPositionHeatmap(projectId, opts, d) {
|
|
|
185
277
|
FROM events
|
|
186
278
|
WHERE project_id = ${pid}
|
|
187
279
|
AND event_type = 'camera_sample'
|
|
188
|
-
AND length(position) = 3${range}${scene}${session}${cameraMode}
|
|
280
|
+
AND length(position) = 3${range}${scene}${session}${cameraMode}${region}
|
|
189
281
|
GROUP BY gx, gz
|
|
190
282
|
ORDER BY count DESC
|
|
191
283
|
LIMIT ${limit}
|
|
@@ -223,6 +315,43 @@ export function buildSessionTrajectory(projectId, opts, d) {
|
|
|
223
315
|
query_params: bag.values,
|
|
224
316
|
};
|
|
225
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Aggregate desire lines (#73): the *crowd* analog of a single session's
|
|
320
|
+
* trajectory. Every session's `camera_sample` path is binned onto the X/Z ground
|
|
321
|
+
* grid (`cellSize` world units) and returned as ordered points keyed by
|
|
322
|
+
* `session_id`, oldest first. Overlaying many low-opacity poly-lines lets the
|
|
323
|
+
* common routes self-reinforce into "desire lines" — the paths visitors actually
|
|
324
|
+
* walk, vs. the ones the level designer intended (ADR 0037).
|
|
325
|
+
*
|
|
326
|
+
* Binning in SQL caps cardinality and removes sub-cell jitter; the consumer
|
|
327
|
+
* dedupes consecutive identical cells and draws one poly-line per session. The
|
|
328
|
+
* row cap (`limit`) is a volume guard for busy projects.
|
|
329
|
+
*/
|
|
330
|
+
export function buildAggregateTrajectories(projectId, opts, d) {
|
|
331
|
+
const bag = new ParamBag(d);
|
|
332
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
333
|
+
const range = rangeClause(bag, opts);
|
|
334
|
+
const scene = sceneClause(bag, opts);
|
|
335
|
+
const cameraMode = cameraModeClause(bag, d, projectId, opts);
|
|
336
|
+
const cellSize = bag.add("cellSize", "f64", opts.cellSize ?? 1);
|
|
337
|
+
const limit = bag.add("limit", "u32", opts.limit ?? 20000);
|
|
338
|
+
return {
|
|
339
|
+
query: `
|
|
340
|
+
SELECT
|
|
341
|
+
session_id,
|
|
342
|
+
${d.epochMs("ts")} AS ts,
|
|
343
|
+
floor(position[1] / ${cellSize}) AS gx,
|
|
344
|
+
floor(position[3] / ${cellSize}) AS gz
|
|
345
|
+
FROM events
|
|
346
|
+
WHERE project_id = ${pid}
|
|
347
|
+
AND event_type = 'camera_sample'
|
|
348
|
+
AND length(position) = 3${range}${scene}${cameraMode}
|
|
349
|
+
ORDER BY session_id ASC, ts ASC
|
|
350
|
+
LIMIT ${limit}
|
|
351
|
+
`,
|
|
352
|
+
query_params: bag.values,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
226
355
|
/**
|
|
227
356
|
* Click ↔ gaze correlation: aggregate each `pointer_click` into a ray from an
|
|
228
357
|
* origin voxel to a hit voxel, where the voxel size matches the world heatmap so
|
|
@@ -431,6 +560,72 @@ export function buildTopMeshes(projectId, opts, d) {
|
|
|
431
560
|
query_params: bag.values,
|
|
432
561
|
};
|
|
433
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Per-mesh source split (#74): the most-interacted-mesh tally broken out by the
|
|
565
|
+
* input `source` that drove each interaction (mouse / touch / xr-controller /
|
|
566
|
+
* hand / …). Scoped to **active** interactions — `mesh_interaction` (hover / pick
|
|
567
|
+
* / click / drag) and `pointer_click` — so passive `camera_sample` gaze hits do
|
|
568
|
+
* NOT inflate popularity (this is the deliberate difference from
|
|
569
|
+
* {@link buildTopMeshes}, which counts every mesh-referencing event). Summing a
|
|
570
|
+
* mesh's rows gives its leaderboard total; ranked by count.
|
|
571
|
+
*/
|
|
572
|
+
export function buildTopMeshesBySource(projectId, opts, d) {
|
|
573
|
+
const bag = new ParamBag(d);
|
|
574
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
575
|
+
const range = rangeClause(bag, opts);
|
|
576
|
+
const scene = sceneClause(bag, opts);
|
|
577
|
+
const source = sourceClause(bag, opts);
|
|
578
|
+
const session = sessionClause(bag, opts);
|
|
579
|
+
const limit = bag.add("limit", "u32", opts.limit ?? 200);
|
|
580
|
+
return {
|
|
581
|
+
query: `
|
|
582
|
+
SELECT mesh, source, count() AS count
|
|
583
|
+
FROM events
|
|
584
|
+
WHERE project_id = ${pid}
|
|
585
|
+
AND event_type IN ('mesh_interaction', 'pointer_click')
|
|
586
|
+
AND mesh != ''${range}${scene}${source}${session}
|
|
587
|
+
GROUP BY mesh, source
|
|
588
|
+
ORDER BY count DESC
|
|
589
|
+
LIMIT ${limit}
|
|
590
|
+
`,
|
|
591
|
+
query_params: bag.values,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Per-mesh interaction trend (#74): the active-interaction tally bucketed into
|
|
596
|
+
* fixed `interval`-second time windows, so the leaderboard can draw a per-mesh
|
|
597
|
+
* sparkline and a rising/falling delta over the active range. Scoped to the same
|
|
598
|
+
* `mesh_interaction` + `pointer_click` events as {@link buildTopMeshesBySource}
|
|
599
|
+
* (passive gaze excluded). Each row is a `(mesh, bucket)` count; the consumer
|
|
600
|
+
* orders buckets per mesh and compares the recent half against the earlier half.
|
|
601
|
+
* Ordered oldest bucket first for drawing.
|
|
602
|
+
*/
|
|
603
|
+
export function buildTopMeshesTrend(projectId, opts, d) {
|
|
604
|
+
const bag = new ParamBag(d);
|
|
605
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
606
|
+
const interval = bag.add("interval", "u32", opts.interval ?? 3600);
|
|
607
|
+
const range = rangeClause(bag, opts);
|
|
608
|
+
const scene = sceneClause(bag, opts);
|
|
609
|
+
const source = sourceClause(bag, opts);
|
|
610
|
+
const session = sessionClause(bag, opts);
|
|
611
|
+
const limit = bag.add("limit", "u32", opts.limit ?? 2000);
|
|
612
|
+
return {
|
|
613
|
+
query: `
|
|
614
|
+
SELECT
|
|
615
|
+
mesh,
|
|
616
|
+
${d.timeBucketMs("ts", interval)} AS bucket,
|
|
617
|
+
count() AS count
|
|
618
|
+
FROM events
|
|
619
|
+
WHERE project_id = ${pid}
|
|
620
|
+
AND event_type IN ('mesh_interaction', 'pointer_click')
|
|
621
|
+
AND mesh != ''${range}${scene}${source}${session}
|
|
622
|
+
GROUP BY mesh, bucket
|
|
623
|
+
ORDER BY bucket ASC
|
|
624
|
+
LIMIT ${limit}
|
|
625
|
+
`,
|
|
626
|
+
query_params: bag.values,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
434
629
|
/**
|
|
435
630
|
* Per-object attention / dwell from `mesh_visibility` summaries (#37). Sums the
|
|
436
631
|
* bucketed on-screen and gaze-centred time per mesh and tracks the peak screen
|
|
@@ -460,6 +655,38 @@ export function buildMeshDwell(projectId, opts, d) {
|
|
|
460
655
|
query_params: bag.values,
|
|
461
656
|
};
|
|
462
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Interaction-kind breakdown (#72): per-mesh counts of each interaction *kind*
|
|
660
|
+
* (hover / pick / click / drag / select / squeeze / grab / release / teleport)
|
|
661
|
+
* from `mesh_interaction` events (ADR 0023). The dwell ranking says *which*
|
|
662
|
+
* objects draw attention; this says *how* people act on them — separating a mesh
|
|
663
|
+
* that's merely hovered from one that's actually picked or dragged. The kind is
|
|
664
|
+
* carried in the engine-neutral `name` column (events.ts maps
|
|
665
|
+
* `mesh_interaction.kind` → `name`). Ranked by count, capped to `limit`.
|
|
666
|
+
*/
|
|
667
|
+
export function buildMeshInteractionKinds(projectId, opts, d) {
|
|
668
|
+
const bag = new ParamBag(d);
|
|
669
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
670
|
+
const range = rangeClause(bag, opts);
|
|
671
|
+
const scene = sceneClause(bag, opts);
|
|
672
|
+
const source = sourceClause(bag, opts);
|
|
673
|
+
const session = sessionClause(bag, opts);
|
|
674
|
+
const limit = bag.add("limit", "u32", opts.limit ?? 100);
|
|
675
|
+
return {
|
|
676
|
+
query: `
|
|
677
|
+
SELECT
|
|
678
|
+
mesh,
|
|
679
|
+
name AS kind,
|
|
680
|
+
count() AS count
|
|
681
|
+
FROM events
|
|
682
|
+
WHERE project_id = ${pid} AND event_type = 'mesh_interaction' AND mesh != ''${range}${scene}${source}${session}
|
|
683
|
+
GROUP BY mesh, name
|
|
684
|
+
ORDER BY count DESC
|
|
685
|
+
LIMIT ${limit}
|
|
686
|
+
`,
|
|
687
|
+
query_params: bag.values,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
463
690
|
/**
|
|
464
691
|
* Dead-click rate (#46): of all `pointer_click` events, how many hit nothing
|
|
465
692
|
* (the hit-test missed, so `mesh` is empty / no `hitMesh`). A high dead-click
|
|
@@ -768,6 +995,40 @@ export function buildPerfSummary(projectId, opts, d) {
|
|
|
768
995
|
query_params: bag.values,
|
|
769
996
|
};
|
|
770
997
|
}
|
|
998
|
+
/**
|
|
999
|
+
* Render-scale truth (#71): pairs the FPS headline with the **resolution** the
|
|
1000
|
+
* engine actually rendered at. A scene can report a healthy frame rate only
|
|
1001
|
+
* because an adaptive renderer quietly dropped `render_scale` below 1 — so a
|
|
1002
|
+
* "good FPS" number is only honest alongside the render scale that bought it.
|
|
1003
|
+
*
|
|
1004
|
+
* From `frame_perf` samples (ADR 0021): average + median FPS, average + median
|
|
1005
|
+
* `render_scale` (the 0 sentinel for "not reported" is excluded via NULLIF), and
|
|
1006
|
+
* the counts needed to derive the *downscaled share* — the fraction of reported
|
|
1007
|
+
* samples that rendered below native resolution. The share is derived consumer-
|
|
1008
|
+
* side from `downscaled_samples / scale_samples` to keep it integer-exact across
|
|
1009
|
+
* engines.
|
|
1010
|
+
*/
|
|
1011
|
+
export function buildRenderScaleTruth(projectId, opts, d) {
|
|
1012
|
+
const bag = new ParamBag(d);
|
|
1013
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
1014
|
+
const range = rangeClause(bag, opts);
|
|
1015
|
+
const session = sessionClause(bag, opts);
|
|
1016
|
+
return {
|
|
1017
|
+
query: `
|
|
1018
|
+
SELECT
|
|
1019
|
+
count() AS samples,
|
|
1020
|
+
avg(fps) AS avg_fps,
|
|
1021
|
+
${d.quantile("fps", 0.5)} AS p50_fps,
|
|
1022
|
+
avg(NULLIF(render_scale, 0)) AS avg_render_scale,
|
|
1023
|
+
${d.quantile("nullIf(render_scale, 0)", 0.5)} AS p50_render_scale,
|
|
1024
|
+
sum(CASE WHEN render_scale > 0 AND render_scale < 1 THEN 1 ELSE 0 END) AS downscaled_samples,
|
|
1025
|
+
sum(CASE WHEN render_scale > 0 THEN 1 ELSE 0 END) AS scale_samples
|
|
1026
|
+
FROM events
|
|
1027
|
+
WHERE project_id = ${pid} AND event_type = 'frame_perf'${range}${session}
|
|
1028
|
+
`,
|
|
1029
|
+
query_params: bag.values,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
771
1032
|
/**
|
|
772
1033
|
* FPS distribution, computed **per-session then aggregated** (ADR 0028 §1). Each
|
|
773
1034
|
* session contributes its own p05/p50/p95 FPS, so neither long sessions nor
|
|
@@ -1287,6 +1548,38 @@ export function buildInteractionsBySource(projectId, opts, d) {
|
|
|
1287
1548
|
query_params: bag.values,
|
|
1288
1549
|
};
|
|
1289
1550
|
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Most-used shortcuts / actions (#75, ADR 0023): rank the discrete `input_action`
|
|
1553
|
+
* events (keyboard chords, gamepad buttons) by their app-level `action` label,
|
|
1554
|
+
* split by `source` (keyboard / gamepad / …). The action label is carried in the
|
|
1555
|
+
* engine-neutral `name` column (events.ts maps `input_action.action` → `name`),
|
|
1556
|
+
* so a connector's semantic binding — `"rotate-left"`, `"next-camera"` — surfaces
|
|
1557
|
+
* as a leaderboard. Pairs with {@link buildInteractionsBySource} (the modality
|
|
1558
|
+
* share) to answer "which keys/buttons do people actually press". Ranked by count.
|
|
1559
|
+
*/
|
|
1560
|
+
export function buildTopInputActions(projectId, opts, d) {
|
|
1561
|
+
const bag = new ParamBag(d);
|
|
1562
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
1563
|
+
const range = rangeClause(bag, opts);
|
|
1564
|
+
const scene = sceneClause(bag, opts);
|
|
1565
|
+
const source = sourceClause(bag, opts);
|
|
1566
|
+
const session = sessionClause(bag, opts);
|
|
1567
|
+
const limit = bag.add("limit", "u32", opts.limit ?? 100);
|
|
1568
|
+
return {
|
|
1569
|
+
query: `
|
|
1570
|
+
SELECT
|
|
1571
|
+
name AS action,
|
|
1572
|
+
source,
|
|
1573
|
+
count() AS count
|
|
1574
|
+
FROM events
|
|
1575
|
+
WHERE project_id = ${pid} AND event_type = 'input_action' AND name != ''${range}${scene}${source}${session}
|
|
1576
|
+
GROUP BY name, source
|
|
1577
|
+
ORDER BY count DESC
|
|
1578
|
+
LIMIT ${limit}
|
|
1579
|
+
`,
|
|
1580
|
+
query_params: bag.values,
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1290
1583
|
/**
|
|
1291
1584
|
* XR motion-sickness proxy (derived, scene-metrics §F): per session, the
|
|
1292
1585
|
* head/view rotation *rate* over the `camera_sample` pose stream. Each sample is
|
|
@@ -1409,4 +1702,88 @@ export function buildXrAbandonment(projectId, opts, d) {
|
|
|
1409
1702
|
query_params: bag.values,
|
|
1410
1703
|
};
|
|
1411
1704
|
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Render one funnel step's predicate against the wide `events` columns (ADR
|
|
1707
|
+
* 0038). Every field compiles to plain equality on a promoted column, so the
|
|
1708
|
+
* predicate is engine-agnostic and parameter-bound (injection-safe): `type` →
|
|
1709
|
+
* `event_type`; `name` → the `name` column (which already carries the
|
|
1710
|
+
* `camera_gesture` / `mesh_interaction` kind and the `custom` event name); and
|
|
1711
|
+
* `mesh` → the `mesh` column. Columns are unqualified — they resolve to `events`
|
|
1712
|
+
* even inside the joined CTEs because the joined funnel CTEs expose only
|
|
1713
|
+
* `session_id` and `t`.
|
|
1714
|
+
*/
|
|
1715
|
+
function funnelStepPredicate(bag, step, i) {
|
|
1716
|
+
const parts = [`event_type = ${bag.add(`fType${i}`, "string", step.type)}`];
|
|
1717
|
+
if (step.name != null && step.name.length > 0) {
|
|
1718
|
+
parts.push(`name = ${bag.add(`fName${i}`, "string", step.name)}`);
|
|
1719
|
+
}
|
|
1720
|
+
if (step.mesh != null && step.mesh.length > 0) {
|
|
1721
|
+
parts.push(`mesh = ${bag.add(`fMesh${i}`, "string", step.mesh)}`);
|
|
1722
|
+
}
|
|
1723
|
+
return parts.join(" AND ");
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Single-project configurator funnel (#78, ADR 0038): ordered, per-session
|
|
1727
|
+
* step-reach with the drop-off between consecutive steps.
|
|
1728
|
+
*
|
|
1729
|
+
* Semantics — a session **reaches step N** iff there is an event matching step
|
|
1730
|
+
* N's predicate at a timestamp **≥ the first time it reached step N−1**, within
|
|
1731
|
+
* the same `session_id` (step 0 is reached on its first matching event). This is
|
|
1732
|
+
* an ordered, first-touch, monotonic funnel: steps must occur in order, only the
|
|
1733
|
+
* first qualifying occurrence per step counts, and a row's `sessions` is the
|
|
1734
|
+
* number of sessions reaching that step.
|
|
1735
|
+
*
|
|
1736
|
+
* Implementation — a CTE chain, one level per step. Level 0 takes each session's
|
|
1737
|
+
* first matching timestamp; level K joins the prior level on `session_id` and
|
|
1738
|
+
* takes the first matching timestamp `≥` the prior level's. This uses only
|
|
1739
|
+
* `JOIN` / `min` / `GROUP BY` — **no window or ASOF functions** — so it renders
|
|
1740
|
+
* identically on DuckDB (OSS) and ClickHouse (scale tier) and is covered by a
|
|
1741
|
+
* hand-verified parity golden (ADR 0020). The final `UNION ALL` counts the
|
|
1742
|
+
* sessions surviving each level; the consumer derives the conversion rates.
|
|
1743
|
+
*
|
|
1744
|
+
* The `steps` come from the caller (request input / CLI / hosted), not a stored
|
|
1745
|
+
* config — OSS has no authoring surface (ADR 0038).
|
|
1746
|
+
*/
|
|
1747
|
+
export function buildFunnel(projectId, opts, d) {
|
|
1748
|
+
const bag = new ParamBag(d);
|
|
1749
|
+
const pid = bag.add("projectId", "string", projectId);
|
|
1750
|
+
const range = rangeClause(bag, opts);
|
|
1751
|
+
const scene = sceneClause(bag, opts);
|
|
1752
|
+
const cameraMode = cameraModeClause(bag, d, projectId, opts);
|
|
1753
|
+
const steps = opts.steps;
|
|
1754
|
+
const ctes = steps.map((step, i) => {
|
|
1755
|
+
const pred = funnelStepPredicate(bag, step, i);
|
|
1756
|
+
if (i === 0) {
|
|
1757
|
+
// Level 0: each session's first event matching step 0. Session-level
|
|
1758
|
+
// filters (range / scene / camera-mode) apply here and the subset
|
|
1759
|
+
// propagates through the joins below.
|
|
1760
|
+
return `s0 AS (
|
|
1761
|
+
SELECT session_id, min(ts) AS t
|
|
1762
|
+
FROM events
|
|
1763
|
+
WHERE project_id = ${pid} AND ${pred}${range}${scene}${cameraMode}
|
|
1764
|
+
GROUP BY session_id
|
|
1765
|
+
)`;
|
|
1766
|
+
}
|
|
1767
|
+
// Level i: the first matching event at or after the prior step's reach time.
|
|
1768
|
+
// `events.session_id` is qualified (ambiguous with the joined CTE); the
|
|
1769
|
+
// unqualified predicate / range / scene columns resolve to `events`.
|
|
1770
|
+
return `s${i} AS (
|
|
1771
|
+
SELECT events.session_id AS session_id, min(events.ts) AS t
|
|
1772
|
+
FROM events JOIN s${i - 1} ON events.session_id = s${i - 1}.session_id
|
|
1773
|
+
WHERE project_id = ${pid} AND ${pred} AND ts >= s${i - 1}.t${range}${scene}
|
|
1774
|
+
GROUP BY events.session_id
|
|
1775
|
+
)`;
|
|
1776
|
+
});
|
|
1777
|
+
const counts = steps
|
|
1778
|
+
.map((_step, i) => `SELECT ${i} AS step, count() AS sessions FROM s${i}`)
|
|
1779
|
+
.join("\n UNION ALL ");
|
|
1780
|
+
return {
|
|
1781
|
+
query: `
|
|
1782
|
+
WITH ${ctes.join(",\n ")}
|
|
1783
|
+
${counts}
|
|
1784
|
+
ORDER BY step ASC
|
|
1785
|
+
`,
|
|
1786
|
+
query_params: bag.values,
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1412
1789
|
//# sourceMappingURL=aggregations.js.map
|