@jeffjassky/telemetry 0.5.0 → 0.6.1
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 +172 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +172 -43
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/ui/_assets/{index-kvwB9_3A.js → index-BVZYi-43.js} +8 -8
- package/dist/ui/_assets/index-BVZYi-43.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +75 -1
- package/types/test-d.ts +18 -0
- package/dist/ui/_assets/index-kvwB9_3A.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -560,10 +560,11 @@ var truncate = (d, b) => {
|
|
|
560
560
|
if (b === "day") return day;
|
|
561
561
|
return new Date(day.getTime() - (day.getUTCDay() + 6) % 7 * 864e5);
|
|
562
562
|
};
|
|
563
|
-
async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
563
|
+
async function recordRollup(RollupModel, doc, name, spec, counters, opts) {
|
|
564
|
+
const dry = opts?.dryRun === true;
|
|
564
565
|
if (spec.actors && doc.actor) {
|
|
565
566
|
const actorType = String(doc.actor).split(":")[0];
|
|
566
|
-
if (!spec.actors.includes(actorType)) return;
|
|
567
|
+
if (!spec.actors.includes(actorType)) return 0;
|
|
567
568
|
}
|
|
568
569
|
const as = spec.as ?? name;
|
|
569
570
|
const at = doc.occurredAt;
|
|
@@ -575,9 +576,11 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
575
576
|
let v = resolveDim(src, doc);
|
|
576
577
|
if (v == null || v === "") {
|
|
577
578
|
if (spec.dimDefault === void 0) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
579
|
+
if (!dry) {
|
|
580
|
+
counters.rollupSkipped++;
|
|
581
|
+
bumpCounterMap(counters.rollupSkippedBy, `${as}|${label(src)}`);
|
|
582
|
+
}
|
|
583
|
+
return 0;
|
|
581
584
|
}
|
|
582
585
|
v = spec.dimDefault;
|
|
583
586
|
}
|
|
@@ -587,48 +590,48 @@ async function recordRollup(RollupModel, doc, name, spec, counters) {
|
|
|
587
590
|
const refs = fansOut ? (doc.subjectKeys ?? []).filter(
|
|
588
591
|
(r) => !spec.subjects || spec.subjects.includes(r.split(":")[0])
|
|
589
592
|
) : [null];
|
|
590
|
-
if (!refs.length) return;
|
|
593
|
+
if (!refs.length) return 0;
|
|
591
594
|
const firstCapture = Object.fromEntries(
|
|
592
595
|
(spec.capture ?? []).map((src) => [label(src), resolveDim(src, doc)]).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
|
|
593
596
|
);
|
|
594
597
|
const expiresAt = spec.retentionDays != null ? new Date(at.getTime() + spec.retentionDays * 864e5) : void 0;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
{
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
firstCapture: { $cond: [isNewFirst, { $literal: firstCapture }, "$firstCapture"] }
|
|
623
|
-
}
|
|
598
|
+
const ops = refs.map((ref) => {
|
|
599
|
+
const dims = spec.by.map((src) => src === "subject" ? ref : fixed.get(src));
|
|
600
|
+
const isNewFirst = {
|
|
601
|
+
$or: [{ $eq: [{ $type: "$firstAt" }, "missing"] }, { $lt: [at, "$firstAt"] }]
|
|
602
|
+
};
|
|
603
|
+
const sums = Object.fromEntries(
|
|
604
|
+
(spec.sum ?? []).map((k) => [k, doc.metrics?.get(k)]).filter(([, v]) => typeof v === "number").map(([k, v]) => [`sums.${k}`, { $add: [{ $ifNull: [`$sums.${k}`, 0] }, v] }])
|
|
605
|
+
);
|
|
606
|
+
return {
|
|
607
|
+
updateOne: {
|
|
608
|
+
filter: { _id: `${doc.tenantId}|${as}|${dims.join("|")}|${bucketKey}` },
|
|
609
|
+
update: [
|
|
610
|
+
{
|
|
611
|
+
$set: {
|
|
612
|
+
tenantId: doc.tenantId,
|
|
613
|
+
as,
|
|
614
|
+
dims,
|
|
615
|
+
...ref ? { subjectType: ref.split(":")[0] } : {},
|
|
616
|
+
...bucketAt ? { bucketAt } : {},
|
|
617
|
+
...expiresAt ? { expiresAt } : {},
|
|
618
|
+
// aggregation $min/$max ignore missing, so correct on insert too
|
|
619
|
+
firstAt: { $min: ["$firstAt", at] },
|
|
620
|
+
lastAt: { $max: ["$lastAt", at] },
|
|
621
|
+
count: { $add: [{ $ifNull: ["$count", 0] }, 1] },
|
|
622
|
+
...sums,
|
|
623
|
+
firstTraceId: { $cond: [isNewFirst, doc.traceId ?? null, "$firstTraceId"] },
|
|
624
|
+
firstCapture: { $cond: [isNewFirst, { $literal: firstCapture }, "$firstCapture"] }
|
|
624
625
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
);
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
upsert: true
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
});
|
|
632
|
+
if (dry) return ops.length;
|
|
633
|
+
await RollupModel.bulkWrite(ops, { ordered: false });
|
|
634
|
+
return ops.length;
|
|
632
635
|
}
|
|
633
636
|
function buildCheckpointModel(connection, modelName, collection) {
|
|
634
637
|
const existing = connection.models?.[modelName];
|
|
@@ -992,6 +995,109 @@ function createForget(ctx) {
|
|
|
992
995
|
};
|
|
993
996
|
}
|
|
994
997
|
|
|
998
|
+
// src/server/relink.ts
|
|
999
|
+
var RELINK_BATCH_SIZE = 500;
|
|
1000
|
+
var refKey = (s) => `${s.type}:${s.id}`;
|
|
1001
|
+
function createRelink(ctx) {
|
|
1002
|
+
const { registry, TelemetryModel, RollupModel, counters, logger, linkSubjects } = ctx;
|
|
1003
|
+
return async function relink(opts = {}) {
|
|
1004
|
+
const dryRun = opts.dryRun !== false;
|
|
1005
|
+
const batchSize = opts.batchSize ?? RELINK_BATCH_SIZE;
|
|
1006
|
+
const result = {
|
|
1007
|
+
examined: 0,
|
|
1008
|
+
linked: 0,
|
|
1009
|
+
subjects: 0,
|
|
1010
|
+
rollups: 0,
|
|
1011
|
+
misses: 0,
|
|
1012
|
+
errors: 0,
|
|
1013
|
+
skipped: 0
|
|
1014
|
+
};
|
|
1015
|
+
if (opts.names) {
|
|
1016
|
+
const unknown = opts.names.filter((n) => !registry[n]);
|
|
1017
|
+
if (unknown.length) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`telemetry: relink() was given names this registry does not declare: ${unknown.join(", ")}. A name with no spec has no rollup families to replay, so relinking it could only desynchronize its rows from its aggregates. Omit \`names\` to sweep everything.`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (!linkSubjects) {
|
|
1024
|
+
logger.warn(
|
|
1025
|
+
"[telemetry] relink() has no subjectLinker to ask \u2014 nothing was read and nothing was written. Configure createTelemetry({ subjectLinker }) first; relink() backfills what that hook would have done, it does not replace it."
|
|
1026
|
+
);
|
|
1027
|
+
result.skipped = 1;
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
if (opts.limit != null && opts.limit <= 0) return result;
|
|
1031
|
+
const filter = {};
|
|
1032
|
+
if (opts.names) filter.name = { $in: opts.names };
|
|
1033
|
+
if (opts.since) filter.occurredAt = { $gte: opts.since };
|
|
1034
|
+
const query = TelemetryModel.find(filter).hint({ _id: 1 }).batchSize(batchSize);
|
|
1035
|
+
if (opts.limit != null) query.limit(opts.limit);
|
|
1036
|
+
const progress = () => {
|
|
1037
|
+
if (!opts.onProgress) return;
|
|
1038
|
+
try {
|
|
1039
|
+
opts.onProgress({ ...result });
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
logger.warn(`[telemetry] relink() onProgress threw \u2014 ignored, the backfill continues: ${e}`);
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
const cursor = query.cursor();
|
|
1045
|
+
let sinceProgress = 0;
|
|
1046
|
+
try {
|
|
1047
|
+
for await (const doc of cursor) {
|
|
1048
|
+
result.examined++;
|
|
1049
|
+
sinceProgress++;
|
|
1050
|
+
if (sinceProgress >= batchSize) {
|
|
1051
|
+
sinceProgress = 0;
|
|
1052
|
+
progress();
|
|
1053
|
+
}
|
|
1054
|
+
const spec = registry[doc.name];
|
|
1055
|
+
if (!spec) {
|
|
1056
|
+
result.skipped++;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const declared = (doc.subjects ?? []).map(
|
|
1060
|
+
(s) => s.role ? { type: s.type, id: s.id, role: s.role } : { type: s.type, id: s.id }
|
|
1061
|
+
);
|
|
1062
|
+
const m0 = counters.subjectLinkMisses;
|
|
1063
|
+
const e0 = counters.subjectLinkErrors + counters.subjectLinkTimeouts;
|
|
1064
|
+
const merged = await linkSubjects(doc.name, spec, doc.tenantId, declared);
|
|
1065
|
+
result.misses += counters.subjectLinkMisses - m0;
|
|
1066
|
+
if (counters.subjectLinkErrors + counters.subjectLinkTimeouts > e0) result.errors++;
|
|
1067
|
+
if (!merged) continue;
|
|
1068
|
+
const had = new Set(declared.map(refKey));
|
|
1069
|
+
const mergedKeys = [];
|
|
1070
|
+
const newKeys = [];
|
|
1071
|
+
for (const s of merged) {
|
|
1072
|
+
const key = refKey(s);
|
|
1073
|
+
if (mergedKeys.includes(key)) continue;
|
|
1074
|
+
mergedKeys.push(key);
|
|
1075
|
+
if (!had.has(key)) newKeys.push(key);
|
|
1076
|
+
}
|
|
1077
|
+
if (!newKeys.length) continue;
|
|
1078
|
+
result.linked++;
|
|
1079
|
+
result.subjects += newKeys.length;
|
|
1080
|
+
if (!dryRun) {
|
|
1081
|
+
await TelemetryModel.updateOne(
|
|
1082
|
+
{ _id: doc._id },
|
|
1083
|
+
{ $set: { subjects: merged, subjectKeys: mergedKeys } }
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
doc.subjectKeys = newKeys;
|
|
1087
|
+
for (const r of spec.rollups ?? []) {
|
|
1088
|
+
if (!r.by.includes("subject")) continue;
|
|
1089
|
+
result.rollups += await recordRollup(RollupModel, doc, doc.name, r, counters, { dryRun });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} finally {
|
|
1093
|
+
await cursor.close().catch(() => {
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
if (sinceProgress) progress();
|
|
1097
|
+
return result;
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
995
1101
|
// src/server/indexes.ts
|
|
996
1102
|
var INDEX_BUDGET = 24;
|
|
997
1103
|
function createSyncIndexes(ctx) {
|
|
@@ -3783,6 +3889,14 @@ function createTelemetry(config) {
|
|
|
3783
3889
|
},
|
|
3784
3890
|
globalSubjectRefs: () => config.globalSubjectRefs === true
|
|
3785
3891
|
});
|
|
3892
|
+
const relink = createRelink({
|
|
3893
|
+
registry,
|
|
3894
|
+
TelemetryModel,
|
|
3895
|
+
RollupModel,
|
|
3896
|
+
counters,
|
|
3897
|
+
logger,
|
|
3898
|
+
linkSubjects
|
|
3899
|
+
});
|
|
3786
3900
|
const syncIndexes = createSyncIndexes({
|
|
3787
3901
|
registry,
|
|
3788
3902
|
TelemetryModel,
|
|
@@ -3794,6 +3908,21 @@ function createTelemetry(config) {
|
|
|
3794
3908
|
emit,
|
|
3795
3909
|
/** erasure: delete sole-party rows, redact shared ones, rekey rollups, drop aliases */
|
|
3796
3910
|
forget,
|
|
3911
|
+
/**
|
|
3912
|
+
* Backfill: re-ask the `subjectLinker` about records ALREADY on disk, and
|
|
3913
|
+
* replay the rollups the new subjects reach.
|
|
3914
|
+
*
|
|
3915
|
+
* Linking happens at write time, so configuring it fixes the future and
|
|
3916
|
+
* nothing else — a lifetime `by:['subject']` family is keyed on the subject
|
|
3917
|
+
* the record was written with, and a read-time join cannot reach back into
|
|
3918
|
+
* it. This is how a host catches up the backlog it adopted the hook with.
|
|
3919
|
+
*
|
|
3920
|
+
* DRY RUN BY DEFAULT: it rewrites historical aggregates, so the short call
|
|
3921
|
+
* reports and the writing call says `{ dryRun: false }`. Idempotent by
|
|
3922
|
+
* construction — a row that already carries the linked subject yields
|
|
3923
|
+
* nothing new, so a second run is a no-op.
|
|
3924
|
+
*/
|
|
3925
|
+
relink,
|
|
3797
3926
|
/**
|
|
3798
3927
|
* Tenant scope is not optional — force every read through here. The five
|
|
3799
3928
|
* dashboard query primitives (records/series/distribution/rollups/journey)
|
|
@@ -3866,6 +3995,7 @@ exports.LogLevel = LogLevel;
|
|
|
3866
3995
|
exports.MAX_SUGGESTIONS = MAX_SUGGESTIONS;
|
|
3867
3996
|
exports.Origin = Origin;
|
|
3868
3997
|
exports.PLATFORM_SCOPE = PLATFORM_SCOPE;
|
|
3998
|
+
exports.RELINK_BATCH_SIZE = RELINK_BATCH_SIZE;
|
|
3869
3999
|
exports.RETENTION_DAYS = RETENTION_DAYS;
|
|
3870
4000
|
exports.SAMPLE_RATE = SAMPLE_RATE;
|
|
3871
4001
|
exports.SCHEMA_VERSION = SCHEMA_VERSION;
|