@jeffjassky/telemetry 0.3.0 → 0.5.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.cjs CHANGED
@@ -68,8 +68,30 @@ var newCounters = () => ({
68
68
  capped: 0,
69
69
  rollupSkipped: 0,
70
70
  deduped: 0,
71
- truncated: 0
71
+ truncated: 0,
72
+ rollupSkippedBy: {},
73
+ undeclaredAttrs: {},
74
+ subjectsLinked: 0,
75
+ subjectLinkMisses: 0,
76
+ subjectLinkErrors: 0,
77
+ subjectLinkTimeouts: 0,
78
+ subjectLinkUndeclared: 0,
79
+ subjectLinkCapped: 0
72
80
  });
81
+ var COUNTER_MAP_MAX = 1e3;
82
+ var COUNTER_OVERFLOW_KEY = "(other)|(other)";
83
+ var bumpCounterMap = (map, key) => {
84
+ const seen = map[key];
85
+ if (seen !== void 0) {
86
+ map[key] = seen + 1;
87
+ return;
88
+ }
89
+ if (map[COUNTER_OVERFLOW_KEY] !== void 0 || Object.keys(map).length >= COUNTER_MAP_MAX) {
90
+ map[COUNTER_OVERFLOW_KEY] = (map[COUNTER_OVERFLOW_KEY] ?? 0) + 1;
91
+ return;
92
+ }
93
+ map[key] = 1;
94
+ };
73
95
  var traceKeep = (traceId, rate) => {
74
96
  if (rate >= 1) return true;
75
97
  if (!traceId) return Math.random() < rate;
@@ -397,15 +419,15 @@ function buildBaseSchema(collection, registry, counters, opts) {
397
419
  if (this.kind === TelemetryKind.State && !this.state?.to) {
398
420
  throw new Error("telemetry: state requires state.to");
399
421
  }
400
- const check = (label2, m, zschema) => {
422
+ const check = (label3, m, zschema) => {
401
423
  const obj = Object.fromEntries(m ?? []);
402
424
  if (!zschema) {
403
- if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${label2}`);
425
+ if (Object.keys(obj).length) throw new Error(`telemetry: "${this.name}" declares no ${label3}`);
404
426
  return;
405
427
  }
406
428
  const s = zschema.strict?.() ?? zschema;
407
429
  const r = s.safeParse(obj);
408
- if (!r.success) throw new Error(`telemetry: ${label2} invalid for "${this.name}": ${r.error.message}`);
430
+ if (!r.success) throw new Error(`telemetry: ${label3} invalid for "${this.name}": ${r.error.message}`);
409
431
  };
410
432
  check("attrs", this.attrs, spec.attrs);
411
433
  check("metrics", this.metrics, spec.metrics);
@@ -554,6 +576,7 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
554
576
  if (v == null || v === "") {
555
577
  if (spec.dimDefault === void 0) {
556
578
  counters.rollupSkipped++;
579
+ bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
557
580
  return;
558
581
  }
559
582
  v = spec.dimDefault;
@@ -645,6 +668,120 @@ function createCheckpointFactory(CheckpointModel, logger) {
645
668
  }
646
669
 
647
670
  // src/server/emit.ts
671
+ function noteUndeclaredAttrs(counters, name, spec, attrs) {
672
+ if (!attrs || typeof attrs !== "object") return;
673
+ const keys = attrs instanceof Map ? [...attrs.keys()] : Object.keys(attrs);
674
+ const shape = spec.attrs?.shape;
675
+ for (const raw of keys) {
676
+ const key = String(raw).replace(/\./g, "_");
677
+ if (shape && Object.prototype.hasOwnProperty.call(shape, key)) continue;
678
+ bumpCounterMap(counters.undeclaredAttrs, `${name}|${key}`);
679
+ }
680
+ }
681
+ var SUBJECT_MAX = 8;
682
+ var SUBJECT_LINK_TIMEOUT_MS = 50;
683
+ var LINK_TIMEOUT = /* @__PURE__ */ Symbol("telemetry.subjectLink.timeout");
684
+ function createSubjectLinking(opts) {
685
+ const { linker, counters, logger } = opts;
686
+ if (!linker) return null;
687
+ const timeoutMs = opts.timeoutMs ?? SUBJECT_LINK_TIMEOUT_MS;
688
+ const warned = /* @__PURE__ */ new Set();
689
+ const warnOnce = (key, msg) => {
690
+ if (warned.has(key) || warned.size >= COUNTER_MAP_MAX) return;
691
+ warned.add(key);
692
+ logger.warn(msg);
693
+ };
694
+ return async function linkSubjects(name, spec, tenantId, declared) {
695
+ const have = Array.isArray(declared) ? declared : [];
696
+ const seen = /* @__PURE__ */ new Set();
697
+ const view = [];
698
+ for (const s of have) {
699
+ const ref = wellFormed(s);
700
+ if (!ref) continue;
701
+ seen.add(`${ref.type}:${ref.id}`);
702
+ view.push(ref);
703
+ }
704
+ let out;
705
+ let timer;
706
+ try {
707
+ out = await Promise.race([
708
+ // the async wrapper turns a SYNCHRONOUS throw into a rejection, so a
709
+ // linker that dies on its first line lands in the same catch as one
710
+ // whose promise rejects
711
+ (async () => linker.link(view, { name, tenantId }))(),
712
+ new Promise((_, reject) => {
713
+ timer = setTimeout(() => reject(LINK_TIMEOUT), timeoutMs);
714
+ })
715
+ ]);
716
+ } catch (e) {
717
+ if (e === LINK_TIMEOUT) {
718
+ counters.subjectLinkTimeouts++;
719
+ warnOnce(
720
+ "timeout",
721
+ `[telemetry] subjectLinker.link() exceeded ${timeoutMs}ms \u2014 records are being written UNLINKED rather than waiting. The hook is expected to answer from a cache; a resolver that queries per record cannot keep up with ingest. Warned once \u2014 the count is counters.subjectLinkTimeouts.`
722
+ );
723
+ } else {
724
+ counters.subjectLinkErrors++;
725
+ warnOnce(
726
+ "threw",
727
+ `[telemetry] subjectLinker.link() threw \u2014 records are being written unlinked: ${e}. Warned once \u2014 the count is counters.subjectLinkErrors.`
728
+ );
729
+ }
730
+ return null;
731
+ } finally {
732
+ clearTimeout(timer);
733
+ }
734
+ if (!Array.isArray(out)) {
735
+ counters.subjectLinkErrors++;
736
+ warnOnce(
737
+ "shape",
738
+ `[telemetry] subjectLinker.link() resolved to ${typeof out}, not an array \u2014 records are being written unlinked. Return [] when nothing links. Warned once \u2014 the count is counters.subjectLinkErrors.`
739
+ );
740
+ return null;
741
+ }
742
+ if (!out.length) {
743
+ counters.subjectLinkMisses++;
744
+ return null;
745
+ }
746
+ let room = Math.max(0, SUBJECT_MAX - have.length);
747
+ let capped2 = 0;
748
+ const add = [];
749
+ for (const s of out) {
750
+ const ref = wellFormed(s);
751
+ if (!ref) {
752
+ counters.subjectLinkErrors++;
753
+ warnOnce(
754
+ "entry",
755
+ "[telemetry] subjectLinker returned an entry that is not { type, id } \u2014 dropped. Warned once \u2014 the count is counters.subjectLinkErrors."
756
+ );
757
+ continue;
758
+ }
759
+ const key = `${ref.type}:${ref.id}`;
760
+ if (seen.has(key)) continue;
761
+ if (!spec.subjects.includes(ref.type)) {
762
+ counters.subjectLinkUndeclared++;
763
+ }
764
+ if (room <= 0) {
765
+ capped2++;
766
+ continue;
767
+ }
768
+ seen.add(key);
769
+ room--;
770
+ add.push(ref);
771
+ }
772
+ counters.subjectLinkCapped += capped2;
773
+ if (!add.length) return null;
774
+ counters.subjectsLinked += add.length;
775
+ return [...have, ...add];
776
+ };
777
+ }
778
+ function wellFormed(s) {
779
+ if (!s || typeof s !== "object") return null;
780
+ const { type, id, role } = s;
781
+ if (typeof type !== "string" || !type) return null;
782
+ if (typeof id !== "string" || !id) return null;
783
+ return typeof role === "string" && role ? { type, id, role } : { type, id };
784
+ }
648
785
  function createEmitter(ctx) {
649
786
  const { registry, byKind, RollupModel, rejects, counters } = ctx;
650
787
  const burstBuckets = /* @__PURE__ */ new Map();
@@ -681,16 +818,21 @@ function createEmitter(ctx) {
681
818
  const durable = kind === TelemetryKind.Usage || (doc.durable ?? spec.durable ?? false);
682
819
  const Model = byKind[kind];
683
820
  const { forceKeep: _drop, durable: _durable, ...rest } = doc;
821
+ const linked = ctx.linkSubjects ? await ctx.linkSubjects(name, spec, doc.tenantId, doc.subjects) : null;
684
822
  const safe = (o) => new Map(Object.entries(o ?? {}).map(([k, v]) => [k.replace(/\./g, "_"), v]));
685
823
  const payload = {
686
824
  ...rest,
687
825
  _id: id,
688
826
  name,
827
+ // computed like everything below it, and absent when nothing linked, so a
828
+ // host with no linker hands the model the exact object 0.4.0 did
829
+ ...linked ? { subjects: linked } : {},
689
830
  sampleRate: forced ? 1 : baseRate,
690
831
  forced,
691
832
  attrs: safe(doc.attrs),
692
833
  metrics: safe(doc.metrics)
693
834
  };
835
+ noteUndeclaredAttrs(counters, name, spec, doc.attrs);
694
836
  const onFail = async (e) => {
695
837
  counters.rejected++;
696
838
  await rejects().insertOne({ at: /* @__PURE__ */ new Date(), name, reason: String(e), raw: plain(doc) }).catch(() => {
@@ -896,10 +1038,10 @@ function parseKeyString(raw) {
896
1038
  if (!raw) return null;
897
1039
  const m = KEY_RE.exec(raw.trim());
898
1040
  if (!m) return null;
899
- const [, prefix, label2, id, secret] = m;
1041
+ const [, prefix, label3, id, secret] = m;
900
1042
  if (prefix === "sk" && !secret) return null;
901
1043
  if (prefix === "pk" && secret) return null;
902
- return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label: label2, id, secret };
1044
+ return { kind: prefix === "pk" ? KeyKind.Publishable : KeyKind.Secret, label: label3, id, secret };
903
1045
  }
904
1046
  var SCRYPT_VERSION = "scrypt1";
905
1047
  var SCRYPT = { N: 16384, r: 8, p: 1, keylen: 32 };
@@ -957,7 +1099,7 @@ async function createKey(KeyModel, input) {
957
1099
  tenantId,
958
1100
  service,
959
1101
  env,
960
- label: label2 = "live",
1102
+ label: label3 = "live",
961
1103
  origins = [],
962
1104
  allowedNames,
963
1105
  maxPerMinute = 600
@@ -990,7 +1132,7 @@ async function createKey(KeyModel, input) {
990
1132
  createdAt: /* @__PURE__ */ new Date()
991
1133
  });
992
1134
  const prefix = kind === KeyKind.Publishable ? "pk" : "sk";
993
- return { key: secret ? `${prefix}_${label2}_${id}_${secret}` : `${prefix}_${label2}_${id}`, id };
1135
+ return { key: secret ? `${prefix}_${label3}_${id}_${secret}` : `${prefix}_${label3}_${id}`, id };
994
1136
  }
995
1137
  var BATCH_MAX = 100;
996
1138
  function createIngest(opts) {
@@ -1192,6 +1334,9 @@ function createIngest(opts) {
1192
1334
  const occurredRaw = rec.occurredAt ? Date.parse(rec.occurredAt) : NaN;
1193
1335
  const occurredAt = Number.isFinite(occurredRaw) ? new Date(occurredRaw - clockSkewMs) : receivedAt;
1194
1336
  const safeMap = (o) => o && typeof o === "object" ? new Map(Object.entries(o).map(([k, v]) => [k.replace(/\./g, "_"), v])) : /* @__PURE__ */ new Map();
1337
+ noteUndeclaredAttrs(t.counters, name, spec, rec.attrs);
1338
+ const subjects = mergeSubjects(rec.subjects);
1339
+ const linked = t.linkSubjects ? await t.linkSubjects(name, spec, tenantId, subjects) : null;
1195
1340
  const Model = t.models.byKind[spec.kind];
1196
1341
  const d = new Model({
1197
1342
  // facts the wire may not assert: tenant, service, env, origin, plane
@@ -1202,7 +1347,7 @@ function createIngest(opts) {
1202
1347
  tenantId,
1203
1348
  occurredAt,
1204
1349
  severity: typeof rec.severity === "string" ? rec.severity : void 0,
1205
- subjects: mergeSubjects(rec.subjects),
1350
+ subjects: linked ?? subjects,
1206
1351
  actor: ctx.actor ?? (typeof rec.actor === "string" ? rec.actor : batchActor),
1207
1352
  onBehalfOf: typeof rec.onBehalfOf === "string" ? rec.onBehalfOf : void 0,
1208
1353
  service: key.service,
@@ -1262,6 +1407,292 @@ function createIngest(opts) {
1262
1407
  return router;
1263
1408
  }
1264
1409
 
1410
+ // src/server/catalog.ts
1411
+ var label2 = (src) => src.slice(src.indexOf(":") + 1);
1412
+ var LEAF_TYPES = {
1413
+ string: "string",
1414
+ number: "number",
1415
+ int: "number",
1416
+ bigint: "number",
1417
+ boolean: "boolean",
1418
+ date: "date"
1419
+ };
1420
+ function walkAttr(schema) {
1421
+ let node = schema;
1422
+ let optional = false;
1423
+ for (let depth = 0; node && depth < 20; depth++) {
1424
+ const def = node._zod?.def ?? node.def;
1425
+ if (!def?.type) break;
1426
+ switch (def.type) {
1427
+ // these three all mean "the value may be absent from a stored record",
1428
+ // which is the only thing `optional` claims
1429
+ case "optional":
1430
+ case "nullable":
1431
+ case "default":
1432
+ optional = true;
1433
+ node = def.innerType;
1434
+ continue;
1435
+ case "catch":
1436
+ case "readonly":
1437
+ node = def.innerType;
1438
+ continue;
1439
+ // a pipe is `in -> out`; the INPUT side is what a caller may send and so
1440
+ // what a stored value was validated as. The output of a transform is
1441
+ // frequently a shape no filter could ever be written against.
1442
+ case "pipe":
1443
+ node = def.in;
1444
+ continue;
1445
+ case "enum": {
1446
+ const options = Array.isArray(node.options) ? node.options : Object.values(def.entries ?? {});
1447
+ return { type: "enum", values: options.map(String), optional };
1448
+ }
1449
+ case "literal":
1450
+ return { type: "enum", values: [...def.values ?? []].map(String), optional };
1451
+ default:
1452
+ return { type: LEAF_TYPES[def.type] ?? "string", optional };
1453
+ }
1454
+ }
1455
+ return { type: "string", optional };
1456
+ }
1457
+ var dim = (key, type, o = {}) => ({
1458
+ key,
1459
+ // the two pseudo-dims are derived at query time from subjectKeys / actor, so
1460
+ // they carry no `field:` prefix and label to themselves
1461
+ label: key.startsWith("field:") ? key.slice(6) : key,
1462
+ type,
1463
+ ...o.values ? { values: [...o.values] } : {},
1464
+ optional: o.optional ?? false,
1465
+ indexed: o.indexed ?? false
1466
+ });
1467
+ var envelopeDims = (platforms) => [
1468
+ dim("field:kind", "enum", { values: TELEMETRY_KINDS, indexed: true }),
1469
+ dim("field:name", "string", { indexed: true }),
1470
+ dim("field:severity", "enum", { values: Object.values(LogLevel) }),
1471
+ dim("field:env", "enum", { values: Object.values(Env) }),
1472
+ dim("field:service", "string"),
1473
+ dim("field:release", "string"),
1474
+ dim("field:origin", "enum", { values: Object.values(Origin) }),
1475
+ // client context is absent on server-origin records, so both of its dims are optional
1476
+ dim("field:client.platform", "enum", { values: platforms, optional: true }),
1477
+ dim("field:client.appVersion", "string", { optional: true }),
1478
+ dim("subjectType", "string", { optional: true, indexed: true }),
1479
+ dim("actorType", "string", { optional: true })
1480
+ ];
1481
+ var kindDims = (kind) => {
1482
+ switch (kind) {
1483
+ case TelemetryKind.Usage:
1484
+ return [
1485
+ dim("field:usage.meter", "string", { indexed: true }),
1486
+ dim("field:usage.billedTo", "string"),
1487
+ dim("field:usage.unit", "string")
1488
+ ];
1489
+ case TelemetryKind.State:
1490
+ return [
1491
+ dim("field:state.key", "string", { indexed: true }),
1492
+ dim("field:state.to", "string", { indexed: true })
1493
+ ];
1494
+ case TelemetryKind.Error:
1495
+ return [dim("field:error.type", "string"), dim("field:error.handled", "boolean")];
1496
+ default:
1497
+ return [];
1498
+ }
1499
+ };
1500
+ var RAW_OPS = ["avg", "p50", "p95", "p99"];
1501
+ function deriveCatalog(registry, opts = {}) {
1502
+ const platforms = [.../* @__PURE__ */ new Set([...BUILTIN_PLATFORMS, ...opts.platforms ?? []])];
1503
+ const families = {};
1504
+ for (const [name, spec] of Object.entries(registry)) {
1505
+ for (const r of spec.rollups ?? []) {
1506
+ const as = r.as ?? name;
1507
+ const seen = families[as];
1508
+ if (!seen) {
1509
+ families[as] = {
1510
+ as,
1511
+ by: [...r.by],
1512
+ labels: r.by.map(label2),
1513
+ bucket: r.bucket ?? null,
1514
+ lifetime: !r.bucket,
1515
+ // `subjects` only means anything when there is a subject dim to
1516
+ // restrict; without one it selects nothing and claiming it would
1517
+ // offer a subject filter the family cannot answer
1518
+ subjectTypes: r.by.includes("subject") ? [...r.subjects ?? []] : [],
1519
+ sums: [...r.sum ?? []],
1520
+ capture: (r.capture ?? []).map(label2),
1521
+ feeders: [name],
1522
+ retentionDays: r.retentionDays ?? null
1523
+ };
1524
+ continue;
1525
+ }
1526
+ for (const k of r.sum ?? []) if (!seen.sums.includes(k)) seen.sums.push(k);
1527
+ for (const c of r.capture ?? []) {
1528
+ const l = label2(c);
1529
+ if (!seen.capture.includes(l)) seen.capture.push(l);
1530
+ }
1531
+ if (!seen.feeders.includes(name)) seen.feeders.push(name);
1532
+ }
1533
+ }
1534
+ const events = {};
1535
+ const namespaces = {};
1536
+ const subjectTypes = [];
1537
+ const noteSubject = (t) => {
1538
+ if (!subjectTypes.includes(t)) subjectTypes.push(t);
1539
+ };
1540
+ for (const [name, spec] of Object.entries(registry)) {
1541
+ const dot = name.indexOf(".");
1542
+ const namespace = dot === -1 ? name : name.slice(0, dot);
1543
+ (namespaces[namespace] ??= []).push(name);
1544
+ for (const s of spec.subjects) noteSubject(s);
1545
+ const indexedAttrs = [...spec.indexedAttrs ?? []];
1546
+ const dims = Object.entries(spec.attrs?.shape ?? {}).map(
1547
+ ([key, schema]) => {
1548
+ const walked = walkAttr(schema);
1549
+ return {
1550
+ key: `attr:${key}`,
1551
+ label: key,
1552
+ type: walked.type,
1553
+ ...walked.values ? { values: walked.values } : {},
1554
+ optional: walked.optional,
1555
+ indexed: indexedAttrs.includes(key)
1556
+ };
1557
+ }
1558
+ );
1559
+ dims.push(...kindDims(spec.kind));
1560
+ const eventFamilies = [];
1561
+ const ownSums = /* @__PURE__ */ new Map();
1562
+ for (const r of spec.rollups ?? []) {
1563
+ const as = r.as ?? name;
1564
+ if (!eventFamilies.includes(as)) eventFamilies.push(as);
1565
+ const set = ownSums.get(as) ?? /* @__PURE__ */ new Set();
1566
+ for (const k of r.sum ?? []) set.add(k);
1567
+ ownSums.set(as, set);
1568
+ for (const s of r.subjects ?? []) noteSubject(s);
1569
+ }
1570
+ const measures = [{ key: "count", exactVia: [] }];
1571
+ for (const k of Object.keys(spec.metrics?.shape ?? {})) {
1572
+ measures.push({
1573
+ key: `sum:${k}`,
1574
+ metric: k,
1575
+ exactVia: eventFamilies.filter((as) => ownSums.get(as)?.has(k))
1576
+ });
1577
+ for (const op of RAW_OPS) measures.push({ key: `${op}:${k}`, metric: k, exactVia: [] });
1578
+ }
1579
+ if (spec.kind === TelemetryKind.Span) {
1580
+ for (const op of RAW_OPS) {
1581
+ measures.push({ key: `${op}:durationMs`, metric: "durationMs", exactVia: [] });
1582
+ }
1583
+ }
1584
+ events[name] = {
1585
+ kind: spec.kind,
1586
+ origin: spec.origin,
1587
+ subjects: [...spec.subjects],
1588
+ description: spec.description,
1589
+ namespace,
1590
+ dims,
1591
+ measures,
1592
+ families: eventFamilies,
1593
+ indexedAttrs,
1594
+ indexedMetrics: [...spec.indexedMetrics ?? []],
1595
+ // `hasOwnProperty` rather than `??`, exactly as model.ts stamps expiresAt:
1596
+ // an explicit `retentionDays: null` means immortal and must not fall
1597
+ // through to the per-kind default
1598
+ retentionDays: Object.prototype.hasOwnProperty.call(spec, "retentionDays") ? spec.retentionDays ?? null : RETENTION_DAYS[spec.kind]
1599
+ };
1600
+ }
1601
+ return { events, families, namespaces, envelope: envelopeDims(platforms), subjectTypes };
1602
+ }
1603
+ function projectRegistry(catalog) {
1604
+ return Object.fromEntries(
1605
+ Object.entries(catalog.events).map(([name, e]) => [
1606
+ name,
1607
+ {
1608
+ kind: e.kind,
1609
+ origin: e.origin,
1610
+ subjects: e.subjects,
1611
+ description: e.description,
1612
+ attrKeys: e.dims.filter((d) => d.key.startsWith("attr:")).map((d) => d.label),
1613
+ // every metric key gets exactly one `sum:` measure and nothing else does
1614
+ metricKeys: e.measures.filter((m) => m.key.startsWith("sum:")).map((m) => m.metric),
1615
+ indexedAttrs: e.indexedAttrs,
1616
+ indexedMetrics: e.indexedMetrics,
1617
+ rollups: e.families.map((as) => {
1618
+ const f = catalog.families[as];
1619
+ return { as: f.as, by: f.by, bucket: f.bucket, sum: f.sums, subjects: f.subjectTypes };
1620
+ })
1621
+ }
1622
+ ])
1623
+ );
1624
+ }
1625
+
1626
+ // src/server/suggest.ts
1627
+ var UNREGISTERED_REASON = "unregistered event";
1628
+ var MAX_SUGGESTIONS = 50;
1629
+ var NAME_MAX = 120;
1630
+ var quote = (s) => /^[A-Za-z0-9_.:$-]+$/.test(s) ? `'${s}'` : JSON.stringify(s);
1631
+ var prop = (s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) ? s : quote(s);
1632
+ var times = (n) => `${n} time${n === 1 ? "" : "s"}`;
1633
+ var split = (k) => {
1634
+ const i = k.indexOf("|");
1635
+ return i === -1 ? [k, ""] : [k.slice(0, i), k.slice(i + 1)];
1636
+ };
1637
+ function deriveSuggestions(input) {
1638
+ const { counters, catalog, quarantine = [] } = input;
1639
+ const out = [];
1640
+ for (const [k, count2] of Object.entries(counters.undeclaredAttrs ?? {})) {
1641
+ if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
1642
+ const [name, key] = split(k);
1643
+ if (!name || !key) continue;
1644
+ const facet = catalog.events[name];
1645
+ const line = `${prop(key)}: z.string().max(64),`;
1646
+ const hasAttrs = !!facet?.dims.some((d) => d.key.startsWith("attr:"));
1647
+ out.push({
1648
+ kind: "undeclared_attr",
1649
+ target: name,
1650
+ key,
1651
+ count: count2,
1652
+ message: `\`${name}\` has been sent with attr \`${key}\` ${times(count2)} \u2014 not declared`,
1653
+ fix: hasAttrs ? line : `attrs: z.object({ ${prop(key)}: z.string().max(64) }),`
1654
+ });
1655
+ }
1656
+ for (const [k, count2] of Object.entries(counters.rollupSkippedBy ?? {})) {
1657
+ if (k === COUNTER_OVERFLOW_KEY || !count2) continue;
1658
+ const [as, dim2] = split(k);
1659
+ if (!as || !dim2) continue;
1660
+ const feeders = catalog.families[as]?.feeders ?? [];
1661
+ const where = feeders.length ? `// on the \`${as}\` rollup of ${feeders.map((f) => `\`${f}\``).join(", ")}
1662
+ ` : "";
1663
+ out.push({
1664
+ kind: "missing_dim_default",
1665
+ target: as,
1666
+ key: dim2,
1667
+ count: count2,
1668
+ message: `\`${as}\` skipped ${count2} record${count2 === 1 ? "" : "s"} with no \`${dim2}\` \u2014 declare \`dimDefault\``,
1669
+ fix: `${where}dimDefault: 'unknown',`
1670
+ });
1671
+ }
1672
+ const unregistered = /* @__PURE__ */ new Map();
1673
+ for (const row of quarantine) {
1674
+ if (typeof row?.reason !== "string" || !row.reason.includes(UNREGISTERED_REASON)) continue;
1675
+ const name = typeof row.name === "string" ? row.name.slice(0, NAME_MAX) : "";
1676
+ if (!name || name === "(unnamed)") continue;
1677
+ unregistered.set(name, (unregistered.get(name) ?? 0) + 1);
1678
+ }
1679
+ for (const [name, count2] of unregistered) {
1680
+ out.push({
1681
+ kind: "unregistered_event",
1682
+ target: name,
1683
+ count: count2,
1684
+ message: `\`${name}\` was rejected ${times(count2)} \u2014 not in the registry`,
1685
+ // the minimum that boots: validateRegistry wants a kind, an origin, and
1686
+ // a subjects array, and nothing here can guess the rest
1687
+ fix: `${quote(name)}: { kind: 'event', origin: 'client', subjects: [], description: '' },`
1688
+ });
1689
+ }
1690
+ out.sort(
1691
+ (a, b) => b.count - a.count || a.target.localeCompare(b.target) || (a.key ?? "").localeCompare(b.key ?? "")
1692
+ );
1693
+ return out.slice(0, MAX_SUGGESTIONS);
1694
+ }
1695
+
1265
1696
  // src/server/funnel.ts
1266
1697
  var DAY_MS = 864e5;
1267
1698
  function median(values) {
@@ -1469,6 +1900,10 @@ var DEFAULT_LIMITS = {
1469
1900
  rollups: 500,
1470
1901
  trace: 500,
1471
1902
  journey: 500,
1903
+ breakdown: 50,
1904
+ // top groups — a starting point, to be measured on real hosts
1905
+ values: 200,
1906
+ // top values of one dimension — a picker, not a table
1472
1907
  distribution: 1e5,
1473
1908
  distinct: 1e5,
1474
1909
  funnel: 5e3
@@ -1482,7 +1917,10 @@ function buildMatch(scope, range, f) {
1482
1917
  occurredAt: { $gte: range.from, $lt: range.to }
1483
1918
  };
1484
1919
  for (const k of ["kind", "name", "severity", "env", "service", "release", "traceId"]) {
1485
- if (f[k]) match[k] = f[k];
1920
+ const v = f[k];
1921
+ if (Array.isArray(v)) {
1922
+ if (v.length) match[k] = { $in: v };
1923
+ } else if (v) match[k] = v;
1486
1924
  }
1487
1925
  if (f.subject) match.subjectKeys = f.subject;
1488
1926
  for (const [k, v] of Object.entries(f.attrs ?? {})) match[`attrs.${k}`] = v;
@@ -1505,6 +1943,68 @@ function buildMatch(scope, range, f) {
1505
1943
  }
1506
1944
  return match;
1507
1945
  }
1946
+ var INTERVALS = ["hour", "day", "week", "month"];
1947
+ var truncTo = (path3, unit) => ({
1948
+ $dateTrunc: { date: path3, unit, ...unit === "week" ? { startOfWeek: "monday" } : {} }
1949
+ });
1950
+ function measureAccumulator(measure) {
1951
+ const m = /^(sum|avg):(.+)$/.exec(measure);
1952
+ if (!m) return { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
1953
+ const path3 = m[2] === "durationMs" ? "$durationMs" : `$metrics.${m[2]}`;
1954
+ return m[1] === "sum" ? { $sum: path3 } : { $avg: path3 };
1955
+ }
1956
+ var badRequest = (message) => Object.assign(new Error(`telemetry: breakdown() \u2014 ${message}`), { status: 400 });
1957
+ var BREAKDOWN_FIELDS = [
1958
+ "kind",
1959
+ "name",
1960
+ "severity",
1961
+ "env",
1962
+ "service",
1963
+ "release",
1964
+ "origin",
1965
+ "client.platform",
1966
+ "client.appVersion",
1967
+ "usage.meter",
1968
+ "usage.billedTo",
1969
+ "usage.unit",
1970
+ "state.key",
1971
+ "state.to",
1972
+ "error.type",
1973
+ "error.handled"
1974
+ ];
1975
+ var typePrefix = (ref) => ({
1976
+ $let: {
1977
+ vars: { ref },
1978
+ in: {
1979
+ $cond: [
1980
+ { $eq: [{ $type: "$$ref" }, "string"] },
1981
+ { $arrayElemAt: [{ $split: ["$$ref", ":"] }, 0] },
1982
+ null
1983
+ ]
1984
+ }
1985
+ }
1986
+ });
1987
+ function dimExpression(dim2) {
1988
+ if (dim2.startsWith("attr:")) {
1989
+ const key = dim2.slice(5);
1990
+ if (!key) throw badRequest('`attr:` needs a key, e.g. "attr:plan"');
1991
+ return { $ifNull: [`$attrs.${key}`, null] };
1992
+ }
1993
+ if (dim2.startsWith("field:")) {
1994
+ const path3 = dim2.slice(6);
1995
+ if (!BREAKDOWN_FIELDS.includes(path3)) {
1996
+ throw badRequest(
1997
+ `"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.`
1998
+ );
1999
+ }
2000
+ return { $ifNull: [`$${path3}`, null] };
2001
+ }
2002
+ if (dim2 === "subjectType") return typePrefix({ $arrayElemAt: ["$subjectKeys", 0] });
2003
+ if (dim2 === "actorType") return typePrefix("$actor");
2004
+ throw badRequest(
2005
+ `"${dim2}" is not a dimension. Use "attr:<key>", "field:<path>", "subjectType" or "actorType".`
2006
+ );
2007
+ }
1508
2008
  var QueryCache = class {
1509
2009
  constructor(ttlMs, cap) {
1510
2010
  this.ttlMs = ttlMs;
@@ -1578,16 +2078,9 @@ function createQueries(ctx) {
1578
2078
  return cache.get(
1579
2079
  key,
1580
2080
  () => timed("series", { scope, filter, measure, interval }, async () => {
1581
- const m = /^(sum|avg):(.+)$/.exec(measure);
1582
- const value = !m ? { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } } : m[1] === "sum" ? { $sum: `$metrics.${m[2]}` } : { $avg: `$metrics.${m[2]}` };
1583
2081
  const buckets = await ctx.TelemetryModel.aggregate([
1584
2082
  { $match: buildMatch(scope, range, filter) },
1585
- {
1586
- $group: {
1587
- _id: { $dateTrunc: { date: "$occurredAt", unit: interval, ...interval === "week" ? { startOfWeek: "monday" } : {} } },
1588
- value
1589
- }
1590
- },
2083
+ { $group: { _id: truncTo("$occurredAt", interval), value: measureAccumulator(measure) } },
1591
2084
  { $sort: { _id: 1 } },
1592
2085
  { $limit: limits.series }
1593
2086
  ]);
@@ -1595,6 +2088,114 @@ function createQueries(ctx) {
1595
2088
  })
1596
2089
  );
1597
2090
  },
2091
+ /**
2092
+ * Top groups of a measure by one or two dimensions — "which models cost the
2093
+ * most", "errors by release", "events by platform per week". The primitive
2094
+ * that replaces a page's client-side grouping of whatever rows it happened
2095
+ * to have fetched, which answered "this page" while reading like it
2096
+ * answered the range (reports §6).
2097
+ *
2098
+ * THE CAP IS ON GROUPS RETURNED, NEVER ON ROWS SCANNED. Every `$limit`
2099
+ * below sits AFTER a `$group`, exactly as series() does: the scan is bounded
2100
+ * by buildMatch — tenant, range, filters, indexes — and nothing else, so a
2101
+ * quarter of a million records is one pass and 50 rows. Truncation
2102
+ * therefore keeps the TOP groups by measure, which is what a breakdown
2103
+ * table means; a cap on documents scanned would return an arbitrary prefix
2104
+ * and call it the top.
2105
+ *
2106
+ * With an `interval` this runs a SECOND aggregate restricted to the top
2107
+ * groups, rather than one pipeline that groups by (dims, bucket) and folds.
2108
+ * Two reasons: the ranking must be the measure over the WHOLE range (the
2109
+ * same number the no-interval call reports), and folding in one pass means
2110
+ * `$push`-ing every bucket of every group before the cap can apply — the
2111
+ * unbounded intermediate this primitive exists to avoid. The restriction is
2112
+ * an `$expr`/`$or` over the ≤ cap tuples because a dim can be a computed
2113
+ * expression (subjectType), which a plain `$in` on a path cannot address.
2114
+ *
2115
+ * Under PLATFORM_SCOPE it aggregates ACROSS tenants, like series() — one
2116
+ * set of groups with every tenant summed into it, which is the platform-wide
2117
+ * table a platform operator came for. Ask for a per-tenant split by scoping
2118
+ * to a tenant, or with rollups().
2119
+ */
2120
+ breakdown(scope, range, filter, opts) {
2121
+ const groupBy = opts.groupBy ?? [];
2122
+ if (groupBy.length < 1 || groupBy.length > 2) {
2123
+ throw badRequest(`groupBy takes 1 or 2 dimensions, got ${groupBy.length}`);
2124
+ }
2125
+ const measure = opts.measure ?? "count";
2126
+ const interval = opts.interval;
2127
+ if (interval && !INTERVALS.includes(interval)) {
2128
+ throw badRequest(`interval must be one of ${INTERVALS.join(", ")}`);
2129
+ }
2130
+ const dims = groupBy.map(dimExpression);
2131
+ const cap = Math.min(Math.max(1, opts.limit ?? limits.breakdown), limits.breakdown);
2132
+ const key = JSON.stringify([
2133
+ "breakdown",
2134
+ scope,
2135
+ range.from,
2136
+ range.to,
2137
+ filter,
2138
+ groupBy,
2139
+ measure,
2140
+ interval ?? null,
2141
+ cap
2142
+ ]);
2143
+ return cache.get(
2144
+ key,
2145
+ () => timed("breakdown", { scope, filter, groupBy, measure, interval }, async () => {
2146
+ const match = buildMatch(scope, range, filter);
2147
+ const dimId = Object.fromEntries(dims.map((expr, i) => [`d${i}`, expr]));
2148
+ const top = await ctx.TelemetryModel.aggregate([
2149
+ { $match: match },
2150
+ { $group: { _id: dimId, value: measureAccumulator(measure) } },
2151
+ { $sort: { value: -1, _id: 1 } },
2152
+ { $limit: cap + 1 }
2153
+ ]);
2154
+ const truncated = top.length > cap;
2155
+ if (truncated) top.pop();
2156
+ const tuples = top.map(
2157
+ (g) => groupBy.map((_, i) => g._id?.[`d${i}`] ?? null)
2158
+ );
2159
+ if (!interval) {
2160
+ return {
2161
+ rows: top.map((g, i) => ({ dims: tuples[i], value: g.value })),
2162
+ groups: top.length,
2163
+ truncated,
2164
+ bucketsTruncated: false,
2165
+ dataSource: "raw"
2166
+ };
2167
+ }
2168
+ if (!tuples.length) {
2169
+ return { rows: [], groups: 0, truncated, bucketsTruncated: false, dataSource: "raw" };
2170
+ }
2171
+ const inTop = {
2172
+ $or: tuples.map((t) => ({ $and: dims.map((expr, i) => ({ $eq: [expr, t[i] ?? null] })) }))
2173
+ };
2174
+ const bucketCap = limits.series * top.length;
2175
+ const perBucket = await ctx.TelemetryModel.aggregate([
2176
+ { $match: { ...match, $expr: inTop } },
2177
+ { $group: { _id: { at: truncTo("$occurredAt", interval), ...dimId }, value: measureAccumulator(measure) } },
2178
+ // `at` is the first key of `_id`, so one BSON sort orders by bucket
2179
+ // then by dims — deterministic without a second sort key
2180
+ { $sort: { _id: 1 } },
2181
+ { $limit: bucketCap + 1 }
2182
+ ]);
2183
+ const bucketsTruncated = perBucket.length > bucketCap;
2184
+ if (bucketsTruncated) perBucket.pop();
2185
+ return {
2186
+ rows: perBucket.map((b) => ({
2187
+ dims: groupBy.map((_, i) => b._id?.[`d${i}`] ?? null),
2188
+ at: b._id.at,
2189
+ value: b.value
2190
+ })),
2191
+ groups: top.length,
2192
+ truncated,
2193
+ bucketsTruncated,
2194
+ dataSource: "raw"
2195
+ };
2196
+ })
2197
+ );
2198
+ },
1598
2199
  /**
1599
2200
  * Percentiles + histogram off raw. Keep-all makes the SAMPLE complete —
1600
2201
  * no sampling stands between the match and the math (§5.3) — but the
@@ -1836,6 +2437,820 @@ function requireDistinctFamily(registry, as) {
1836
2437
  }
1837
2438
  return spec;
1838
2439
  }
2440
+
2441
+ // src/server/values.ts
2442
+ var empty = (source) => ({
2443
+ values: [],
2444
+ source,
2445
+ truncated: false,
2446
+ dataSource: source
2447
+ });
2448
+ var SAMPLED_COUNT = { $sum: { $divide: [1, { $ifNull: ["$sampleRate", 1] }] } };
2449
+ function createValues(ctx) {
2450
+ const limits = { ...DEFAULT_LIMITS, ...ctx.limits };
2451
+ const slowMs = ctx.slowMs ?? 500;
2452
+ const cache = new QueryCache(ctx.cacheTtlMs ?? 10 * 6e4, ctx.cacheSize ?? 60);
2453
+ const { catalog } = ctx;
2454
+ const timed = async (op, params, run) => {
2455
+ const t0 = Date.now();
2456
+ try {
2457
+ return await run();
2458
+ } finally {
2459
+ const ms = Date.now() - t0;
2460
+ if (ms > slowMs) ctx.onSlowQuery?.({ op, ms, params });
2461
+ }
2462
+ };
2463
+ const eventNames = (names) => names?.length ? names.filter((n) => catalog.events[n]) : Object.keys(catalog.events);
2464
+ function fromCatalog(dim2, names) {
2465
+ const facets = [];
2466
+ for (const d of catalog.envelope) if (d.key === dim2) facets.push(d);
2467
+ for (const name of eventNames(names)) {
2468
+ for (const d of catalog.events[name].dims) if (d.key === dim2) facets.push(d);
2469
+ }
2470
+ const out = [];
2471
+ for (const f of facets) for (const v of f.values ?? []) if (!out.includes(v)) out.push(v);
2472
+ return out;
2473
+ }
2474
+ function pickFamily(dim2, names) {
2475
+ if (dim2 === "subjectType" || dim2 === "actorType") return null;
2476
+ 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));
2477
+ const best = matches[0];
2478
+ return best ? { as: best.f.as, index: best.index, label: best.f.labels[best.index] } : null;
2479
+ }
2480
+ function rawReadable(dim2, names) {
2481
+ try {
2482
+ dimExpression(dim2);
2483
+ } catch {
2484
+ return false;
2485
+ }
2486
+ if (!dim2.startsWith("attr:")) return true;
2487
+ const key = dim2.slice(5);
2488
+ return eventNames(names).some((n) => catalog.events[n].indexedAttrs.includes(key));
2489
+ }
2490
+ return async function values(scope, params) {
2491
+ const { dim: dim2, names, range } = params;
2492
+ if (!dim2) return empty("none");
2493
+ const cap = Math.min(Math.max(1, params.limit ?? limits.values), limits.values);
2494
+ const key = JSON.stringify([
2495
+ "values",
2496
+ scope,
2497
+ dim2,
2498
+ names ?? null,
2499
+ range?.from ?? null,
2500
+ range?.to ?? null,
2501
+ cap
2502
+ ]);
2503
+ return cache.get(
2504
+ key,
2505
+ () => timed("values", { scope, dim: dim2, names }, async () => {
2506
+ const declared = fromCatalog(dim2, names);
2507
+ if (declared.length) {
2508
+ return { values: declared, source: "catalog", truncated: false, dataSource: "catalog" };
2509
+ }
2510
+ const family = pickFamily(dim2, names);
2511
+ if (family) {
2512
+ const rows2 = await ctx.RollupModel.aggregate([
2513
+ {
2514
+ $match: {
2515
+ ...isPlatformScope(scope) ? {} : { tenantId: scope },
2516
+ as: family.as
2517
+ }
2518
+ },
2519
+ { $project: { v: { $arrayElemAt: ["$dims", family.index] }, count: 1 } },
2520
+ { $match: { v: { $type: "string" } } },
2521
+ { $group: { _id: "$v", count: { $sum: "$count" } } },
2522
+ { $sort: { count: -1, _id: 1 } },
2523
+ { $limit: cap + 1 }
2524
+ ]);
2525
+ const truncated2 = rows2.length > cap;
2526
+ if (truncated2) rows2.pop();
2527
+ const prefix = `${family.label}=`;
2528
+ return {
2529
+ values: rows2.map(
2530
+ (r) => String(r._id).startsWith(prefix) ? String(r._id).slice(prefix.length) : String(r._id)
2531
+ ),
2532
+ counts: rows2.map((r) => r.count),
2533
+ source: "rollups",
2534
+ via: family.as,
2535
+ truncated: truncated2,
2536
+ dataSource: "rollups"
2537
+ };
2538
+ }
2539
+ if (!rawReadable(dim2, names)) return empty("none");
2540
+ if (!range) return empty("none");
2541
+ const rows = await ctx.TelemetryModel.aggregate([
2542
+ { $match: buildMatch(scope, range, names?.length ? { name: names } : {}) },
2543
+ { $group: { _id: dimExpression(dim2), count: SAMPLED_COUNT } },
2544
+ // a "no value" is not a value to pick — the null group is real
2545
+ // (breakdown reports it) but it is not something a filter can name
2546
+ { $match: { _id: { $ne: null } } },
2547
+ { $sort: { count: -1, _id: 1 } },
2548
+ { $limit: cap + 1 }
2549
+ ]);
2550
+ const truncated = rows.length > cap;
2551
+ if (truncated) rows.pop();
2552
+ return {
2553
+ values: rows.map((r) => String(r._id)),
2554
+ counts: rows.map((r) => r.count),
2555
+ source: "raw",
2556
+ truncated,
2557
+ dataSource: "raw"
2558
+ };
2559
+ })
2560
+ );
2561
+ };
2562
+ }
2563
+
2564
+ // src/server/report.ts
2565
+ var RANGE_MS = {
2566
+ "1h": 36e5,
2567
+ "24h": 864e5,
2568
+ "7d": 7 * 864e5,
2569
+ "30d": 30 * 864e5,
2570
+ "90d": 90 * 864e5
2571
+ };
2572
+ var badRequest2 = (message) => Object.assign(new Error(`telemetry: ${message}`), { status: 400 });
2573
+ function rangeOf(range, now = /* @__PURE__ */ new Date()) {
2574
+ if (typeof range === "string") {
2575
+ const ms = RANGE_MS[range] ?? spanOf(range);
2576
+ if (ms == null) {
2577
+ throw badRequest2(
2578
+ `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`
2579
+ );
2580
+ }
2581
+ return { from: new Date(now.getTime() - ms), to: now };
2582
+ }
2583
+ const from = new Date(range.from);
2584
+ const to = new Date(range.to);
2585
+ if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()) || from >= to) {
2586
+ throw badRequest2(
2587
+ `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)`
2588
+ );
2589
+ }
2590
+ return { from, to };
2591
+ }
2592
+ function spanOf(range) {
2593
+ const m = /^(\d+)([hd])$/.exec(range);
2594
+ if (!m) return null;
2595
+ return Number(m[1]) * (m[2] === "h" ? 36e5 : 864e5);
2596
+ }
2597
+ function intervalForRange(range, now = /* @__PURE__ */ new Date()) {
2598
+ if (typeof range === "string" && RANGE_MS[range] != null) {
2599
+ return range === "1h" || range === "24h" ? "hour" : range === "90d" ? "week" : "day";
2600
+ }
2601
+ const { from, to } = rangeOf(range, now);
2602
+ const ms = to.getTime() - from.getTime();
2603
+ if (ms <= 864e5) return "hour";
2604
+ if (ms < 90 * 864e5) return "day";
2605
+ return "week";
2606
+ }
2607
+ var INTERVAL_RANK = { hour: 0, day: 1, week: 2, month: 3 };
2608
+ var shift = (range) => ({
2609
+ from: new Date(range.from.getTime() - (range.to.getTime() - range.from.getTime())),
2610
+ to: range.from
2611
+ });
2612
+ function expandSource(source, catalog) {
2613
+ if ("event" in source) {
2614
+ if (!catalog.events[source.event]) {
2615
+ return unavailable(
2616
+ `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`
2617
+ );
2618
+ }
2619
+ return { form: "event", events: [source.event] };
2620
+ }
2621
+ if ("namespace" in source) {
2622
+ const events = catalog.namespaces[source.namespace];
2623
+ if (!events?.length) {
2624
+ return unavailable(
2625
+ `no event name starts with "${source.namespace}." \u2014 the registered namespaces are ${Object.keys(catalog.namespaces).join(", ")}`
2626
+ );
2627
+ }
2628
+ return { form: "namespace", events: [...events] };
2629
+ }
2630
+ if ("kind" in source) {
2631
+ const events = Object.keys(catalog.events).filter((n) => catalog.events[n].kind === source.kind);
2632
+ if (!events.length) {
2633
+ return unavailable(
2634
+ `no event is registered with kind "${source.kind}" \u2014 declare one, or pick a kind the registry uses`
2635
+ );
2636
+ }
2637
+ return { form: "kind", events, kind: source.kind };
2638
+ }
2639
+ const family = catalog.families[source.family];
2640
+ if (!family) {
2641
+ return unavailable(
2642
+ `no rollup family "${source.family}" is declared \u2014 add a \`rollups: [{ as: '${source.family}', by: [...] }]\` block to the event that should feed it`
2643
+ );
2644
+ }
2645
+ return { form: "family", events: [...family.feeders], family };
2646
+ }
2647
+ var FILTER_ONLY = {
2648
+ "field:subject": "subject",
2649
+ "field:traceId": "traceId"
2650
+ };
2651
+ function dimsFor(catalog, events) {
2652
+ const out = /* @__PURE__ */ new Map();
2653
+ for (const d of catalog.envelope) out.set(d.key, d);
2654
+ for (const name of events) {
2655
+ for (const d of catalog.events[name]?.dims ?? []) {
2656
+ const seen = out.get(d.key);
2657
+ out.set(d.key, seen ? { ...seen, indexed: seen.indexed && d.indexed } : d);
2658
+ }
2659
+ }
2660
+ return out;
2661
+ }
2662
+ var measureDeclared = (catalog, events, key) => key === "count" || events.some((n) => catalog.events[n]?.measures.some((m) => m.key === key));
2663
+ var MEASURE_OP = /^(sum|avg|p50|p90|p95|p99):(.+)$/;
2664
+ var unavailable = (why) => ({ unavailable: true, why });
2665
+ var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
2666
+ function resolveReport(report, catalog, opts = {}) {
2667
+ const now = opts.now ?? /* @__PURE__ */ new Date();
2668
+ const limits = opts.limits ?? {};
2669
+ const src = expandSource(report.source, catalog);
2670
+ if ("unavailable" in src) return src;
2671
+ const measure = report.measure ?? "count";
2672
+ const groupBy = report.groupBy ?? [];
2673
+ if (groupBy.length > 2) {
2674
+ return unavailable(
2675
+ `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`
2676
+ );
2677
+ }
2678
+ if (report.interval && INTERVAL_RANK[report.interval] == null) {
2679
+ return unavailable(`interval "${report.interval}" is not one of hour, day, week, month`);
2680
+ }
2681
+ if (measure === "funnel") return planFunnel(report, catalog, now, limits);
2682
+ if (measure.startsWith("distinct:")) return planDistinct(report, catalog, src, measure, now);
2683
+ const opMatch = MEASURE_OP.exec(measure);
2684
+ if (measure !== "count" && !opMatch) {
2685
+ return unavailable(
2686
+ `measure "${measure}" is not a measure \u2014 use 'count', 'sum:<metric>', 'avg:<metric>', 'p50|p95|p99:<metric>', 'distinct:<subjectType>' or 'funnel'`
2687
+ );
2688
+ }
2689
+ if (opMatch && !measureDeclared(catalog, src.events, measure)) {
2690
+ return unavailable(
2691
+ `"${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)}`
2692
+ );
2693
+ }
2694
+ const exact = planRollups(report, catalog, src, measure, groupBy, now, limits);
2695
+ if (exact) return exact;
2696
+ const filter = toRecordFilter(report, catalog, src);
2697
+ if ("unavailable" in filter) return filter;
2698
+ const range = rangeOf(report.range, now);
2699
+ const dims = dimsFor(catalog, src.events);
2700
+ const touched = [...groupBy, ...(report.filters ?? []).map((f) => f.dim)];
2701
+ const unindexed = touched.find((k) => !(dims.get(k)?.indexed ?? FILTER_ONLY[k] != null));
2702
+ 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` : "") : "";
2703
+ const exactness = unindexed != null ? "scan" : "raw";
2704
+ if (opMatch && opMatch[1] !== "sum" && opMatch[1] !== "avg") {
2705
+ if (groupBy.length) {
2706
+ return unavailable(
2707
+ `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`
2708
+ );
2709
+ }
2710
+ return withCompare(report, {
2711
+ primitive: "distribution",
2712
+ args: [range, filter, { measure: opMatch[2] }],
2713
+ exactness,
2714
+ why: `${measure} is approximate by construction ($percentile t-digest over the matched records)${scanWhy}`
2715
+ });
2716
+ }
2717
+ if (groupBy.length) {
2718
+ const bad = groupBy.find((k) => !dims.has(k));
2719
+ if (bad) {
2720
+ return unavailable(
2721
+ `"${bad}" is not a dimension of ${describe(src)} \u2014 group by one of ${[...dims.keys()].join(", ")}`
2722
+ );
2723
+ }
2724
+ return withCompare(report, {
2725
+ primitive: "breakdown",
2726
+ args: [
2727
+ range,
2728
+ filter,
2729
+ {
2730
+ groupBy,
2731
+ measure,
2732
+ ...report.interval ? { interval: report.interval } : {},
2733
+ ...report.limit ? { limit: capped(report.limit, limits.breakdown) } : {}
2734
+ }
2735
+ ],
2736
+ exactness,
2737
+ why: `raw ${measure} by ${groupBy.join(" \xD7 ")} over the range${scanWhy}`
2738
+ });
2739
+ }
2740
+ if (!report.measure && !report.interval) {
2741
+ return withCompare(report, {
2742
+ primitive: "records",
2743
+ args: [range, filter, report.limit ? { limit: capped(report.limit, limits.records) } : {}],
2744
+ exactness,
2745
+ why: `the matching records themselves, newest first${scanWhy}`
2746
+ });
2747
+ }
2748
+ const interval = report.interval ?? intervalForRange(report.range, now);
2749
+ return withCompare(report, {
2750
+ primitive: "series",
2751
+ args: [range, filter, { measure, interval }],
2752
+ exactness,
2753
+ why: `raw ${measure} per ${interval} over the range${scanWhy}`
2754
+ });
2755
+ }
2756
+ var capped = (limit, cap) => cap == null ? limit : Math.max(1, Math.min(limit, cap));
2757
+ var describe = (src) => src.form === "family" ? `family "${src.family.as}"` : src.events.join(", ");
2758
+ var metricList = (catalog, events) => {
2759
+ const keys = /* @__PURE__ */ new Set();
2760
+ for (const n of events) for (const m of catalog.events[n]?.measures ?? []) keys.add(m.key);
2761
+ return keys.size ? [...keys].join(", ") : "nothing but count";
2762
+ };
2763
+ function planFunnel(report, catalog, now, limits) {
2764
+ const stages = report.stages ?? [];
2765
+ if (!stages.length) {
2766
+ return unavailable(
2767
+ "`measure: 'funnel'` needs `stages` \u2014 one or more lifetime `by: ['subject']` rollup family names, in the order a subject reaches them"
2768
+ );
2769
+ }
2770
+ const anchor = report.anchor ?? stages[0];
2771
+ const exits = report.exits ?? [];
2772
+ for (const as of [...stages, anchor, ...exits]) {
2773
+ const refusal = milestoneRefusal(catalog, as);
2774
+ if (refusal) return unavailable(refusal);
2775
+ }
2776
+ const first = catalog.families[stages[0]];
2777
+ for (const as of [...stages.slice(1), anchor]) {
2778
+ const f = catalog.families[as];
2779
+ if (!sameSet(f.subjectTypes, first.subjectTypes)) {
2780
+ return unavailable(
2781
+ `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`
2782
+ );
2783
+ }
2784
+ }
2785
+ if (report.subjectType && !first.subjectTypes.includes(report.subjectType)) {
2786
+ return unavailable(
2787
+ `subjectType "${report.subjectType}" is not one of the stages' subjects (${first.subjectTypes.join(", ")}) \u2014 the cohort would be empty`
2788
+ );
2789
+ }
2790
+ if (report.interval === "hour") {
2791
+ return unavailable(
2792
+ "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"
2793
+ );
2794
+ }
2795
+ const params = {
2796
+ stages: stages.map((as) => ({ as })),
2797
+ anchor,
2798
+ ...exits.length ? { exits: exits.map((as) => ({ as })) } : {},
2799
+ cohort: rangeOf(report.range, now),
2800
+ ...report.subjectType ? { subjectType: report.subjectType } : {},
2801
+ ...report.interval ? { interval: report.interval } : {},
2802
+ ...report.limit ? { limit: capped(report.limit, limits.funnel) } : {}
2803
+ };
2804
+ return withCompare(report, {
2805
+ primitive: "funnel",
2806
+ args: [params],
2807
+ exactness: "exact",
2808
+ via: anchor,
2809
+ why: `cohort funnel over ${count(stages.length, "lifetime milestone family")}, anchored on "${anchor}" \u2014 rollups only, no raw scan`
2810
+ });
2811
+ }
2812
+ function milestoneRefusal(catalog, as) {
2813
+ const f = catalog.families[as];
2814
+ if (!f) {
2815
+ return `no rollup family "${as}" is declared. Add a \`rollups: [{ as: '${as}', by: ['subject'], subjects: [...] }]\` block to the event that marks it`;
2816
+ }
2817
+ const shape = `by: [${f.by.map((d) => `'${d}'`).join(", ")}]${f.bucket ? `, bucket: '${f.bucket}'` : ""}`;
2818
+ if (!f.lifetime) {
2819
+ 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`;
2820
+ }
2821
+ if (f.by.length !== 1 || f.by[0] !== "subject") {
2822
+ 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`;
2823
+ }
2824
+ return null;
2825
+ }
2826
+ function planDistinct(report, catalog, src, measure, now) {
2827
+ const subjectType = measure.slice("distinct:".length);
2828
+ if (!subjectType) {
2829
+ return unavailable(
2830
+ `"${measure}" needs a subject type \u2014 'distinct:account', one of ${catalog.subjectTypes.join(", ")}`
2831
+ );
2832
+ }
2833
+ if (!catalog.subjectTypes.includes(subjectType)) {
2834
+ return unavailable(
2835
+ `no event or rollup declares the subject type "${subjectType}" \u2014 the registry knows ${catalog.subjectTypes.join(", ") || "no subject types at all"}`
2836
+ );
2837
+ }
2838
+ if (report.groupBy?.length) {
2839
+ return unavailable(
2840
+ `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`
2841
+ );
2842
+ }
2843
+ const wanted = new Set(src.events);
2844
+ const fits = Object.values(catalog.families).filter(
2845
+ (f) => f.bucket != null && f.by.length === 1 && f.by[0] === "subject" && f.subjectTypes.includes(subjectType) && src.events.every((e) => f.feeders.includes(e))
2846
+ );
2847
+ const family = fits.find((f) => sameSet(f.feeders, [...wanted])) ?? fits[0];
2848
+ if (!family) {
2849
+ return unavailable(
2850
+ `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`
2851
+ );
2852
+ }
2853
+ const superset = !sameSet(family.feeders, [...wanted]);
2854
+ return withCompare(report, {
2855
+ primitive: "distinctCount",
2856
+ args: [
2857
+ {
2858
+ as: family.as,
2859
+ subjectType,
2860
+ range: rangeOf(report.range, now),
2861
+ ...report.interval ? { interval: report.interval } : {}
2862
+ }
2863
+ ],
2864
+ exactness: "exact",
2865
+ via: family.as,
2866
+ 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` : "")
2867
+ });
2868
+ }
2869
+ var famDims = (f) => f.by.map((b) => b === "subject" ? "subjectType" : b);
2870
+ function planRollups(report, catalog, src, measure, groupBy, now, limits) {
2871
+ if (report.excludeActorTypes?.length) return null;
2872
+ const candidates = src.family ? [src.family] : Object.values(catalog.families).filter((f) => sameSet(f.feeders, src.events));
2873
+ const op = MEASURE_OP.exec(measure);
2874
+ const filters = report.filters ?? [];
2875
+ const fits = candidates.filter((f) => {
2876
+ const dims2 = famDims(f);
2877
+ if (!groupBy.every((k) => dims2.includes(k))) return false;
2878
+ if (report.interval && (!f.bucket || INTERVAL_RANK[f.bucket] > INTERVAL_RANK[report.interval])) return false;
2879
+ if (op) {
2880
+ if (op[1] !== "sum" && op[1] !== "avg" || !f.sums.includes(op[2])) return false;
2881
+ }
2882
+ return filters.every(
2883
+ (t) => dims2.includes(t.dim) && (t.op === "eq" || t.op === "in") || nameFilterCovers(t, f)
2884
+ );
2885
+ });
2886
+ const family = fits.sort((a, b) => a.by.length - b.by.length)[0];
2887
+ if (!family) return null;
2888
+ const dims = famDims(family);
2889
+ const range = rangeOf(report.range, now);
2890
+ const on = family.lifetime ? "firstAt" : "bucketAt";
2891
+ const fold = filters.filter((t) => dims.includes(t.dim));
2892
+ return withCompare(report, {
2893
+ primitive: "rollups",
2894
+ args: [
2895
+ {
2896
+ as: family.as,
2897
+ on,
2898
+ range,
2899
+ sort: family.lifetime ? "count" : "bucketAt",
2900
+ ...report.limit ? { limit: capped(report.limit, limits.rollups) } : {}
2901
+ }
2902
+ ],
2903
+ exactness: "exact",
2904
+ via: family.as,
2905
+ shape: {
2906
+ groupBy,
2907
+ labels: groupBy.map((k) => family.labels[dims.indexOf(k)]),
2908
+ measure,
2909
+ ...report.interval ? { interval: report.interval } : {},
2910
+ ...fold.length ? { filters: fold.map((t) => ({ ...t, label: family.labels[dims.indexOf(t.dim)] })) } : {}
2911
+ },
2912
+ 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` : "")
2913
+ });
2914
+ }
2915
+ function nameFilterCovers(t, f) {
2916
+ if (t.dim !== "field:name") return false;
2917
+ const admitted = t.op === "eq" ? [String(t.value)] : t.op === "in" ? [...t.value].map(String) : null;
2918
+ return admitted != null && f.feeders.every((n) => admitted.includes(n));
2919
+ }
2920
+ var FIELD_TERMS = {
2921
+ "field:kind": "kind",
2922
+ "field:name": "name",
2923
+ "field:severity": "severity",
2924
+ "field:env": "env",
2925
+ "field:service": "service",
2926
+ "field:release": "release",
2927
+ ...FILTER_ONLY
2928
+ };
2929
+ function toRecordFilter(report, catalog, src) {
2930
+ const filter = {};
2931
+ if (src.events.length === 1) {
2932
+ filter.name = src.events[0];
2933
+ } else if (src.kind) {
2934
+ filter.kind = src.kind;
2935
+ } else {
2936
+ filter.name = [...src.events];
2937
+ }
2938
+ for (const t of report.filters ?? []) {
2939
+ const term = FIELD_TERMS[t.dim];
2940
+ if (term) {
2941
+ if (t.op !== "eq") {
2942
+ return unavailable(
2943
+ `"${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`
2944
+ );
2945
+ }
2946
+ filter[term] = String(t.value);
2947
+ continue;
2948
+ }
2949
+ if (t.dim.startsWith("attr:")) {
2950
+ const key = t.dim.slice(5);
2951
+ if (t.op === "eq") {
2952
+ (filter.attrs ??= {})[key] = String(t.value);
2953
+ continue;
2954
+ }
2955
+ if (t.op === "gte" || t.op === "lte") {
2956
+ if (!measureDeclared(catalog, src.events, `sum:${key}`)) {
2957
+ return unavailable(
2958
+ `"${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`
2959
+ );
2960
+ }
2961
+ const range = (filter.metrics ??= {})[key] ??= {};
2962
+ range[t.op] = Number(t.value);
2963
+ continue;
2964
+ }
2965
+ return unavailable(
2966
+ `"${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`
2967
+ );
2968
+ }
2969
+ if (t.dim === "subjectType" || t.dim === "actorType") {
2970
+ return unavailable(
2971
+ `"${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`" : "")
2972
+ );
2973
+ }
2974
+ return unavailable(
2975
+ `"${t.dim}" is not filterable on the raw path \u2014 RecordFilter carries ${Object.keys(FIELD_TERMS).join(", ")}, \`attr:<key>\` and metric bounds`
2976
+ );
2977
+ }
2978
+ if (report.excludeActorTypes?.length) filter.excludeActorTypes = [...report.excludeActorTypes];
2979
+ return filter;
2980
+ }
2981
+ function withCompare(report, plan) {
2982
+ if (report.compare !== "previous") return plan;
2983
+ const [first, ...rest] = plan.args;
2984
+ if (plan.primitive === "rollups" || plan.primitive === "distinctCount" || plan.primitive === "funnel") {
2985
+ const params = first;
2986
+ const key = plan.primitive === "funnel" ? "cohort" : "range";
2987
+ const window = params[key];
2988
+ if (!window) return plan;
2989
+ return { ...plan, previous: { args: [{ ...params, [key]: { ...window, ...shift(window) } }, ...rest] } };
2990
+ }
2991
+ return { ...plan, previous: { args: [shift(first), ...rest] } };
2992
+ }
2993
+ var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
2994
+ var FILTER_OPS = /* @__PURE__ */ new Set(["eq", "in", "gte", "lte"]);
2995
+ var SORTS = /* @__PURE__ */ new Set(["value", "label", "time"]);
2996
+ function parseReportQuery(q) {
2997
+ const str = (k) => {
2998
+ const v = Array.isArray(q[k]) ? q[k][0] : q[k];
2999
+ return typeof v === "string" && v ? v : void 0;
3000
+ };
3001
+ const list = (k) => (str(k) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3002
+ const raw = str("source");
3003
+ if (!raw) {
3004
+ throw badRequest2(
3005
+ "`source` is required \u2014 one of source=event:<name>, namespace:<ns>, kind:<kind>, family:<as>"
3006
+ );
3007
+ }
3008
+ const cut = raw.indexOf(":");
3009
+ const form = cut > 0 ? raw.slice(0, cut) : "";
3010
+ const named = cut > 0 ? raw.slice(cut + 1) : "";
3011
+ if (!named || !["event", "namespace", "kind", "family"].includes(form)) {
3012
+ throw badRequest2(
3013
+ `\`source\` must be "event:<name>", "namespace:<ns>", "kind:<kind>" or "family:<as>" \u2014 got "${raw}"`
3014
+ );
3015
+ }
3016
+ const source = form === "event" ? { event: named } : form === "namespace" ? { namespace: named } : form === "kind" ? { kind: named } : { family: named };
3017
+ const shorthand = str("range");
3018
+ const from = str("from");
3019
+ const to = str("to");
3020
+ if (!shorthand && !(from && to)) {
3021
+ throw badRequest2("a range is required \u2014 either `range=7d` or both `from` and `to` as ISO times");
3022
+ }
3023
+ const range = shorthand ?? { from, to };
3024
+ const interval = str("interval");
3025
+ if (interval && INTERVAL_RANK[interval] == null) {
3026
+ throw badRequest2(`\`interval\` must be one of hour, day, week, month \u2014 got "${interval}"`);
3027
+ }
3028
+ const sort = str("sort");
3029
+ if (sort && !SORTS.has(sort)) {
3030
+ throw badRequest2(`\`sort\` must be one of value, label, time \u2014 got "${sort}"`);
3031
+ }
3032
+ const compare = str("compare");
3033
+ if (compare && compare !== "previous") {
3034
+ throw badRequest2(`\`compare\` takes only "previous" \u2014 got "${compare}"`);
3035
+ }
3036
+ const limitRaw = str("limit");
3037
+ const limit = limitRaw == null ? void 0 : Number(limitRaw);
3038
+ if (limit != null && (!Number.isInteger(limit) || limit < 1)) {
3039
+ throw badRequest2(`\`limit\` must be a positive integer \u2014 got "${limitRaw}"`);
3040
+ }
3041
+ const measure = str("measure");
3042
+ const groupBy = list("groupBy");
3043
+ const excludeActorTypes = list("excludeActors");
3044
+ const stages = list("stages");
3045
+ const exits = list("exits");
3046
+ const anchor = str("anchor");
3047
+ const subjectType = str("subjectType");
3048
+ const filters = (q.filter == null ? [] : Array.isArray(q.filter) ? q.filter.map(String) : [String(q.filter)]).map(parseFilterTerm);
3049
+ return {
3050
+ source,
3051
+ range,
3052
+ ...interval ? { interval } : {},
3053
+ ...measure ? { measure } : {},
3054
+ ...groupBy.length ? { groupBy } : {},
3055
+ ...filters.length ? { filters } : {},
3056
+ ...excludeActorTypes.length ? { excludeActorTypes } : {},
3057
+ ...sort ? { sort } : {},
3058
+ ...limit != null ? { limit } : {},
3059
+ ...compare ? { compare: "previous" } : {},
3060
+ ...stages.length ? { stages } : {},
3061
+ ...anchor ? { anchor } : {},
3062
+ ...exits.length ? { exits } : {},
3063
+ ...subjectType ? { subjectType } : {}
3064
+ };
3065
+ }
3066
+ function parseFilterTerm(term) {
3067
+ const parts = term.split(":");
3068
+ const i = parts.findIndex((p) => FILTER_OPS.has(p));
3069
+ const rest = i < 0 ? "" : parts.slice(i + 1).join(":");
3070
+ if (i < 1 || !rest) {
3071
+ throw badRequest2(
3072
+ `\`filter\` must be "<dim>:<op>:<value>" with op one of eq, in, gte, lte \u2014 got "${term}"`
3073
+ );
3074
+ }
3075
+ const dim2 = parts.slice(0, i).join(":");
3076
+ const op = parts[i];
3077
+ if (op === "in") {
3078
+ const values = rest.split(",").map((s) => s.trim()).filter(Boolean);
3079
+ if (!values.length) throw badRequest2(`\`filter\` "${term}" has an empty \`in\` list`);
3080
+ return { dim: dim2, op, value: values };
3081
+ }
3082
+ if (op === "gte" || op === "lte") {
3083
+ const n = Number(rest);
3084
+ if (Number.isNaN(n)) throw badRequest2(`\`filter\` bound "${term}" is not a number`);
3085
+ return { dim: dim2, op, value: n };
3086
+ }
3087
+ return { dim: dim2, op, value: rest };
3088
+ }
3089
+ function reportToQuery(report) {
3090
+ const s = report.source;
3091
+ const q = {
3092
+ source: "event" in s ? `event:${s.event}` : "namespace" in s ? `namespace:${s.namespace}` : "kind" in s ? `kind:${s.kind}` : `family:${s.family}`
3093
+ };
3094
+ if (typeof report.range === "string") q.range = report.range;
3095
+ else {
3096
+ q.from = report.range.from;
3097
+ q.to = report.range.to;
3098
+ }
3099
+ if (report.interval) q.interval = report.interval;
3100
+ if (report.measure) q.measure = report.measure;
3101
+ if (report.groupBy?.length) q.groupBy = report.groupBy.join(",");
3102
+ if (report.filters?.length) {
3103
+ const terms = report.filters.map(
3104
+ (f) => `${f.dim}:${f.op}:${Array.isArray(f.value) ? f.value.join(",") : String(f.value)}`
3105
+ );
3106
+ q.filter = terms.length === 1 ? terms[0] : terms;
3107
+ }
3108
+ if (report.excludeActorTypes?.length) q.excludeActors = report.excludeActorTypes.join(",");
3109
+ if (report.sort) q.sort = report.sort;
3110
+ if (report.limit != null) q.limit = String(report.limit);
3111
+ if (report.compare) q.compare = report.compare;
3112
+ if (report.stages?.length) q.stages = report.stages.join(",");
3113
+ if (report.anchor) q.anchor = report.anchor;
3114
+ if (report.exits?.length) q.exits = report.exits.join(",");
3115
+ if (report.subjectType) q.subjectType = report.subjectType;
3116
+ return q;
3117
+ }
3118
+ var LEGACY_DIMS = {
3119
+ kind: "field:kind",
3120
+ name: "field:name",
3121
+ severity: "field:severity",
3122
+ env: "field:env",
3123
+ service: "field:service",
3124
+ release: "field:release",
3125
+ subject: "field:subject",
3126
+ traceId: "field:traceId"
3127
+ };
3128
+ function normalizeQuery(query) {
3129
+ if (!query || typeof query !== "object") return null;
3130
+ if ("source" in query && query.source) return query;
3131
+ const q = query;
3132
+ const filters = q.filters ?? {};
3133
+ const str = (v) => typeof v === "string" && v ? v : null;
3134
+ const name = str(filters.name);
3135
+ const family = str(filters.rollup);
3136
+ const kind = str(filters.kind);
3137
+ const source = name ? { event: name } : family ? { family } : kind ? { kind } : null;
3138
+ if (!source) return null;
3139
+ const consumed = name ? "name" : family ? "rollup" : "kind";
3140
+ const terms = [];
3141
+ for (const [k, v] of Object.entries(filters)) {
3142
+ if (k === consumed || k === "rollup") continue;
3143
+ if (k === "excludeActorTypes") continue;
3144
+ if (k === "attrs") {
3145
+ const entries = typeof v === "string" ? v.split(",").map((pair) => pair.split(":").map((s) => s.trim())) : Object.entries(v ?? {}).map(([a, b]) => [a, String(b)]);
3146
+ for (const [key, value2] of entries) {
3147
+ if (key && value2 != null) terms.push({ dim: `attr:${key}`, op: "eq", value: String(value2) });
3148
+ }
3149
+ continue;
3150
+ }
3151
+ const dim2 = LEGACY_DIMS[k];
3152
+ const value = str(v);
3153
+ if (dim2 && value) terms.push({ dim: dim2, op: "eq", value });
3154
+ }
3155
+ const groupBy = (q.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3156
+ const sort = q.sort === "value" || q.sort === "label" || q.sort === "time" ? q.sort : void 0;
3157
+ const actors = filters.excludeActorTypes;
3158
+ return {
3159
+ source,
3160
+ range: q.range ?? "7d",
3161
+ ...terms.length ? { filters: terms } : {},
3162
+ ...groupBy.length ? { groupBy } : {},
3163
+ ...sort ? { sort } : {},
3164
+ ...Array.isArray(actors) && actors.length ? { excludeActorTypes: actors.map(String) } : {}
3165
+ };
3166
+ }
3167
+
3168
+ // src/server/execute.ts
3169
+ async function executeReport(q, scope, report, catalog, opts = {}) {
3170
+ const plan = resolveReport(report, catalog, { now: opts.now, limits: opts.limits });
3171
+ if ("unavailable" in plan) throw Object.assign(new Error(plan.why), { status: 400 });
3172
+ const run = async (args) => {
3173
+ const raw = await q[plan.primitive](scope, ...args);
3174
+ if (plan.primitive === "rollups" && plan.shape) {
3175
+ return foldRollups(raw?.rows ?? [], plan.shape, !!raw?.truncated);
3176
+ }
3177
+ if (plan.primitive === "records" && opts.redact) {
3178
+ return { ...raw, items: opts.redact(raw?.items ?? []) };
3179
+ }
3180
+ return raw;
3181
+ };
3182
+ const [result, previous] = await Promise.all([
3183
+ run(plan.args),
3184
+ plan.previous ? run(plan.previous.args) : Promise.resolve(void 0)
3185
+ ]);
3186
+ return {
3187
+ report,
3188
+ plan,
3189
+ result,
3190
+ ...plan.previous ? { previous } : {},
3191
+ dataSource: result?.dataSource ?? "raw"
3192
+ };
3193
+ }
3194
+ var MEASURE_OP2 = /^(sum|avg):(.+)$/;
3195
+ function foldRollups(rows, shape, truncated = false) {
3196
+ const op = MEASURE_OP2.exec(shape.measure);
3197
+ const groups = /* @__PURE__ */ new Map();
3198
+ for (const doc of rows) {
3199
+ const dims = doc?.dims ?? [];
3200
+ if (!(shape.filters ?? []).every((f) => admits(f, dimValue(dims, f.label)))) continue;
3201
+ const tuple = shape.labels.map((label3) => dimValue(dims, label3));
3202
+ const at = shape.interval && doc.bucketAt ? truncate(new Date(doc.bucketAt), shape.interval) : void 0;
3203
+ const key = `${JSON.stringify(tuple)}|${at ? at.getTime() : ""}`;
3204
+ let g = groups.get(key);
3205
+ if (!g) groups.set(key, g = { dims: tuple, ...at ? { at } : {}, sum: 0, count: 0 });
3206
+ g.count += typeof doc.count === "number" ? doc.count : 0;
3207
+ if (op) g.sum += sumOf(doc.sums, op[2]);
3208
+ }
3209
+ const rowsOut = [...groups.values()].map((g) => ({
3210
+ dims: g.dims,
3211
+ ...g.at ? { at: g.at } : {},
3212
+ // avg is sums[k]/count off the SAME doc, which is exact — not an average of
3213
+ // averages, which is what folding a per-bucket mean would have produced
3214
+ value: !op ? g.count : op[1] === "sum" ? g.sum : g.count ? g.sum / g.count : 0
3215
+ }));
3216
+ rowsOut.sort(
3217
+ shape.interval ? (a, b) => (a.at?.getTime() ?? 0) - (b.at?.getTime() ?? 0) || byDims(a, b) : (a, b) => b.value - a.value || byDims(a, b)
3218
+ );
3219
+ return {
3220
+ rows: rowsOut,
3221
+ groups: new Set([...groups.values()].map((g) => JSON.stringify(g.dims))).size,
3222
+ truncated,
3223
+ dataSource: "rollups"
3224
+ };
3225
+ }
3226
+ function dimValue(dims, label3) {
3227
+ const prefix = `${label3}=`;
3228
+ for (const d of dims) if (d.startsWith(prefix)) return d.slice(prefix.length);
3229
+ for (const d of dims) if (!d.includes("=")) return d;
3230
+ return null;
3231
+ }
3232
+ function admits(f, value) {
3233
+ if (f.op === "in") return f.value.map(String).includes(String(value));
3234
+ if (f.op === "gte" || f.op === "lte") {
3235
+ const n = Number(value);
3236
+ if (Number.isNaN(n)) return false;
3237
+ return f.op === "gte" ? n >= Number(f.value) : n <= Number(f.value);
3238
+ }
3239
+ return String(value) === String(f.value);
3240
+ }
3241
+ function sumOf(sums, key) {
3242
+ if (!sums) return 0;
3243
+ const v = sums instanceof Map ? sums.get(key) : sums[key];
3244
+ return typeof v === "number" ? v : 0;
3245
+ }
3246
+ function byDims(a, b) {
3247
+ for (let i = 0; i < a.dims.length; i++) {
3248
+ const x = a.dims[i] ?? "";
3249
+ const y = b.dims[i] ?? "";
3250
+ if (x !== y) return x < y ? -1 : 1;
3251
+ }
3252
+ return 0;
3253
+ }
1839
3254
  function buildViewModel(connection, modelName, collection) {
1840
3255
  const existing = connection.models?.[modelName];
1841
3256
  if (existing) return existing;
@@ -1862,31 +3277,58 @@ var KIND_PAGE = {
1862
3277
  state: "journeys",
1863
3278
  usage: "usage"
1864
3279
  };
1865
- function deriveViews(registry) {
3280
+ function deriveViews(registry, catalog = deriveCatalog(registry)) {
1866
3281
  const views = [];
1867
- const families = /* @__PURE__ */ new Set();
1868
- for (const [name, spec] of Object.entries(registry)) {
1869
- views.push({
1870
- origin: "derived",
1871
- name,
1872
- page: KIND_PAGE[spec.kind] ?? "events",
1873
- query: { range: "7d", filters: { name }, display: spec.kind === "event" ? "series" : "table" }
3282
+ const derived = (name, page, query) => views.push({ origin: "derived", name, page, query });
3283
+ for (const [name, e] of Object.entries(catalog.events)) {
3284
+ derived(name, KIND_PAGE[e.kind] ?? "events", {
3285
+ source: { event: name },
3286
+ range: "7d",
3287
+ interval: intervalForRange("7d")
1874
3288
  });
1875
- for (const r of spec.rollups ?? []) families.add(r.as ?? name);
1876
- }
1877
- for (const as of families) {
1878
- views.push({
1879
- origin: "derived",
1880
- name: `rollup: ${as}`,
1881
- page: "journeys",
1882
- query: { range: "30d", filters: { rollup: as }, display: "breakdown" }
3289
+ }
3290
+ for (const as of Object.keys(catalog.families)) {
3291
+ derived(`rollup: ${as}`, "journeys", { source: { family: as }, range: "30d" });
3292
+ }
3293
+ for (const [ns, names] of Object.entries(catalog.namespaces)) {
3294
+ if (names.length < 2) continue;
3295
+ derived(`namespace: ${ns}`, "explore", {
3296
+ source: { namespace: ns },
3297
+ range: "30d",
3298
+ interval: "day",
3299
+ groupBy: ["field:name"]
3300
+ });
3301
+ }
3302
+ for (const [name, e] of Object.entries(catalog.events)) {
3303
+ if (e.kind !== "usage") continue;
3304
+ const money = e.measures.find((m) => m.key.startsWith("sum:") && m.key.endsWith("_usd"));
3305
+ if (!money) continue;
3306
+ derived(`spend: ${name}`, "usage", {
3307
+ source: { event: name },
3308
+ range: "30d",
3309
+ interval: "day",
3310
+ measure: money.key
3311
+ });
3312
+ }
3313
+ for (const subjectType of catalog.subjectTypes) {
3314
+ 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);
3315
+ if (stages.length < 2) continue;
3316
+ derived(`funnel: ${subjectType}`, "journeys", {
3317
+ // any source expands; the family the funnel is anchored on is the honest one
3318
+ source: { family: stages[0] },
3319
+ range: "30d",
3320
+ interval: "week",
3321
+ measure: "funnel",
3322
+ stages,
3323
+ anchor: stages[0],
3324
+ subjectType
1883
3325
  });
1884
3326
  }
1885
3327
  return views;
1886
3328
  }
1887
3329
  async function resolveViews(opts) {
1888
3330
  const byName = /* @__PURE__ */ new Map();
1889
- for (const v of deriveViews(opts.registry)) byName.set(v.name, v);
3331
+ for (const v of deriveViews(opts.registry, opts.catalog)) byName.set(v.name, v);
1890
3332
  for (const v of opts.configured) byName.set(v.name, { ...v, origin: "configured" });
1891
3333
  const saved = await opts.ViewModel.find({
1892
3334
  tenantId: opts.tenantId,
@@ -1945,6 +3387,10 @@ var parseFilter = (q) => {
1945
3387
  for (const k of ["kind", "name", "severity", "env", "service", "release", "subject", "traceId"]) {
1946
3388
  if (typeof q[k] === "string" && q[k]) f[k] = q[k];
1947
3389
  }
3390
+ if (typeof q.name === "string" && q.name.includes(",")) {
3391
+ const names = q.name.split(",").map((s) => s.trim()).filter(Boolean);
3392
+ if (names.length) f.name = names.length === 1 ? names[0] : names;
3393
+ }
1948
3394
  if (typeof q.attrs === "string" && q.attrs) {
1949
3395
  f.attrs = Object.fromEntries(
1950
3396
  String(q.attrs).split(",").map((p) => p.split(":")).filter((p) => p.length >= 2).map(([k, ...v]) => [k, v.join(":")])
@@ -1976,30 +3422,6 @@ var parseDims = (v) => {
1976
3422
  }
1977
3423
  return v.length ? v : void 0;
1978
3424
  };
1979
- function registryProjection(t) {
1980
- return Object.fromEntries(
1981
- Object.entries(t.registry).map(([name, spec]) => [
1982
- name,
1983
- {
1984
- kind: spec.kind,
1985
- origin: spec.origin,
1986
- subjects: spec.subjects,
1987
- description: spec.description,
1988
- attrKeys: spec.attrs ? Object.keys(spec.attrs.shape) : [],
1989
- metricKeys: spec.metrics ? Object.keys(spec.metrics.shape) : [],
1990
- indexedAttrs: spec.indexedAttrs ?? [],
1991
- indexedMetrics: spec.indexedMetrics ?? [],
1992
- rollups: (spec.rollups ?? []).map((r) => ({
1993
- as: r.as ?? name,
1994
- by: r.by,
1995
- bucket: r.bucket ?? null,
1996
- sum: r.sum ?? [],
1997
- subjects: r.subjects ?? []
1998
- }))
1999
- }
2000
- ])
2001
- );
2002
- }
2003
3425
  function createDashboard(opts) {
2004
3426
  const { telemetry: t, viewerAdapter, subjectAdapter, views: configured = [] } = opts;
2005
3427
  if (!viewerAdapter?.resolveViewer) {
@@ -2029,6 +3451,20 @@ function createDashboard(opts) {
2029
3451
  cacheTtlMs: opts.cacheTtlMs,
2030
3452
  cacheSize: opts.cacheSize
2031
3453
  });
3454
+ const catalog = deriveCatalog(t.registry, {
3455
+ platforms: t.models.telemetry.schema.path("client")?.schema?.path("platform")?.enumValues
3456
+ });
3457
+ const registry = projectRegistry(catalog);
3458
+ const values = createValues({
3459
+ catalog,
3460
+ TelemetryModel: t.models.telemetry,
3461
+ RollupModel: t.models.rollups,
3462
+ limits: opts.queryLimits,
3463
+ onSlowQuery: opts.onSlowQuery,
3464
+ slowMs: opts.slowMs,
3465
+ cacheTtlMs: opts.cacheTtlMs,
3466
+ cacheSize: opts.cacheSize
3467
+ });
2032
3468
  const api = express2__default.default.Router();
2033
3469
  api.use(express2__default.default.json({ limit: "64kb" }));
2034
3470
  api.use(async (req, res, next) => {
@@ -2047,7 +3483,8 @@ function createDashboard(opts) {
2047
3483
  }, next);
2048
3484
  };
2049
3485
  api.get("/registry", h(async (req) => ({
2050
- registry: registryProjection(t),
3486
+ registry,
3487
+ catalog,
2051
3488
  kinds: ["event", "error", "span", "state", "usage"],
2052
3489
  role: req.viewer.role,
2053
3490
  scope: req.viewer.tenantId,
@@ -2070,6 +3507,14 @@ function createDashboard(opts) {
2070
3507
  measure: typeof req.query.measure === "string" ? req.query.measure : void 0
2071
3508
  })
2072
3509
  ));
3510
+ api.get("/breakdown", h(
3511
+ async (req) => q.breakdown(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
3512
+ groupBy: String(req.query.groupBy ?? "").split(",").map((s) => s.trim()).filter(Boolean),
3513
+ measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
3514
+ interval: req.query.interval || void 0,
3515
+ limit: req.query.limit ? Number(req.query.limit) : void 0
3516
+ })
3517
+ ));
2073
3518
  api.get("/rollups", h(async (req) => {
2074
3519
  if (typeof req.query.as !== "string" || !req.query.as) {
2075
3520
  throw Object.assign(new Error("rollup family required"), { status: 400 });
@@ -2100,7 +3545,7 @@ function createDashboard(opts) {
2100
3545
  limit: req.query.limit ? Number(req.query.limit) : void 0
2101
3546
  })
2102
3547
  ));
2103
- const badRequest = async (run) => {
3548
+ const badRequest3 = async (run) => {
2104
3549
  try {
2105
3550
  return await run();
2106
3551
  } catch (e) {
@@ -2113,7 +3558,7 @@ function createDashboard(opts) {
2113
3558
  if (!stages.length) {
2114
3559
  throw Object.assign(new Error("funnel needs `stages` \u2014 a comma-separated list of rollup families"), { status: 400 });
2115
3560
  }
2116
- return badRequest(() => q.funnel(req.viewer.tenantId, {
3561
+ return badRequest3(() => q.funnel(req.viewer.tenantId, {
2117
3562
  stages,
2118
3563
  exits: parseStages(req.query.exits),
2119
3564
  anchor: typeof req.query.anchor === "string" ? req.query.anchor : void 0,
@@ -2128,22 +3573,46 @@ function createDashboard(opts) {
2128
3573
  if (typeof req.query.as !== "string" || !req.query.as) {
2129
3574
  throw Object.assign(new Error("rollup family required"), { status: 400 });
2130
3575
  }
2131
- return badRequest(() => q.distinctCount(req.viewer.tenantId, {
3576
+ return badRequest3(() => q.distinctCount(req.viewer.tenantId, {
2132
3577
  as: req.query.as,
2133
3578
  subjectType: typeof req.query.subjectType === "string" ? req.query.subjectType : void 0,
2134
3579
  range: parseRange(req.query),
2135
3580
  interval: req.query.interval || void 0
2136
3581
  }));
2137
3582
  }));
3583
+ api.get("/values", h(async (req) => {
3584
+ const dim2 = typeof req.query.dim === "string" ? req.query.dim.trim() : "";
3585
+ if (!dim2) {
3586
+ throw Object.assign(new Error("dim required"), { status: 400 });
3587
+ }
3588
+ const names = String(req.query.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3589
+ return values(req.viewer.tenantId, {
3590
+ dim: dim2,
3591
+ names: names.length ? names : void 0,
3592
+ range: req.query.from || req.query.to ? parseRange(req.query) : void 0,
3593
+ limit: req.query.limit ? Number(req.query.limit) : void 0
3594
+ });
3595
+ }));
2138
3596
  api.get("/subjects/describe", h(async (req) => {
2139
3597
  const refs = String(req.query.refs ?? "").split(",").filter(Boolean).slice(0, 100);
2140
3598
  if (!subjectAdapter) return { refs: {} };
2141
3599
  return { refs: await subjectAdapter.describe(refs) };
2142
3600
  }));
3601
+ api.get("/report", h(
3602
+ async (req) => badRequest3(
3603
+ () => executeReport(q, req.viewer.tenantId, parseReportQuery(req.query), catalog)
3604
+ )
3605
+ ));
3606
+ api.get("/report/plan", h(
3607
+ async (req) => badRequest3(
3608
+ async () => resolveReport(parseReportQuery(req.query), catalog)
3609
+ )
3610
+ ));
2143
3611
  api.get("/views", h(async (req) => ({
2144
3612
  views: await resolveViews({
2145
3613
  ViewModel,
2146
3614
  registry: t.registry,
3615
+ catalog,
2147
3616
  configured,
2148
3617
  tenantId: req.viewer.tenantId,
2149
3618
  viewerRef: req.viewer.viewerRef
@@ -2186,7 +3655,13 @@ function createDashboard(opts) {
2186
3655
  indexCount: indexes.length,
2187
3656
  indexBudget: INDEX_BUDGET,
2188
3657
  keys,
2189
- role: req.viewer.role
3658
+ role: req.viewer.role,
3659
+ // The same three sources, read the other way round: what the data says
3660
+ // the registry is missing, each with the line that would fix it. Derived
3661
+ // from the counters and the quarantine ALREADY fetched above, so the
3662
+ // page costs no extra read. Nothing is written — the host still edits
3663
+ // the registry by hand (reports §9).
3664
+ suggestions: deriveSuggestions({ counters: t.counters, catalog, quarantine })
2190
3665
  };
2191
3666
  }));
2192
3667
  api.post("/system/keys/:id/revoke", h(async (req, res) => {
@@ -2275,7 +3750,22 @@ function createTelemetry(config) {
2275
3750
  inFlight.add(p);
2276
3751
  void p.finally(() => inFlight.delete(p));
2277
3752
  };
2278
- const emit = createEmitter({ registry, byKind, RollupModel, rejects, counters, logger, track });
3753
+ const linkSubjects = createSubjectLinking({
3754
+ linker: config.subjectLinker,
3755
+ timeoutMs: config.subjectLinkTimeoutMs,
3756
+ counters,
3757
+ logger
3758
+ });
3759
+ const emit = createEmitter({
3760
+ registry,
3761
+ byKind,
3762
+ RollupModel,
3763
+ rejects,
3764
+ counters,
3765
+ logger,
3766
+ track,
3767
+ linkSubjects
3768
+ });
2279
3769
  const forget = createForget({
2280
3770
  TelemetryModel,
2281
3771
  RollupModel,
@@ -2338,6 +3828,17 @@ function createTelemetry(config) {
2338
3828
  counters,
2339
3829
  /** the registry, exposed for the router factories — hosts should import their own */
2340
3830
  registry,
3831
+ /**
3832
+ * Write-time subject linking, exposed for the router factories. `null` when
3833
+ * no `subjectLinker` is configured.
3834
+ *
3835
+ * The wire path does not go through emit() — createIngest() builds its
3836
+ * record itself, because at-least-once delivery inverts the plane order
3837
+ * (insert first, THEN aggregate). So it reaches the linker the same way it
3838
+ * reaches the registry and the models: off the instance, running the one
3839
+ * implementation, rather than growing a second copy of the rules.
3840
+ */
3841
+ linkSubjects,
2341
3842
  logger,
2342
3843
  /** mint an ingest key; the full key string is returned once, never again */
2343
3844
  createKey: (input) => createKey(KeyModel, input),
@@ -2355,16 +3856,21 @@ function createTelemetry(config) {
2355
3856
  }
2356
3857
 
2357
3858
  exports.BODY_MAX_CHARS = BODY_MAX_CHARS;
3859
+ exports.COUNTER_MAP_MAX = COUNTER_MAP_MAX;
3860
+ exports.COUNTER_OVERFLOW_KEY = COUNTER_OVERFLOW_KEY;
2358
3861
  exports.DEFAULT_LIMITS = DEFAULT_LIMITS;
2359
3862
  exports.Env = Env;
2360
3863
  exports.INDEX_BUDGET = INDEX_BUDGET;
2361
3864
  exports.KeyKind = KeyKind;
2362
3865
  exports.LogLevel = LogLevel;
3866
+ exports.MAX_SUGGESTIONS = MAX_SUGGESTIONS;
2363
3867
  exports.Origin = Origin;
2364
3868
  exports.PLATFORM_SCOPE = PLATFORM_SCOPE;
2365
3869
  exports.RETENTION_DAYS = RETENTION_DAYS;
2366
3870
  exports.SAMPLE_RATE = SAMPLE_RATE;
2367
3871
  exports.SCHEMA_VERSION = SCHEMA_VERSION;
3872
+ exports.SUBJECT_LINK_TIMEOUT_MS = SUBJECT_LINK_TIMEOUT_MS;
3873
+ exports.SUBJECT_MAX = SUBJECT_MAX;
2368
3874
  exports.TelemetryKind = TelemetryKind;
2369
3875
  exports.TenantMode = TenantMode;
2370
3876
  exports.boundedMeta = boundedMeta;
@@ -2373,18 +3879,30 @@ exports.createIngest = createIngest;
2373
3879
  exports.createKey = createKey;
2374
3880
  exports.createQueries = createQueries;
2375
3881
  exports.createTelemetry = createTelemetry;
3882
+ exports.createValues = createValues;
2376
3883
  exports.defaultSpaDir = defaultSpaDir;
2377
3884
  exports.defineRegistry = defineRegistry;
3885
+ exports.deriveCatalog = deriveCatalog;
3886
+ exports.deriveSuggestions = deriveSuggestions;
2378
3887
  exports.deriveViews = deriveViews;
3888
+ exports.executeReport = executeReport;
2379
3889
  exports.findFamily = findFamily;
3890
+ exports.foldRollups = foldRollups;
2380
3891
  exports.hashSecret = hashSecret;
3892
+ exports.intervalForRange = intervalForRange;
2381
3893
  exports.isPlatformScope = isPlatformScope;
2382
3894
  exports.median = median;
2383
3895
  exports.newId = newId;
3896
+ exports.normalizeQuery = normalizeQuery;
2384
3897
  exports.parseKeyString = parseKeyString;
3898
+ exports.parseReportQuery = parseReportQuery;
2385
3899
  exports.plain = plain;
3900
+ exports.projectRegistry = projectRegistry;
3901
+ exports.rangeOf = rangeOf;
3902
+ exports.reportToQuery = reportToQuery;
2386
3903
  exports.requireMilestoneFamily = requireMilestoneFamily;
2387
3904
  exports.resolveDim = resolveDim;
3905
+ exports.resolveReport = resolveReport;
2388
3906
  exports.summarizeStages = summarizeStages;
2389
3907
  exports.traceKeep = traceKeep;
2390
3908
  exports.truncate = truncate;