@jeffjassky/telemetry 0.6.1 → 0.8.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 +198 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +198 -9
- 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-BVZYi-43.js → index-GvGDQ08Z.js} +10 -10
- package/dist/ui/_assets/index-GvGDQ08Z.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +15 -5
- package/types/index.d.ts +45 -0
- package/dist/ui/_assets/index-BVZYi-43.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ var uuidv7 = require('uuidv7');
|
|
|
4
4
|
var zod = require('zod');
|
|
5
5
|
var mongoose = require('mongoose');
|
|
6
6
|
var crypto = require('crypto');
|
|
7
|
+
var traceMapping = require('@jridgewell/trace-mapping');
|
|
7
8
|
var express2 = require('express');
|
|
8
9
|
var fs = require('fs');
|
|
9
10
|
var path2 = require('path');
|
|
@@ -71,6 +72,8 @@ var newCounters = () => ({
|
|
|
71
72
|
truncated: 0,
|
|
72
73
|
rollupSkippedBy: {},
|
|
73
74
|
undeclaredAttrs: {},
|
|
75
|
+
attrsDropped: {},
|
|
76
|
+
metricsDropped: {},
|
|
74
77
|
subjectsLinked: 0,
|
|
75
78
|
subjectLinkMisses: 0,
|
|
76
79
|
subjectLinkErrors: 0,
|
|
@@ -280,6 +283,7 @@ var UsageDetailSchema = new mongoose.Schema(
|
|
|
280
283
|
{ _id: false }
|
|
281
284
|
);
|
|
282
285
|
function buildBaseSchema(collection, registry, counters, opts) {
|
|
286
|
+
const validation = opts.validation;
|
|
283
287
|
const schema = new mongoose.Schema(
|
|
284
288
|
{
|
|
285
289
|
/** UUIDv7 — sortable, insertion-local, replaces the ObjectId */
|
|
@@ -421,13 +425,39 @@ function buildBaseSchema(collection, registry, counters, opts) {
|
|
|
421
425
|
}
|
|
422
426
|
const check = (label3, m, zschema) => {
|
|
423
427
|
const obj = Object.fromEntries(m ?? []);
|
|
428
|
+
const dropped = label3 === "attrs" ? counters.attrsDropped : counters.metricsDropped;
|
|
429
|
+
const note = (key) => bumpCounterMap(dropped, `${this.name}|${key}`);
|
|
424
430
|
if (!zschema) {
|
|
425
|
-
if (Object.keys(obj).length)
|
|
431
|
+
if (!Object.keys(obj).length) return;
|
|
432
|
+
if (validation === "strict") throw new Error(`telemetry: "${this.name}" declares no ${label3}`);
|
|
433
|
+
for (const key of Object.keys(obj)) {
|
|
434
|
+
note(key);
|
|
435
|
+
m?.delete(key);
|
|
436
|
+
}
|
|
426
437
|
return;
|
|
427
438
|
}
|
|
428
439
|
const s = zschema.strict?.() ?? zschema;
|
|
429
440
|
const r = s.safeParse(obj);
|
|
430
|
-
if (
|
|
441
|
+
if (r.success) return;
|
|
442
|
+
if (validation === "strict") {
|
|
443
|
+
throw new Error(`telemetry: ${label3} invalid for "${this.name}": ${r.error.message}`);
|
|
444
|
+
}
|
|
445
|
+
const bad = /* @__PURE__ */ new Set();
|
|
446
|
+
for (const issue of r.error.issues) {
|
|
447
|
+
if (issue.code === "unrecognized_keys" && Array.isArray(issue.keys)) {
|
|
448
|
+
for (const k of issue.keys) bad.add(String(k));
|
|
449
|
+
} else if (Array.isArray(issue.path) && issue.path.length) {
|
|
450
|
+
bad.add(String(issue.path[0]));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const key of bad) {
|
|
454
|
+
if (m?.has(key)) {
|
|
455
|
+
note(key);
|
|
456
|
+
m.delete(key);
|
|
457
|
+
} else {
|
|
458
|
+
note("(missing)");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
431
461
|
};
|
|
432
462
|
check("attrs", this.attrs, spec.attrs);
|
|
433
463
|
check("metrics", this.metrics, spec.metrics);
|
|
@@ -460,7 +490,11 @@ function buildTelemetryModels(opts) {
|
|
|
460
490
|
)
|
|
461
491
|
};
|
|
462
492
|
}
|
|
463
|
-
const base = buildBaseSchema(collection, registry, counters, {
|
|
493
|
+
const base = buildBaseSchema(collection, registry, counters, {
|
|
494
|
+
platforms,
|
|
495
|
+
bodyMax,
|
|
496
|
+
validation: opts.validation ?? "lenient"
|
|
497
|
+
});
|
|
464
498
|
const TelemetryModel = connection.model(modelName, base);
|
|
465
499
|
const disc = (kind, build) => TelemetryModel.discriminator(`${modelName}_${kind}`, build(), kind);
|
|
466
500
|
const byKind = {
|
|
@@ -1137,6 +1171,145 @@ function createSyncIndexes(ctx) {
|
|
|
1137
1171
|
await ctx.rejects().createIndex({ at: 1 }, { expireAfterSeconds: REJECT_TTL_DAYS * 86400 });
|
|
1138
1172
|
};
|
|
1139
1173
|
}
|
|
1174
|
+
var SOURCEMAP_MAX_BYTES = 15 * 1024 * 1024;
|
|
1175
|
+
var SOURCEMAP_RETENTION_DAYS = 90;
|
|
1176
|
+
var CACHE_SIZE = 8;
|
|
1177
|
+
var CONTEXT_MAX = 200;
|
|
1178
|
+
var bundleFile = (filename) => String(filename ?? "").split(/[?#]/)[0].split(/[\\/]/).pop() ?? "";
|
|
1179
|
+
var cleanSource = (source) => (
|
|
1180
|
+
// webpack:///src/x.ts, webpack://app/./src/x.ts → src/x.ts
|
|
1181
|
+
source.replace(/^webpack:\/\/[^/]*\//, "").replace(/^\.\//, "")
|
|
1182
|
+
);
|
|
1183
|
+
function createSourcemaps(opts) {
|
|
1184
|
+
const coll = () => opts.connection.db.collection(opts.collection);
|
|
1185
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1186
|
+
const remember = (key, value) => {
|
|
1187
|
+
cache.delete(key);
|
|
1188
|
+
cache.set(key, value);
|
|
1189
|
+
while (cache.size > CACHE_SIZE) cache.delete(cache.keys().next().value);
|
|
1190
|
+
};
|
|
1191
|
+
const keyOf = (tenantId, service, release, file) => `${tenantId}\0${service}\0${release}\0${file}`;
|
|
1192
|
+
async function load(tenantId, service, release, file) {
|
|
1193
|
+
const key = keyOf(tenantId, service, release, file);
|
|
1194
|
+
if (cache.has(key)) {
|
|
1195
|
+
const hit = cache.get(key);
|
|
1196
|
+
remember(key, hit);
|
|
1197
|
+
return hit;
|
|
1198
|
+
}
|
|
1199
|
+
let parsed = null;
|
|
1200
|
+
try {
|
|
1201
|
+
const doc = await coll().findOne({ tenantId, service, release, file }, { projection: { map: 1 } });
|
|
1202
|
+
if (doc?.map) parsed = new traceMapping.TraceMap(String(doc.map));
|
|
1203
|
+
} catch (e) {
|
|
1204
|
+
opts.logger.warn("[telemetry] sourcemap load failed", file, e?.message);
|
|
1205
|
+
}
|
|
1206
|
+
remember(key, parsed);
|
|
1207
|
+
return parsed;
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
async ensureIndexes() {
|
|
1211
|
+
await coll().createIndex(
|
|
1212
|
+
{ tenantId: 1, service: 1, release: 1, file: 1 },
|
|
1213
|
+
{ unique: true, name: "sourcemap_identity" }
|
|
1214
|
+
);
|
|
1215
|
+
await coll().createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0, name: "sourcemap_ttl" });
|
|
1216
|
+
},
|
|
1217
|
+
/**
|
|
1218
|
+
* Store maps for one release. Idempotent: registering the same release
|
|
1219
|
+
* again replaces its maps and refreshes their retention, so a host can
|
|
1220
|
+
* simply call this on every boot.
|
|
1221
|
+
*/
|
|
1222
|
+
async register(input) {
|
|
1223
|
+
const { tenantId, service, release } = input;
|
|
1224
|
+
if (!tenantId || !service || !release) {
|
|
1225
|
+
throw new Error("telemetry: sourcemaps.register needs tenantId, service and release");
|
|
1226
|
+
}
|
|
1227
|
+
if (release === "unknown") {
|
|
1228
|
+
throw new Error('telemetry: refusing to register sourcemaps for release "unknown" \u2014 set a real release on the client');
|
|
1229
|
+
}
|
|
1230
|
+
const now = /* @__PURE__ */ new Date();
|
|
1231
|
+
const expiresAt = new Date(now.getTime() + SOURCEMAP_RETENTION_DAYS * 864e5);
|
|
1232
|
+
const skipped = [];
|
|
1233
|
+
let stored = 0;
|
|
1234
|
+
for (const f of input.files) {
|
|
1235
|
+
const file = bundleFile(f.file);
|
|
1236
|
+
const map = typeof f.map === "string" ? f.map : JSON.stringify(f.map);
|
|
1237
|
+
if (!file || !map) {
|
|
1238
|
+
skipped.push(f.file);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (Buffer.byteLength(map) > SOURCEMAP_MAX_BYTES) {
|
|
1242
|
+
opts.logger.warn(`[telemetry] sourcemap ${file} exceeds ${SOURCEMAP_MAX_BYTES} bytes \u2014 skipped`);
|
|
1243
|
+
skipped.push(f.file);
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
JSON.parse(map);
|
|
1248
|
+
} catch {
|
|
1249
|
+
skipped.push(f.file);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
await coll().updateOne(
|
|
1253
|
+
{ tenantId, service, release, file },
|
|
1254
|
+
{ $set: { map, bytes: Buffer.byteLength(map), registeredAt: now, expiresAt } },
|
|
1255
|
+
{ upsert: true }
|
|
1256
|
+
);
|
|
1257
|
+
cache.delete(keyOf(tenantId, service, release, file));
|
|
1258
|
+
stored++;
|
|
1259
|
+
}
|
|
1260
|
+
return { stored, skipped };
|
|
1261
|
+
},
|
|
1262
|
+
/** translate one position; null when no map is registered or it has no mapping there */
|
|
1263
|
+
async resolve(where, lineno, colno) {
|
|
1264
|
+
const file = bundleFile(where.filename);
|
|
1265
|
+
if (!file || !where.release || where.release === "unknown") return null;
|
|
1266
|
+
if (!Number.isFinite(lineno) || !Number.isFinite(colno)) return null;
|
|
1267
|
+
const map = await load(where.tenantId, where.service, where.release, file);
|
|
1268
|
+
if (!map) return null;
|
|
1269
|
+
const pos = traceMapping.originalPositionFor(map, { line: lineno, column: Math.max(0, colno - 1) });
|
|
1270
|
+
if (!pos.source || pos.line == null) return null;
|
|
1271
|
+
const out = {
|
|
1272
|
+
source: cleanSource(pos.source),
|
|
1273
|
+
line: pos.line,
|
|
1274
|
+
column: (pos.column ?? 0) + 1
|
|
1275
|
+
};
|
|
1276
|
+
if (pos.name) out.name = pos.name;
|
|
1277
|
+
const content = traceMapping.sourceContentFor(map, pos.source);
|
|
1278
|
+
const text = content?.split("\n")[pos.line - 1];
|
|
1279
|
+
if (text != null) out.context = text.trim().slice(0, CONTEXT_MAX);
|
|
1280
|
+
return out;
|
|
1281
|
+
},
|
|
1282
|
+
/**
|
|
1283
|
+
* A copy of an error record with `original` set on every frame a map
|
|
1284
|
+
* could translate. Anything else comes back unchanged — never throws.
|
|
1285
|
+
*/
|
|
1286
|
+
async symbolicate(record) {
|
|
1287
|
+
const frames = record?.error?.frames;
|
|
1288
|
+
if (record?.kind !== "error" || !Array.isArray(frames) || !frames.length) return record;
|
|
1289
|
+
try {
|
|
1290
|
+
const translated = await Promise.all(
|
|
1291
|
+
frames.map(async (f) => {
|
|
1292
|
+
const original = await this.resolve(
|
|
1293
|
+
{
|
|
1294
|
+
tenantId: record.tenantId,
|
|
1295
|
+
service: record.service,
|
|
1296
|
+
release: record.release,
|
|
1297
|
+
filename: f?.filename
|
|
1298
|
+
},
|
|
1299
|
+
Number(f?.lineno),
|
|
1300
|
+
Number(f?.colno)
|
|
1301
|
+
);
|
|
1302
|
+
return original ? { ...f, original } : f;
|
|
1303
|
+
})
|
|
1304
|
+
);
|
|
1305
|
+
return { ...record, error: { ...record.error, frames: translated } };
|
|
1306
|
+
} catch (e) {
|
|
1307
|
+
opts.logger.warn("[telemetry] symbolicate failed", e?.message);
|
|
1308
|
+
return record;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1140
1313
|
var KeyKind = { Publishable: "publishable", Secret: "secret" };
|
|
1141
1314
|
var TenantMode = { Fixed: "fixed", Session: "session", Claimed: "claimed" };
|
|
1142
1315
|
var KEY_RE = /^(pk|sk)_([a-z0-9]+)_(tk_[a-f0-9]{24})(?:_([a-f0-9]{48}))?$/;
|
|
@@ -3596,12 +3769,14 @@ function createDashboard(opts) {
|
|
|
3596
3769
|
scope: req.viewer.tenantId,
|
|
3597
3770
|
platform: isPlatformScope(req.viewer.tenantId)
|
|
3598
3771
|
})));
|
|
3599
|
-
api.get("/records", h(
|
|
3600
|
-
|
|
3772
|
+
api.get("/records", h(async (req) => {
|
|
3773
|
+
const page = await q.records(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3601
3774
|
limit: req.query.limit ? Number(req.query.limit) : void 0,
|
|
3602
3775
|
cursor: typeof req.query.cursor === "string" ? req.query.cursor : void 0
|
|
3603
|
-
})
|
|
3604
|
-
|
|
3776
|
+
});
|
|
3777
|
+
if (!t.sourcemaps || !page?.items?.some((r) => r?.kind === "error")) return page;
|
|
3778
|
+
return { ...page, items: await Promise.all(page.items.map((r) => t.sourcemaps.symbolicate(r))) };
|
|
3779
|
+
}));
|
|
3605
3780
|
api.get("/series", h(
|
|
3606
3781
|
async (req) => q.series(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3607
3782
|
measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
|
|
@@ -3843,7 +4018,8 @@ function createTelemetry(config) {
|
|
|
3843
4018
|
modelName,
|
|
3844
4019
|
collection,
|
|
3845
4020
|
platforms: config.platforms,
|
|
3846
|
-
bodyMax: config.bodyMax
|
|
4021
|
+
bodyMax: config.bodyMax,
|
|
4022
|
+
validation: config.validation
|
|
3847
4023
|
});
|
|
3848
4024
|
const RollupModel = buildRollupModel(conn, `${modelName}Rollup`, `${collection}_rollups`);
|
|
3849
4025
|
const CheckpointModel = buildCheckpointModel(conn, `${modelName}Checkpoint`, `${collection}_checkpoints`);
|
|
@@ -3897,12 +4073,18 @@ function createTelemetry(config) {
|
|
|
3897
4073
|
logger,
|
|
3898
4074
|
linkSubjects
|
|
3899
4075
|
});
|
|
3900
|
-
const
|
|
4076
|
+
const syncModelIndexes = createSyncIndexes({
|
|
3901
4077
|
registry,
|
|
3902
4078
|
TelemetryModel,
|
|
3903
4079
|
models: [TelemetryModel, ...Object.values(byKind), RollupModel, CheckpointModel, KeyModel],
|
|
3904
4080
|
rejects
|
|
3905
4081
|
});
|
|
4082
|
+
const sourcemaps = createSourcemaps({ connection: conn, collection: `${collection}_sourcemaps`, logger });
|
|
4083
|
+
const syncIndexes = async (...args) => {
|
|
4084
|
+
const result = await syncModelIndexes(...args);
|
|
4085
|
+
await sourcemaps.ensureIndexes();
|
|
4086
|
+
return result;
|
|
4087
|
+
};
|
|
3906
4088
|
return {
|
|
3907
4089
|
/** write — the only write */
|
|
3908
4090
|
emit,
|
|
@@ -3969,6 +4151,13 @@ function createTelemetry(config) {
|
|
|
3969
4151
|
*/
|
|
3970
4152
|
linkSubjects,
|
|
3971
4153
|
logger,
|
|
4154
|
+
/**
|
|
4155
|
+
* Sourcemaps for minified clients. `register()` stores a release's maps
|
|
4156
|
+
* (server-side only — there is no HTTP route); the dashboard translates
|
|
4157
|
+
* error frames against them at read time, so errors recorded before the
|
|
4158
|
+
* maps were registered are translated too. See sourcemaps.ts.
|
|
4159
|
+
*/
|
|
4160
|
+
sourcemaps,
|
|
3972
4161
|
/** mint an ingest key; the full key string is returned once, never again */
|
|
3973
4162
|
createKey: (input) => createKey(KeyModel, input),
|
|
3974
4163
|
/** the models, exposed for hosts and the router factories */
|