@jeffjassky/telemetry 0.2.0 → 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/index.js CHANGED
@@ -59,8 +59,24 @@ var newCounters = () => ({
59
59
  capped: 0,
60
60
  rollupSkipped: 0,
61
61
  deduped: 0,
62
- truncated: 0
62
+ truncated: 0,
63
+ rollupSkippedBy: {},
64
+ undeclaredAttrs: {}
63
65
  });
66
+ var COUNTER_MAP_MAX = 1e3;
67
+ var COUNTER_OVERFLOW_KEY = "(other)|(other)";
68
+ var bumpCounterMap = (map, key) => {
69
+ const seen = map[key];
70
+ if (seen !== void 0) {
71
+ map[key] = seen + 1;
72
+ return;
73
+ }
74
+ if (map[COUNTER_OVERFLOW_KEY] !== void 0 || Object.keys(map).length >= COUNTER_MAP_MAX) {
75
+ map[COUNTER_OVERFLOW_KEY] = (map[COUNTER_OVERFLOW_KEY] ?? 0) + 1;
76
+ return;
77
+ }
78
+ map[key] = 1;
79
+ };
64
80
  var traceKeep = (traceId, rate) => {
65
81
  if (rate >= 1) return true;
66
82
  if (!traceId) return Math.random() < rate;
@@ -388,15 +404,15 @@ function buildBaseSchema(collection, registry, counters, opts) {
388
404
  if (this.kind === TelemetryKind.State && !this.state?.to) {
389
405
  throw new Error("telemetry: state requires state.to");
390
406
  }
391
- const check = (label2, m, zschema) => {
407
+ const check = (label3, m, zschema) => {
392
408
  const obj = Object.fromEntries(m ?? []);
393
409
  if (!zschema) {
394
- if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${label2}`);
410
+ if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${label3}`);
395
411
  return;
396
412
  }
397
413
  const s = zschema.strict?.() ?? zschema;
398
414
  const r = s.safeParse(obj);
399
- if (!r.success) throw new Error(`telemetry: ${label2} invalid for "${this.name}": ${r.error.message}`);
415
+ if (!r.success) throw new Error(`telemetry: ${label3} invalid for "${this.name}": ${r.error.message}`);
400
416
  };
401
417
  check("attrs", this.attrs, spec.attrs);
402
418
  check("metrics", this.metrics, spec.metrics);
@@ -545,6 +561,7 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
545
561
  if (v == null || v === "") {
546
562
  if (spec.dimDefault === void 0) {
547
563
  counters.rollupSkipped++;
564
+ bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
548
565
  return;
549
566
  }
550
567
  v = spec.dimDefault;
@@ -636,6 +653,16 @@ function createCheckpointFactory(CheckpointModel, logger) {
636
653
  }
637
654
 
638
655
  // src/server/emit.ts
656
+ function noteUndeclaredAttrs(counters, name, spec, attrs) {
657
+ if (!attrs || typeof attrs !== "object") return;
658
+ const keys = attrs instanceof Map ? [...attrs.keys()] : Object.keys(attrs);
659
+ const shape = spec.attrs?.shape;
660
+ for (const raw of keys) {
661
+ const key = String(raw).replace(/\./g, "_");
662
+ if (shape && Object.prototype.hasOwnProperty.call(shape, key)) continue;
663
+ bumpCounterMap(counters.undeclaredAttrs, `${name}|${key}`);
664
+ }
665
+ }
639
666
  function createEmitter(ctx) {
640
667
  const { registry, byKind, RollupModel, rejects, counters } = ctx;
641
668
  const burstBuckets = /* @__PURE__ */ new Map();
@@ -682,6 +709,7 @@ function createEmitter(ctx) {
682
709
  attrs: safe(doc.attrs),
683
710
  metrics: safe(doc.metrics)
684
711
  };
712
+ noteUndeclaredAttrs(counters, name, spec, doc.attrs);
685
713
  const onFail = async (e) => {
686
714
  counters.rejected++;
687
715
  await rejects().insertOne({ at: /* @__PURE__ */ new Date(), name, reason: String(e), raw: plain(doc) }).catch(() => {
@@ -887,10 +915,10 @@ function parseKeyString(raw) {
887
915
  if (!raw) return null;
888
916
  const m = KEY_RE.exec(raw.trim());
889
917
  if (!m) return null;
890
- const [, prefix, label2, id, secret] = m;
918
+ const [, prefix, label3, id, secret] = m;
891
919
  if (prefix === "sk" && !secret) return null;
892
920
  if (prefix === "pk" && secret) return null;
893
- return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label: label2, id, secret };
921
+ return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label: label3, id, secret };
894
922
  }
895
923
  var SCRYPT_VERSION = "scrypt1";
896
924
  var SCRYPT = { N: 16384, r: 8, p: 1, keylen: 32 };
@@ -948,7 +976,7 @@ async function createKey(KeyModel, input) {
948
976
  tenantId,
949
977
  service,
950
978
  env,
951
- label: label2 = "live",
979
+ label: label3 = "live",
952
980
  origins = [],
953
981
  allowedNames,
954
982
  maxPerMinute = 600
@@ -981,7 +1009,7 @@ async function createKey(KeyModel, input) {
981
1009
  createdAt: /* @__PURE__ */ new Date()
982
1010
  });
983
1011
  const prefix = kind === KeyKind.Publishable ? "pk" : "sk";
984
- return { key: secret ? `${prefix}_${label2}_${id}_${secret}` : `${prefix}_${label2}_${id}`, id };
1012
+ return { key: secret ? `${prefix}_${label3}_${id}_${secret}` : `${prefix}_${label3}_${id}`, id };
985
1013
  }
986
1014
  var BATCH_MAX = 100;
987
1015
  function createIngest(opts) {
@@ -1183,6 +1211,7 @@ function createIngest(opts) {
1183
1211
  const occurredRaw = rec.occurredAt ? Date.parse(rec.occurredAt) : NaN;
1184
1212
  const occurredAt = Number.isFinite(occurredRaw) ? new Date(occurredRaw - clockSkewMs) : receivedAt;
1185
1213
  const safeMap = (o) => o && typeof o === "object" ? new Map(Object.entries(o).map(([k, v]) => [k.replace(/\./g, "_"), v])) : /* @__PURE__ */ new Map();
1214
+ noteUndeclaredAttrs(t.counters, name, spec, rec.attrs);
1186
1215
  const Model = t.models.byKind[spec.kind];
1187
1216
  const d = new Model({
1188
1217
  // facts the wire may not assert: tenant, service, env, origin, plane
@@ -1253,6 +1282,292 @@ function createIngest(opts) {
1253
1282
  return router;
1254
1283
  }
1255
1284
 
1285
+ // src/server/catalog.ts
1286
+ var label2 = (src) => src.slice(src.indexOf(":") + 1);
1287
+ var LEAF_TYPES = {
1288
+ string: "string",
1289
+ number: "number",
1290
+ int: "number",
1291
+ bigint: "number",
1292
+ boolean: "boolean",
1293
+ date: "date"
1294
+ };
1295
+ function walkAttr(schema) {
1296
+ let node = schema;
1297
+ let optional = false;
1298
+ for (let depth = 0; node && depth < 20; depth++) {
1299
+ const def = node._zod?.def ?? node.def;
1300
+ if (!def?.type) break;
1301
+ switch (def.type) {
1302
+ // these three all mean "the value may be absent from a stored record",
1303
+ // which is the only thing `optional` claims
1304
+ case "optional":
1305
+ case "nullable":
1306
+ case "default":
1307
+ optional = true;
1308
+ node = def.innerType;
1309
+ continue;
1310
+ case "catch":
1311
+ case "readonly":
1312
+ node = def.innerType;
1313
+ continue;
1314
+ // a pipe is `in -> out`; the INPUT side is what a caller may send and so
1315
+ // what a stored value was validated as. The output of a transform is
1316
+ // frequently a shape no filter could ever be written against.
1317
+ case "pipe":
1318
+ node = def.in;
1319
+ continue;
1320
+ case "enum": {
1321
+ const options = Array.isArray(node.options) ? node.options : Object.values(def.entries ?? {});
1322
+ return { type: "enum", values: options.map(String), optional };
1323
+ }
1324
+ case "literal":
1325
+ return { type: "enum", values: [...def.values ?? []].map(String), optional };
1326
+ default:
1327
+ return { type: LEAF_TYPES[def.type] ?? "string", optional };
1328
+ }
1329
+ }
1330
+ return { type: "string", optional };
1331
+ }
1332
+ var dim = (key, type, o = {}) => ({
1333
+ key,
1334
+ // the two pseudo-dims are derived at query time from subjectKeys / actor, so
1335
+ // they carry no `field:` prefix and label to themselves
1336
+ label: key.startsWith("field:") ? key.slice(6) : key,
1337
+ type,
1338
+ ...o.values ? { values: [...o.values] } : {},
1339
+ optional: o.optional ?? false,
1340
+ indexed: o.indexed ?? false
1341
+ });
1342
+ var envelopeDims = (platforms) => [
1343
+ dim("field:kind", "enum", { values: TELEMETRY_KINDS, indexed: true }),
1344
+ dim("field:name", "string", { indexed: true }),
1345
+ dim("field:severity", "enum", { values: Object.values(LogLevel) }),
1346
+ dim("field:env", "enum", { values: Object.values(Env) }),
1347
+ dim("field:service", "string"),
1348
+ dim("field:release", "string"),
1349
+ dim("field:origin", "enum", { values: Object.values(Origin) }),
1350
+ // client context is absent on server-origin records, so both of its dims are optional
1351
+ dim("field:client.platform", "enum", { values: platforms, optional: true }),
1352
+ dim("field:client.appVersion", "string", { optional: true }),
1353
+ dim("subjectType", "string", { optional: true, indexed: true }),
1354
+ dim("actorType", "string", { optional: true })
1355
+ ];
1356
+ var kindDims = (kind) => {
1357
+ switch (kind) {
1358
+ case TelemetryKind.Usage:
1359
+ return [
1360
+ dim("field:usage.meter", "string", { indexed: true }),
1361
+ dim("field:usage.billedTo", "string"),
1362
+ dim("field:usage.unit", "string")
1363
+ ];
1364
+ case TelemetryKind.State:
1365
+ return [
1366
+ dim("field:state.key", "string", { indexed: true }),
1367
+ dim("field:state.to", "string", { indexed: true })
1368
+ ];
1369
+ case TelemetryKind.Error:
1370
+ return [dim("field:error.type", "string"), dim("field:error.handled", "boolean")];
1371
+ default:
1372
+ return [];
1373
+ }
1374
+ };
1375
+ var RAW_OPS = ["avg", "p50", "p95", "p99"];
1376
+ function deriveCatalog(registry, opts = {}) {
1377
+ const platforms = [.../* @__PURE__ */ new Set([...BUILTIN_PLATFORMS, ...opts.platforms ?? []])];
1378
+ const families = {};
1379
+ for (const [name, spec] of Object.entries(registry)) {
1380
+ for (const r of spec.rollups ?? []) {
1381
+ const as = r.as ?? name;
1382
+ const seen = families[as];
1383
+ if (!seen) {
1384
+ families[as] = {
1385
+ as,
1386
+ by: [...r.by],
1387
+ labels: r.by.map(label2),
1388
+ bucket: r.bucket ?? null,
1389
+ lifetime: !r.bucket,
1390
+ // `subjects` only means anything when there is a subject dim to
1391
+ // restrict; without one it selects nothing and claiming it would
1392
+ // offer a subject filter the family cannot answer
1393
+ subjectTypes: r.by.includes("subject") ? [...r.subjects ?? []] : [],
1394
+ sums: [...r.sum ?? []],
1395
+ capture: (r.capture ?? []).map(label2),
1396
+ feeders: [name],
1397
+ retentionDays: r.retentionDays ?? null
1398
+ };
1399
+ continue;
1400
+ }
1401
+ for (const k of r.sum ?? []) if (!seen.sums.includes(k)) seen.sums.push(k);
1402
+ for (const c of r.capture ?? []) {
1403
+ const l = label2(c);
1404
+ if (!seen.capture.includes(l)) seen.capture.push(l);
1405
+ }
1406
+ if (!seen.feeders.includes(name)) seen.feeders.push(name);
1407
+ }
1408
+ }
1409
+ const events = {};
1410
+ const namespaces = {};
1411
+ const subjectTypes = [];
1412
+ const noteSubject = (t) => {
1413
+ if (!subjectTypes.includes(t)) subjectTypes.push(t);
1414
+ };
1415
+ for (const [name, spec] of Object.entries(registry)) {
1416
+ const dot = name.indexOf(".");
1417
+ const namespace = dot === -1 ? name : name.slice(0, dot);
1418
+ (namespaces[namespace] ??= []).push(name);
1419
+ for (const s of spec.subjects) noteSubject(s);
1420
+ const indexedAttrs = [...spec.indexedAttrs ?? []];
1421
+ const dims = Object.entries(spec.attrs?.shape ?? {}).map(
1422
+ ([key, schema]) => {
1423
+ const walked = walkAttr(schema);
1424
+ return {
1425
+ key: `attr:${key}`,
1426
+ label: key,
1427
+ type: walked.type,
1428
+ ...walked.values ? { values: walked.values } : {},
1429
+ optional: walked.optional,
1430
+ indexed: indexedAttrs.includes(key)
1431
+ };
1432
+ }
1433
+ );
1434
+ dims.push(...kindDims(spec.kind));
1435
+ const eventFamilies = [];
1436
+ const ownSums = /* @__PURE__ */ new Map();
1437
+ for (const r of spec.rollups ?? []) {
1438
+ const as = r.as ?? name;
1439
+ if (!eventFamilies.includes(as)) eventFamilies.push(as);
1440
+ const set = ownSums.get(as) ?? /* @__PURE__ */ new Set();
1441
+ for (const k of r.sum ?? []) set.add(k);
1442
+ ownSums.set(as, set);
1443
+ for (const s of r.subjects ?? []) noteSubject(s);
1444
+ }
1445
+ const measures = [{ key: "count", exactVia: [] }];
1446
+ for (const k of Object.keys(spec.metrics?.shape ?? {})) {
1447
+ measures.push({
1448
+ key: `sum:${k}`,
1449
+ metric: k,
1450
+ exactVia: eventFamilies.filter((as) => ownSums.get(as)?.has(k))
1451
+ });
1452
+ for (const op of RAW_OPS) measures.push({ key: `${op}:${k}`, metric: k, exactVia: [] });
1453
+ }
1454
+ if (spec.kind === TelemetryKind.Span) {
1455
+ for (const op of RAW_OPS) {
1456
+ measures.push({ key: `${op}:durationMs`, metric: "durationMs", exactVia: [] });
1457
+ }
1458
+ }
1459
+ events[name] = {
1460
+ kind: spec.kind,
1461
+ origin: spec.origin,
1462
+ subjects: [...spec.subjects],
1463
+ description: spec.description,
1464
+ namespace,
1465
+ dims,
1466
+ measures,
1467
+ families: eventFamilies,
1468
+ indexedAttrs,
1469
+ indexedMetrics: [...spec.indexedMetrics ?? []],
1470
+ // `hasOwnProperty` rather than `??`, exactly as model.ts stamps expiresAt:
1471
+ // an explicit `retentionDays: null` means immortal and must not fall
1472
+ // through to the per-kind default
1473
+ retentionDays: Object.prototype.hasOwnProperty.call(spec, "retentionDays") ? spec.retentionDays ?? null : RETENTION_DAYS[spec.kind]
1474
+ };
1475
+ }
1476
+ return { events, families, namespaces, envelope: envelopeDims(platforms), subjectTypes };
1477
+ }
1478
+ function projectRegistry(catalog) {
1479
+ return Object.fromEntries(
1480
+ Object.entries(catalog.events).map(([name, e]) => [
1481
+ name,
1482
+ {
1483
+ kind: e.kind,
1484
+ origin: e.origin,
1485
+ subjects: e.subjects,
1486
+ description: e.description,
1487
+ attrKeys: e.dims.filter((d) => d.key.startsWith("attr:")).map((d) => d.label),
1488
+ // every metric key gets exactly one `sum:` measure and nothing else does
1489
+ metricKeys: e.measures.filter((m) => m.key.startsWith("sum:")).map((m) => m.metric),
1490
+ indexedAttrs: e.indexedAttrs,
1491
+ indexedMetrics: e.indexedMetrics,
1492
+ rollups: e.families.map((as) => {
1493
+ const f = catalog.families[as];
1494
+ return { as: f.as, by: f.by, bucket: f.bucket, sum: f.sums, subjects: f.subjectTypes };
1495
+ })
1496
+ }
1497
+ ])
1498
+ );
1499
+ }
1500
+
1501
+ // src/server/suggest.ts
1502
+ var UNREGISTERED_REASON = "unregistered event";
1503
+ var MAX_SUGGESTIONS = 50;
1504
+ var NAME_MAX = 120;
1505
+ var quote = (s) => /^[A-Za-z0-9_.:$-]+$/.test(s) ? `'${s}'` : JSON.stringify(s);
1506
+ var prop = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) ? s : quote(s);
1507
+ var times = (n) => `${n} time${n === 1 ? "" : "s"}`;
1508
+ var split = (k) => {
1509
+ const i = k.indexOf("|");
1510
+ return i === -1 ? [k, ""] : [k.slice(0, i), k.slice(i + 1)];
1511
+ };
1512
+ function deriveSuggestions(input) {
1513
+ const { counters, catalog, quarantine = [] } = input;
1514
+ const out = [];
1515
+ for (const [k, count2] of Object.entries(counters.undeclaredAttrs ?? {})) {
1516
+ if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
1517
+ const [name, key] = split(k);
1518
+ if (!name || !key) continue;
1519
+ const facet = catalog.events[name];
1520
+ const line = `${prop(key)}: z.string().max(64),`;
1521
+ const hasAttrs = !!facet?.dims.some((d) => d.key.startsWith("attr:"));
1522
+ out.push({
1523
+ kind: "undeclared_attr",
1524
+ target: name,
1525
+ key,
1526
+ count: count2,
1527
+ message: `\`${name}\` has been sent with attr \`${key}\` ${times(count2)} \u2014 not declared`,
1528
+ fix: hasAttrs ? line : `attrs: z.object({ ${prop(key)}: z.string().max(64) }),`
1529
+ });
1530
+ }
1531
+ for (const [k, count2] of Object.entries(counters.rollupSkippedBy ?? {})) {
1532
+ if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
1533
+ const [as, dim2] = split(k);
1534
+ if (!as || !dim2) continue;
1535
+ const feeders = catalog.families[as]?.feeders ?? [];
1536
+ const where = feeders.length ? `// on the \`${as}\` rollup of ${feeders.map((f) => `\`${f}\``).join(", ")}
1537
+ ` : "";
1538
+ out.push({
1539
+ kind: "missing_dim_default",
1540
+ target: as,
1541
+ key: dim2,
1542
+ count: count2,
1543
+ message: `\`${as}\` skipped ${count2} record${count2 === 1 ? "" : "s"} with no \`${dim2}\` \u2014 declare \`dimDefault\``,
1544
+ fix: `${where}dimDefault: 'unknown',`
1545
+ });
1546
+ }
1547
+ const unregistered = /* @__PURE__ */ new Map();
1548
+ for (const row of quarantine) {
1549
+ if (typeof row?.reason !== "string" || !row.reason.includes(UNREGISTERED_REASON)) continue;
1550
+ const name = typeof row.name === "string" ? row.name.slice(0, NAME_MAX) : "";
1551
+ if (!name || name === "(unnamed)") continue;
1552
+ unregistered.set(name, (unregistered.get(name) ?? 0) + 1);
1553
+ }
1554
+ for (const [name, count2] of unregistered) {
1555
+ out.push({
1556
+ kind: "unregistered_event",
1557
+ target: name,
1558
+ count: count2,
1559
+ message: `\`${name}\` was rejected ${times(count2)} \u2014 not in the registry`,
1560
+ // the minimum that boots: validateRegistry wants a kind, an origin, and
1561
+ // a subjects array, and nothing here can guess the rest
1562
+ fix: `${quote(name)}: { kind: 'event', origin: 'client', subjects: [], description: '' },`
1563
+ });
1564
+ }
1565
+ out.sort(
1566
+ (a, b) => b.count - a.count || a.target.localeCompare(b.target) || (a.key ?? "").localeCompare(b.key ?? "")
1567
+ );
1568
+ return out.slice(0, MAX_SUGGESTIONS);
1569
+ }
1570
+
1256
1571
  // src/server/funnel.ts
1257
1572
  var DAY_MS = 864e5;
1258
1573
  function median(values) {
@@ -1460,6 +1775,10 @@ var DEFAULT_LIMITS = {
1460
1775
  rollups: 500,
1461
1776
  trace: 500,
1462
1777
  journey: 500,
1778
+ breakdown: 50,
1779
+ // top groups — a starting point, to be measured on real hosts
1780
+ values: 200,
1781
+ // top values of one dimension — a picker, not a table
1463
1782
  distribution: 1e5,
1464
1783
  distinct: 1e5,
1465
1784
  funnel: 5e3
@@ -1473,7 +1792,10 @@ function buildMatch(scope, range, f) {
1473
1792
  occurredAt: { $gte: range.from, $lt: range.to }
1474
1793
  };
1475
1794
  for (const k of ["kind", "name", "severity", "env", "service", "release", "traceId"]) {
1476
- if (f[k]) match[k] = f[k];
1795
+ const v = f[k];
1796
+ if (Array.isArray(v)) {
1797
+ if (v.length) match[k] = { $in: v };
1798
+ } else if (v) match[k] = v;
1477
1799
  }
1478
1800
  if (f.subject) match.subjectKeys = f.subject;
1479
1801
  for (const [k, v] of Object.entries(f.attrs ?? {})) match[`attrs.${k}`] = v;
@@ -1496,6 +1818,68 @@ function buildMatch(scope, range, f) {
1496
1818
  }
1497
1819
  return match;
1498
1820
  }
1821
+ var INTERVALS = ["hour", "day", "week", "month"];
1822
+ var truncTo = (path3, unit) => ({
1823
+ $dateTrunc: { date: path3, unit, ...unit === "week" ? { startOfWeek: "monday" } : {} }
1824
+ });
1825
+ function measureAccumulator(measure) {
1826
+ const m = /^(sum|avg):(.+)$/.exec(measure);
1827
+ if (!m) return { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
1828
+ const path3 = m[2] === "durationMs" ? "$durationMs" : `$metrics.${m[2]}`;
1829
+ return m[1] === "sum" ? { $sum: path3 } : { $avg: path3 };
1830
+ }
1831
+ var badRequest = (message) => Object.assign(new Error(`telemetry: breakdown() \u2014 ${message}`), { status: 400 });
1832
+ var BREAKDOWN_FIELDS = [
1833
+ "kind",
1834
+ "name",
1835
+ "severity",
1836
+ "env",
1837
+ "service",
1838
+ "release",
1839
+ "origin",
1840
+ "client.platform",
1841
+ "client.appVersion",
1842
+ "usage.meter",
1843
+ "usage.billedTo",
1844
+ "usage.unit",
1845
+ "state.key",
1846
+ "state.to",
1847
+ "error.type",
1848
+ "error.handled"
1849
+ ];
1850
+ var typePrefix = (ref) => ({
1851
+ $let: {
1852
+ vars: { ref },
1853
+ in: {
1854
+ $cond: [
1855
+ { $eq: [{ $type: "$$ref" }, "string"] },
1856
+ { $arrayElemAt: [{ $split: ["$$ref", ":"] }, 0] },
1857
+ null
1858
+ ]
1859
+ }
1860
+ }
1861
+ });
1862
+ function dimExpression(dim2) {
1863
+ if (dim2.startsWith("attr:")) {
1864
+ const key = dim2.slice(5);
1865
+ if (!key) throw badRequest('`attr:` needs a key, e.g. "attr:plan"');
1866
+ return { $ifNull: [`$attrs.${key}`, null] };
1867
+ }
1868
+ if (dim2.startsWith("field:")) {
1869
+ const path3 = dim2.slice(6);
1870
+ if (!BREAKDOWN_FIELDS.includes(path3)) {
1871
+ throw badRequest(
1872
+ `"field:${path3}" is not groupable. Allowed paths: ${BREAKDOWN_FIELDS.join(", ")}. Grouping by \`data.*\`, \`body\`, or an arbitrary path is refused \u2014 those are unindexed free-form content, and a $group over them scans it all. Use \`attr:<key>\` for a declared attr.`
1873
+ );
1874
+ }
1875
+ return { $ifNull: [`$${path3}`, null] };
1876
+ }
1877
+ if (dim2 === "subjectType") return typePrefix({ $arrayElemAt: ["$subjectKeys", 0] });
1878
+ if (dim2 === "actorType") return typePrefix("$actor");
1879
+ throw badRequest(
1880
+ `"${dim2}" is not a dimension. Use "attr:<key>", "field:<path>", "subjectType" or "actorType".`
1881
+ );
1882
+ }
1499
1883
  var QueryCache = class {
1500
1884
  constructor(ttlMs, cap) {
1501
1885
  this.ttlMs = ttlMs;
@@ -1569,16 +1953,9 @@ function createQueries(ctx) {
1569
1953
  return cache.get(
1570
1954
  key,
1571
1955
  () => timed("series", { scope, filter, measure, interval }, async () => {
1572
- const m = /^(sum|avg):(.+)$/.exec(measure);
1573
- const value = !m ? { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } } : m[1] === "sum" ? { $sum: `$metrics.${m[2]}` } : { $avg: `$metrics.${m[2]}` };
1574
1956
  const buckets = await ctx.TelemetryModel.aggregate([
1575
1957
  { $match: buildMatch(scope, range, filter) },
1576
- {
1577
- $group: {
1578
- _id: { $dateTrunc: { date: "$occurredAt", unit: interval, ...interval === "week" ? { startOfWeek: "monday" } : {} } },
1579
- value
1580
- }
1581
- },
1958
+ { $group: { _id: truncTo("$occurredAt", interval), value: measureAccumulator(measure) } },
1582
1959
  { $sort: { _id: 1 } },
1583
1960
  { $limit: limits.series }
1584
1961
  ]);
@@ -1586,6 +1963,114 @@ function createQueries(ctx) {
1586
1963
  })
1587
1964
  );
1588
1965
  },
1966
+ /**
1967
+ * Top groups of a measure by one or two dimensions — "which models cost the
1968
+ * most", "errors by release", "events by platform per week". The primitive
1969
+ * that replaces a page's client-side grouping of whatever rows it happened
1970
+ * to have fetched, which answered "this page" while reading like it
1971
+ * answered the range (reports §6).
1972
+ *
1973
+ * THE CAP IS ON GROUPS RETURNED, NEVER ON ROWS SCANNED. Every `$limit`
1974
+ * below sits AFTER a `$group`, exactly as series() does: the scan is bounded
1975
+ * by buildMatch — tenant, range, filters, indexes — and nothing else, so a
1976
+ * quarter of a million records is one pass and 50 rows. Truncation
1977
+ * therefore keeps the TOP groups by measure, which is what a breakdown
1978
+ * table means; a cap on documents scanned would return an arbitrary prefix
1979
+ * and call it the top.
1980
+ *
1981
+ * With an `interval` this runs a SECOND aggregate restricted to the top
1982
+ * groups, rather than one pipeline that groups by (dims, bucket) and folds.
1983
+ * Two reasons: the ranking must be the measure over the WHOLE range (the
1984
+ * same number the no-interval call reports), and folding in one pass means
1985
+ * `$push`-ing every bucket of every group before the cap can apply — the
1986
+ * unbounded intermediate this primitive exists to avoid. The restriction is
1987
+ * an `$expr`/`$or` over the ≤ cap tuples because a dim can be a computed
1988
+ * expression (subjectType), which a plain `$in` on a path cannot address.
1989
+ *
1990
+ * Under PLATFORM_SCOPE it aggregates ACROSS tenants, like series() — one
1991
+ * set of groups with every tenant summed into it, which is the platform-wide
1992
+ * table a platform operator came for. Ask for a per-tenant split by scoping
1993
+ * to a tenant, or with rollups().
1994
+ */
1995
+ breakdown(scope, range, filter, opts) {
1996
+ const groupBy = opts.groupBy ?? [];
1997
+ if (groupBy.length < 1 || groupBy.length > 2) {
1998
+ throw badRequest(`groupBy takes 1 or 2 dimensions, got ${groupBy.length}`);
1999
+ }
2000
+ const measure = opts.measure ?? "count";
2001
+ const interval = opts.interval;
2002
+ if (interval && !INTERVALS.includes(interval)) {
2003
+ throw badRequest(`interval must be one of ${INTERVALS.join(", ")}`);
2004
+ }
2005
+ const dims = groupBy.map(dimExpression);
2006
+ const cap = Math.min(Math.max(1, opts.limit ?? limits.breakdown), limits.breakdown);
2007
+ const key = JSON.stringify([
2008
+ "breakdown",
2009
+ scope,
2010
+ range.from,
2011
+ range.to,
2012
+ filter,
2013
+ groupBy,
2014
+ measure,
2015
+ interval ?? null,
2016
+ cap
2017
+ ]);
2018
+ return cache.get(
2019
+ key,
2020
+ () => timed("breakdown", { scope, filter, groupBy, measure, interval }, async () => {
2021
+ const match = buildMatch(scope, range, filter);
2022
+ const dimId = Object.fromEntries(dims.map((expr, i) => [`d${i}`, expr]));
2023
+ const top = await ctx.TelemetryModel.aggregate([
2024
+ { $match: match },
2025
+ { $group: { _id: dimId, value: measureAccumulator(measure) } },
2026
+ { $sort: { value: -1, _id: 1 } },
2027
+ { $limit: cap + 1 }
2028
+ ]);
2029
+ const truncated = top.length > cap;
2030
+ if (truncated) top.pop();
2031
+ const tuples = top.map(
2032
+ (g) => groupBy.map((_, i) => g._id?.[`d${i}`] ?? null)
2033
+ );
2034
+ if (!interval) {
2035
+ return {
2036
+ rows: top.map((g, i) => ({ dims: tuples[i], value: g.value })),
2037
+ groups: top.length,
2038
+ truncated,
2039
+ bucketsTruncated: false,
2040
+ dataSource: "raw"
2041
+ };
2042
+ }
2043
+ if (!tuples.length) {
2044
+ return { rows: [], groups: 0, truncated, bucketsTruncated: false, dataSource: "raw" };
2045
+ }
2046
+ const inTop = {
2047
+ $or: tuples.map((t) => ({ $and: dims.map((expr, i) => ({ $eq: [expr, t[i] ?? null] })) }))
2048
+ };
2049
+ const bucketCap = limits.series * top.length;
2050
+ const perBucket = await ctx.TelemetryModel.aggregate([
2051
+ { $match: { ...match, $expr: inTop } },
2052
+ { $group: { _id: { at: truncTo("$occurredAt", interval), ...dimId }, value: measureAccumulator(measure) } },
2053
+ // `at` is the first key of `_id`, so one BSON sort orders by bucket
2054
+ // then by dims — deterministic without a second sort key
2055
+ { $sort: { _id: 1 } },
2056
+ { $limit: bucketCap + 1 }
2057
+ ]);
2058
+ const bucketsTruncated = perBucket.length > bucketCap;
2059
+ if (bucketsTruncated) perBucket.pop();
2060
+ return {
2061
+ rows: perBucket.map((b) => ({
2062
+ dims: groupBy.map((_, i) => b._id?.[`d${i}`] ?? null),
2063
+ at: b._id.at,
2064
+ value: b.value
2065
+ })),
2066
+ groups: top.length,
2067
+ truncated,
2068
+ bucketsTruncated,
2069
+ dataSource: "raw"
2070
+ };
2071
+ })
2072
+ );
2073
+ },
1589
2074
  /**
1590
2075
  * Percentiles + histogram off raw. Keep-all makes the SAMPLE complete —
1591
2076
  * no sampling stands between the match and the math (§5.3) — but the
@@ -1827,6 +2312,820 @@ function requireDistinctFamily(registry, as) {
1827
2312
  }
1828
2313
  return spec;
1829
2314
  }
2315
+
2316
+ // src/server/values.ts
2317
+ var empty = (source) => ({
2318
+ values: [],
2319
+ source,
2320
+ truncated: false,
2321
+ dataSource: source
2322
+ });
2323
+ var SAMPLED_COUNT = { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
2324
+ function createValues(ctx) {
2325
+ const limits = { ...DEFAULT_LIMITS, ...ctx.limits };
2326
+ const slowMs = ctx.slowMs ?? 500;
2327
+ const cache = new QueryCache(ctx.cacheTtlMs ?? 10 * 6e4, ctx.cacheSize ?? 60);
2328
+ const { catalog } = ctx;
2329
+ const timed = async (op, params, run) => {
2330
+ const t0 = Date.now();
2331
+ try {
2332
+ return await run();
2333
+ } finally {
2334
+ const ms = Date.now() - t0;
2335
+ if (ms > slowMs) ctx.onSlowQuery?.({ op, ms, params });
2336
+ }
2337
+ };
2338
+ const eventNames = (names) => names?.length ? names.filter((n) => catalog.events[n]) : Object.keys(catalog.events);
2339
+ function fromCatalog(dim2, names) {
2340
+ const facets = [];
2341
+ for (const d of catalog.envelope) if (d.key === dim2) facets.push(d);
2342
+ for (const name of eventNames(names)) {
2343
+ for (const d of catalog.events[name].dims) if (d.key === dim2) facets.push(d);
2344
+ }
2345
+ const out = [];
2346
+ for (const f of facets) for (const v of f.values ?? []) if (!out.includes(v)) out.push(v);
2347
+ return out;
2348
+ }
2349
+ function pickFamily(dim2, names) {
2350
+ if (dim2 === "subjectType" || dim2 === "actorType") return null;
2351
+ const matches = Object.values(catalog.families).map((f) => ({ f, index: f.by.indexOf(dim2) })).filter(({ f, index }) => index !== -1 && (!names?.length || f.feeders.some((n) => names.includes(n)))).sort((a, b) => a.f.by.length - b.f.by.length || a.f.as.localeCompare(b.f.as));
2352
+ const best = matches[0];
2353
+ return best ? { as: best.f.as, index: best.index, label: best.f.labels[best.index] } : null;
2354
+ }
2355
+ function rawReadable(dim2, names) {
2356
+ try {
2357
+ dimExpression(dim2);
2358
+ } catch {
2359
+ return false;
2360
+ }
2361
+ if (!dim2.startsWith("attr:")) return true;
2362
+ const key = dim2.slice(5);
2363
+ return eventNames(names).some((n) => catalog.events[n].indexedAttrs.includes(key));
2364
+ }
2365
+ return async function values(scope, params) {
2366
+ const { dim: dim2, names, range } = params;
2367
+ if (!dim2) return empty("none");
2368
+ const cap = Math.min(Math.max(1, params.limit ?? limits.values), limits.values);
2369
+ const key = JSON.stringify([
2370
+ "values",
2371
+ scope,
2372
+ dim2,
2373
+ names ?? null,
2374
+ range?.from ?? null,
2375
+ range?.to ?? null,
2376
+ cap
2377
+ ]);
2378
+ return cache.get(
2379
+ key,
2380
+ () => timed("values", { scope, dim: dim2, names }, async () => {
2381
+ const declared = fromCatalog(dim2, names);
2382
+ if (declared.length) {
2383
+ return { values: declared, source: "catalog", truncated: false, dataSource: "catalog" };
2384
+ }
2385
+ const family = pickFamily(dim2, names);
2386
+ if (family) {
2387
+ const rows2 = await ctx.RollupModel.aggregate([
2388
+ {
2389
+ $match: {
2390
+ ...isPlatformScope(scope) ? {} : { tenantId: scope },
2391
+ as: family.as
2392
+ }
2393
+ },
2394
+ { $project: { v: { $arrayElemAt: ["$dims", family.index] }, count: 1 } },
2395
+ { $match: { v: { $type: "string" } } },
2396
+ { $group: { _id: "$v", count: { $sum: "$count" } } },
2397
+ { $sort: { count: -1, _id: 1 } },
2398
+ { $limit: cap + 1 }
2399
+ ]);
2400
+ const truncated2 = rows2.length > cap;
2401
+ if (truncated2) rows2.pop();
2402
+ const prefix = `${family.label}=`;
2403
+ return {
2404
+ values: rows2.map(
2405
+ (r) => String(r._id).startsWith(prefix) ? String(r._id).slice(prefix.length) : String(r._id)
2406
+ ),
2407
+ counts: rows2.map((r) => r.count),
2408
+ source: "rollups",
2409
+ via: family.as,
2410
+ truncated: truncated2,
2411
+ dataSource: "rollups"
2412
+ };
2413
+ }
2414
+ if (!rawReadable(dim2, names)) return empty("none");
2415
+ if (!range) return empty("none");
2416
+ const rows = await ctx.TelemetryModel.aggregate([
2417
+ { $match: buildMatch(scope, range, names?.length ? { name: names } : {}) },
2418
+ { $group: { _id: dimExpression(dim2), count: SAMPLED_COUNT } },
2419
+ // a "no value" is not a value to pick — the null group is real
2420
+ // (breakdown reports it) but it is not something a filter can name
2421
+ { $match: { _id: { $ne: null } } },
2422
+ { $sort: { count: -1, _id: 1 } },
2423
+ { $limit: cap + 1 }
2424
+ ]);
2425
+ const truncated = rows.length > cap;
2426
+ if (truncated) rows.pop();
2427
+ return {
2428
+ values: rows.map((r) => String(r._id)),
2429
+ counts: rows.map((r) => r.count),
2430
+ source: "raw",
2431
+ truncated,
2432
+ dataSource: "raw"
2433
+ };
2434
+ })
2435
+ );
2436
+ };
2437
+ }
2438
+
2439
+ // src/server/report.ts
2440
+ var RANGE_MS = {
2441
+ "1h": 36e5,
2442
+ "24h": 864e5,
2443
+ "7d": 7 * 864e5,
2444
+ "30d": 30 * 864e5,
2445
+ "90d": 90 * 864e5
2446
+ };
2447
+ var badRequest2 = (message) => Object.assign(new Error(`telemetry: ${message}`), { status: 400 });
2448
+ function rangeOf(range, now = /* @__PURE__ */ new Date()) {
2449
+ if (typeof range === "string") {
2450
+ const ms = RANGE_MS[range] ?? spanOf(range);
2451
+ if (ms == null) {
2452
+ throw badRequest2(
2453
+ `range "${range}" is not a known shorthand \u2014 use one of ${Object.keys(RANGE_MS).join(", ")}, an \`<n>h\`/\`<n>d\` form, or an explicit { from, to } ISO pair`
2454
+ );
2455
+ }
2456
+ return { from: new Date(now.getTime() - ms), to: now };
2457
+ }
2458
+ const from = new Date(range.from);
2459
+ const to = new Date(range.to);
2460
+ if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from >= to) {
2461
+ throw badRequest2(
2462
+ `range { from: "${range.from}", to: "${range.to}" } is not valid \u2014 \`from\` must be a valid ISO time strictly before \`to\` (the package is half-open everywhere)`
2463
+ );
2464
+ }
2465
+ return { from, to };
2466
+ }
2467
+ function spanOf(range) {
2468
+ const m = /^(\d+)([hd])$/.exec(range);
2469
+ if (!m) return null;
2470
+ return Number(m[1]) * (m[2] === "h" ? 36e5 : 864e5);
2471
+ }
2472
+ function intervalForRange(range, now = /* @__PURE__ */ new Date()) {
2473
+ if (typeof range === "string" && RANGE_MS[range] != null) {
2474
+ return range === "1h" || range === "24h" ? "hour" : range === "90d" ? "week" : "day";
2475
+ }
2476
+ const { from, to } = rangeOf(range, now);
2477
+ const ms = to.getTime() - from.getTime();
2478
+ if (ms <= 864e5) return "hour";
2479
+ if (ms < 90 * 864e5) return "day";
2480
+ return "week";
2481
+ }
2482
+ var INTERVAL_RANK = { hour: 0, day: 1, week: 2, month: 3 };
2483
+ var shift = (range) => ({
2484
+ from: new Date(range.from.getTime() - (range.to.getTime() - range.from.getTime())),
2485
+ to: range.from
2486
+ });
2487
+ function expandSource(source, catalog) {
2488
+ if ("event" in source) {
2489
+ if (!catalog.events[source.event]) {
2490
+ return unavailable(
2491
+ `no event named "${source.event}" is registered \u2014 the catalog knows ${count(Object.keys(catalog.events).length, "event")}, so either the name is a typo or the registry never declared it`
2492
+ );
2493
+ }
2494
+ return { form: "event", events: [source.event] };
2495
+ }
2496
+ if ("namespace" in source) {
2497
+ const events = catalog.namespaces[source.namespace];
2498
+ if (!events?.length) {
2499
+ return unavailable(
2500
+ `no event name starts with "${source.namespace}." \u2014 the registered namespaces are ${Object.keys(catalog.namespaces).join(", ")}`
2501
+ );
2502
+ }
2503
+ return { form: "namespace", events: [...events] };
2504
+ }
2505
+ if ("kind" in source) {
2506
+ const events = Object.keys(catalog.events).filter((n) => catalog.events[n].kind === source.kind);
2507
+ if (!events.length) {
2508
+ return unavailable(
2509
+ `no event is registered with kind "${source.kind}" \u2014 declare one, or pick a kind the registry uses`
2510
+ );
2511
+ }
2512
+ return { form: "kind", events, kind: source.kind };
2513
+ }
2514
+ const family = catalog.families[source.family];
2515
+ if (!family) {
2516
+ return unavailable(
2517
+ `no rollup family "${source.family}" is declared \u2014 add a \`rollups: [{ as: '${source.family}', by: [...] }]\` block to the event that should feed it`
2518
+ );
2519
+ }
2520
+ return { form: "family", events: [...family.feeders], family };
2521
+ }
2522
+ var FILTER_ONLY = {
2523
+ "field:subject": "subject",
2524
+ "field:traceId": "traceId"
2525
+ };
2526
+ function dimsFor(catalog, events) {
2527
+ const out = /* @__PURE__ */ new Map();
2528
+ for (const d of catalog.envelope) out.set(d.key, d);
2529
+ for (const name of events) {
2530
+ for (const d of catalog.events[name]?.dims ?? []) {
2531
+ const seen = out.get(d.key);
2532
+ out.set(d.key, seen ? { ...seen, indexed: seen.indexed && d.indexed } : d);
2533
+ }
2534
+ }
2535
+ return out;
2536
+ }
2537
+ var measureDeclared = (catalog, events, key) => key === "count" || events.some((n) => catalog.events[n]?.measures.some((m) => m.key === key));
2538
+ var MEASURE_OP = /^(sum|avg|p50|p90|p95|p99):(.+)$/;
2539
+ var unavailable = (why) => ({ unavailable: true, why });
2540
+ var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
2541
+ function resolveReport(report, catalog, opts = {}) {
2542
+ const now = opts.now ?? /* @__PURE__ */ new Date();
2543
+ const limits = opts.limits ?? {};
2544
+ const src = expandSource(report.source, catalog);
2545
+ if ("unavailable" in src) return src;
2546
+ const measure = report.measure ?? "count";
2547
+ const groupBy = report.groupBy ?? [];
2548
+ if (groupBy.length > 2) {
2549
+ return unavailable(
2550
+ `groupBy takes at most 2 dimensions, got ${groupBy.length} (${groupBy.join(", ")}) \u2014 three dims is a pivot table nobody can read and a group count that multiplies`
2551
+ );
2552
+ }
2553
+ if (report.interval && INTERVAL_RANK[report.interval] == null) {
2554
+ return unavailable(`interval "${report.interval}" is not one of hour, day, week, month`);
2555
+ }
2556
+ if (measure === "funnel") return planFunnel(report, catalog, now, limits);
2557
+ if (measure.startsWith("distinct:")) return planDistinct(report, catalog, src, measure, now);
2558
+ const opMatch = MEASURE_OP.exec(measure);
2559
+ if (measure !== "count" && !opMatch) {
2560
+ return unavailable(
2561
+ `measure "${measure}" is not a measure \u2014 use 'count', 'sum:<metric>', 'avg:<metric>', 'p50|p95|p99:<metric>', 'distinct:<subjectType>' or 'funnel'`
2562
+ );
2563
+ }
2564
+ if (opMatch && !measureDeclared(catalog, src.events, measure)) {
2565
+ return unavailable(
2566
+ `"${measure}" names a metric no source event declares \u2014 add \`${opMatch[2]}\` to the \`metrics\` object of ${src.events.join(", ")}, or pick one of ${metricList(catalog, src.events)}`
2567
+ );
2568
+ }
2569
+ const exact = planRollups(report, catalog, src, measure, groupBy, now, limits);
2570
+ if (exact) return exact;
2571
+ const filter = toRecordFilter(report, catalog, src);
2572
+ if ("unavailable" in filter) return filter;
2573
+ const range = rangeOf(report.range, now);
2574
+ const dims = dimsFor(catalog, src.events);
2575
+ const touched = [...groupBy, ...(report.filters ?? []).map((f) => f.dim)];
2576
+ const unindexed = touched.find((k) => !(dims.get(k)?.indexed ?? FILTER_ONLY[k] != null));
2577
+ const scanWhy = unindexed ? ` \u2014 "${unindexed}" has no index behind it, so this is a collection scan bounded only by the range` + (unindexed.startsWith("attr:") ? `; add "${unindexed.slice(5)}" to \`indexedAttrs\` to make it a lookup` : "") : "";
2578
+ const exactness = unindexed != null ? "scan" : "raw";
2579
+ if (opMatch && opMatch[1] !== "sum" && opMatch[1] !== "avg") {
2580
+ if (groupBy.length) {
2581
+ return unavailable(
2582
+ `percentiles per group are not offered yet (reports \xA713) \u2014 "${measure}" with groupBy ${groupBy.join(", ")} would be one distribution() read per group. Drop the groupBy, or filter to one group and ask again`
2583
+ );
2584
+ }
2585
+ return withCompare(report, {
2586
+ primitive: "distribution",
2587
+ args: [range, filter, { measure: opMatch[2] }],
2588
+ exactness,
2589
+ why: `${measure} is approximate by construction ($percentile t-digest over the matched records)${scanWhy}`
2590
+ });
2591
+ }
2592
+ if (groupBy.length) {
2593
+ const bad = groupBy.find((k) => !dims.has(k));
2594
+ if (bad) {
2595
+ return unavailable(
2596
+ `"${bad}" is not a dimension of ${describe(src)} \u2014 group by one of ${[...dims.keys()].join(", ")}`
2597
+ );
2598
+ }
2599
+ return withCompare(report, {
2600
+ primitive: "breakdown",
2601
+ args: [
2602
+ range,
2603
+ filter,
2604
+ {
2605
+ groupBy,
2606
+ measure,
2607
+ ...report.interval ? { interval: report.interval } : {},
2608
+ ...report.limit ? { limit: capped(report.limit, limits.breakdown) } : {}
2609
+ }
2610
+ ],
2611
+ exactness,
2612
+ why: `raw ${measure} by ${groupBy.join(" \xD7 ")} over the range${scanWhy}`
2613
+ });
2614
+ }
2615
+ if (!report.measure && !report.interval) {
2616
+ return withCompare(report, {
2617
+ primitive: "records",
2618
+ args: [range, filter, report.limit ? { limit: capped(report.limit, limits.records) } : {}],
2619
+ exactness,
2620
+ why: `the matching records themselves, newest first${scanWhy}`
2621
+ });
2622
+ }
2623
+ const interval = report.interval ?? intervalForRange(report.range, now);
2624
+ return withCompare(report, {
2625
+ primitive: "series",
2626
+ args: [range, filter, { measure, interval }],
2627
+ exactness,
2628
+ why: `raw ${measure} per ${interval} over the range${scanWhy}`
2629
+ });
2630
+ }
2631
+ var capped = (limit, cap) => cap == null ? limit : Math.max(1, Math.min(limit, cap));
2632
+ var describe = (src) => src.form === "family" ? `family "${src.family.as}"` : src.events.join(", ");
2633
+ var metricList = (catalog, events) => {
2634
+ const keys = /* @__PURE__ */ new Set();
2635
+ for (const n of events) for (const m of catalog.events[n]?.measures ?? []) keys.add(m.key);
2636
+ return keys.size ? [...keys].join(", ") : "nothing but count";
2637
+ };
2638
+ function planFunnel(report, catalog, now, limits) {
2639
+ const stages = report.stages ?? [];
2640
+ if (!stages.length) {
2641
+ return unavailable(
2642
+ "`measure: 'funnel'` needs `stages` \u2014 one or more lifetime `by: ['subject']` rollup family names, in the order a subject reaches them"
2643
+ );
2644
+ }
2645
+ const anchor = report.anchor ?? stages[0];
2646
+ const exits = report.exits ?? [];
2647
+ for (const as of [...stages, anchor, ...exits]) {
2648
+ const refusal = milestoneRefusal(catalog, as);
2649
+ if (refusal) return unavailable(refusal);
2650
+ }
2651
+ const first = catalog.families[stages[0]];
2652
+ for (const as of [...stages.slice(1), anchor]) {
2653
+ const f = catalog.families[as];
2654
+ if (!sameSet(f.subjectTypes, first.subjectTypes)) {
2655
+ return unavailable(
2656
+ `funnel stages must share one subject type: "${stages[0]}" is declared \`subjects: [${first.subjectTypes.join(", ")}]\` and "${as}" is \`subjects: [${f.subjectTypes.join(", ")}]\` \u2014 two populations cannot convert into each other`
2657
+ );
2658
+ }
2659
+ }
2660
+ if (report.subjectType && !first.subjectTypes.includes(report.subjectType)) {
2661
+ return unavailable(
2662
+ `subjectType "${report.subjectType}" is not one of the stages' subjects (${first.subjectTypes.join(", ")}) \u2014 the cohort would be empty`
2663
+ );
2664
+ }
2665
+ if (report.interval === "hour") {
2666
+ return unavailable(
2667
+ "funnel slices are day, week or month \u2014 an hourly cohort slice is not offered, because a cohort is assembled from lifetime milestones with no hourly grain to slice on"
2668
+ );
2669
+ }
2670
+ const params = {
2671
+ stages: stages.map((as) => ({ as })),
2672
+ anchor,
2673
+ ...exits.length ? { exits: exits.map((as) => ({ as })) } : {},
2674
+ cohort: rangeOf(report.range, now),
2675
+ ...report.subjectType ? { subjectType: report.subjectType } : {},
2676
+ ...report.interval ? { interval: report.interval } : {},
2677
+ ...report.limit ? { limit: capped(report.limit, limits.funnel) } : {}
2678
+ };
2679
+ return withCompare(report, {
2680
+ primitive: "funnel",
2681
+ args: [params],
2682
+ exactness: "exact",
2683
+ via: anchor,
2684
+ why: `cohort funnel over ${count(stages.length, "lifetime milestone family")}, anchored on "${anchor}" \u2014 rollups only, no raw scan`
2685
+ });
2686
+ }
2687
+ function milestoneRefusal(catalog, as) {
2688
+ const f = catalog.families[as];
2689
+ if (!f) {
2690
+ return `no rollup family "${as}" is declared. Add a \`rollups: [{ as: '${as}', by: ['subject'], subjects: [...] }]\` block to the event that marks it`;
2691
+ }
2692
+ const shape = `by: [${f.by.map((d) => `'${d}'`).join(", ")}]${f.bucket ? `, bucket: '${f.bucket}'` : ""}`;
2693
+ if (!f.lifetime) {
2694
+ return `rollup family "${as}" (declared on "${f.feeders[0]}") is BUCKETED (${shape}). A milestone needs a lifetime family so \`firstAt\` is the one moment the subject reached it; a bucketed family has one doc per period and would count the same subject repeatedly`;
2695
+ }
2696
+ if (f.by.length !== 1 || f.by[0] !== "subject") {
2697
+ return `rollup family "${as}" (declared on "${f.feeders[0]}") is keyed ${shape}, but a milestone must be keyed by exactly one subject dim (\`by: ['subject']\`). Extra dims split one subject across several docs, which would over-count every stage`;
2698
+ }
2699
+ return null;
2700
+ }
2701
+ function planDistinct(report, catalog, src, measure, now) {
2702
+ const subjectType = measure.slice("distinct:".length);
2703
+ if (!subjectType) {
2704
+ return unavailable(
2705
+ `"${measure}" needs a subject type \u2014 'distinct:account', one of ${catalog.subjectTypes.join(", ")}`
2706
+ );
2707
+ }
2708
+ if (!catalog.subjectTypes.includes(subjectType)) {
2709
+ return unavailable(
2710
+ `no event or rollup declares the subject type "${subjectType}" \u2014 the registry knows ${catalog.subjectTypes.join(", ") || "no subject types at all"}`
2711
+ );
2712
+ }
2713
+ if (report.groupBy?.length) {
2714
+ return unavailable(
2715
+ `distinct counts take no groupBy: distinctCount() answers one series per family, and "${report.groupBy.join(", ")}" would need a family keyed by those dims AND \`by: ['subject']\`, which cannot count subjects exactly`
2716
+ );
2717
+ }
2718
+ const wanted = new Set(src.events);
2719
+ const fits = Object.values(catalog.families).filter(
2720
+ (f) => f.bucket != null && f.by.length === 1 && f.by[0] === "subject" && f.subjectTypes.includes(subjectType) && src.events.every((e) => f.feeders.includes(e))
2721
+ );
2722
+ const family = fits.find((f) => sameSet(f.feeders, [...wanted])) ?? fits[0];
2723
+ if (!family) {
2724
+ return unavailable(
2725
+ `no bucketed \`by: ['subject']\` family covers ${src.events.join(", ")} for subject type "${subjectType}" \u2014 declare \`rollups: [{ as: 'activity', by: ['subject'], subjects: ['${subjectType}'], bucket: 'day' }]\` on the events that count as activity`
2726
+ );
2727
+ }
2728
+ const superset = !sameSet(family.feeders, [...wanted]);
2729
+ return withCompare(report, {
2730
+ primitive: "distinctCount",
2731
+ args: [
2732
+ {
2733
+ as: family.as,
2734
+ subjectType,
2735
+ range: rangeOf(report.range, now),
2736
+ ...report.interval ? { interval: report.interval } : {}
2737
+ }
2738
+ ],
2739
+ exactness: "exact",
2740
+ via: family.as,
2741
+ why: `"${family.as}" writes exactly one doc per (subject, ${family.bucket}), so distinct subjects IS the doc count \u2014 exact, no sketch` + (superset ? `. Its feeders (${family.feeders.join(", ")}) are a SUPERSET of the source (${src.events.join(", ")}), so the count includes subjects who only did the others` : "")
2742
+ });
2743
+ }
2744
+ var famDims = (f) => f.by.map((b) => b === "subject" ? "subjectType" : b);
2745
+ function planRollups(report, catalog, src, measure, groupBy, now, limits) {
2746
+ if (report.excludeActorTypes?.length) return null;
2747
+ const candidates = src.family ? [src.family] : Object.values(catalog.families).filter((f) => sameSet(f.feeders, src.events));
2748
+ const op = MEASURE_OP.exec(measure);
2749
+ const filters = report.filters ?? [];
2750
+ const fits = candidates.filter((f) => {
2751
+ const dims2 = famDims(f);
2752
+ if (!groupBy.every((k) => dims2.includes(k))) return false;
2753
+ if (report.interval && (!f.bucket || INTERVAL_RANK[f.bucket] > INTERVAL_RANK[report.interval])) return false;
2754
+ if (op) {
2755
+ if (op[1] !== "sum" && op[1] !== "avg" || !f.sums.includes(op[2])) return false;
2756
+ }
2757
+ return filters.every(
2758
+ (t) => dims2.includes(t.dim) && (t.op === "eq" || t.op === "in") || nameFilterCovers(t, f)
2759
+ );
2760
+ });
2761
+ const family = fits.sort((a, b) => a.by.length - b.by.length)[0];
2762
+ if (!family) return null;
2763
+ const dims = famDims(family);
2764
+ const range = rangeOf(report.range, now);
2765
+ const on = family.lifetime ? "firstAt" : "bucketAt";
2766
+ const fold = filters.filter((t) => dims.includes(t.dim));
2767
+ return withCompare(report, {
2768
+ primitive: "rollups",
2769
+ args: [
2770
+ {
2771
+ as: family.as,
2772
+ on,
2773
+ range,
2774
+ sort: family.lifetime ? "count" : "bucketAt",
2775
+ ...report.limit ? { limit: capped(report.limit, limits.rollups) } : {}
2776
+ }
2777
+ ],
2778
+ exactness: "exact",
2779
+ via: family.as,
2780
+ shape: {
2781
+ groupBy,
2782
+ labels: groupBy.map((k) => family.labels[dims.indexOf(k)]),
2783
+ measure,
2784
+ ...report.interval ? { interval: report.interval } : {},
2785
+ ...fold.length ? { filters: fold.map((t) => ({ ...t, label: family.labels[dims.indexOf(t.dim)] })) } : {}
2786
+ },
2787
+ why: `family "${family.as}" is keyed by ${dims.join(", ") || "nothing but its feeders"} and maintained on write, so this is one indexed rollup read over \`${on}\` instead of a raw scan` + (op?.[1] === "avg" ? `. avg:${op[2]} is exact off a rollup: the executor divides sums.${op[2]} by count` : "") + (report.interval && family.bucket !== report.interval ? `. Its ${family.bucket} buckets roll up into ${report.interval} without splitting one` : "")
2788
+ });
2789
+ }
2790
+ function nameFilterCovers(t, f) {
2791
+ if (t.dim !== "field:name") return false;
2792
+ const admitted = t.op === "eq" ? [String(t.value)] : t.op === "in" ? [...t.value].map(String) : null;
2793
+ return admitted != null && f.feeders.every((n) => admitted.includes(n));
2794
+ }
2795
+ var FIELD_TERMS = {
2796
+ "field:kind": "kind",
2797
+ "field:name": "name",
2798
+ "field:severity": "severity",
2799
+ "field:env": "env",
2800
+ "field:service": "service",
2801
+ "field:release": "release",
2802
+ ...FILTER_ONLY
2803
+ };
2804
+ function toRecordFilter(report, catalog, src) {
2805
+ const filter = {};
2806
+ if (src.events.length === 1) {
2807
+ filter.name = src.events[0];
2808
+ } else if (src.kind) {
2809
+ filter.kind = src.kind;
2810
+ } else {
2811
+ filter.name = [...src.events];
2812
+ }
2813
+ for (const t of report.filters ?? []) {
2814
+ const term = FIELD_TERMS[t.dim];
2815
+ if (term) {
2816
+ if (t.op !== "eq") {
2817
+ return unavailable(
2818
+ `"${t.dim}" supports equality only on the raw path \u2014 RecordFilter.${String(term)} is one string, and "${t.op}" would need a term query.ts does not build`
2819
+ );
2820
+ }
2821
+ filter[term] = String(t.value);
2822
+ continue;
2823
+ }
2824
+ if (t.dim.startsWith("attr:")) {
2825
+ const key = t.dim.slice(5);
2826
+ if (t.op === "eq") {
2827
+ (filter.attrs ??= {})[key] = String(t.value);
2828
+ continue;
2829
+ }
2830
+ if (t.op === "gte" || t.op === "lte") {
2831
+ if (!measureDeclared(catalog, src.events, `sum:${key}`)) {
2832
+ return unavailable(
2833
+ `"${t.dim} ${t.op}" is a numeric bound, which only a declared metric can carry \u2014 add \`${key}\` to the \`metrics\` object of ${src.events.join(", ")}, or filter it as an equality`
2834
+ );
2835
+ }
2836
+ const range = (filter.metrics ??= {})[key] ??= {};
2837
+ range[t.op] = Number(t.value);
2838
+ continue;
2839
+ }
2840
+ return unavailable(
2841
+ `"${t.dim}" supports equality (or a gte/lte bound on a metric) \u2014 an \`in\` over attrs would need a \`$in\` term buildMatch does not write`
2842
+ );
2843
+ }
2844
+ if (t.dim === "subjectType" || t.dim === "actorType") {
2845
+ return unavailable(
2846
+ `"${t.dim}" is derived at query time from ${t.dim === "subjectType" ? "`subjectKeys`" : "`actor`"} and RecordFilter has no term for it \u2014 group by it instead, pin one subject with \`field:subject\`` + (t.dim === "actorType" ? ", or use `excludeActorTypes`" : "")
2847
+ );
2848
+ }
2849
+ return unavailable(
2850
+ `"${t.dim}" is not filterable on the raw path \u2014 RecordFilter carries ${Object.keys(FIELD_TERMS).join(", ")}, \`attr:<key>\` and metric bounds`
2851
+ );
2852
+ }
2853
+ if (report.excludeActorTypes?.length) filter.excludeActorTypes = [...report.excludeActorTypes];
2854
+ return filter;
2855
+ }
2856
+ function withCompare(report, plan) {
2857
+ if (report.compare !== "previous") return plan;
2858
+ const [first, ...rest] = plan.args;
2859
+ if (plan.primitive === "rollups" || plan.primitive === "distinctCount" || plan.primitive === "funnel") {
2860
+ const params = first;
2861
+ const key = plan.primitive === "funnel" ? "cohort" : "range";
2862
+ const window = params[key];
2863
+ if (!window) return plan;
2864
+ return { ...plan, previous: { args: [{ ...params, [key]: { ...window, ...shift(window) } }, ...rest] } };
2865
+ }
2866
+ return { ...plan, previous: { args: [shift(first), ...rest] } };
2867
+ }
2868
+ var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
2869
+ var FILTER_OPS = /* @__PURE__ */ new Set(["eq", "in", "gte", "lte"]);
2870
+ var SORTS = /* @__PURE__ */ new Set(["value", "label", "time"]);
2871
+ function parseReportQuery(q) {
2872
+ const str = (k) => {
2873
+ const v = Array.isArray(q[k]) ? q[k][0] : q[k];
2874
+ return typeof v === "string" && v ? v : void 0;
2875
+ };
2876
+ const list = (k) => (str(k) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
2877
+ const raw = str("source");
2878
+ if (!raw) {
2879
+ throw badRequest2(
2880
+ "`source` is required \u2014 one of source=event:<name>, namespace:<ns>, kind:<kind>, family:<as>"
2881
+ );
2882
+ }
2883
+ const cut = raw.indexOf(":");
2884
+ const form = cut > 0 ? raw.slice(0, cut) : "";
2885
+ const named = cut > 0 ? raw.slice(cut + 1) : "";
2886
+ if (!named || !["event", "namespace", "kind", "family"].includes(form)) {
2887
+ throw badRequest2(
2888
+ `\`source\` must be "event:<name>", "namespace:<ns>", "kind:<kind>" or "family:<as>" \u2014 got "${raw}"`
2889
+ );
2890
+ }
2891
+ const source = form === "event" ? { event: named } : form === "namespace" ? { namespace: named } : form === "kind" ? { kind: named } : { family: named };
2892
+ const shorthand = str("range");
2893
+ const from = str("from");
2894
+ const to = str("to");
2895
+ if (!shorthand && !(from && to)) {
2896
+ throw badRequest2("a range is required \u2014 either `range=7d` or both `from` and `to` as ISO times");
2897
+ }
2898
+ const range = shorthand ?? { from, to };
2899
+ const interval = str("interval");
2900
+ if (interval && INTERVAL_RANK[interval] == null) {
2901
+ throw badRequest2(`\`interval\` must be one of hour, day, week, month \u2014 got "${interval}"`);
2902
+ }
2903
+ const sort = str("sort");
2904
+ if (sort && !SORTS.has(sort)) {
2905
+ throw badRequest2(`\`sort\` must be one of value, label, time \u2014 got "${sort}"`);
2906
+ }
2907
+ const compare = str("compare");
2908
+ if (compare && compare !== "previous") {
2909
+ throw badRequest2(`\`compare\` takes only "previous" \u2014 got "${compare}"`);
2910
+ }
2911
+ const limitRaw = str("limit");
2912
+ const limit = limitRaw == null ? void 0 : Number(limitRaw);
2913
+ if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
2914
+ throw badRequest2(`\`limit\` must be a positive integer \u2014 got "${limitRaw}"`);
2915
+ }
2916
+ const measure = str("measure");
2917
+ const groupBy = list("groupBy");
2918
+ const excludeActorTypes = list("excludeActors");
2919
+ const stages = list("stages");
2920
+ const exits = list("exits");
2921
+ const anchor = str("anchor");
2922
+ const subjectType = str("subjectType");
2923
+ const filters = (q.filter == null ? [] : Array.isArray(q.filter) ? q.filter.map(String) : [String(q.filter)]).map(parseFilterTerm);
2924
+ return {
2925
+ source,
2926
+ range,
2927
+ ...interval ? { interval } : {},
2928
+ ...measure ? { measure } : {},
2929
+ ...groupBy.length ? { groupBy } : {},
2930
+ ...filters.length ? { filters } : {},
2931
+ ...excludeActorTypes.length ? { excludeActorTypes } : {},
2932
+ ...sort ? { sort } : {},
2933
+ ...limit != null ? { limit } : {},
2934
+ ...compare ? { compare: "previous" } : {},
2935
+ ...stages.length ? { stages } : {},
2936
+ ...anchor ? { anchor } : {},
2937
+ ...exits.length ? { exits } : {},
2938
+ ...subjectType ? { subjectType } : {}
2939
+ };
2940
+ }
2941
+ function parseFilterTerm(term) {
2942
+ const parts = term.split(":");
2943
+ const i = parts.findIndex((p) => FILTER_OPS.has(p));
2944
+ const rest = i < 0 ? "" : parts.slice(i + 1).join(":");
2945
+ if (i < 1 || !rest) {
2946
+ throw badRequest2(
2947
+ `\`filter\` must be "<dim>:<op>:<value>" with op one of eq, in, gte, lte \u2014 got "${term}"`
2948
+ );
2949
+ }
2950
+ const dim2 = parts.slice(0, i).join(":");
2951
+ const op = parts[i];
2952
+ if (op === "in") {
2953
+ const values = rest.split(",").map((s) => s.trim()).filter(Boolean);
2954
+ if (!values.length) throw badRequest2(`\`filter\` "${term}" has an empty \`in\` list`);
2955
+ return { dim: dim2, op, value: values };
2956
+ }
2957
+ if (op === "gte" || op === "lte") {
2958
+ const n = Number(rest);
2959
+ if (Number.isNaN(n)) throw badRequest2(`\`filter\` bound "${term}" is not a number`);
2960
+ return { dim: dim2, op, value: n };
2961
+ }
2962
+ return { dim: dim2, op, value: rest };
2963
+ }
2964
+ function reportToQuery(report) {
2965
+ const s = report.source;
2966
+ const q = {
2967
+ source: "event" in s ? `event:${s.event}` : "namespace" in s ? `namespace:${s.namespace}` : "kind" in s ? `kind:${s.kind}` : `family:${s.family}`
2968
+ };
2969
+ if (typeof report.range === "string") q.range = report.range;
2970
+ else {
2971
+ q.from = report.range.from;
2972
+ q.to = report.range.to;
2973
+ }
2974
+ if (report.interval) q.interval = report.interval;
2975
+ if (report.measure) q.measure = report.measure;
2976
+ if (report.groupBy?.length) q.groupBy = report.groupBy.join(",");
2977
+ if (report.filters?.length) {
2978
+ const terms = report.filters.map(
2979
+ (f) => `${f.dim}:${f.op}:${Array.isArray(f.value) ? f.value.join(",") : String(f.value)}`
2980
+ );
2981
+ q.filter = terms.length === 1 ? terms[0] : terms;
2982
+ }
2983
+ if (report.excludeActorTypes?.length) q.excludeActors = report.excludeActorTypes.join(",");
2984
+ if (report.sort) q.sort = report.sort;
2985
+ if (report.limit != null) q.limit = String(report.limit);
2986
+ if (report.compare) q.compare = report.compare;
2987
+ if (report.stages?.length) q.stages = report.stages.join(",");
2988
+ if (report.anchor) q.anchor = report.anchor;
2989
+ if (report.exits?.length) q.exits = report.exits.join(",");
2990
+ if (report.subjectType) q.subjectType = report.subjectType;
2991
+ return q;
2992
+ }
2993
+ var LEGACY_DIMS = {
2994
+ kind: "field:kind",
2995
+ name: "field:name",
2996
+ severity: "field:severity",
2997
+ env: "field:env",
2998
+ service: "field:service",
2999
+ release: "field:release",
3000
+ subject: "field:subject",
3001
+ traceId: "field:traceId"
3002
+ };
3003
+ function normalizeQuery(query) {
3004
+ if (!query || typeof query !== "object") return null;
3005
+ if ("source" in query && query.source) return query;
3006
+ const q = query;
3007
+ const filters = q.filters ?? {};
3008
+ const str = (v) => typeof v === "string" && v ? v : null;
3009
+ const name = str(filters.name);
3010
+ const family = str(filters.rollup);
3011
+ const kind = str(filters.kind);
3012
+ const source = name ? { event: name } : family ? { family } : kind ? { kind } : null;
3013
+ if (!source) return null;
3014
+ const consumed = name ? "name" : family ? "rollup" : "kind";
3015
+ const terms = [];
3016
+ for (const [k, v] of Object.entries(filters)) {
3017
+ if (k === consumed || k === "rollup") continue;
3018
+ if (k === "excludeActorTypes") continue;
3019
+ if (k === "attrs") {
3020
+ const entries = typeof v === "string" ? v.split(",").map((pair) => pair.split(":").map((s) => s.trim())) : Object.entries(v ?? {}).map(([a, b]) => [a, String(b)]);
3021
+ for (const [key, value2] of entries) {
3022
+ if (key && value2 != null) terms.push({ dim: `attr:${key}`, op: "eq", value: String(value2) });
3023
+ }
3024
+ continue;
3025
+ }
3026
+ const dim2 = LEGACY_DIMS[k];
3027
+ const value = str(v);
3028
+ if (dim2 && value) terms.push({ dim: dim2, op: "eq", value });
3029
+ }
3030
+ const groupBy = (q.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3031
+ const sort = q.sort === "value" || q.sort === "label" || q.sort === "time" ? q.sort : void 0;
3032
+ const actors = filters.excludeActorTypes;
3033
+ return {
3034
+ source,
3035
+ range: q.range ?? "7d",
3036
+ ...terms.length ? { filters: terms } : {},
3037
+ ...groupBy.length ? { groupBy } : {},
3038
+ ...sort ? { sort } : {},
3039
+ ...Array.isArray(actors) && actors.length ? { excludeActorTypes: actors.map(String) } : {}
3040
+ };
3041
+ }
3042
+
3043
+ // src/server/execute.ts
3044
+ async function executeReport(q, scope, report, catalog, opts = {}) {
3045
+ const plan = resolveReport(report, catalog, { now: opts.now, limits: opts.limits });
3046
+ if ("unavailable" in plan) throw Object.assign(new Error(plan.why), { status: 400 });
3047
+ const run = async (args) => {
3048
+ const raw = await q[plan.primitive](scope, ...args);
3049
+ if (plan.primitive === "rollups" && plan.shape) {
3050
+ return foldRollups(raw?.rows ?? [], plan.shape, !!raw?.truncated);
3051
+ }
3052
+ if (plan.primitive === "records" && opts.redact) {
3053
+ return { ...raw, items: opts.redact(raw?.items ?? []) };
3054
+ }
3055
+ return raw;
3056
+ };
3057
+ const [result, previous] = await Promise.all([
3058
+ run(plan.args),
3059
+ plan.previous ? run(plan.previous.args) : Promise.resolve(void 0)
3060
+ ]);
3061
+ return {
3062
+ report,
3063
+ plan,
3064
+ result,
3065
+ ...plan.previous ? { previous } : {},
3066
+ dataSource: result?.dataSource ?? "raw"
3067
+ };
3068
+ }
3069
+ var MEASURE_OP2 = /^(sum|avg):(.+)$/;
3070
+ function foldRollups(rows, shape, truncated = false) {
3071
+ const op = MEASURE_OP2.exec(shape.measure);
3072
+ const groups = /* @__PURE__ */ new Map();
3073
+ for (const doc of rows) {
3074
+ const dims = doc?.dims ?? [];
3075
+ if (!(shape.filters ?? []).every((f) => admits(f, dimValue(dims, f.label)))) continue;
3076
+ const tuple = shape.labels.map((label3) => dimValue(dims, label3));
3077
+ const at = shape.interval && doc.bucketAt ? truncate(new Date(doc.bucketAt), shape.interval) : void 0;
3078
+ const key = `${JSON.stringify(tuple)}|${at ? at.getTime() : ""}`;
3079
+ let g = groups.get(key);
3080
+ if (!g) groups.set(key, g = { dims: tuple, ...at ? { at } : {}, sum: 0, count: 0 });
3081
+ g.count += typeof doc.count === "number" ? doc.count : 0;
3082
+ if (op) g.sum += sumOf(doc.sums, op[2]);
3083
+ }
3084
+ const rowsOut = [...groups.values()].map((g) => ({
3085
+ dims: g.dims,
3086
+ ...g.at ? { at: g.at } : {},
3087
+ // avg is sums[k]/count off the SAME doc, which is exact — not an average of
3088
+ // averages, which is what folding a per-bucket mean would have produced
3089
+ value: !op ? g.count : op[1] === "sum" ? g.sum : g.count ? g.sum / g.count : 0
3090
+ }));
3091
+ rowsOut.sort(
3092
+ shape.interval ? (a, b) => (a.at?.getTime() ?? 0) - (b.at?.getTime() ?? 0) || byDims(a, b) : (a, b) => b.value - a.value || byDims(a, b)
3093
+ );
3094
+ return {
3095
+ rows: rowsOut,
3096
+ groups: new Set([...groups.values()].map((g) => JSON.stringify(g.dims))).size,
3097
+ truncated,
3098
+ dataSource: "rollups"
3099
+ };
3100
+ }
3101
+ function dimValue(dims, label3) {
3102
+ const prefix = `${label3}=`;
3103
+ for (const d of dims) if (d.startsWith(prefix)) return d.slice(prefix.length);
3104
+ for (const d of dims) if (!d.includes("=")) return d;
3105
+ return null;
3106
+ }
3107
+ function admits(f, value) {
3108
+ if (f.op === "in") return f.value.map(String).includes(String(value));
3109
+ if (f.op === "gte" || f.op === "lte") {
3110
+ const n = Number(value);
3111
+ if (Number.isNaN(n)) return false;
3112
+ return f.op === "gte" ? n >= Number(f.value) : n <= Number(f.value);
3113
+ }
3114
+ return String(value) === String(f.value);
3115
+ }
3116
+ function sumOf(sums, key) {
3117
+ if (!sums) return 0;
3118
+ const v = sums instanceof Map ? sums.get(key) : sums[key];
3119
+ return typeof v === "number" ? v : 0;
3120
+ }
3121
+ function byDims(a, b) {
3122
+ for (let i = 0; i < a.dims.length; i++) {
3123
+ const x = a.dims[i] ?? "";
3124
+ const y = b.dims[i] ?? "";
3125
+ if (x !== y) return x < y ? -1 : 1;
3126
+ }
3127
+ return 0;
3128
+ }
1830
3129
  function buildViewModel(connection, modelName, collection) {
1831
3130
  const existing = connection.models?.[modelName];
1832
3131
  if (existing) return existing;
@@ -1853,31 +3152,58 @@ var KIND_PAGE = {
1853
3152
  state: "journeys",
1854
3153
  usage: "usage"
1855
3154
  };
1856
- function deriveViews(registry) {
3155
+ function deriveViews(registry, catalog = deriveCatalog(registry)) {
1857
3156
  const views = [];
1858
- const families = /* @__PURE__ */ new Set();
1859
- for (const [name, spec] of Object.entries(registry)) {
1860
- views.push({
1861
- origin: "derived",
1862
- name,
1863
- page: KIND_PAGE[spec.kind] ?? "events",
1864
- query: { range: "7d", filters: { name }, display: spec.kind === "event" ? "series" : "table" }
3157
+ const derived = (name, page, query) => views.push({ origin: "derived", name, page, query });
3158
+ for (const [name, e] of Object.entries(catalog.events)) {
3159
+ derived(name, KIND_PAGE[e.kind] ?? "events", {
3160
+ source: { event: name },
3161
+ range: "7d",
3162
+ interval: intervalForRange("7d")
1865
3163
  });
1866
- for (const r of spec.rollups ?? []) families.add(r.as ?? name);
1867
- }
1868
- for (const as of families) {
1869
- views.push({
1870
- origin: "derived",
1871
- name: `rollup: ${as}`,
1872
- page: "journeys",
1873
- query: { range: "30d", filters: { rollup: as }, display: "breakdown" }
3164
+ }
3165
+ for (const as of Object.keys(catalog.families)) {
3166
+ derived(`rollup: ${as}`, "journeys", { source: { family: as }, range: "30d" });
3167
+ }
3168
+ for (const [ns, names] of Object.entries(catalog.namespaces)) {
3169
+ if (names.length < 2) continue;
3170
+ derived(`namespace: ${ns}`, "explore", {
3171
+ source: { namespace: ns },
3172
+ range: "30d",
3173
+ interval: "day",
3174
+ groupBy: ["field:name"]
3175
+ });
3176
+ }
3177
+ for (const [name, e] of Object.entries(catalog.events)) {
3178
+ if (e.kind !== "usage") continue;
3179
+ const money = e.measures.find((m) => m.key.startsWith("sum:") && m.key.endsWith("_usd"));
3180
+ if (!money) continue;
3181
+ derived(`spend: ${name}`, "usage", {
3182
+ source: { event: name },
3183
+ range: "30d",
3184
+ interval: "day",
3185
+ measure: money.key
3186
+ });
3187
+ }
3188
+ for (const subjectType of catalog.subjectTypes) {
3189
+ const stages = Object.values(catalog.families).filter((f) => f.lifetime && f.by.length === 1 && f.by[0] === "subject" && f.subjectTypes.includes(subjectType)).map((f) => f.as);
3190
+ if (stages.length < 2) continue;
3191
+ derived(`funnel: ${subjectType}`, "journeys", {
3192
+ // any source expands; the family the funnel is anchored on is the honest one
3193
+ source: { family: stages[0] },
3194
+ range: "30d",
3195
+ interval: "week",
3196
+ measure: "funnel",
3197
+ stages,
3198
+ anchor: stages[0],
3199
+ subjectType
1874
3200
  });
1875
3201
  }
1876
3202
  return views;
1877
3203
  }
1878
3204
  async function resolveViews(opts) {
1879
3205
  const byName = /* @__PURE__ */ new Map();
1880
- for (const v of deriveViews(opts.registry)) byName.set(v.name, v);
3206
+ for (const v of deriveViews(opts.registry, opts.catalog)) byName.set(v.name, v);
1881
3207
  for (const v of opts.configured) byName.set(v.name, { ...v, origin: "configured" });
1882
3208
  const saved = await opts.ViewModel.find({
1883
3209
  tenantId: opts.tenantId,
@@ -1936,6 +3262,10 @@ var parseFilter = (q) => {
1936
3262
  for (const k of ["kind", "name", "severity", "env", "service", "release", "subject", "traceId"]) {
1937
3263
  if (typeof q[k] === "string" && q[k]) f[k] = q[k];
1938
3264
  }
3265
+ if (typeof q.name === "string" && q.name.includes(",")) {
3266
+ const names = q.name.split(",").map((s) => s.trim()).filter(Boolean);
3267
+ if (names.length) f.name = names.length === 1 ? names[0] : names;
3268
+ }
1939
3269
  if (typeof q.attrs === "string" && q.attrs) {
1940
3270
  f.attrs = Object.fromEntries(
1941
3271
  String(q.attrs).split(",").map((p) => p.split(":")).filter((p) => p.length >= 2).map(([k, ...v]) => [k, v.join(":")])
@@ -1967,30 +3297,6 @@ var parseDims = (v) => {
1967
3297
  }
1968
3298
  return v.length ? v : void 0;
1969
3299
  };
1970
- function registryProjection(t) {
1971
- return Object.fromEntries(
1972
- Object.entries(t.registry).map(([name, spec]) => [
1973
- name,
1974
- {
1975
- kind: spec.kind,
1976
- origin: spec.origin,
1977
- subjects: spec.subjects,
1978
- description: spec.description,
1979
- attrKeys: spec.attrs ? Object.keys(spec.attrs.shape) : [],
1980
- metricKeys: spec.metrics ? Object.keys(spec.metrics.shape) : [],
1981
- indexedAttrs: spec.indexedAttrs ?? [],
1982
- indexedMetrics: spec.indexedMetrics ?? [],
1983
- rollups: (spec.rollups ?? []).map((r) => ({
1984
- as: r.as ?? name,
1985
- by: r.by,
1986
- bucket: r.bucket ?? null,
1987
- sum: r.sum ?? [],
1988
- subjects: r.subjects ?? []
1989
- }))
1990
- }
1991
- ])
1992
- );
1993
- }
1994
3300
  function createDashboard(opts) {
1995
3301
  const { telemetry: t, viewerAdapter, subjectAdapter, views: configured = [] } = opts;
1996
3302
  if (!viewerAdapter?.resolveViewer) {
@@ -2020,6 +3326,20 @@ function createDashboard(opts) {
2020
3326
  cacheTtlMs: opts.cacheTtlMs,
2021
3327
  cacheSize: opts.cacheSize
2022
3328
  });
3329
+ const catalog = deriveCatalog(t.registry, {
3330
+ platforms: t.models.telemetry.schema.path("client")?.schema?.path("platform")?.enumValues
3331
+ });
3332
+ const registry = projectRegistry(catalog);
3333
+ const values = createValues({
3334
+ catalog,
3335
+ TelemetryModel: t.models.telemetry,
3336
+ RollupModel: t.models.rollups,
3337
+ limits: opts.queryLimits,
3338
+ onSlowQuery: opts.onSlowQuery,
3339
+ slowMs: opts.slowMs,
3340
+ cacheTtlMs: opts.cacheTtlMs,
3341
+ cacheSize: opts.cacheSize
3342
+ });
2023
3343
  const api = express2.Router();
2024
3344
  api.use(express2.json({ limit: "64kb" }));
2025
3345
  api.use(async (req, res, next) => {
@@ -2038,7 +3358,8 @@ function createDashboard(opts) {
2038
3358
  }, next);
2039
3359
  };
2040
3360
  api.get("/registry", h(async (req) => ({
2041
- registry: registryProjection(t),
3361
+ registry,
3362
+ catalog,
2042
3363
  kinds: ["event", "error", "span", "state", "usage"],
2043
3364
  role: req.viewer.role,
2044
3365
  scope: req.viewer.tenantId,
@@ -2061,6 +3382,14 @@ function createDashboard(opts) {
2061
3382
  measure: typeof req.query.measure === "string" ? req.query.measure : void 0
2062
3383
  })
2063
3384
  ));
3385
+ api.get("/breakdown", h(
3386
+ async (req) => q.breakdown(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
3387
+ groupBy: String(req.query.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean),
3388
+ measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
3389
+ interval: req.query.interval || void 0,
3390
+ limit: req.query.limit ? Number(req.query.limit) : void 0
3391
+ })
3392
+ ));
2064
3393
  api.get("/rollups", h(async (req) => {
2065
3394
  if (typeof req.query.as !== "string" || !req.query.as) {
2066
3395
  throw Object.assign(new Error("rollup family required"), { status: 400 });
@@ -2091,7 +3420,7 @@ function createDashboard(opts) {
2091
3420
  limit: req.query.limit ? Number(req.query.limit) : void 0
2092
3421
  })
2093
3422
  ));
2094
- const badRequest = async (run) => {
3423
+ const badRequest3 = async (run) => {
2095
3424
  try {
2096
3425
  return await run();
2097
3426
  } catch (e) {
@@ -2104,7 +3433,7 @@ function createDashboard(opts) {
2104
3433
  if (!stages.length) {
2105
3434
  throw Object.assign(new Error("funnel needs `stages` \u2014 a comma-separated list of rollup families"), { status: 400 });
2106
3435
  }
2107
- return badRequest(() => q.funnel(req.viewer.tenantId, {
3436
+ return badRequest3(() => q.funnel(req.viewer.tenantId, {
2108
3437
  stages,
2109
3438
  exits: parseStages(req.query.exits),
2110
3439
  anchor: typeof req.query.anchor === "string" ? req.query.anchor : void 0,
@@ -2119,22 +3448,46 @@ function createDashboard(opts) {
2119
3448
  if (typeof req.query.as !== "string" || !req.query.as) {
2120
3449
  throw Object.assign(new Error("rollup family required"), { status: 400 });
2121
3450
  }
2122
- return badRequest(() => q.distinctCount(req.viewer.tenantId, {
3451
+ return badRequest3(() => q.distinctCount(req.viewer.tenantId, {
2123
3452
  as: req.query.as,
2124
3453
  subjectType: typeof req.query.subjectType === "string" ? req.query.subjectType : void 0,
2125
3454
  range: parseRange(req.query),
2126
3455
  interval: req.query.interval || void 0
2127
3456
  }));
2128
3457
  }));
3458
+ api.get("/values", h(async (req) => {
3459
+ const dim2 = typeof req.query.dim === "string" ? req.query.dim.trim() : "";
3460
+ if (!dim2) {
3461
+ throw Object.assign(new Error("dim required"), { status: 400 });
3462
+ }
3463
+ const names = String(req.query.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3464
+ return values(req.viewer.tenantId, {
3465
+ dim: dim2,
3466
+ names: names.length ? names : void 0,
3467
+ range: req.query.from || req.query.to ? parseRange(req.query) : void 0,
3468
+ limit: req.query.limit ? Number(req.query.limit) : void 0
3469
+ });
3470
+ }));
2129
3471
  api.get("/subjects/describe", h(async (req) => {
2130
3472
  const refs = String(req.query.refs ?? "").split(",").filter(Boolean).slice(0, 100);
2131
3473
  if (!subjectAdapter) return { refs: {} };
2132
3474
  return { refs: await subjectAdapter.describe(refs) };
2133
3475
  }));
3476
+ api.get("/report", h(
3477
+ async (req) => badRequest3(
3478
+ () => executeReport(q, req.viewer.tenantId, parseReportQuery(req.query), catalog)
3479
+ )
3480
+ ));
3481
+ api.get("/report/plan", h(
3482
+ async (req) => badRequest3(
3483
+ async () => resolveReport(parseReportQuery(req.query), catalog)
3484
+ )
3485
+ ));
2134
3486
  api.get("/views", h(async (req) => ({
2135
3487
  views: await resolveViews({
2136
3488
  ViewModel,
2137
3489
  registry: t.registry,
3490
+ catalog,
2138
3491
  configured,
2139
3492
  tenantId: req.viewer.tenantId,
2140
3493
  viewerRef: req.viewer.viewerRef
@@ -2177,7 +3530,13 @@ function createDashboard(opts) {
2177
3530
  indexCount: indexes.length,
2178
3531
  indexBudget: INDEX_BUDGET,
2179
3532
  keys,
2180
- role: req.viewer.role
3533
+ role: req.viewer.role,
3534
+ // The same three sources, read the other way round: what the data says
3535
+ // the registry is missing, each with the line that would fix it. Derived
3536
+ // from the counters and the quarantine ALREADY fetched above, so the
3537
+ // page costs no extra read. Nothing is written — the host still edits
3538
+ // the registry by hand (reports §9).
3539
+ suggestions: deriveSuggestions({ counters: t.counters, catalog, quarantine })
2181
3540
  };
2182
3541
  }));
2183
3542
  api.post("/system/keys/:id/revoke", h(async (req, res) => {
@@ -2345,6 +3704,6 @@ function createTelemetry(config) {
2345
3704
  };
2346
3705
  }
2347
3706
 
2348
- export { BODY_MAX_CHARS, DEFAULT_LIMITS, Env, INDEX_BUDGET, KeyKind, LogLevel, Origin, PLATFORM_SCOPE, RETENTION_DAYS, SAMPLE_RATE, SCHEMA_VERSION, TelemetryKind, TenantMode, boundedMeta, createDashboard, createIngest, createKey, createQueries, createTelemetry, defaultSpaDir, defineRegistry, deriveViews, findFamily, hashSecret, isPlatformScope, median, newId, parseKeyString, plain, requireMilestoneFamily, resolveDim, summarizeStages, traceKeep, truncate, validateRegistry, verifySecret };
3707
+ export { BODY_MAX_CHARS, COUNTER_MAP_MAX, COUNTER_OVERFLOW_KEY, DEFAULT_LIMITS, Env, INDEX_BUDGET, KeyKind, LogLevel, MAX_SUGGESTIONS, Origin, PLATFORM_SCOPE, RETENTION_DAYS, SAMPLE_RATE, SCHEMA_VERSION, TelemetryKind, TenantMode, boundedMeta, createDashboard, createIngest, createKey, createQueries, createTelemetry, createValues, defaultSpaDir, defineRegistry, deriveCatalog, deriveSuggestions, deriveViews, executeReport, findFamily, foldRollups, hashSecret, intervalForRange, isPlatformScope, median, newId, normalizeQuery, parseKeyString, parseReportQuery, plain, projectRegistry, rangeOf, reportToQuery, requireMilestoneFamily, resolveDim, resolveReport, summarizeStages, traceKeep, truncate, validateRegistry, verifySecret };
2349
3708
  //# sourceMappingURL=index.js.map
2350
3709
  //# sourceMappingURL=index.js.map