@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.js
CHANGED
|
@@ -2,6 +2,7 @@ import { uuidv7 } from 'uuidv7';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { Schema } from 'mongoose';
|
|
4
4
|
import { randomBytes, scryptSync, timingSafeEqual, createHash } from 'crypto';
|
|
5
|
+
import { originalPositionFor, sourceContentFor, TraceMap } from '@jridgewell/trace-mapping';
|
|
5
6
|
import express2 from 'express';
|
|
6
7
|
import fs from 'fs';
|
|
7
8
|
import path2 from 'path';
|
|
@@ -62,6 +63,8 @@ var newCounters = () => ({
|
|
|
62
63
|
truncated: 0,
|
|
63
64
|
rollupSkippedBy: {},
|
|
64
65
|
undeclaredAttrs: {},
|
|
66
|
+
attrsDropped: {},
|
|
67
|
+
metricsDropped: {},
|
|
65
68
|
subjectsLinked: 0,
|
|
66
69
|
subjectLinkMisses: 0,
|
|
67
70
|
subjectLinkErrors: 0,
|
|
@@ -271,6 +274,7 @@ var UsageDetailSchema = new Schema(
|
|
|
271
274
|
{ _id: false }
|
|
272
275
|
);
|
|
273
276
|
function buildBaseSchema(collection, registry, counters, opts) {
|
|
277
|
+
const validation = opts.validation;
|
|
274
278
|
const schema = new Schema(
|
|
275
279
|
{
|
|
276
280
|
/** UUIDv7 — sortable, insertion-local, replaces the ObjectId */
|
|
@@ -412,13 +416,39 @@ function buildBaseSchema(collection, registry, counters, opts) {
|
|
|
412
416
|
}
|
|
413
417
|
const check = (label3, m, zschema) => {
|
|
414
418
|
const obj = Object.fromEntries(m ?? []);
|
|
419
|
+
const dropped = label3 === "attrs" ? counters.attrsDropped : counters.metricsDropped;
|
|
420
|
+
const note = (key) => bumpCounterMap(dropped, `${this.name}|${key}`);
|
|
415
421
|
if (!zschema) {
|
|
416
|
-
if (Object.keys(obj).length)
|
|
422
|
+
if (!Object.keys(obj).length) return;
|
|
423
|
+
if (validation === "strict") throw new Error(`telemetry: "${this.name}" declares no ${label3}`);
|
|
424
|
+
for (const key of Object.keys(obj)) {
|
|
425
|
+
note(key);
|
|
426
|
+
m?.delete(key);
|
|
427
|
+
}
|
|
417
428
|
return;
|
|
418
429
|
}
|
|
419
430
|
const s = zschema.strict?.() ?? zschema;
|
|
420
431
|
const r = s.safeParse(obj);
|
|
421
|
-
if (
|
|
432
|
+
if (r.success) return;
|
|
433
|
+
if (validation === "strict") {
|
|
434
|
+
throw new Error(`telemetry: ${label3} invalid for "${this.name}": ${r.error.message}`);
|
|
435
|
+
}
|
|
436
|
+
const bad = /* @__PURE__ */ new Set();
|
|
437
|
+
for (const issue of r.error.issues) {
|
|
438
|
+
if (issue.code === "unrecognized_keys" && Array.isArray(issue.keys)) {
|
|
439
|
+
for (const k of issue.keys) bad.add(String(k));
|
|
440
|
+
} else if (Array.isArray(issue.path) && issue.path.length) {
|
|
441
|
+
bad.add(String(issue.path[0]));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const key of bad) {
|
|
445
|
+
if (m?.has(key)) {
|
|
446
|
+
note(key);
|
|
447
|
+
m.delete(key);
|
|
448
|
+
} else {
|
|
449
|
+
note("(missing)");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
422
452
|
};
|
|
423
453
|
check("attrs", this.attrs, spec.attrs);
|
|
424
454
|
check("metrics", this.metrics, spec.metrics);
|
|
@@ -451,7 +481,11 @@ function buildTelemetryModels(opts) {
|
|
|
451
481
|
)
|
|
452
482
|
};
|
|
453
483
|
}
|
|
454
|
-
const base = buildBaseSchema(collection, registry, counters, {
|
|
484
|
+
const base = buildBaseSchema(collection, registry, counters, {
|
|
485
|
+
platforms,
|
|
486
|
+
bodyMax,
|
|
487
|
+
validation: opts.validation ?? "lenient"
|
|
488
|
+
});
|
|
455
489
|
const TelemetryModel = connection.model(modelName, base);
|
|
456
490
|
const disc = (kind, build) => TelemetryModel.discriminator(`${modelName}_${kind}`, build(), kind);
|
|
457
491
|
const byKind = {
|
|
@@ -1128,6 +1162,145 @@ function createSyncIndexes(ctx) {
|
|
|
1128
1162
|
await ctx.rejects().createIndex({ at: 1 }, { expireAfterSeconds: REJECT_TTL_DAYS * 86400 });
|
|
1129
1163
|
};
|
|
1130
1164
|
}
|
|
1165
|
+
var SOURCEMAP_MAX_BYTES = 15 * 1024 * 1024;
|
|
1166
|
+
var SOURCEMAP_RETENTION_DAYS = 90;
|
|
1167
|
+
var CACHE_SIZE = 8;
|
|
1168
|
+
var CONTEXT_MAX = 200;
|
|
1169
|
+
var bundleFile = (filename) => String(filename ?? "").split(/[?#]/)[0].split(/[\\/]/).pop() ?? "";
|
|
1170
|
+
var cleanSource = (source) => (
|
|
1171
|
+
// webpack:///src/x.ts, webpack://app/./src/x.ts → src/x.ts
|
|
1172
|
+
source.replace(/^webpack:\/\/[^/]*\//, "").replace(/^\.\//, "")
|
|
1173
|
+
);
|
|
1174
|
+
function createSourcemaps(opts) {
|
|
1175
|
+
const coll = () => opts.connection.db.collection(opts.collection);
|
|
1176
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1177
|
+
const remember = (key, value) => {
|
|
1178
|
+
cache.delete(key);
|
|
1179
|
+
cache.set(key, value);
|
|
1180
|
+
while (cache.size > CACHE_SIZE) cache.delete(cache.keys().next().value);
|
|
1181
|
+
};
|
|
1182
|
+
const keyOf = (tenantId, service, release, file) => `${tenantId}\0${service}\0${release}\0${file}`;
|
|
1183
|
+
async function load(tenantId, service, release, file) {
|
|
1184
|
+
const key = keyOf(tenantId, service, release, file);
|
|
1185
|
+
if (cache.has(key)) {
|
|
1186
|
+
const hit = cache.get(key);
|
|
1187
|
+
remember(key, hit);
|
|
1188
|
+
return hit;
|
|
1189
|
+
}
|
|
1190
|
+
let parsed = null;
|
|
1191
|
+
try {
|
|
1192
|
+
const doc = await coll().findOne({ tenantId, service, release, file }, { projection: { map: 1 } });
|
|
1193
|
+
if (doc?.map) parsed = new TraceMap(String(doc.map));
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
opts.logger.warn("[telemetry] sourcemap load failed", file, e?.message);
|
|
1196
|
+
}
|
|
1197
|
+
remember(key, parsed);
|
|
1198
|
+
return parsed;
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
async ensureIndexes() {
|
|
1202
|
+
await coll().createIndex(
|
|
1203
|
+
{ tenantId: 1, service: 1, release: 1, file: 1 },
|
|
1204
|
+
{ unique: true, name: "sourcemap_identity" }
|
|
1205
|
+
);
|
|
1206
|
+
await coll().createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0, name: "sourcemap_ttl" });
|
|
1207
|
+
},
|
|
1208
|
+
/**
|
|
1209
|
+
* Store maps for one release. Idempotent: registering the same release
|
|
1210
|
+
* again replaces its maps and refreshes their retention, so a host can
|
|
1211
|
+
* simply call this on every boot.
|
|
1212
|
+
*/
|
|
1213
|
+
async register(input) {
|
|
1214
|
+
const { tenantId, service, release } = input;
|
|
1215
|
+
if (!tenantId || !service || !release) {
|
|
1216
|
+
throw new Error("telemetry: sourcemaps.register needs tenantId, service and release");
|
|
1217
|
+
}
|
|
1218
|
+
if (release === "unknown") {
|
|
1219
|
+
throw new Error('telemetry: refusing to register sourcemaps for release "unknown" \u2014 set a real release on the client');
|
|
1220
|
+
}
|
|
1221
|
+
const now = /* @__PURE__ */ new Date();
|
|
1222
|
+
const expiresAt = new Date(now.getTime() + SOURCEMAP_RETENTION_DAYS * 864e5);
|
|
1223
|
+
const skipped = [];
|
|
1224
|
+
let stored = 0;
|
|
1225
|
+
for (const f of input.files) {
|
|
1226
|
+
const file = bundleFile(f.file);
|
|
1227
|
+
const map = typeof f.map === "string" ? f.map : JSON.stringify(f.map);
|
|
1228
|
+
if (!file || !map) {
|
|
1229
|
+
skipped.push(f.file);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (Buffer.byteLength(map) > SOURCEMAP_MAX_BYTES) {
|
|
1233
|
+
opts.logger.warn(`[telemetry] sourcemap ${file} exceeds ${SOURCEMAP_MAX_BYTES} bytes \u2014 skipped`);
|
|
1234
|
+
skipped.push(f.file);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
JSON.parse(map);
|
|
1239
|
+
} catch {
|
|
1240
|
+
skipped.push(f.file);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
await coll().updateOne(
|
|
1244
|
+
{ tenantId, service, release, file },
|
|
1245
|
+
{ $set: { map, bytes: Buffer.byteLength(map), registeredAt: now, expiresAt } },
|
|
1246
|
+
{ upsert: true }
|
|
1247
|
+
);
|
|
1248
|
+
cache.delete(keyOf(tenantId, service, release, file));
|
|
1249
|
+
stored++;
|
|
1250
|
+
}
|
|
1251
|
+
return { stored, skipped };
|
|
1252
|
+
},
|
|
1253
|
+
/** translate one position; null when no map is registered or it has no mapping there */
|
|
1254
|
+
async resolve(where, lineno, colno) {
|
|
1255
|
+
const file = bundleFile(where.filename);
|
|
1256
|
+
if (!file || !where.release || where.release === "unknown") return null;
|
|
1257
|
+
if (!Number.isFinite(lineno) || !Number.isFinite(colno)) return null;
|
|
1258
|
+
const map = await load(where.tenantId, where.service, where.release, file);
|
|
1259
|
+
if (!map) return null;
|
|
1260
|
+
const pos = originalPositionFor(map, { line: lineno, column: Math.max(0, colno - 1) });
|
|
1261
|
+
if (!pos.source || pos.line == null) return null;
|
|
1262
|
+
const out = {
|
|
1263
|
+
source: cleanSource(pos.source),
|
|
1264
|
+
line: pos.line,
|
|
1265
|
+
column: (pos.column ?? 0) + 1
|
|
1266
|
+
};
|
|
1267
|
+
if (pos.name) out.name = pos.name;
|
|
1268
|
+
const content = sourceContentFor(map, pos.source);
|
|
1269
|
+
const text = content?.split("\n")[pos.line - 1];
|
|
1270
|
+
if (text != null) out.context = text.trim().slice(0, CONTEXT_MAX);
|
|
1271
|
+
return out;
|
|
1272
|
+
},
|
|
1273
|
+
/**
|
|
1274
|
+
* A copy of an error record with `original` set on every frame a map
|
|
1275
|
+
* could translate. Anything else comes back unchanged — never throws.
|
|
1276
|
+
*/
|
|
1277
|
+
async symbolicate(record) {
|
|
1278
|
+
const frames = record?.error?.frames;
|
|
1279
|
+
if (record?.kind !== "error" || !Array.isArray(frames) || !frames.length) return record;
|
|
1280
|
+
try {
|
|
1281
|
+
const translated = await Promise.all(
|
|
1282
|
+
frames.map(async (f) => {
|
|
1283
|
+
const original = await this.resolve(
|
|
1284
|
+
{
|
|
1285
|
+
tenantId: record.tenantId,
|
|
1286
|
+
service: record.service,
|
|
1287
|
+
release: record.release,
|
|
1288
|
+
filename: f?.filename
|
|
1289
|
+
},
|
|
1290
|
+
Number(f?.lineno),
|
|
1291
|
+
Number(f?.colno)
|
|
1292
|
+
);
|
|
1293
|
+
return original ? { ...f, original } : f;
|
|
1294
|
+
})
|
|
1295
|
+
);
|
|
1296
|
+
return { ...record, error: { ...record.error, frames: translated } };
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
opts.logger.warn("[telemetry] symbolicate failed", e?.message);
|
|
1299
|
+
return record;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1131
1304
|
var KeyKind = { Publishable: "publishable", Secret: "secret" };
|
|
1132
1305
|
var TenantMode = { Fixed: "fixed", Session: "session", Claimed: "claimed" };
|
|
1133
1306
|
var KEY_RE = /^(pk|sk)_([a-z0-9]+)_(tk_[a-f0-9]{24})(?:_([a-f0-9]{48}))?$/;
|
|
@@ -3587,12 +3760,14 @@ function createDashboard(opts) {
|
|
|
3587
3760
|
scope: req.viewer.tenantId,
|
|
3588
3761
|
platform: isPlatformScope(req.viewer.tenantId)
|
|
3589
3762
|
})));
|
|
3590
|
-
api.get("/records", h(
|
|
3591
|
-
|
|
3763
|
+
api.get("/records", h(async (req) => {
|
|
3764
|
+
const page = await q.records(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3592
3765
|
limit: req.query.limit ? Number(req.query.limit) : void 0,
|
|
3593
3766
|
cursor: typeof req.query.cursor === "string" ? req.query.cursor : void 0
|
|
3594
|
-
})
|
|
3595
|
-
|
|
3767
|
+
});
|
|
3768
|
+
if (!t.sourcemaps || !page?.items?.some((r) => r?.kind === "error")) return page;
|
|
3769
|
+
return { ...page, items: await Promise.all(page.items.map((r) => t.sourcemaps.symbolicate(r))) };
|
|
3770
|
+
}));
|
|
3596
3771
|
api.get("/series", h(
|
|
3597
3772
|
async (req) => q.series(req.viewer.tenantId, parseRange(req.query), parseFilter(req.query), {
|
|
3598
3773
|
measure: typeof req.query.measure === "string" ? req.query.measure : void 0,
|
|
@@ -3834,7 +4009,8 @@ function createTelemetry(config) {
|
|
|
3834
4009
|
modelName,
|
|
3835
4010
|
collection,
|
|
3836
4011
|
platforms: config.platforms,
|
|
3837
|
-
bodyMax: config.bodyMax
|
|
4012
|
+
bodyMax: config.bodyMax,
|
|
4013
|
+
validation: config.validation
|
|
3838
4014
|
});
|
|
3839
4015
|
const RollupModel = buildRollupModel(conn, `${modelName}Rollup`, `${collection}_rollups`);
|
|
3840
4016
|
const CheckpointModel = buildCheckpointModel(conn, `${modelName}Checkpoint`, `${collection}_checkpoints`);
|
|
@@ -3888,12 +4064,18 @@ function createTelemetry(config) {
|
|
|
3888
4064
|
logger,
|
|
3889
4065
|
linkSubjects
|
|
3890
4066
|
});
|
|
3891
|
-
const
|
|
4067
|
+
const syncModelIndexes = createSyncIndexes({
|
|
3892
4068
|
registry,
|
|
3893
4069
|
TelemetryModel,
|
|
3894
4070
|
models: [TelemetryModel, ...Object.values(byKind), RollupModel, CheckpointModel, KeyModel],
|
|
3895
4071
|
rejects
|
|
3896
4072
|
});
|
|
4073
|
+
const sourcemaps = createSourcemaps({ connection: conn, collection: `${collection}_sourcemaps`, logger });
|
|
4074
|
+
const syncIndexes = async (...args) => {
|
|
4075
|
+
const result = await syncModelIndexes(...args);
|
|
4076
|
+
await sourcemaps.ensureIndexes();
|
|
4077
|
+
return result;
|
|
4078
|
+
};
|
|
3897
4079
|
return {
|
|
3898
4080
|
/** write — the only write */
|
|
3899
4081
|
emit,
|
|
@@ -3960,6 +4142,13 @@ function createTelemetry(config) {
|
|
|
3960
4142
|
*/
|
|
3961
4143
|
linkSubjects,
|
|
3962
4144
|
logger,
|
|
4145
|
+
/**
|
|
4146
|
+
* Sourcemaps for minified clients. `register()` stores a release's maps
|
|
4147
|
+
* (server-side only — there is no HTTP route); the dashboard translates
|
|
4148
|
+
* error frames against them at read time, so errors recorded before the
|
|
4149
|
+
* maps were registered are translated too. See sourcemaps.ts.
|
|
4150
|
+
*/
|
|
4151
|
+
sourcemaps,
|
|
3963
4152
|
/** mint an ingest key; the full key string is returned once, never again */
|
|
3964
4153
|
createKey: (input) => createKey(KeyModel, input),
|
|
3965
4154
|
/** the models, exposed for hosts and the router factories */
|