@jeffjassky/telemetry 0.7.0 → 0.8.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 +186 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +186 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +26 -1
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +26 -1
- package/dist/mcp.js.map +1 -1
- package/dist/ui/_assets/{index-BksXA5iG.js → index-GvGDQ08Z.js} +2 -2
- 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-BksXA5iG.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');
|
|
@@ -1170,6 +1171,145 @@ function createSyncIndexes(ctx) {
|
|
|
1170
1171
|
await ctx.rejects().createIndex({ at: 1 }, { expireAfterSeconds: REJECT_TTL_DAYS * 86400 });
|
|
1171
1172
|
};
|
|
1172
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
|
+
}
|
|
1173
1313
|
var KeyKind = { Publishable: "publishable", Secret: "secret" };
|
|
1174
1314
|
var TenantMode = { Fixed: "fixed", Session: "session", Claimed: "claimed" };
|
|
1175
1315
|
var KEY_RE = /^(pk|sk)_([a-z0-9]+)_(tk_[a-f0-9]{24})(?:_([a-f0-9]{48}))?$/;
|
|
@@ -2111,6 +2251,31 @@ var BREAKDOWN_FIELDS = [
|
|
|
2111
2251
|
"error.type",
|
|
2112
2252
|
"error.handled"
|
|
2113
2253
|
];
|
|
2254
|
+
var GENERIC_SUBJECT_TYPES = ["anon", "session"];
|
|
2255
|
+
var mostSpecificSubject = {
|
|
2256
|
+
$let: {
|
|
2257
|
+
vars: { keys: { $ifNull: ["$subjectKeys", []] } },
|
|
2258
|
+
in: {
|
|
2259
|
+
$ifNull: [
|
|
2260
|
+
{
|
|
2261
|
+
$arrayElemAt: [
|
|
2262
|
+
{
|
|
2263
|
+
$filter: {
|
|
2264
|
+
input: "$$keys",
|
|
2265
|
+
as: "k",
|
|
2266
|
+
cond: {
|
|
2267
|
+
$not: [{ $in: [{ $arrayElemAt: [{ $split: ["$$k", ":"] }, 0] }, GENERIC_SUBJECT_TYPES] }]
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
},
|
|
2271
|
+
0
|
|
2272
|
+
]
|
|
2273
|
+
},
|
|
2274
|
+
{ $arrayElemAt: ["$$keys", 0] }
|
|
2275
|
+
]
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2114
2279
|
var typePrefix = (ref) => ({
|
|
2115
2280
|
$let: {
|
|
2116
2281
|
vars: { ref },
|
|
@@ -2138,7 +2303,7 @@ function dimExpression(dim2) {
|
|
|
2138
2303
|
}
|
|
2139
2304
|
return { $ifNull: [`$${path3}`, null] };
|
|
2140
2305
|
}
|
|
2141
|
-
if (dim2 === "subjectType") return typePrefix(
|
|
2306
|
+
if (dim2 === "subjectType") return typePrefix(mostSpecificSubject);
|
|
2142
2307
|
if (dim2 === "actorType") return typePrefix("$actor");
|
|
2143
2308
|
throw badRequest(
|
|
2144
2309
|
`"${dim2}" is not a dimension. Use "attr:<key>", "field:<path>", "subjectType" or "actorType".`
|
|
@@ -3629,12 +3794,14 @@ function createDashboard(opts) {
|
|
|
3629
3794
|
scope: req.viewer.tenantId,
|
|
3630
3795
|
platform: isPlatformScope(req.viewer.tenantId)
|
|
3631
3796
|
})));
|
|
3632
|
-
api.get("/records", h(
|
|
3633
|
-
|
|
3797
|
+
api.get("/records", h(async (req) => {
|
|
3798
|
+
const page = await q.records(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3634
3799
|
limit: req.query.limit ? Number(req.query.limit) : void 0,
|
|
3635
3800
|
cursor: typeof req.query.cursor === "string" ? req.query.cursor : void 0
|
|
3636
|
-
})
|
|
3637
|
-
|
|
3801
|
+
});
|
|
3802
|
+
if (!t.sourcemaps || !page?.items?.some((r) => r?.kind === "error")) return page;
|
|
3803
|
+
return { ...page, items: await Promise.all(page.items.map((r) => t.sourcemaps.symbolicate(r))) };
|
|
3804
|
+
}));
|
|
3638
3805
|
api.get("/series", h(
|
|
3639
3806
|
async (req) => q.series(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3640
3807
|
measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
|
|
@@ -3931,12 +4098,18 @@ function createTelemetry(config) {
|
|
|
3931
4098
|
logger,
|
|
3932
4099
|
linkSubjects
|
|
3933
4100
|
});
|
|
3934
|
-
const
|
|
4101
|
+
const syncModelIndexes = createSyncIndexes({
|
|
3935
4102
|
registry,
|
|
3936
4103
|
TelemetryModel,
|
|
3937
4104
|
models: [TelemetryModel, ...Object.values(byKind), RollupModel, CheckpointModel, KeyModel],
|
|
3938
4105
|
rejects
|
|
3939
4106
|
});
|
|
4107
|
+
const sourcemaps = createSourcemaps({ connection: conn, collection: `${collection}_sourcemaps`, logger });
|
|
4108
|
+
const syncIndexes = async (...args) => {
|
|
4109
|
+
const result = await syncModelIndexes(...args);
|
|
4110
|
+
await sourcemaps.ensureIndexes();
|
|
4111
|
+
return result;
|
|
4112
|
+
};
|
|
3940
4113
|
return {
|
|
3941
4114
|
/** write — the only write */
|
|
3942
4115
|
emit,
|
|
@@ -4003,6 +4176,13 @@ function createTelemetry(config) {
|
|
|
4003
4176
|
*/
|
|
4004
4177
|
linkSubjects,
|
|
4005
4178
|
logger,
|
|
4179
|
+
/**
|
|
4180
|
+
* Sourcemaps for minified clients. `register()` stores a release's maps
|
|
4181
|
+
* (server-side only — there is no HTTP route); the dashboard translates
|
|
4182
|
+
* error frames against them at read time, so errors recorded before the
|
|
4183
|
+
* maps were registered are translated too. See sourcemaps.ts.
|
|
4184
|
+
*/
|
|
4185
|
+
sourcemaps,
|
|
4006
4186
|
/** mint an ingest key; the full key string is returned once, never again */
|
|
4007
4187
|
createKey: (input) => createKey(KeyModel, input),
|
|
4008
4188
|
/** the models, exposed for hosts and the router factories */
|